allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
This commit is contained in:
79
vendor/psy/psysh/src/CodeCleaner/AbstractClassPass.php
vendored
Normal file
79
vendor/psy/psysh/src/CodeCleaner/AbstractClassPass.php
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use PhpParser\Node\Stmt\ClassMethod;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* The abstract class pass handles abstract classes and methods, complaining if there are too few or too many of either.
|
||||
*/
|
||||
class AbstractClassPass extends CodeCleanerPass
|
||||
{
|
||||
private Class_ $class;
|
||||
private array $abstractMethods;
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException if the node is an abstract function with a body
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Class_) {
|
||||
$this->class = $node;
|
||||
$this->abstractMethods = [];
|
||||
} elseif ($node instanceof ClassMethod) {
|
||||
if ($node->isAbstract()) {
|
||||
$name = \sprintf('%s::%s', $this->class->name, $node->name);
|
||||
$this->abstractMethods[] = $name;
|
||||
|
||||
if ($node->stmts !== null) {
|
||||
$msg = \sprintf('Abstract function %s cannot contain body', $name);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException if the node is a non-abstract class with abstract methods
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Class_) {
|
||||
$count = \count($this->abstractMethods);
|
||||
if ($count > 0 && !$node->isAbstract()) {
|
||||
$msg = \sprintf(
|
||||
'Class %s contains %d abstract method%s must therefore be declared abstract or implement the remaining methods (%s)',
|
||||
$node->name,
|
||||
$count,
|
||||
($count === 1) ? '' : 's',
|
||||
\implode(', ', $this->abstractMethods)
|
||||
);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
43
vendor/psy/psysh/src/CodeCleaner/AssignThisVariablePass.php
vendored
Normal file
43
vendor/psy/psysh/src/CodeCleaner/AssignThisVariablePass.php
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that the user input does not assign the `$this` variable.
|
||||
*
|
||||
* @author Martin Hasoň <martin.hason@gmail.com>
|
||||
*/
|
||||
class AssignThisVariablePass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* Validate that the user input does not assign the `$this` variable.
|
||||
*
|
||||
* @throws FatalErrorException if the user assign the `$this` variable
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Assign && $node->var instanceof Variable && $node->var->name === 'this') {
|
||||
throw new FatalErrorException('Cannot re-assign $this', 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
59
vendor/psy/psysh/src/CodeCleaner/CallTimePassByReferencePass.php
vendored
Normal file
59
vendor/psy/psysh/src/CodeCleaner/CallTimePassByReferencePass.php
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\VariadicPlaceholder;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that the user did not use the call-time pass-by-reference that causes a fatal error.
|
||||
*
|
||||
* As of PHP 5.4.0, call-time pass-by-reference was removed, so using it will raise a fatal error.
|
||||
*
|
||||
* @author Martin Hasoň <martin.hason@gmail.com>
|
||||
*/
|
||||
class CallTimePassByReferencePass extends CodeCleanerPass
|
||||
{
|
||||
const EXCEPTION_MESSAGE = 'Call-time pass-by-reference has been removed';
|
||||
|
||||
/**
|
||||
* Validate of use call-time pass-by-reference.
|
||||
*
|
||||
* @throws FatalErrorException if the user used call-time pass-by-reference
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if (!$node instanceof FuncCall && !$node instanceof MethodCall && !$node instanceof StaticCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($node->args as $arg) {
|
||||
if ($arg instanceof VariadicPlaceholder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg->byRef) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
104
vendor/psy/psysh/src/CodeCleaner/CalledClassPass.php
vendored
Normal file
104
vendor/psy/psysh/src/CodeCleaner/CalledClassPass.php
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\ConstFetch;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use PhpParser\Node\Stmt\Trait_;
|
||||
use PhpParser\Node\VariadicPlaceholder;
|
||||
use Psy\Exception\ErrorException;
|
||||
|
||||
/**
|
||||
* The called class pass throws warnings for get_class() and get_called_class()
|
||||
* outside a class context.
|
||||
*/
|
||||
class CalledClassPass extends CodeCleanerPass
|
||||
{
|
||||
private bool $inClass = false;
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->inClass = false;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ErrorException if get_class or get_called_class is called without an object from outside a class
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Class_ || $node instanceof Trait_) {
|
||||
$this->inClass = true;
|
||||
} elseif ($node instanceof FuncCall && !$this->inClass) {
|
||||
// We'll give any args at all (besides null) a pass.
|
||||
// Technically we should be checking whether the args are objects, but this will do for now.
|
||||
//
|
||||
// @todo switch this to actually validate args when we get context-aware code cleaner passes.
|
||||
if (!empty($node->args) && !$this->isNull($node->args[0])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We'll ignore name expressions as well (things like `$foo()`)
|
||||
if (!($node->name instanceof Name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = \strtolower($node->name);
|
||||
if (\in_array($name, ['get_class', 'get_called_class'])) {
|
||||
$msg = \sprintf('%s() called without object from outside a class', $name);
|
||||
throw new ErrorException($msg, 0, \E_USER_WARNING, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Class_) {
|
||||
$this->inClass = false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isNull(Node $node): bool
|
||||
{
|
||||
if ($node instanceof VariadicPlaceholder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\property_exists($node, 'value')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $node->value instanceof ConstFetch && \strtolower($node->value->name) === 'null';
|
||||
}
|
||||
}
|
||||
22
vendor/psy/psysh/src/CodeCleaner/CodeCleanerPass.php
vendored
Normal file
22
vendor/psy/psysh/src/CodeCleaner/CodeCleanerPass.php
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
/**
|
||||
* A CodeCleaner pass is a PhpParser Node Visitor.
|
||||
*/
|
||||
abstract class CodeCleanerPass extends NodeVisitorAbstract
|
||||
{
|
||||
// Wheee!
|
||||
}
|
||||
70
vendor/psy/psysh/src/CodeCleaner/EmptyArrayDimFetchPass.php
vendored
Normal file
70
vendor/psy/psysh/src/CodeCleaner/EmptyArrayDimFetchPass.php
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\AssignRef;
|
||||
use PhpParser\Node\Stmt\Foreach_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate empty brackets are only used for assignment.
|
||||
*/
|
||||
class EmptyArrayDimFetchPass extends CodeCleanerPass
|
||||
{
|
||||
const EXCEPTION_MESSAGE = 'Cannot use [] for reading';
|
||||
|
||||
private array $theseOnesAreFine = [];
|
||||
|
||||
/**
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->theseOnesAreFine = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException if the user used empty array dim fetch outside of assignment
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Assign && $node->var instanceof ArrayDimFetch) {
|
||||
$this->theseOnesAreFine[] = $node->var;
|
||||
} elseif ($node instanceof AssignRef && $node->expr instanceof ArrayDimFetch) {
|
||||
$this->theseOnesAreFine[] = $node->expr;
|
||||
} elseif ($node instanceof Foreach_ && $node->valueVar instanceof ArrayDimFetch) {
|
||||
$this->theseOnesAreFine[] = $node->valueVar;
|
||||
} elseif ($node instanceof ArrayDimFetch && $node->var instanceof ArrayDimFetch) {
|
||||
// $a[]['b'] = 'c'
|
||||
if (\in_array($node, $this->theseOnesAreFine)) {
|
||||
$this->theseOnesAreFine[] = $node->var;
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof ArrayDimFetch && $node->dim === null) {
|
||||
if (!\in_array($node, $this->theseOnesAreFine)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
40
vendor/psy/psysh/src/CodeCleaner/ExitPass.php
vendored
Normal file
40
vendor/psy/psysh/src/CodeCleaner/ExitPass.php
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Arg;
|
||||
use PhpParser\Node\Expr\Exit_;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
use Psy\Exception\BreakException;
|
||||
|
||||
class ExitPass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* Converts exit calls to BreakExceptions.
|
||||
*
|
||||
* @param \PhpParser\Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Exit_) {
|
||||
$args = $node->expr ? [new Arg($node->expr)] : [];
|
||||
|
||||
return new StaticCall(new FullyQualifiedName(BreakException::class), 'exitShell', $args);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
76
vendor/psy/psysh/src/CodeCleaner/FinalClassPass.php
vendored
Normal file
76
vendor/psy/psysh/src/CodeCleaner/FinalClassPass.php
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* The final class pass handles final classes.
|
||||
*/
|
||||
class FinalClassPass extends CodeCleanerPass
|
||||
{
|
||||
private array $finalClasses = [];
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->finalClasses = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException if the node is a class that extends a final class
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Class_) {
|
||||
if ($node->extends) {
|
||||
$extends = (string) $node->extends;
|
||||
if ($this->isFinalClass($extends)) {
|
||||
$msg = \sprintf('Class %s may not inherit from final class (%s)', $node->name, $extends);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
if ($node->isFinal()) {
|
||||
$this->finalClasses[\strtolower($node->name)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Class name
|
||||
*/
|
||||
private function isFinalClass(string $name): bool
|
||||
{
|
||||
if (!\class_exists($name)) {
|
||||
return isset($this->finalClasses[\strtolower($name)]);
|
||||
}
|
||||
|
||||
$refl = new \ReflectionClass($name);
|
||||
|
||||
return $refl->isFinal();
|
||||
}
|
||||
}
|
||||
73
vendor/psy/psysh/src/CodeCleaner/FunctionContextPass.php
vendored
Normal file
73
vendor/psy/psysh/src/CodeCleaner/FunctionContextPass.php
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Yield_;
|
||||
use PhpParser\Node\FunctionLike;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
class FunctionContextPass extends CodeCleanerPass
|
||||
{
|
||||
private int $functionDepth = 0;
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->functionDepth = 0;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof FunctionLike) {
|
||||
$this->functionDepth++;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// node is inside function context
|
||||
if ($this->functionDepth !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// It causes fatal error.
|
||||
if ($node instanceof Yield_) {
|
||||
$msg = 'The "yield" expression can only be used inside a function';
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \PhpParser\Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof FunctionLike) {
|
||||
$this->functionDepth--;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
79
vendor/psy/psysh/src/CodeCleaner/FunctionReturnInWriteContextPass.php
vendored
Normal file
79
vendor/psy/psysh/src/CodeCleaner/FunctionReturnInWriteContextPass.php
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\Isset_;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\Stmt\Unset_;
|
||||
use PhpParser\Node\VariadicPlaceholder;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that the functions are used correctly.
|
||||
*
|
||||
* @author Martin Hasoň <martin.hason@gmail.com>
|
||||
*/
|
||||
class FunctionReturnInWriteContextPass extends CodeCleanerPass
|
||||
{
|
||||
const ISSET_MESSAGE = 'Cannot use isset() on the result of an expression (you can use "null !== expression" instead)';
|
||||
const EXCEPTION_MESSAGE = "Can't use function return value in write context";
|
||||
|
||||
/**
|
||||
* Validate that the functions are used correctly.
|
||||
*
|
||||
* @throws FatalErrorException if a function is passed as an argument reference
|
||||
* @throws FatalErrorException if a function is used as an argument in the isset
|
||||
* @throws FatalErrorException if a value is assigned to a function
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Array_ || $this->isCallNode($node)) {
|
||||
$items = $node instanceof Array_ ? $node->items : $node->args;
|
||||
foreach ($items as $item) {
|
||||
if ($item instanceof VariadicPlaceholder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($item && $item->byRef && $this->isCallNode($item->value)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
} elseif ($node instanceof Isset_ || $node instanceof Unset_) {
|
||||
foreach ($node->vars as $var) {
|
||||
if (!$this->isCallNode($var)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$msg = $node instanceof Isset_ ? self::ISSET_MESSAGE : self::EXCEPTION_MESSAGE;
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
} elseif ($node instanceof Assign && $this->isCallNode($node->var)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isCallNode(Node $node): bool
|
||||
{
|
||||
return $node instanceof FuncCall || $node instanceof MethodCall || $node instanceof StaticCall;
|
||||
}
|
||||
}
|
||||
125
vendor/psy/psysh/src/CodeCleaner/ImplicitReturnPass.php
vendored
Normal file
125
vendor/psy/psysh/src/CodeCleaner/ImplicitReturnPass.php
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\Exit_;
|
||||
use PhpParser\Node\Stmt;
|
||||
use PhpParser\Node\Stmt\Break_;
|
||||
use PhpParser\Node\Stmt\Expression;
|
||||
use PhpParser\Node\Stmt\If_;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use PhpParser\Node\Stmt\Return_;
|
||||
use PhpParser\Node\Stmt\Switch_;
|
||||
|
||||
/**
|
||||
* Add an implicit "return" to the last statement, provided it can be returned.
|
||||
*/
|
||||
class ImplicitReturnPass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function beforeTraverse(array $nodes): array
|
||||
{
|
||||
return $this->addImplicitReturn($nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function addImplicitReturn(array $nodes): array
|
||||
{
|
||||
// If nodes is empty, it can't have a return value.
|
||||
if (empty($nodes)) {
|
||||
return [new Return_(NoReturnValue::create())];
|
||||
}
|
||||
|
||||
$last = \end($nodes);
|
||||
|
||||
// Special case a few types of statements to add an implicit return
|
||||
// value (even though they technically don't have any return value)
|
||||
// because showing a return value in these instances is useful and not
|
||||
// very surprising.
|
||||
if ($last instanceof If_) {
|
||||
$last->stmts = $this->addImplicitReturn($last->stmts);
|
||||
|
||||
foreach ($last->elseifs as $elseif) {
|
||||
$elseif->stmts = $this->addImplicitReturn($elseif->stmts);
|
||||
}
|
||||
|
||||
if ($last->else) {
|
||||
$last->else->stmts = $this->addImplicitReturn($last->else->stmts);
|
||||
}
|
||||
} elseif ($last instanceof Switch_) {
|
||||
foreach ($last->cases as $case) {
|
||||
// only add an implicit return to cases which end in break
|
||||
$caseLast = \end($case->stmts);
|
||||
if ($caseLast instanceof Break_) {
|
||||
$case->stmts = $this->addImplicitReturn(\array_slice($case->stmts, 0, -1));
|
||||
$case->stmts[] = $caseLast;
|
||||
}
|
||||
}
|
||||
} elseif ($last instanceof Expr && !($last instanceof Exit_)) {
|
||||
// @codeCoverageIgnoreStart
|
||||
$nodes[\count($nodes) - 1] = new Return_($last, [
|
||||
'startLine' => $last->getStartLine(),
|
||||
'endLine' => $last->getEndLine(),
|
||||
]);
|
||||
// @codeCoverageIgnoreEnd
|
||||
} elseif ($last instanceof Expression && !($last->expr instanceof Exit_)) {
|
||||
$nodes[\count($nodes) - 1] = new Return_($last->expr, [
|
||||
'startLine' => $last->getStartLine(),
|
||||
'endLine' => $last->getEndLine(),
|
||||
]);
|
||||
} elseif ($last instanceof Namespace_) {
|
||||
$last->stmts = $this->addImplicitReturn($last->stmts);
|
||||
}
|
||||
|
||||
// Return a "no return value" for all non-expression statements, so that
|
||||
// PsySH can suppress the `null` that `eval()` returns otherwise.
|
||||
//
|
||||
// Note that statements special cased above (if/elseif/else, switch)
|
||||
// _might_ implicitly return a value before this catch-all return is
|
||||
// reached.
|
||||
//
|
||||
// We're not adding a fallback return after namespace statements,
|
||||
// because code outside namespace statements doesn't really work, and
|
||||
// there's already an implicit return in the namespace statement anyway.
|
||||
if (self::isNonExpressionStmt($last)) {
|
||||
$nodes[] = new Return_(NoReturnValue::create());
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given node is a non-expression statement.
|
||||
*
|
||||
* As of PHP Parser 4.x, Expressions are now instances of Stmt as well, so
|
||||
* we'll exclude them here.
|
||||
*
|
||||
* @param Node $node
|
||||
*/
|
||||
private static function isNonExpressionStmt(Node $node): bool
|
||||
{
|
||||
return $node instanceof Stmt &&
|
||||
!$node instanceof Expression &&
|
||||
!$node instanceof Return_ &&
|
||||
!$node instanceof Namespace_;
|
||||
}
|
||||
}
|
||||
396
vendor/psy/psysh/src/CodeCleaner/ImplicitUsePass.php
vendored
Normal file
396
vendor/psy/psysh/src/CodeCleaner/ImplicitUsePass.php
vendored
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
use PhpParser\Node\Stmt\GroupUse;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use PhpParser\Node\Stmt\Use_;
|
||||
use PhpParser\Node\Stmt\UseItem;
|
||||
use PhpParser\Node\Stmt\UseUse;
|
||||
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||
use Psy\CodeCleaner;
|
||||
|
||||
/**
|
||||
* Automatically add use statements for unqualified class references.
|
||||
*
|
||||
* When a user references a class by its short name (e.g., `User`), this pass attempts to find a
|
||||
* fully-qualified class name that matches. A use statement is added if:
|
||||
*
|
||||
* - There is no unqualified name (class/function/constant) with that short name
|
||||
* - There is no existing use statement or alias with that short name
|
||||
* - There is exactly one matching class/interface/trait in the configured namespaces
|
||||
*
|
||||
* For example, in a project with `App\Model\User` and `App\View\User` classes, if configured with
|
||||
* 'includeNamespaces' => ['App\Model'], `new User` would become `use App\Model\User; new User;`
|
||||
* even though there's also an `App\View\User` class.
|
||||
*
|
||||
* Works great with autoload warming (--warm-autoload) to pre-load classes.
|
||||
*/
|
||||
class ImplicitUsePass extends CodeCleanerPass
|
||||
{
|
||||
private ?array $shortNameMap = null;
|
||||
private array $implicitUses = [];
|
||||
private array $seenNames = [];
|
||||
private array $existingAliases = [];
|
||||
private array $includeNamespaces = [];
|
||||
private array $excludeNamespaces = [];
|
||||
private ?string $currentNamespace = null;
|
||||
private ?CodeCleaner $cleaner = null;
|
||||
private ?PrettyPrinter $printer = null;
|
||||
|
||||
/**
|
||||
* @param array $config Configuration array with 'includeNamespaces' and/or 'excludeNamespaces'
|
||||
* @param CodeCleaner|null $cleaner CodeCleaner instance for logging
|
||||
*/
|
||||
public function __construct(array $config = [], ?CodeCleaner $cleaner = null)
|
||||
{
|
||||
$this->includeNamespaces = $this->normalizeNamespaces($config['includeNamespaces'] ?? []);
|
||||
$this->excludeNamespaces = $this->normalizeNamespaces($config['excludeNamespaces'] ?? []);
|
||||
$this->cleaner = $cleaner;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
if (empty($this->includeNamespaces) && empty($this->excludeNamespaces)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->buildShortNameMap();
|
||||
|
||||
// Reset state for this traversal
|
||||
$this->implicitUses = [];
|
||||
$this->seenNames = [];
|
||||
$this->existingAliases = [];
|
||||
$this->currentNamespace = null;
|
||||
|
||||
$modified = false;
|
||||
|
||||
// Collect use statements and seen names for each namespace
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Namespace_) {
|
||||
$this->currentNamespace = $node->name ? $node->name->toString() : null;
|
||||
|
||||
$perNamespaceAliases = [];
|
||||
$perNamespaceUses = [];
|
||||
$perNamespaceSeen = [];
|
||||
|
||||
if ($node->stmts !== null) {
|
||||
$this->collectAliasesInNodes($node->stmts, $perNamespaceAliases);
|
||||
$this->collectNamesInNodes($node->stmts, $perNamespaceSeen, $perNamespaceAliases, $perNamespaceUses);
|
||||
}
|
||||
|
||||
if (!empty($perNamespaceUses)) {
|
||||
$this->logAddedUses($perNamespaceUses);
|
||||
$node->stmts = \array_merge($this->createUseStatements($perNamespaceUses), $node->stmts ?? []);
|
||||
$modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$hasNamespace = false;
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Namespace_) {
|
||||
$hasNamespace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect use statements and seen names for top-level namespace
|
||||
if (!$hasNamespace) {
|
||||
$this->currentNamespace = null;
|
||||
$topLevelAliases = [];
|
||||
$topLevelUses = [];
|
||||
$topLevelSeen = [];
|
||||
|
||||
$this->collectAliasesInNodes($nodes, $topLevelAliases);
|
||||
$this->collectNamesInNodes($nodes, $topLevelSeen, $topLevelAliases, $topLevelUses);
|
||||
|
||||
if (!empty($topLevelUses)) {
|
||||
$this->logAddedUses($topLevelUses);
|
||||
|
||||
return \array_merge($this->createUseStatements($topLevelUses), $nodes);
|
||||
}
|
||||
}
|
||||
|
||||
return $modified ? $nodes : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect aliases in a set of nodes.
|
||||
*
|
||||
* @param array $nodes Array of Node objects
|
||||
* @param array $aliases Associative array mapping lowercase alias names to true
|
||||
*/
|
||||
private function collectAliasesInNodes(array $nodes, array &$aliases): void
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Use_ || $node instanceof GroupUse) {
|
||||
foreach ($node->uses as $useItem) {
|
||||
$alias = $useItem->getAlias();
|
||||
if ($alias !== null) {
|
||||
$aliasStr = $alias instanceof Name ? $alias->toString() : (string) $alias;
|
||||
$aliases[\strtolower($aliasStr)] = true;
|
||||
} else {
|
||||
$aliases[\strtolower($this->getShortName($useItem->name))] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect unqualified names in nodes.
|
||||
*
|
||||
* @param array $nodes Array of Node objects to traverse
|
||||
* @param array $seen Lowercase short names already processed
|
||||
* @param array $aliases Lowercase alias names that exist in this namespace
|
||||
* @param array $uses Map of short names to FQNs for implicit use statements
|
||||
*/
|
||||
private function collectNamesInNodes(array $nodes, array &$seen, array $aliases, array &$uses): void
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
if (!$node instanceof Node || $node instanceof Use_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node instanceof Name && !$node instanceof FullyQualifiedName) {
|
||||
if (!$this->isQualified($node)) {
|
||||
$shortName = $this->getShortName($node);
|
||||
$shortNameLower = \strtolower($shortName);
|
||||
|
||||
if (isset($seen[$shortNameLower])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$shortNameLower] = true;
|
||||
|
||||
if ($this->shouldAddImplicitUseInContext($shortName, $shortNameLower, $aliases)) {
|
||||
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable - shortNameMap is initialized in beforeTraverse
|
||||
$uses[$shortName] = $this->shortNameMap[$shortNameLower];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($node->getSubNodeNames() as $subNodeName) {
|
||||
$subNode = $node->$subNodeName;
|
||||
if ($subNode instanceof Node) {
|
||||
$subNode = [$subNode];
|
||||
}
|
||||
|
||||
if (\is_array($subNode)) {
|
||||
$this->collectNamesInNodes($subNode, $seen, $aliases, $uses);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Use_ statement nodes from uses array.
|
||||
*
|
||||
* @param array $uses Associative array mapping short names to FQNs
|
||||
*
|
||||
* @return Use_[]
|
||||
*/
|
||||
private function createUseStatements(array $uses): array
|
||||
{
|
||||
\asort($uses);
|
||||
|
||||
$useStatements = [];
|
||||
foreach ($uses as $fqn) {
|
||||
$useItem = \class_exists(UseItem::class) ? new UseItem(new Name($fqn)) : new UseUse(new Name($fqn));
|
||||
$useStatements[] = new Use_([$useItem]);
|
||||
}
|
||||
|
||||
return $useStatements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should add an implicit use statement for this name in current context.
|
||||
*
|
||||
* @param string $shortName Original case short name
|
||||
* @param string $shortNameLower Lowercase short name for comparison
|
||||
* @param array $aliases Lowercase alias names that exist in this namespace
|
||||
*/
|
||||
private function shouldAddImplicitUseInContext(string $shortName, string $shortNameLower, array $aliases): bool
|
||||
{
|
||||
// Rule 1: No existing unqualified name (class/interface/trait) with that short name
|
||||
if (\class_exists($shortName, false) || \interface_exists($shortName, false) || \trait_exists($shortName, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rule 2: No existing use statement or alias with that short name
|
||||
if (isset($aliases[$shortNameLower])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rule 3: Exactly one matching short class/interface/trait in configured namespaces
|
||||
if (!isset($this->shortNameMap[$shortNameLower]) || $this->shortNameMap[$shortNameLower] === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rule 4: Don't add use statement if the class exists in the current namespace
|
||||
if ($this->currentNamespace !== null) {
|
||||
$expectedFqn = \trim($this->currentNamespace, '\\').'\\'.$shortName;
|
||||
|
||||
if (\class_exists($expectedFqn, false) || \interface_exists($expectedFqn, false) || \trait_exists($expectedFqn, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map of short class names to fully-qualified names.
|
||||
*
|
||||
* Uses get_declared_classes(), get_declared_interfaces(), and get_declared_traits()
|
||||
* to find all currently loaded classes. Only includes classes matching the configured
|
||||
* namespace filters. Detects ambiguous short names (multiple FQNs with same short name
|
||||
* within the filtered namespaces) and marks them as null.
|
||||
*/
|
||||
private function buildShortNameMap(): void
|
||||
{
|
||||
$this->shortNameMap = [];
|
||||
|
||||
$allClasses = \array_merge(
|
||||
\get_declared_classes(),
|
||||
\get_declared_interfaces(),
|
||||
\get_declared_traits()
|
||||
);
|
||||
|
||||
// First pass: collect all matching classes
|
||||
$candidatesByShortName = [];
|
||||
foreach ($allClasses as $fqn) {
|
||||
if (!$this->shouldIncludeClass($fqn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = \explode('\\', $fqn);
|
||||
$shortName = \strtolower(\end($parts));
|
||||
|
||||
if (!isset($candidatesByShortName[$shortName])) {
|
||||
$candidatesByShortName[$shortName] = [];
|
||||
}
|
||||
$candidatesByShortName[$shortName][] = $fqn;
|
||||
}
|
||||
|
||||
// Second pass: determine if each short name is unique or ambiguous
|
||||
foreach ($candidatesByShortName as $shortName => $fqns) {
|
||||
$uniqueFqns = \array_unique($fqns);
|
||||
// Mark as null if ambiguous (multiple FQNs with same short name)
|
||||
$this->shortNameMap[$shortName] = (\count($uniqueFqns) === 1) ? $uniqueFqns[0] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class should be aliased based on namespace filters.
|
||||
*
|
||||
* @param string $fqn Fully-qualified class name
|
||||
*/
|
||||
private function shouldIncludeClass(string $fqn): bool
|
||||
{
|
||||
if (\strpos($fqn, '\\') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($this->includeNamespaces) && empty($this->excludeNamespaces)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->excludeNamespaces as $namespace) {
|
||||
if (\stripos($fqn, $namespace) === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->includeNamespaces)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->includeNamespaces as $namespace) {
|
||||
if (\stripos($fqn, $namespace) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize namespace prefixes.
|
||||
*
|
||||
* Removes leading backslash and ensures trailing backslash.
|
||||
*
|
||||
* @param string[] $namespaces
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function normalizeNamespaces(array $namespaces): array
|
||||
{
|
||||
return \array_map(function ($namespace) {
|
||||
return \trim($namespace, '\\').'\\';
|
||||
}, $namespaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get short name from a Name node.
|
||||
*/
|
||||
private function getShortName(Name $name): string
|
||||
{
|
||||
$parts = $this->getParts($name);
|
||||
|
||||
return \end($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a name is qualified (contains namespace separator).
|
||||
*/
|
||||
private function isQualified(Name $name): bool
|
||||
{
|
||||
return \count($this->getParts($name)) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatibility shim for PHP-Parser 4.x.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function getParts(Name $name): array
|
||||
{
|
||||
return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log added use statements to the CodeCleaner.
|
||||
*
|
||||
* @param array $uses Associative array mapping short names to FQNs
|
||||
*/
|
||||
private function logAddedUses(array $uses): void
|
||||
{
|
||||
if ($this->cleaner === null || empty($uses)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->printer === null) {
|
||||
$this->printer = new PrettyPrinter();
|
||||
}
|
||||
|
||||
$useStmts = $this->createUseStatements($uses);
|
||||
$this->cleaner->log($this->printer->prettyPrint($useStmts));
|
||||
}
|
||||
}
|
||||
51
vendor/psy/psysh/src/CodeCleaner/IssetPass.php
vendored
Normal file
51
vendor/psy/psysh/src/CodeCleaner/IssetPass.php
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||
use PhpParser\Node\Expr\Isset_;
|
||||
use PhpParser\Node\Expr\NullsafePropertyFetch;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Code cleaner pass to ensure we only allow variables, array fetch and property
|
||||
* fetch expressions in isset() calls.
|
||||
*/
|
||||
class IssetPass extends CodeCleanerPass
|
||||
{
|
||||
const EXCEPTION_MSG = 'Cannot use isset() on the result of an expression (you can use "null !== expression" instead)';
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if (!$node instanceof Isset_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($node->vars as $var) {
|
||||
if (!$var instanceof Variable && !$var instanceof ArrayDimFetch && !$var instanceof PropertyFetch && !$var instanceof NullsafePropertyFetch) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MSG, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
105
vendor/psy/psysh/src/CodeCleaner/LabelContextPass.php
vendored
Normal file
105
vendor/psy/psysh/src/CodeCleaner/LabelContextPass.php
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\FunctionLike;
|
||||
use PhpParser\Node\Stmt\Goto_;
|
||||
use PhpParser\Node\Stmt\Label;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* CodeCleanerPass for label context.
|
||||
*
|
||||
* This class partially emulates the PHP label specification.
|
||||
* PsySH can not declare labels by sequentially executing lines with eval,
|
||||
* but since it is not a syntax error, no error is raised.
|
||||
* This class warns before invalid goto causes a fatal error.
|
||||
* Since this is a simple checker, it does not block real fatal error
|
||||
* with complex syntax. (ex. it does not parse inside function.)
|
||||
*
|
||||
* @see http://php.net/goto
|
||||
*/
|
||||
class LabelContextPass extends CodeCleanerPass
|
||||
{
|
||||
private int $functionDepth = 0;
|
||||
private array $labelDeclarations = [];
|
||||
private array $labelGotos = [];
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->functionDepth = 0;
|
||||
$this->labelDeclarations = [];
|
||||
$this->labelGotos = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof FunctionLike) {
|
||||
$this->functionDepth++;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// node is inside function context
|
||||
if ($this->functionDepth !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($node instanceof Goto_) {
|
||||
$this->labelGotos[\strtolower($node->name)] = $node->getStartLine();
|
||||
} elseif ($node instanceof Label) {
|
||||
$this->labelDeclarations[\strtolower($node->name)] = $node->getStartLine();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \PhpParser\Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof FunctionLike) {
|
||||
$this->functionDepth--;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function afterTraverse(array $nodes)
|
||||
{
|
||||
foreach ($this->labelGotos as $name => $line) {
|
||||
if (!isset($this->labelDeclarations[$name])) {
|
||||
$msg = "'goto' to undefined label '{$name}'";
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $line);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
40
vendor/psy/psysh/src/CodeCleaner/LeavePsyshAlonePass.php
vendored
Normal file
40
vendor/psy/psysh/src/CodeCleaner/LeavePsyshAlonePass.php
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use Psy\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Validate that the user input does not reference the `$__psysh__` variable.
|
||||
*/
|
||||
class LeavePsyshAlonePass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* Validate that the user input does not reference the `$__psysh__` variable.
|
||||
*
|
||||
* @throws RuntimeException if the user is messing with $__psysh__
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Variable && $node->name === '__psysh__') {
|
||||
throw new RuntimeException('Don\'t mess with $__psysh__; bad things will happen');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
97
vendor/psy/psysh/src/CodeCleaner/ListPass.php
vendored
Normal file
97
vendor/psy/psysh/src/CodeCleaner/ListPass.php
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\ArrayItem;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||
// @todo Drop PhpParser\Node\Expr\ArrayItem once we drop support for PHP-Parser 4.x
|
||||
use PhpParser\Node\Expr\ArrayItem as LegacyArrayItem;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\List_;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use Psy\Exception\ParseErrorException;
|
||||
|
||||
/**
|
||||
* Validate that the list assignment.
|
||||
*/
|
||||
class ListPass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* Validate use of list assignment.
|
||||
*
|
||||
* @throws ParseErrorException if the user used empty with anything but a variable
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if (!$node instanceof Assign) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$node->var instanceof Array_ && !$node->var instanceof List_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Polyfill for PHP-Parser 2.x
|
||||
$items = isset($node->var->items) ? $node->var->items : (\property_exists($node->var, 'vars') ? $node->var->vars : []);
|
||||
|
||||
if ($items === [] || $items === [null]) {
|
||||
throw new ParseErrorException('Cannot use empty list', ['startLine' => $node->var->getStartLine(), 'endLine' => $node->var->getEndLine()]);
|
||||
}
|
||||
|
||||
$itemFound = false;
|
||||
foreach ($items as $item) {
|
||||
if ($item === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$itemFound = true;
|
||||
|
||||
if (!self::isValidArrayItem($item)) {
|
||||
$msg = 'Assignments can only happen to writable values';
|
||||
throw new ParseErrorException($msg, ['startLine' => $item->getStartLine(), 'endLine' => $item->getEndLine()]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$itemFound) {
|
||||
throw new ParseErrorException('Cannot use empty list');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether a given item in an array is valid for short assignment.
|
||||
*
|
||||
* @param Node $item
|
||||
*/
|
||||
private static function isValidArrayItem(Node $item): bool
|
||||
{
|
||||
$value = ($item instanceof ArrayItem || $item instanceof LegacyArrayItem) ? $item->value : $item;
|
||||
|
||||
while ($value instanceof ArrayDimFetch || $value instanceof PropertyFetch) {
|
||||
$value = $value->var;
|
||||
}
|
||||
|
||||
// We just kind of give up if it's a method call. We can't tell if it's
|
||||
// valid via static analysis.
|
||||
return $value instanceof Variable || $value instanceof MethodCall || $value instanceof FuncCall;
|
||||
}
|
||||
}
|
||||
123
vendor/psy/psysh/src/CodeCleaner/LoopContextPass.php
vendored
Normal file
123
vendor/psy/psysh/src/CodeCleaner/LoopContextPass.php
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Scalar\DNumber;
|
||||
use PhpParser\Node\Scalar\Float_;
|
||||
use PhpParser\Node\Scalar\Int_;
|
||||
use PhpParser\Node\Scalar\LNumber;
|
||||
use PhpParser\Node\Stmt\Break_;
|
||||
use PhpParser\Node\Stmt\Continue_;
|
||||
use PhpParser\Node\Stmt\Do_;
|
||||
use PhpParser\Node\Stmt\For_;
|
||||
use PhpParser\Node\Stmt\Foreach_;
|
||||
use PhpParser\Node\Stmt\Switch_;
|
||||
use PhpParser\Node\Stmt\While_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* The loop context pass handles invalid `break` and `continue` statements.
|
||||
*/
|
||||
class LoopContextPass extends CodeCleanerPass
|
||||
{
|
||||
private int $loopDepth = 0;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->loopDepth = 0;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException if the node is a break or continue in a non-loop or switch context
|
||||
* @throws FatalErrorException if the node is trying to break out of more nested structures than exist
|
||||
* @throws FatalErrorException if the node is a break or continue and has a non-numeric argument
|
||||
* @throws FatalErrorException if the node is a break or continue and has an argument less than 1
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
switch (true) {
|
||||
case $node instanceof Do_:
|
||||
case $node instanceof For_:
|
||||
case $node instanceof Foreach_:
|
||||
case $node instanceof Switch_:
|
||||
case $node instanceof While_:
|
||||
$this->loopDepth++;
|
||||
break;
|
||||
|
||||
case $node instanceof Break_:
|
||||
case $node instanceof Continue_:
|
||||
$operator = $node instanceof Break_ ? 'break' : 'continue';
|
||||
|
||||
if ($this->loopDepth === 0) {
|
||||
$msg = \sprintf("'%s' not in the 'loop' or 'switch' context", $operator);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
// @todo Remove LNumber and DNumber once we drop support for PHP-Parser 4.x
|
||||
if (
|
||||
$node->num instanceof LNumber ||
|
||||
$node->num instanceof DNumber ||
|
||||
$node->num instanceof Int_ ||
|
||||
$node->num instanceof Float_
|
||||
) {
|
||||
$num = $node->num->value;
|
||||
if ($node->num instanceof DNumber || $num < 1) {
|
||||
$msg = \sprintf("'%s' operator accepts only positive numbers", $operator);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
if ($num > $this->loopDepth) {
|
||||
$msg = \sprintf("Cannot '%s' %d levels", $operator, $num);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
} elseif ($node->num) {
|
||||
$msg = \sprintf("'%s' operator with non-constant operand is no longer supported", $operator);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
switch (true) {
|
||||
case $node instanceof Do_:
|
||||
case $node instanceof For_:
|
||||
case $node instanceof Foreach_:
|
||||
case $node instanceof Switch_:
|
||||
case $node instanceof While_:
|
||||
$this->loopDepth--;
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
44
vendor/psy/psysh/src/CodeCleaner/MagicConstantsPass.php
vendored
Normal file
44
vendor/psy/psysh/src/CodeCleaner/MagicConstantsPass.php
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Scalar\MagicConst\Dir;
|
||||
use PhpParser\Node\Scalar\MagicConst\File;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
|
||||
/**
|
||||
* Swap out __DIR__ and __FILE__ magic constants with our best guess?
|
||||
*/
|
||||
class MagicConstantsPass extends CodeCleanerPass
|
||||
{
|
||||
/**
|
||||
* Swap out __DIR__ and __FILE__ constants, because the default ones when
|
||||
* calling eval() don't make sense.
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return FuncCall|String_|null
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Dir) {
|
||||
return new FuncCall(new Name('getcwd'), [], $node->getAttributes());
|
||||
} elseif ($node instanceof File) {
|
||||
return new String_('', $node->getAttributes());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
169
vendor/psy/psysh/src/CodeCleaner/NamespaceAwarePass.php
vendored
Normal file
169
vendor/psy/psysh/src/CodeCleaner/NamespaceAwarePass.php
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
use PhpParser\Node\Stmt\GroupUse;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use PhpParser\Node\Stmt\Use_;
|
||||
use Psy\CodeCleaner;
|
||||
|
||||
/**
|
||||
* Abstract namespace-aware code cleaner pass.
|
||||
*
|
||||
* Tracks both namespace and use statement aliases for proper name resolution.
|
||||
*/
|
||||
abstract class NamespaceAwarePass extends CodeCleanerPass
|
||||
{
|
||||
protected array $namespace = [];
|
||||
protected array $currentScope = [];
|
||||
protected array $aliases = [];
|
||||
protected ?CodeCleaner $cleaner = null;
|
||||
|
||||
/**
|
||||
* Set the CodeCleaner instance for state management.
|
||||
*/
|
||||
public function setCleaner(CodeCleaner $cleaner)
|
||||
{
|
||||
$this->cleaner = $cleaner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo should this be final? Extending classes should be sure to either
|
||||
* use afterTraverse or call parent::beforeTraverse() when overloading.
|
||||
*
|
||||
* Reset the namespace and the current scope before beginning analysis
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->namespace = [];
|
||||
$this->currentScope = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo should this be final? Extending classes should be sure to either use
|
||||
* leaveNode or call parent::enterNode() when overloading
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Namespace_) {
|
||||
$this->namespace = isset($node->name) ? $this->getParts($node->name) : [];
|
||||
|
||||
// Only restore use statement aliases for PsySH re-injected namespaces.
|
||||
// Explicit namespace declarations start with a clean slate.
|
||||
if ($this->cleaner && $node->getAttribute('psyshReinjected')) {
|
||||
$this->aliases = $this->cleaner->getAliasesForNamespace($node->name);
|
||||
} else {
|
||||
$this->aliases = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Track use statements for alias resolution
|
||||
if ($node instanceof Use_) {
|
||||
foreach ($node->uses as $useItem) {
|
||||
$this->aliases[\strtolower($useItem->getAlias())] = $useItem->name;
|
||||
}
|
||||
}
|
||||
|
||||
// Track group use statements
|
||||
if ($node instanceof GroupUse) {
|
||||
foreach ($node->uses as $useItem) {
|
||||
$this->aliases[\strtolower($useItem->getAlias())] = Name::concat($node->prefix, $useItem->name);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save alias state when leaving a namespace.
|
||||
*
|
||||
* Braced namespaces (like `namespace { ... }`) are self-contained and don't persist their use
|
||||
* statements between executions.
|
||||
*
|
||||
* Only save aliases for open namespaces (like `namespace Foo;`), or implicit namespace wrappers
|
||||
* re-injected by PsySH (psyshReinjected).
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Namespace_) {
|
||||
// Open namespaces (like `namespace Foo;`) have kind == KIND_SEMICOLON.
|
||||
if ($node->getAttribute('kind') === Namespace_::KIND_SEMICOLON || $node->getAttribute('psyshReinjected')) {
|
||||
if ($this->cleaner) {
|
||||
$this->cleaner->setAliasesForNamespace($node->name, $this->aliases);
|
||||
}
|
||||
}
|
||||
|
||||
$this->aliases = [];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fully-qualified name (class, function, interface, etc).
|
||||
*
|
||||
* Resolves use statement aliases before applying namespace.
|
||||
*
|
||||
* @param mixed $name
|
||||
*/
|
||||
protected function getFullyQualifiedName($name): string
|
||||
{
|
||||
if ($name instanceof FullyQualifiedName) {
|
||||
return \implode('\\', $this->getParts($name));
|
||||
}
|
||||
|
||||
// Check if this name matches a use statement alias
|
||||
if ($name instanceof Name) {
|
||||
$nameParts = $this->getParts($name);
|
||||
$firstPart = \strtolower($nameParts[0]);
|
||||
|
||||
if (isset($this->aliases[$firstPart])) {
|
||||
// Replace first part with the aliased namespace
|
||||
$aliasedParts = $this->getParts($this->aliases[$firstPart]);
|
||||
\array_shift($nameParts); // Remove first part
|
||||
|
||||
return \implode('\\', \array_merge($aliasedParts, $nameParts));
|
||||
}
|
||||
}
|
||||
|
||||
if ($name instanceof Name) {
|
||||
$name = $this->getParts($name);
|
||||
} elseif (!\is_array($name)) {
|
||||
$name = [$name];
|
||||
}
|
||||
|
||||
return \implode('\\', \array_merge($this->namespace, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatibility shim for PHP-Parser 4.x.
|
||||
*
|
||||
* At some point we might want to make $namespace a plain string, to match how Name works?
|
||||
*/
|
||||
protected function getParts(Name $name): array
|
||||
{
|
||||
return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts;
|
||||
}
|
||||
}
|
||||
119
vendor/psy/psysh/src/CodeCleaner/NamespacePass.php
vendored
Normal file
119
vendor/psy/psysh/src/CodeCleaner/NamespacePass.php
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use Psy\CodeCleaner;
|
||||
|
||||
/**
|
||||
* Provide implicit namespaces for subsequent execution.
|
||||
*
|
||||
* The namespace pass remembers the last standalone namespace line encountered:
|
||||
*
|
||||
* namespace Foo\Bar;
|
||||
*
|
||||
* ... which it then applies implicitly to all future evaluated code, until the
|
||||
* namespace is replaced by another namespace. To reset to the top level
|
||||
* namespace, enter `namespace {}`. This is a bit ugly, but it does the trick :)
|
||||
*/
|
||||
class NamespacePass extends NamespaceAwarePass
|
||||
{
|
||||
/**
|
||||
* @param ?CodeCleaner $cleaner deprecated parameter, use setCleaner() instead
|
||||
*
|
||||
* @phpstan-ignore-next-line method.unused
|
||||
*/
|
||||
public function __construct(?CodeCleaner $cleaner = null)
|
||||
{
|
||||
// No-op, since cleaner is provided by NamespaceAwarePass
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is a standalone namespace line, remember it for later.
|
||||
*
|
||||
* Otherwise, apply remembered namespaces to the code until a new namespace
|
||||
* is encountered.
|
||||
*
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
if (empty($nodes)) {
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
$last = \end($nodes);
|
||||
|
||||
if ($last instanceof Namespace_) {
|
||||
$kind = $last->getAttribute('kind');
|
||||
|
||||
if ($kind === Namespace_::KIND_SEMICOLON) {
|
||||
// Save the current namespace for open namespaces
|
||||
$this->setNamespace($last->name);
|
||||
} else {
|
||||
// Clear the current namespace after a braced namespace
|
||||
$this->setNamespace(null);
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
// Wrap in current namespace if one is set
|
||||
$currentNamespace = $this->getCurrentNamespace();
|
||||
|
||||
if (!$currentNamespace) {
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
// Mark as re-injected so UseStatementPass knows it can re-inject use statements
|
||||
return [new Namespace_($currentNamespace, $nodes, ['psyshReinjected' => true])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current namespace as a Name node.
|
||||
*
|
||||
* This is more complicated than it needs to be, because we're not storing namespace as a Name.
|
||||
*
|
||||
* @return Name|null
|
||||
*/
|
||||
private function getCurrentNamespace(): ?Name
|
||||
{
|
||||
$namespace = $this->cleaner->getNamespace();
|
||||
|
||||
return $namespace ? new Name($namespace) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the namespace in CodeCleaner and clear aliases.
|
||||
*
|
||||
* @param Name|null $namespace
|
||||
*/
|
||||
private function setNamespace(?Name $namespace)
|
||||
{
|
||||
$this->cleaner->setNamespace($namespace);
|
||||
|
||||
// Always clear aliases when changing namespace
|
||||
$this->cleaner->setAliasesForNamespace($namespace, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated unused and will be removed in a future version
|
||||
*/
|
||||
protected function getParts(Name $name): array
|
||||
{
|
||||
return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts;
|
||||
}
|
||||
}
|
||||
33
vendor/psy/psysh/src/CodeCleaner/NoReturnValue.php
vendored
Normal file
33
vendor/psy/psysh/src/CodeCleaner/NoReturnValue.php
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node\Expr\New_;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
|
||||
/**
|
||||
* A class used internally by CodeCleaner to represent input, such as
|
||||
* non-expression statements, with no return value.
|
||||
*
|
||||
* Note that user code returning an instance of this class will act like it
|
||||
* has no return value, so you prolly shouldn't do that.
|
||||
*/
|
||||
class NoReturnValue
|
||||
{
|
||||
/**
|
||||
* Get PhpParser AST expression for creating a new NoReturnValue.
|
||||
*/
|
||||
public static function create(): New_
|
||||
{
|
||||
return new New_(new FullyQualifiedName(self::class));
|
||||
}
|
||||
}
|
||||
137
vendor/psy/psysh/src/CodeCleaner/PassableByReferencePass.php
vendored
Normal file
137
vendor/psy/psysh/src/CodeCleaner/PassableByReferencePass.php
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||
use PhpParser\Node\Expr\ClassConstFetch;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use PhpParser\Node\VariadicPlaceholder;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that only variables (and variable-like things) are passed by reference.
|
||||
*/
|
||||
class PassableByReferencePass extends CodeCleanerPass
|
||||
{
|
||||
const EXCEPTION_MESSAGE = 'Only variables can be passed by reference';
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException if non-variables are passed by reference
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
// @todo support MethodCall and StaticCall as well.
|
||||
if ($node instanceof FuncCall) {
|
||||
// if function name is an expression or a variable, give it a pass for now.
|
||||
if ($node->name instanceof Expr || $node->name instanceof Variable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = (string) $node->name;
|
||||
|
||||
if ($name === 'array_multisort') {
|
||||
return $this->validateArrayMultisort($node);
|
||||
}
|
||||
|
||||
try {
|
||||
$refl = new \ReflectionFunction($name);
|
||||
} catch (\ReflectionException $e) {
|
||||
// Well, we gave it a shot!
|
||||
return null;
|
||||
}
|
||||
|
||||
$args = [];
|
||||
foreach ($node->args as $position => $arg) {
|
||||
if ($arg instanceof VariadicPlaceholder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Named arguments were added in php-parser 4.1, so we need to check if the property exists
|
||||
$key = (\property_exists($arg, 'name') && $arg->name !== null) ? $arg->name->name : $position;
|
||||
$args[$key] = $arg;
|
||||
}
|
||||
|
||||
foreach ($refl->getParameters() as $key => $param) {
|
||||
if (\array_key_exists($key, $args) || \array_key_exists($param->name, $args)) {
|
||||
$arg = $args[$param->name] ?? $args[$key];
|
||||
if ($param->isPassedByReference() && !$this->isPassableByReference($arg)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isPassableByReference(Node $arg): bool
|
||||
{
|
||||
if (!\property_exists($arg, 'value')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unpacked arrays can be passed by reference
|
||||
if ($arg->value instanceof Array_) {
|
||||
return \property_exists($arg, 'unpack') && $arg->unpack;
|
||||
}
|
||||
|
||||
// FuncCall, MethodCall and StaticCall are all PHP _warnings_ not fatal errors, so we'll let
|
||||
// PHP handle those ones :)
|
||||
return $arg->value instanceof ClassConstFetch ||
|
||||
$arg->value instanceof PropertyFetch ||
|
||||
$arg->value instanceof Variable ||
|
||||
$arg->value instanceof FuncCall ||
|
||||
$arg->value instanceof MethodCall ||
|
||||
$arg->value instanceof StaticCall ||
|
||||
$arg->value instanceof ArrayDimFetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Because array_multisort has a problematic signature...
|
||||
*
|
||||
* The argument order is all sorts of wonky, and whether something is passed
|
||||
* by reference or not depends on the values of the two arguments before it.
|
||||
* We'll do a good faith attempt at validating this, but err on the side of
|
||||
* permissive.
|
||||
*
|
||||
* This is why you don't design languages where core code and extensions can
|
||||
* implement APIs that wouldn't be possible in userland code.
|
||||
*
|
||||
* @throws FatalErrorException for clearly invalid arguments
|
||||
*
|
||||
* @param Node $node
|
||||
*/
|
||||
private function validateArrayMultisort(Node $node)
|
||||
{
|
||||
$nonPassable = 2; // start with 2 because the first one has to be passable by reference
|
||||
foreach ($node->args as $arg) {
|
||||
if ($this->isPassableByReference($arg)) {
|
||||
$nonPassable = 0;
|
||||
} elseif (++$nonPassable > 2) {
|
||||
// There can be *at most* two non-passable-by-reference args in a row. This is about
|
||||
// as close as we can get to validating the arguments for this function :-/
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
vendor/psy/psysh/src/CodeCleaner/RequirePass.php
vendored
Normal file
138
vendor/psy/psysh/src/CodeCleaner/RequirePass.php
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Arg;
|
||||
use PhpParser\Node\Expr\Include_;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
||||
use PhpParser\Node\Scalar\LNumber;
|
||||
use Psy\Exception\ErrorException;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Add runtime validation for `require` and `require_once` calls.
|
||||
*/
|
||||
class RequirePass extends CodeCleanerPass
|
||||
{
|
||||
private const REQUIRE_TYPES = [Include_::TYPE_REQUIRE, Include_::TYPE_REQUIRE_ONCE];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $origNode)
|
||||
{
|
||||
if (!$this->isRequireNode($origNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$node = clone $origNode;
|
||||
|
||||
/*
|
||||
* rewrite
|
||||
*
|
||||
* $foo = require $bar
|
||||
*
|
||||
* to
|
||||
*
|
||||
* $foo = require \Psy\CodeCleaner\RequirePass::resolve($bar)
|
||||
*/
|
||||
// PHP-Parser 4.x uses LNumber, 5.x has LNumber as an alias to Int_
|
||||
// Just use LNumber for compatibility with both versions
|
||||
// @todo Switch to Int_ once we drop support for PHP-Parser 4.x
|
||||
$arg = new LNumber($origNode->getStartLine());
|
||||
|
||||
$node->expr = new StaticCall(
|
||||
new FullyQualifiedName(self::class),
|
||||
'resolve',
|
||||
[new Arg($origNode->expr), new Arg($arg)],
|
||||
$origNode->getAttributes()
|
||||
);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime validation that $file can be resolved as an include path.
|
||||
*
|
||||
* If $file can be resolved, return $file. Otherwise throw a fatal error exception.
|
||||
*
|
||||
* If $file collides with a path in the currently running PsySH phar, it will be resolved
|
||||
* relative to the include path, to prevent PHP from grabbing the phar version of the file.
|
||||
*
|
||||
* @throws FatalErrorException when unable to resolve include path for $file
|
||||
* @throws ErrorException if $file is empty and E_WARNING is included in error_reporting level
|
||||
*
|
||||
* @param string $file
|
||||
* @param int $startLine Line number of the original require expression
|
||||
*
|
||||
* @return string Exactly the same as $file, unless $file collides with a path in the currently running phar
|
||||
*/
|
||||
public static function resolve($file, $startLine = null): string
|
||||
{
|
||||
$file = (string) $file;
|
||||
|
||||
if ($file === '') {
|
||||
// @todo Shell::handleError would be better here, because we could
|
||||
// fake the file and line number, but we can't call it statically.
|
||||
// So we're duplicating some of the logics here.
|
||||
if (\E_WARNING & \error_reporting()) {
|
||||
ErrorException::throwException(\E_WARNING, 'Filename cannot be empty', null, $startLine);
|
||||
}
|
||||
// @todo trigger an error as fallback? this is pretty ugly…
|
||||
// trigger_error('Filename cannot be empty', E_USER_WARNING);
|
||||
}
|
||||
|
||||
$resolvedPath = \stream_resolve_include_path($file);
|
||||
if ($file === '' || !$resolvedPath) {
|
||||
$msg = \sprintf("Failed opening required '%s'", $file);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $startLine);
|
||||
}
|
||||
|
||||
// Special case: if the path is not already relative or absolute, and it would resolve to
|
||||
// something inside the currently running phar (e.g. `vendor/autoload.php`), we'll resolve
|
||||
// it relative to the include path so PHP won't grab the phar version.
|
||||
//
|
||||
// Note that this only works if the phar has `psysh` in the path. We might want to lift this
|
||||
// restriction and special case paths that would collide with any running phar?
|
||||
if ($resolvedPath !== $file && $file[0] !== '.') {
|
||||
$runningPhar = \Phar::running();
|
||||
if (\strpos($runningPhar, 'psysh') !== false && \is_file($runningPhar.\DIRECTORY_SEPARATOR.$file)) {
|
||||
foreach (self::getIncludePath() as $prefix) {
|
||||
$resolvedPath = $prefix.\DIRECTORY_SEPARATOR.$file;
|
||||
if (\is_file($resolvedPath)) {
|
||||
return $resolvedPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
private function isRequireNode(Node $node): bool
|
||||
{
|
||||
return $node instanceof Include_ && \in_array($node->type, self::REQUIRE_TYPES);
|
||||
}
|
||||
|
||||
private static function getIncludePath(): array
|
||||
{
|
||||
if (\PATH_SEPARATOR === ':') {
|
||||
return \preg_split('#:(?!//)#', \get_include_path());
|
||||
}
|
||||
|
||||
return \explode(\PATH_SEPARATOR, \get_include_path());
|
||||
}
|
||||
}
|
||||
123
vendor/psy/psysh/src/CodeCleaner/ReturnTypePass.php
vendored
Normal file
123
vendor/psy/psysh/src/CodeCleaner/ReturnTypePass.php
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Closure;
|
||||
use PhpParser\Node\Expr\ConstFetch;
|
||||
use PhpParser\Node\Identifier;
|
||||
use PhpParser\Node\IntersectionType;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\NullableType;
|
||||
use PhpParser\Node\Stmt\Function_;
|
||||
use PhpParser\Node\Stmt\Return_;
|
||||
use PhpParser\Node\UnionType;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Add runtime validation for return types.
|
||||
*/
|
||||
class ReturnTypePass extends CodeCleanerPass
|
||||
{
|
||||
const MESSAGE = 'A function with return type must return a value';
|
||||
const NULLABLE_MESSAGE = 'A function with return type must return a value (did you mean "return null;" instead of "return;"?)';
|
||||
const VOID_MESSAGE = 'A void function must not return a value';
|
||||
const VOID_NULL_MESSAGE = 'A void function must not return a value (did you mean "return;" instead of "return null;"?)';
|
||||
const NULLABLE_VOID_MESSAGE = 'Void type cannot be nullable';
|
||||
|
||||
private array $returnTypeStack = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($this->isFunctionNode($node)) {
|
||||
$this->returnTypeStack[] = \property_exists($node, 'returnType') ? $node->returnType : null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!empty($this->returnTypeStack) && $node instanceof Return_) {
|
||||
$expectedType = \end($this->returnTypeStack);
|
||||
if ($expectedType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$msg = null;
|
||||
|
||||
if ($this->typeName($expectedType) === 'void') {
|
||||
// Void functions
|
||||
if ($expectedType instanceof NullableType) {
|
||||
$msg = self::NULLABLE_VOID_MESSAGE;
|
||||
} elseif ($node->expr instanceof ConstFetch && \strtolower($node->expr->name) === 'null') {
|
||||
$msg = self::VOID_NULL_MESSAGE;
|
||||
} elseif ($node->expr !== null) {
|
||||
$msg = self::VOID_MESSAGE;
|
||||
}
|
||||
} else {
|
||||
// Everything else
|
||||
if ($node->expr === null) {
|
||||
$msg = $expectedType instanceof NullableType ? self::NULLABLE_MESSAGE : self::MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
if ($msg !== null) {
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if (!empty($this->returnTypeStack) && $this->isFunctionNode($node)) {
|
||||
\array_pop($this->returnTypeStack);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isFunctionNode(Node $node): bool
|
||||
{
|
||||
return $node instanceof Function_ || $node instanceof Closure;
|
||||
}
|
||||
|
||||
private function typeName(Node $node): string
|
||||
{
|
||||
if ($node instanceof UnionType) {
|
||||
return \implode('|', \array_map([$this, 'typeName'], $node->types));
|
||||
}
|
||||
|
||||
if ($node instanceof IntersectionType) {
|
||||
return \implode('&', \array_map([$this, 'typeName'], $node->types));
|
||||
}
|
||||
|
||||
if ($node instanceof NullableType) {
|
||||
return $this->typeName($node->type);
|
||||
}
|
||||
|
||||
if ($node instanceof Identifier || $node instanceof Name) {
|
||||
return $node->toLowerString();
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Unable to find type name');
|
||||
}
|
||||
}
|
||||
94
vendor/psy/psysh/src/CodeCleaner/StrictTypesPass.php
vendored
Normal file
94
vendor/psy/psysh/src/CodeCleaner/StrictTypesPass.php
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\DeclareItem;
|
||||
use PhpParser\Node\Scalar\Int_;
|
||||
use PhpParser\Node\Scalar\LNumber;
|
||||
use PhpParser\Node\Stmt\Declare_;
|
||||
use PhpParser\Node\Stmt\DeclareDeclare;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Provide implicit strict types declarations for for subsequent execution.
|
||||
*
|
||||
* The strict types pass remembers the last strict types declaration:
|
||||
*
|
||||
* declare(strict_types=1);
|
||||
*
|
||||
* ... which it then applies implicitly to all future evaluated code, until it
|
||||
* is replaced by a new declaration.
|
||||
*/
|
||||
class StrictTypesPass extends CodeCleanerPass
|
||||
{
|
||||
const EXCEPTION_MESSAGE = 'strict_types declaration must have 0 or 1 as its value';
|
||||
|
||||
private bool $strictTypes;
|
||||
|
||||
/**
|
||||
* @param bool $strictTypes enforce strict types by default
|
||||
*/
|
||||
public function __construct(bool $strictTypes = false)
|
||||
{
|
||||
$this->strictTypes = $strictTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is a standalone strict types declaration, remember it for later.
|
||||
*
|
||||
* Otherwise, apply remembered strict types declaration to to the code until
|
||||
* a new declaration is encountered.
|
||||
*
|
||||
* @throws FatalErrorException if an invalid `strict_types` declaration is found
|
||||
*
|
||||
* @param array $nodes
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$prependStrictTypes = $this->strictTypes;
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Declare_) {
|
||||
foreach ($node->declares as $declare) {
|
||||
if ($declare->key->toString() === 'strict_types') {
|
||||
$value = $declare->value;
|
||||
// @todo Remove LNumber once we drop support for PHP-Parser 4.x
|
||||
if ((!$value instanceof LNumber && !$value instanceof Int_) || ($value->value !== 0 && $value->value !== 1)) {
|
||||
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
|
||||
$this->strictTypes = $value->value === 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($prependStrictTypes) {
|
||||
$first = \reset($nodes);
|
||||
if (!$first instanceof Declare_) {
|
||||
// @todo Switch to PhpParser\Node\DeclareItem once we drop support for PHP-Parser 4.x
|
||||
// @todo Remove LNumber once we drop support for PHP-Parser 4.x
|
||||
$arg = \class_exists('PhpParser\Node\Scalar\Int_') ? new Int_(1) : new LNumber(1);
|
||||
$declareItem = \class_exists('PhpParser\Node\DeclareItem') ?
|
||||
new DeclareItem('strict_types', $arg) :
|
||||
new DeclareDeclare('strict_types', $arg);
|
||||
$declare = new Declare_([$declareItem]);
|
||||
\array_unshift($nodes, $declare);
|
||||
}
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
}
|
||||
155
vendor/psy/psysh/src/CodeCleaner/UseStatementPass.php
vendored
Normal file
155
vendor/psy/psysh/src/CodeCleaner/UseStatementPass.php
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Identifier;
|
||||
use PhpParser\Node\Name; // @phan-suppress-current-line PhanUnreferencedUseNormal - used for type checks
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use PhpParser\Node\Stmt\Use_;
|
||||
use PhpParser\Node\Stmt\UseItem;
|
||||
use PhpParser\Node\Stmt\UseUse;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Provide implicit use statements for subsequent execution.
|
||||
*
|
||||
* The use statement pass remembers the last use statement line encountered:
|
||||
*
|
||||
* use Foo\Bar as Baz;
|
||||
*
|
||||
* ... which it then applies implicitly to all future evaluated code, until the
|
||||
* current namespace is replaced by another namespace.
|
||||
*
|
||||
* Extends NamespaceAwarePass to leverage shared alias tracking.
|
||||
*/
|
||||
class UseStatementPass extends NamespaceAwarePass
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
// Check for use statement conflicts BEFORE parent adds it to aliases
|
||||
// Skip re-injected use statements (marked with 'psyshReinjected' attribute)
|
||||
if ($node instanceof Use_ && !$node->getAttribute('psyshReinjected')) {
|
||||
$this->validateUseStatement($node);
|
||||
}
|
||||
|
||||
return parent::enterNode($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-inject use statements from previous inputs.
|
||||
*
|
||||
* Each REPL input is evaluated separately; re-injecting use statements matches PHP behavior for
|
||||
* namespaces and use statements in a file.
|
||||
*
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
parent::beforeTraverse($nodes);
|
||||
|
||||
if (!$this->cleaner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for namespace declarations in the input
|
||||
foreach ($nodes as $node) {
|
||||
if ($node instanceof Namespace_) {
|
||||
// Only re-inject use statements if this is a wrapper created by NamespacePass.
|
||||
// This matches PHP behavior: explicit namespace declaration clears use statements.
|
||||
if ($node->getAttribute('psyshReinjected')) {
|
||||
$aliases = $this->cleaner->getAliasesForNamespace($node->name);
|
||||
if (!empty($aliases)) {
|
||||
$useStatements = $this->createUseStatements($aliases);
|
||||
$node->stmts = \array_merge($useStatements, $node->stmts ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't process other nodes or return modified nodes
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// No namespace declaration in input, or re-applied by NamespacePass; re-inject use
|
||||
// statements for the empty namespace.
|
||||
$aliases = $this->cleaner->getAliasesForNamespace(null);
|
||||
if (!empty($aliases)) {
|
||||
$useStatements = $this->createUseStatements($aliases);
|
||||
$nodes = \array_merge($useStatements, $nodes);
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have aliases but didn't leave a namespace (global namespace case), persist them to
|
||||
* CodeCleaner for the next traversal.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function afterTraverse(array $nodes)
|
||||
{
|
||||
if (!$this->cleaner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Persist aliases if they're at the global level (not inside any namespace)
|
||||
if (!empty($this->aliases)) {
|
||||
$this->cleaner->setAliasesForNamespace(null, $this->aliases);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a use statement doesn't conflict with existing aliases.
|
||||
*
|
||||
* @throws FatalErrorException if the alias is already in use
|
||||
*
|
||||
* @param Use_ $stmt The use statement node
|
||||
*/
|
||||
private function validateUseStatement(Use_ $stmt): void
|
||||
{
|
||||
foreach ($stmt->uses as $useItem) {
|
||||
$alias = \strtolower($useItem->getAlias());
|
||||
|
||||
if (isset($this->aliases[$alias])) {
|
||||
throw new FatalErrorException(\sprintf('Cannot use %s as %s because the name is already in use', $useItem->name->toString(), $useItem->getAlias()), 0, \E_ERROR, null, $stmt->getStartLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create use statement nodes from stored aliases.
|
||||
*
|
||||
* @param array $aliases Map of lowercase alias names to Name nodes
|
||||
*
|
||||
* @return Use_[] Array of use statement nodes
|
||||
*/
|
||||
private function createUseStatements(array $aliases): array
|
||||
{
|
||||
$useStatements = [];
|
||||
foreach ($aliases as $alias => $name) {
|
||||
// Create UseItem (PHP-Parser 5.x) or UseUse (PHP-Parser 4.x)
|
||||
$useItem = \class_exists(UseItem::class)
|
||||
? new UseItem($name, new Identifier($alias))
|
||||
: new UseUse($name, $alias);
|
||||
// Mark as re-injected so we don't validate it
|
||||
$useStatements[] = new Use_([$useItem], Use_::TYPE_NORMAL, ['psyshReinjected' => true]);
|
||||
}
|
||||
|
||||
return $useStatements;
|
||||
}
|
||||
}
|
||||
332
vendor/psy/psysh/src/CodeCleaner/ValidClassNamePass.php
vendored
Normal file
332
vendor/psy/psysh/src/CodeCleaner/ValidClassNamePass.php
vendored
Normal file
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\Ternary;
|
||||
use PhpParser\Node\Stmt;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use PhpParser\Node\Stmt\Do_;
|
||||
use PhpParser\Node\Stmt\If_;
|
||||
use PhpParser\Node\Stmt\Interface_;
|
||||
use PhpParser\Node\Stmt\Switch_;
|
||||
use PhpParser\Node\Stmt\Trait_;
|
||||
use PhpParser\Node\Stmt\While_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that classes exist.
|
||||
*
|
||||
* This pass throws a FatalErrorException rather than letting PHP run
|
||||
* headfirst into a real fatal error and die.
|
||||
*/
|
||||
class ValidClassNamePass extends NamespaceAwarePass
|
||||
{
|
||||
const CLASS_TYPE = 'class';
|
||||
const INTERFACE_TYPE = 'interface';
|
||||
const TRAIT_TYPE = 'trait';
|
||||
|
||||
private int $conditionalScopes = 0;
|
||||
|
||||
/**
|
||||
* Validate class, interface and trait definitions.
|
||||
*
|
||||
* Validate them upon entering the node, so that we know about their
|
||||
* presence and can validate constant fetches and static calls in class or
|
||||
* trait methods.
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
parent::enterNode($node);
|
||||
|
||||
if (self::isConditional($node)) {
|
||||
$this->conditionalScopes++;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->conditionalScopes === 0) {
|
||||
if ($node instanceof Class_) {
|
||||
$this->validateClassStatement($node);
|
||||
} elseif ($node instanceof Interface_) {
|
||||
$this->validateInterfaceStatement($node);
|
||||
} elseif ($node instanceof Trait_) {
|
||||
$this->validateTraitStatement($node);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if (self::isConditional($node)) {
|
||||
$this->conditionalScopes--;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function isConditional(Node $node): bool
|
||||
{
|
||||
return $node instanceof If_ ||
|
||||
$node instanceof While_ ||
|
||||
$node instanceof Do_ ||
|
||||
$node instanceof Switch_ ||
|
||||
$node instanceof Ternary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a class definition statement.
|
||||
*
|
||||
* @param Class_ $stmt
|
||||
*/
|
||||
protected function validateClassStatement(Class_ $stmt)
|
||||
{
|
||||
$this->ensureCanDefine($stmt, self::CLASS_TYPE);
|
||||
if (isset($stmt->extends)) {
|
||||
$this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt);
|
||||
}
|
||||
$this->ensureInterfacesExist($stmt->implements, $stmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an interface definition statement.
|
||||
*
|
||||
* @param Interface_ $stmt
|
||||
*/
|
||||
protected function validateInterfaceStatement(Interface_ $stmt)
|
||||
{
|
||||
$this->ensureCanDefine($stmt, self::INTERFACE_TYPE);
|
||||
$this->ensureInterfacesExist($stmt->extends, $stmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a trait definition statement.
|
||||
*
|
||||
* @param Trait_ $stmt
|
||||
*/
|
||||
protected function validateTraitStatement(Trait_ $stmt)
|
||||
{
|
||||
$this->ensureCanDefine($stmt, self::TRAIT_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that no class, interface or trait name collides with a new definition.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param Stmt $stmt
|
||||
* @param string $scopeType
|
||||
*/
|
||||
protected function ensureCanDefine(Stmt $stmt, string $scopeType = self::CLASS_TYPE)
|
||||
{
|
||||
// Anonymous classes don't have a name, and uniqueness shouldn't be enforced.
|
||||
if (!\property_exists($stmt, 'name') || $stmt->name === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $this->getFullyQualifiedName($stmt->name);
|
||||
|
||||
// check for name collisions
|
||||
$errorType = null;
|
||||
if ($this->classExists($name)) {
|
||||
$errorType = self::CLASS_TYPE;
|
||||
} elseif ($this->interfaceExists($name)) {
|
||||
$errorType = self::INTERFACE_TYPE;
|
||||
} elseif ($this->traitExists($name)) {
|
||||
$errorType = self::TRAIT_TYPE;
|
||||
}
|
||||
|
||||
if ($errorType !== null) {
|
||||
throw $this->createError(\sprintf('%s named %s already exists', \ucfirst($errorType), $name), $stmt);
|
||||
}
|
||||
|
||||
// Store creation for the rest of this code snippet so we can find local
|
||||
// issue too
|
||||
$this->currentScope[\strtolower($name)] = $scopeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a referenced class exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param string $name
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureClassExists(string $name, Stmt $stmt)
|
||||
{
|
||||
if (!$this->classExists($name)) {
|
||||
throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a referenced class _or interface_ exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param string $name
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureClassOrInterfaceExists(string $name, Stmt $stmt)
|
||||
{
|
||||
if (!$this->classExists($name) && !$this->interfaceExists($name)) {
|
||||
throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a referenced class _or trait_ exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param string $name
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureClassOrTraitExists(string $name, Stmt $stmt)
|
||||
{
|
||||
if (!$this->classExists($name) && !$this->traitExists($name)) {
|
||||
throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a statically called method exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param string $class
|
||||
* @param string $name
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureMethodExists(string $class, string $name, Stmt $stmt)
|
||||
{
|
||||
$this->ensureClassOrTraitExists($class, $stmt);
|
||||
|
||||
// let's pretend all calls to self, parent and static are valid
|
||||
if (\in_array(\strtolower($class), ['self', 'parent', 'static'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ... and all calls to classes defined right now
|
||||
if ($this->findInScope($class) === self::CLASS_TYPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if method name is an expression, give it a pass for now
|
||||
if ($name instanceof Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\method_exists($class, $name) && !\method_exists($class, '__callStatic')) {
|
||||
throw $this->createError(\sprintf('Call to undefined method %s::%s()', $class, $name), $stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a referenced interface exists.
|
||||
*
|
||||
* @throws FatalErrorException
|
||||
*
|
||||
* @param Interface_[] $interfaces
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function ensureInterfacesExist(array $interfaces, Stmt $stmt)
|
||||
{
|
||||
foreach ($interfaces as $interface) {
|
||||
/** @var string $name */
|
||||
$name = $this->getFullyQualifiedName($interface);
|
||||
if (!$this->interfaceExists($name)) {
|
||||
throw $this->createError(\sprintf('Interface \'%s\' not found', $name), $stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a class exists, or has been defined in the current code snippet.
|
||||
*
|
||||
* Gives `self`, `static` and `parent` a free pass.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
protected function classExists(string $name): bool
|
||||
{
|
||||
// Give `self`, `static` and `parent` a pass. This will actually let
|
||||
// some errors through, since we're not checking whether the keyword is
|
||||
// being used in a class scope.
|
||||
if (\in_array(\strtolower($name), ['self', 'static', 'parent'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return \class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an interface exists, or has been defined in the current code snippet.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
protected function interfaceExists(string $name): bool
|
||||
{
|
||||
return \interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a trait exists, or has been defined in the current code snippet.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
protected function traitExists(string $name): bool
|
||||
{
|
||||
return \trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a symbol in the current code snippet scope.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function findInScope(string $name)
|
||||
{
|
||||
$name = \strtolower($name);
|
||||
if (isset($this->currentScope[$name])) {
|
||||
return $this->currentScope[$name];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error creation factory.
|
||||
*
|
||||
* @param string $msg
|
||||
* @param Stmt $stmt
|
||||
*/
|
||||
protected function createError(string $msg, Stmt $stmt): FatalErrorException
|
||||
{
|
||||
return new FatalErrorException($msg, 0, \E_ERROR, null, $stmt->getStartLine());
|
||||
}
|
||||
}
|
||||
125
vendor/psy/psysh/src/CodeCleaner/ValidConstructorPass.php
vendored
Normal file
125
vendor/psy/psysh/src/CodeCleaner/ValidConstructorPass.php
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use PhpParser\Node\Stmt\ClassMethod;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that the constructor method is not static, and does not have a
|
||||
* return type.
|
||||
*
|
||||
* Checks both explicit __construct methods as well as old-style constructor
|
||||
* methods with the same name as the class (for non-namespaced classes).
|
||||
*
|
||||
* As of PHP 5.3.3, methods with the same name as the last element of a
|
||||
* namespaced class name will no longer be treated as constructor. This change
|
||||
* doesn't affect non-namespaced classes.
|
||||
*
|
||||
* @author Martin Hasoň <martin.hason@gmail.com>
|
||||
*/
|
||||
class ValidConstructorPass extends CodeCleanerPass
|
||||
{
|
||||
private array $namespace = [];
|
||||
|
||||
/**
|
||||
* @return Node[]|null Array of nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes)
|
||||
{
|
||||
$this->namespace = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the constructor is not static and does not have a return type.
|
||||
*
|
||||
* @throws FatalErrorException the constructor function is static
|
||||
* @throws FatalErrorException the constructor function has a return type
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Namespace_) {
|
||||
$this->namespace = isset($node->name) ? $this->getParts($node->name) : [];
|
||||
} elseif ($node instanceof Class_) {
|
||||
$constructor = null;
|
||||
foreach ($node->stmts as $stmt) {
|
||||
if ($stmt instanceof ClassMethod) {
|
||||
// If we find a new-style constructor, no need to look for the old-style
|
||||
if (\property_exists($stmt, 'name') && \strtolower($stmt->name) === '__construct') {
|
||||
$this->validateConstructor($stmt, $node);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// We found a possible old-style constructor (unless there is also a __construct method)
|
||||
if (empty($this->namespace) && $node->name !== null && \property_exists($stmt, 'name') && \strtolower($node->name) === \strtolower($stmt->name)) {
|
||||
$constructor = $stmt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($constructor) {
|
||||
$this->validateConstructor($constructor, $node);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FatalErrorException the constructor function is static
|
||||
* @throws FatalErrorException the constructor function has a return type
|
||||
*
|
||||
* @param Node $constructor
|
||||
* @param Node $classNode
|
||||
*/
|
||||
private function validateConstructor(Node $constructor, Node $classNode)
|
||||
{
|
||||
if (\method_exists($constructor, 'isStatic') && $constructor->isStatic()) {
|
||||
$msg = \sprintf(
|
||||
'Constructor %s::%s() cannot be static',
|
||||
\implode('\\', \array_merge($this->namespace, (array) $classNode->name->toString())),
|
||||
\property_exists($constructor, 'name') ? $constructor->name : '__construct'
|
||||
);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $classNode->getStartLine());
|
||||
}
|
||||
|
||||
if (\method_exists($constructor, 'getReturnType') && $constructor->getReturnType()) {
|
||||
$msg = \sprintf(
|
||||
'Constructor %s::%s() cannot declare a return type',
|
||||
\implode('\\', \array_merge($this->namespace, (array) $classNode->name->toString())),
|
||||
\property_exists($constructor, 'name') ? $constructor->name : '__construct'
|
||||
);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $classNode->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatibility shim for PHP-Parser 4.x.
|
||||
*
|
||||
* At some point we might want to make $namespace a plain string, to match how Name works?
|
||||
*/
|
||||
protected function getParts(Name $name): array
|
||||
{
|
||||
return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts;
|
||||
}
|
||||
}
|
||||
87
vendor/psy/psysh/src/CodeCleaner/ValidFunctionNamePass.php
vendored
Normal file
87
vendor/psy/psysh/src/CodeCleaner/ValidFunctionNamePass.php
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\CodeCleaner;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Do_;
|
||||
use PhpParser\Node\Stmt\Function_;
|
||||
use PhpParser\Node\Stmt\If_;
|
||||
use PhpParser\Node\Stmt\Switch_;
|
||||
use PhpParser\Node\Stmt\While_;
|
||||
use Psy\Exception\FatalErrorException;
|
||||
|
||||
/**
|
||||
* Validate that function calls will succeed.
|
||||
*
|
||||
* This pass throws a FatalErrorException rather than letting PHP run
|
||||
* headfirst into a real fatal error and die.
|
||||
*/
|
||||
class ValidFunctionNamePass extends NamespaceAwarePass
|
||||
{
|
||||
private int $conditionalScopes = 0;
|
||||
|
||||
/**
|
||||
* Store newly defined function names on the way in, to allow recursion.
|
||||
*
|
||||
* @throws FatalErrorException if a function is redefined in a non-conditional scope
|
||||
*
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|null Replacement node (or special return value)
|
||||
*/
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
parent::enterNode($node);
|
||||
|
||||
if (self::isConditional($node)) {
|
||||
$this->conditionalScopes++;
|
||||
} elseif ($node instanceof Function_) {
|
||||
$name = $this->getFullyQualifiedName($node->name);
|
||||
|
||||
// @todo add an "else" here which adds a runtime check for instances where we can't tell
|
||||
// whether a function is being redefined by static analysis alone.
|
||||
if ($this->conditionalScopes === 0) {
|
||||
if (\function_exists($name) ||
|
||||
isset($this->currentScope[\strtolower($name)])) {
|
||||
$msg = \sprintf('Cannot redeclare %s()', $name);
|
||||
throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine());
|
||||
}
|
||||
}
|
||||
|
||||
$this->currentScope[\strtolower($name)] = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
*
|
||||
* @return int|Node|Node[]|null Replacement node (or special return value)
|
||||
*/
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if (self::isConditional($node)) {
|
||||
$this->conditionalScopes--;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function isConditional(Node $node)
|
||||
{
|
||||
return $node instanceof If_ ||
|
||||
$node instanceof While_ ||
|
||||
$node instanceof Do_ ||
|
||||
$node instanceof Switch_;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user