Composer update

* updated Laravel to v5.6.38
* Added laravel tinker in dev dependencies
This commit is contained in:
Manish Verma
2018-09-17 10:07:24 +05:30
committed by Manish Verma
parent be4b1231b6
commit 6742e13d81
781 changed files with 32607 additions and 942 deletions

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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;
private $abstractMethods;
/**
* @throws RuntimeException if the node is an abstract function with a body
*
* @param Node $node
*/
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->getLine());
}
}
}
}
/**
* @throws RuntimeException if the node is a non-abstract class with abstract methods
*
* @param Node $node
*/
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->getLine());
}
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 RuntimeException if the user assign the `$this` variable
*
* @param Node $node
*/
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->getLine());
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 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 RuntimeException if the user used call-time pass-by-reference
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if (!$node instanceof FuncCall && !$node instanceof MethodCall && !$node instanceof StaticCall) {
return;
}
foreach ($node->args as $arg) {
if ($arg->byRef) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 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 $inClass;
/**
* @param array $nodes
*/
public function beforeTraverse(array $nodes)
{
$this->inClass = false;
}
/**
* @throws ErrorException if get_class or get_called_class is called without an object from outside a class
*
* @param Node $node
*/
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;
}
// We'll ignore name expressions as well (things like `$foo()`)
if (!($node->name instanceof Name)) {
return;
}
$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->getLine());
}
}
}
/**
* @param Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof Class_) {
$this->inClass = false;
}
}
private function isNull(Node $node)
{
return $node->value instanceof ConstFetch && \strtolower($node->value->name) === 'null';
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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!
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Exit_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
class ExitPass extends CodeCleanerPass
{
/**
* Converts exit calls to BreakExceptions.
*
* @param \PhpParser\Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof Exit_) {
return new StaticCall(new FullyQualifiedName('Psy\Exception\BreakException'), 'exitShell');
}
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 $finalClasses;
/**
* @param array $nodes
*/
public function beforeTraverse(array $nodes)
{
$this->finalClasses = [];
}
/**
* @throws RuntimeException if the node is a class that extends a final class
*
* @param Node $node
*/
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->getLine());
}
}
if ($node->isFinal()) {
$this->finalClasses[\strtolower($node->name)] = true;
}
}
}
/**
* @param string $name Class name
*
* @return bool
*/
private function isFinalClass($name)
{
if (!\class_exists($name)) {
return isset($this->finalClasses[\strtolower($name)]);
}
$refl = new \ReflectionClass($name);
return $refl->isFinal();
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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
{
/** @var int */
private $functionDepth;
/**
* @param array $nodes
*/
public function beforeTraverse(array $nodes)
{
$this->functionDepth = 0;
}
public function enterNode(Node $node)
{
if ($node instanceof FunctionLike) {
$this->functionDepth++;
return;
}
// node is inside function context
if ($this->functionDepth !== 0) {
return;
}
// 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->getLine());
}
}
/**
* @param \PhpParser\Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof FunctionLike) {
$this->functionDepth--;
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Empty_;
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 Psy\Exception\FatalErrorException;
/**
* Validate that the functions are used correctly.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class FunctionReturnInWriteContextPass extends CodeCleanerPass
{
const PHP55_MESSAGE = 'Cannot use isset() on the result of a function call (you can use "null !== func()" instead)';
const EXCEPTION_MESSAGE = "Can't use function return value in write context";
private $atLeastPhp55;
public function __construct()
{
$this->atLeastPhp55 = \version_compare(PHP_VERSION, '5.5', '>=');
}
/**
* 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 function is used as an argument in the empty, only for PHP < 5.5
* @throws FatalErrorException if a value is assigned to a function
*
* @param Node $node
*/
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 && $item->byRef && $this->isCallNode($item->value)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
}
} elseif ($node instanceof Isset_ || $node instanceof Unset_) {
foreach ($node->vars as $var) {
if (!$this->isCallNode($var)) {
continue;
}
$msg = ($node instanceof Isset_ && $this->atLeastPhp55) ? self::PHP55_MESSAGE : self::EXCEPTION_MESSAGE;
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
} elseif ($node instanceof Empty_ && !$this->atLeastPhp55 && $this->isCallNode($node->expr)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine()); // @codeCoverageIgnore
} elseif ($node instanceof Assign && $this->isCallNode($node->var)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
}
private function isCallNode(Node $node)
{
return $node instanceof FuncCall || $node instanceof MethodCall || $node instanceof StaticCall;
}
}

View File

@@ -0,0 +1,128 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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)
{
return $this->addImplicitReturn($nodes);
}
/**
* @param array $nodes
*
* @return array
*/
private function addImplicitReturn(array $nodes)
{
// 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->getLine(),
'endLine' => $last->getLine(),
]);
// @codeCoverageIgnoreEnd
} elseif ($last instanceof Expression && !($last->expr instanceof Exit_)) {
// For PHP Parser 4.x
$nodes[\count($nodes) - 1] = new Return_($last->expr, [
'startLine' => $last->getLine(),
'endLine' => $last->getLine(),
]);
} 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
*
* @return bool
*/
private static function isNonExpressionStmt(Node $node)
{
return $node instanceof Stmt &&
!$node instanceof Expression &&
!$node instanceof Return_ &&
!$node instanceof Namespace_;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Instanceof_;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\Encapsed;
use Psy\Exception\FatalErrorException;
/**
* Validate that the instanceof statement does not receive a scalar value or a non-class constant.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class InstanceOfPass extends CodeCleanerPass
{
const EXCEPTION_MSG = 'instanceof expects an object instance, constant given';
/**
* Validate that the instanceof statement does not receive a scalar value or a non-class constant.
*
* @throws FatalErrorException if a scalar or a non-class constant is given
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if (!$node instanceof Instanceof_) {
return;
}
if (($node->expr instanceof Scalar && !$node->expr instanceof Encapsed) || $node->expr instanceof ConstFetch) {
throw new FatalErrorException(self::EXCEPTION_MSG, 0, E_ERROR, null, $node->getLine());
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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
*/
public function enterNode(Node $node)
{
if ($node instanceof Variable && $node->name === '__psysh__') {
throw new RuntimeException('Don\'t mess with $__psysh__; bad things will happen');
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Empty_;
use PhpParser\Node\Expr\Variable;
use Psy\Exception\ParseErrorException;
/**
* Validate that the user did not call the language construct `empty()` on a
* statement in PHP < 5.5.
*
* @codeCoverageIgnore
*/
class LegacyEmptyPass extends CodeCleanerPass
{
private $atLeastPhp55;
public function __construct()
{
$this->atLeastPhp55 = \version_compare(PHP_VERSION, '5.5', '>=');
}
/**
* Validate use of empty in PHP < 5.5.
*
* @throws ParseErrorException if the user used empty with anything but a variable
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($this->atLeastPhp55) {
return;
}
if (!$node instanceof Empty_) {
return;
}
if (!$node->expr instanceof Variable) {
$msg = \sprintf('syntax error, unexpected %s', $this->getUnexpectedThing($node->expr));
throw new ParseErrorException($msg, $node->expr->getLine());
}
}
private function getUnexpectedThing(Node $node)
{
switch ($node->getType()) {
case 'Scalar_String':
case 'Scalar_LNumber':
case 'Scalar_DNumber':
return \json_encode($node->value);
case 'Expr_ConstFetch':
return (string) $node->name;
default:
return $node->getType();
}
}
}

View File

@@ -0,0 +1,112 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\ArrayItem;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\List_;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use Psy\Exception\ParseErrorException;
/**
* Validate that the list assignment.
*/
class ListPass extends CodeCleanerPass
{
private $atLeastPhp71;
public function __construct()
{
$this->atLeastPhp71 = \version_compare(PHP_VERSION, '7.1', '>=');
}
/**
* Validate use of list assignment.
*
* @throws ParseErrorException if the user used empty with anything but a variable
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if (!$node instanceof Assign) {
return;
}
if (!$node->var instanceof Array_ && !$node->var instanceof List_) {
return;
}
if (!$this->atLeastPhp71 && $node->var instanceof Array_) {
$msg = "syntax error, unexpected '='";
throw new ParseErrorException($msg, $node->expr->getLine());
}
// Polyfill for PHP-Parser 2.x
$items = isset($node->var->items) ? $node->var->items : $node->var->vars;
if ($items === [] || $items === [null]) {
throw new ParseErrorException('Cannot use empty list', $node->var->getLine());
}
$itemFound = false;
foreach ($items as $item) {
if ($item === null) {
continue;
}
$itemFound = true;
// List_->$vars in PHP-Parser 2.x is Variable instead of ArrayItem.
if (!$this->atLeastPhp71 && $item instanceof ArrayItem && $item->key !== null) {
$msg = 'Syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting \',\' or \')\'';
throw new ParseErrorException($msg, $item->key->getLine());
}
if (!self::isValidArrayItem($item)) {
$msg = 'Assignments can only happen to writable values';
throw new ParseErrorException($msg, $item->getLine());
}
}
if (!$itemFound) {
throw new ParseErrorException('Cannot use empty list');
}
}
/**
* Validate whether a given item in an array is valid for short assignment.
*
* @param Expr $item
*
* @return bool
*/
private static function isValidArrayItem(Expr $item)
{
$value = ($item instanceof ArrayItem) ? $item->value : $item;
if ($value instanceof Variable) {
return true;
}
if ($value instanceof ArrayDimFetch || $value instanceof PropertyFetch) {
return isset($value->var) && $value->var instanceof Variable;
}
return false;
}
}

View File

@@ -0,0 +1,103 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\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 $loopDepth;
/**
* {@inheritdoc}
*/
public function beforeTraverse(array $nodes)
{
$this->loopDepth = 0;
}
/**
* @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
*/
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->getLine());
}
if ($node->num instanceof LNumber || $node->num instanceof DNumber) {
$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->getLine());
}
if ($num > $this->loopDepth) {
$msg = \sprintf("Cannot '%s' %d levels", $operator, $num);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
} 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->getLine());
}
break;
}
}
/**
* @param Node $node
*/
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;
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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 null|FuncCall|String_
*/
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());
}
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Namespace_;
/**
* Abstract namespace-aware code cleaner pass.
*/
abstract class NamespaceAwarePass extends CodeCleanerPass
{
protected $namespace;
protected $currentScope;
/**
* @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
*/
public function beforeTraverse(array $nodes)
{
$this->namespace = [];
$this->currentScope = [];
}
/**
* @todo should this be final? Extending classes should be sure to either use
* leaveNode or call parent::enterNode() when overloading
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Namespace_) {
$this->namespace = isset($node->name) ? $node->name->parts : [];
}
}
/**
* Get a fully-qualified name (class, function, interface, etc).
*
* @param mixed $name
*
* @return string
*/
protected function getFullyQualifiedName($name)
{
if ($name instanceof FullyQualifiedName) {
return \implode('\\', $name->parts);
} elseif ($name instanceof Name) {
$name = $name->parts;
} elseif (!\is_array($name)) {
$name = [$name];
}
return \implode('\\', \array_merge($this->namespace, $name));
}
}

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\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 CodeCleanerPass
{
private $namespace = null;
private $cleaner;
/**
* @param CodeCleaner $cleaner
*/
public function __construct(CodeCleaner $cleaner)
{
$this->cleaner = $cleaner;
}
/**
* 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
*/
public function beforeTraverse(array $nodes)
{
if (empty($nodes)) {
return $nodes;
}
$last = \end($nodes);
if ($last instanceof Namespace_) {
$kind = $last->getAttribute('kind');
// Treat all namespace statements pre-PHP-Parser v3.1.2 as "open",
// even though we really have no way of knowing.
if ($kind === null || $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;
}
return $this->namespace ? [new Namespace_($this->namespace, $nodes)] : $nodes;
}
/**
* Remember the namespace and (re)set the namespace on the CodeCleaner as
* well.
*
* @param null|Name $namespace
*/
private function setNamespace($namespace)
{
$this->namespace = $namespace;
$this->cleaner->setNamespace($namespace === null ? null : $namespace->parts);
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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.
*
* @return PhpParser\Node\Expr\New_
*/
public static function create()
{
return new New_(new FullyQualifiedName('Psy\CodeCleaner\NoReturnValue'));
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\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 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
*/
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;
}
$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;
}
foreach ($refl->getParameters() as $key => $param) {
if (\array_key_exists($key, $node->args)) {
$arg = $node->args[$key];
if ($param->isPassedByReference() && !$this->isPassableByReference($arg)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
}
}
}
}
private function isPassableByReference(Node $arg)
{
// 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;
}
/**
* 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->getLine());
}
}
}
}

View File

@@ -0,0 +1,101 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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;
use Psy\Shell;
/**
* Add runtime validation for `require` and `require_once` calls.
*/
class RequirePass extends CodeCleanerPass
{
private static $requireTypes = [Include_::TYPE_REQUIRE, Include_::TYPE_REQUIRE_ONCE];
/**
* {@inheritdoc}
*/
public function enterNode(Node $origNode)
{
if (!$this->isRequireNode($origNode)) {
return;
}
$node = clone $origNode;
/*
* rewrite
*
* $foo = require $bar
*
* to
*
* $foo = require \Psy\CodeCleaner\RequirePass::resolve($bar)
*/
$node->expr = new StaticCall(
new FullyQualifiedName('Psy\CodeCleaner\RequirePass'),
'resolve',
[new Arg($origNode->expr), new Arg(new LNumber($origNode->getLine()))],
$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.
*
* @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 $lineNumber Line number of the original require expression
*
* @return string Exactly the same as $file
*/
public static function resolve($file, $lineNumber = null)
{
$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, $lineNumber);
}
// @todo trigger an error as fallback? this is pretty ugly…
// trigger_error('Filename cannot be empty', E_USER_WARNING);
}
if ($file === '' || !\stream_resolve_include_path($file)) {
$msg = \sprintf("Failed opening required '%s'", $file);
throw new FatalErrorException($msg, 0, E_ERROR, null, $lineNumber);
}
return $file;
}
private function isRequireNode(Node $node)
{
return $node instanceof Include_ && \in_array($node->type, self::$requireTypes);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\Identifier;
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 $strictTypes = false;
private $atLeastPhp7;
public function __construct()
{
$this->atLeastPhp7 = \version_compare(PHP_VERSION, '7.0', '>=');
}
/**
* 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
*/
public function beforeTraverse(array $nodes)
{
if (!$this->atLeastPhp7) {
return; // @codeCoverageIgnore
}
$prependStrictTypes = $this->strictTypes;
foreach ($nodes as $key => $node) {
if ($node instanceof Declare_) {
foreach ($node->declares as $declare) {
// For PHP Parser 4.x
$declareKey = $declare->key instanceof Identifier ? $declare->key->toString() : $declare->key;
if ($declareKey === 'strict_types') {
$value = $declare->value;
if (!$value instanceof LNumber || ($value->value !== 0 && $value->value !== 1)) {
throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, E_ERROR, null, $node->getLine());
}
$this->strictTypes = $value->value === 1;
}
}
}
}
if ($prependStrictTypes) {
$first = \reset($nodes);
if (!$first instanceof Declare_) {
$declare = new Declare_([new DeclareDeclare('strict_types', new LNumber(1))]);
\array_unshift($nodes, $declare);
}
}
return $nodes;
}
}

View File

@@ -0,0 +1,126 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\NodeTraverser;
/**
* 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.
*/
class UseStatementPass extends CodeCleanerPass
{
private $aliases = [];
private $lastAliases = [];
private $lastNamespace = null;
/**
* Re-load the last set of use statements on re-entering a namespace.
*
* This isn't how namespaces normally work, but because PsySH has to spin
* up a new namespace for every line of code, we do this to make things
* work like you'd expect.
*
* @param Node $node
*/
public function enterNode(Node $node)
{
if ($node instanceof Namespace_) {
// If this is the same namespace as last namespace, let's do ourselves
// a favor and reload all the aliases...
if (\strtolower($node->name) === \strtolower($this->lastNamespace)) {
$this->aliases = $this->lastAliases;
}
}
}
/**
* If this statement is a namespace, forget all the aliases we had.
*
* If it's a use statement, remember the alias for later. Otherwise, apply
* remembered aliases to the code.
*
* @param Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof Use_) {
// Store a reference to every "use" statement, because we'll need
// them in a bit.
foreach ($node->uses as $use) {
$alias = $use->alias ?: \end($use->name->parts);
$this->aliases[\strtolower($alias)] = $use->name;
}
return NodeTraverser::REMOVE_NODE;
} elseif ($node instanceof GroupUse) {
// Expand every "use" statement in the group into a full, standalone
// "use" and store 'em with the others.
foreach ($node->uses as $use) {
$alias = $use->alias ?: \end($use->name->parts);
$this->aliases[\strtolower($alias)] = Name::concat($node->prefix, $use->name, [
'startLine' => $node->prefix->getAttribute('startLine'),
'endLine' => $use->name->getAttribute('endLine'),
]);
}
return NodeTraverser::REMOVE_NODE;
} elseif ($node instanceof Namespace_) {
// Start fresh, since we're done with this namespace.
$this->lastNamespace = $node->name;
$this->lastAliases = $this->aliases;
$this->aliases = [];
} else {
foreach ($node as $name => $subNode) {
if ($subNode instanceof Name) {
// Implicitly thunk all aliases.
if ($replacement = $this->findAlias($subNode)) {
$node->$name = $replacement;
}
}
}
return $node;
}
}
/**
* Find class/namespace aliases.
*
* @param Name $name
*
* @return FullyQualifiedName|null
*/
private function findAlias(Name $name)
{
$that = \strtolower($name);
foreach ($this->aliases as $alias => $prefix) {
if ($that === $alias) {
return new FullyQualifiedName($prefix->toString());
} elseif (\substr($that, 0, \strlen($alias) + 1) === $alias . '\\') {
return new FullyQualifiedName($prefix->toString() . \substr($name, \strlen($alias)));
}
}
}
}

View File

@@ -0,0 +1,411 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\ClassConstFetch;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
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 $conditionalScopes = 0;
private $atLeastPhp55;
public function __construct()
{
$this->atLeastPhp55 = \version_compare(PHP_VERSION, '5.5', '>=');
}
/**
* 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
*/
public function enterNode(Node $node)
{
parent::enterNode($node);
if (self::isConditional($node)) {
$this->conditionalScopes++;
} else {
// @todo add an "else" here which adds a runtime check for instances where we can't tell
// whether a class is being redefined by static analysis alone.
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);
}
}
}
}
/**
* Validate `new` expressions, class constant fetches, and static calls.
*
* @throws FatalErrorException if a class, interface or trait is referenced which does not exist
* @throws FatalErrorException if a class extends something that is not a class
* @throws FatalErrorException if a class implements something that is not an interface
* @throws FatalErrorException if an interface extends something that is not an interface
* @throws FatalErrorException if a class, interface or trait redefines an existing class, interface or trait name
*
* @param Node $node
*/
public function leaveNode(Node $node)
{
if (self::isConditional($node)) {
$this->conditionalScopes--;
} elseif ($node instanceof New_) {
$this->validateNewExpression($node);
} elseif ($node instanceof ClassConstFetch) {
$this->validateClassConstFetchExpression($node);
} elseif ($node instanceof StaticCall) {
$this->validateStaticCallExpression($node);
}
}
private static function isConditional(Node $node)
{
return $node instanceof If_ ||
$node instanceof While_ ||
$node instanceof Do_ ||
$node instanceof Switch_;
}
/**
* 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);
}
/**
* Validate a `new` expression.
*
* @param New_ $stmt
*/
protected function validateNewExpression(New_ $stmt)
{
// if class name is an expression or an anonymous class, give it a pass for now
if (!$stmt->class instanceof Expr && !$stmt->class instanceof Class_) {
$this->ensureClassExists($this->getFullyQualifiedName($stmt->class), $stmt);
}
}
/**
* Validate a class constant fetch expression's class.
*
* @param ClassConstFetch $stmt
*/
protected function validateClassConstFetchExpression(ClassConstFetch $stmt)
{
// there is no need to check exists for ::class const for php 5.5 or newer
if (\strtolower($stmt->name) === 'class' && $this->atLeastPhp55) {
return;
}
// if class name is an expression, give it a pass for now
if (!$stmt->class instanceof Expr) {
$this->ensureClassOrInterfaceExists($this->getFullyQualifiedName($stmt->class), $stmt);
}
}
/**
* Validate a class constant fetch expression's class.
*
* @param StaticCall $stmt
*/
protected function validateStaticCallExpression(StaticCall $stmt)
{
// if class name is an expression, give it a pass for now
if (!$stmt->class instanceof Expr) {
$this->ensureMethodExists($this->getFullyQualifiedName($stmt->class), $stmt->name, $stmt);
}
}
/**
* 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, $scopeType = self::CLASS_TYPE)
{
$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($name, $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($name, $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($name, $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($class, $name, $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($interfaces, $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);
}
}
}
/**
* Get a symbol type key for storing in the scope name cache.
*
* @deprecated No longer used. Scope type should be passed into ensureCanDefine directly.
* @codeCoverageIgnore
*
* @param Stmt $stmt
*
* @return string
*/
protected function getScopeType(Stmt $stmt)
{
if ($stmt instanceof Class_) {
return self::CLASS_TYPE;
} elseif ($stmt instanceof Interface_) {
return self::INTERFACE_TYPE;
} elseif ($stmt instanceof Trait_) {
return self::TRAIT_TYPE;
}
}
/**
* 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
*
* @return bool
*/
protected function classExists($name)
{
// 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
*
* @return bool
*/
protected function interfaceExists($name)
{
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
*
* @return bool
*/
protected function traitExists($name)
{
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($name)
{
$name = \strtolower($name);
if (isset($this->currentScope[$name])) {
return $this->currentScope[$name];
}
}
/**
* Error creation factory.
*
* @param string $msg
* @param Stmt $stmt
*
* @return FatalErrorException
*/
protected function createError($msg, $stmt)
{
return new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine());
}
}

View File

@@ -0,0 +1,90 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Identifier;
use Psy\Exception\FatalErrorException;
/**
* Validate that namespaced constant references will succeed.
*
* This pass throws a FatalErrorException rather than letting PHP run
* headfirst into a real fatal error and die.
*
* @todo Detect constants defined in the current code snippet?
* ... Might not be worth it, since it would need to both be defining and
* referencing a namespaced constant, which doesn't seem like that big of
* a target for failure
*/
class ValidConstantPass extends NamespaceAwarePass
{
/**
* Validate that namespaced constant references will succeed.
*
* Note that this does not (yet) detect constants defined in the current code
* snippet. It won't happen very often, so we'll punt for now.
*
* @throws FatalErrorException if a constant reference is not defined
*
* @param Node $node
*/
public function leaveNode(Node $node)
{
if ($node instanceof ConstFetch && \count($node->name->parts) > 1) {
$name = $this->getFullyQualifiedName($node->name);
if (!\defined($name)) {
$msg = \sprintf('Undefined constant %s', $name);
throw new FatalErrorException($msg, 0, E_ERROR, null, $node->getLine());
}
} elseif ($node instanceof ClassConstFetch) {
$this->validateClassConstFetchExpression($node);
}
}
/**
* Validate a class constant fetch expression.
*
* @throws FatalErrorException if a class constant is not defined
*
* @param ClassConstFetch $stmt
*/
protected function validateClassConstFetchExpression(ClassConstFetch $stmt)
{
// For PHP Parser 4.x
$constName = $stmt->name instanceof Identifier ? $stmt->name->toString() : $stmt->name;
// give the `class` pseudo-constant a pass
if ($constName === 'class') {
return;
}
// if class name is an expression, give it a pass for now
if (!$stmt->class instanceof Expr) {
$className = $this->getFullyQualifiedName($stmt->class);
// if the class doesn't exist, don't throw an exception… it might be
// defined in the same line it's used or something stupid like that.
if (\class_exists($className) || \interface_exists($className)) {
$refl = new \ReflectionClass($className);
if (!$refl->hasConstant($constName)) {
$constType = \class_exists($className) ? 'Class' : 'Interface';
$msg = \sprintf('%s constant \'%s::%s\' not found', $constType, $className, $constName);
throw new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine());
}
}
}
}
}

View File

@@ -0,0 +1,112 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\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 $namespace;
public function beforeTraverse(array $nodes)
{
$this->namespace = [];
}
/**
* 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
*/
public function enterNode(Node $node)
{
if ($node instanceof Namespace_) {
$this->namespace = isset($node->name) ? $node->name->parts : [];
} 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 ('__construct' === \strtolower($stmt->name)) {
$this->validateConstructor($stmt, $node);
return;
}
// We found a possible old-style constructor (unless there is also a __construct method)
if (empty($this->namespace) && \strtolower($node->name) === \strtolower($stmt->name)) {
$constructor = $stmt;
}
}
}
if ($constructor) {
$this->validateConstructor($constructor, $node);
}
}
}
/**
* @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 ($constructor->isStatic()) {
// For PHP Parser 4.x
$className = $classNode->name instanceof Identifier ? $classNode->name->toString() : $classNode->name;
$msg = \sprintf(
'Constructor %s::%s() cannot be static',
\implode('\\', \array_merge($this->namespace, (array) $className)),
$constructor->name
);
throw new FatalErrorException($msg, 0, E_ERROR, null, $classNode->getLine());
}
if (\method_exists($constructor, 'getReturnType') && $constructor->getReturnType()) {
// For PHP Parser 4.x
$className = $classNode->name instanceof Identifier ? $classNode->name->toString() : $classNode->name;
$msg = \sprintf(
'Constructor %s::%s() cannot declare a return type',
\implode('\\', \array_merge($this->namespace, (array) $className)),
$constructor->name
);
throw new FatalErrorException($msg, 0, E_ERROR, null, $classNode->getLine());
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2018 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\FuncCall;
use PhpParser\Node\Expr\Variable;
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 $conditionalScopes = 0;
/**
* Store newly defined function names on the way in, to allow recursion.
*
* @param Node $node
*/
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->getLine());
}
}
$this->currentScope[\strtolower($name)] = true;
}
}
/**
* Validate that function calls will succeed.
*
* @throws FatalErrorException if a function is redefined
* @throws FatalErrorException if the function name is a string (not an expression) and is not defined
*
* @param Node $node
*/
public function leaveNode(Node $node)
{
if (self::isConditional($node)) {
$this->conditionalScopes--;
} elseif ($node instanceof FuncCall) {
// if function name is an expression or a variable, give it a pass for now.
$name = $node->name;
if (!$name instanceof Expr && !$name instanceof Variable) {
$shortName = \implode('\\', $name->parts);
$fullName = $this->getFullyQualifiedName($name);
$inScope = isset($this->currentScope[\strtolower($fullName)]);
if (!$inScope && !\function_exists($shortName) && !\function_exists($fullName)) {
$message = \sprintf('Call to undefined function %s()', $name);
throw new FatalErrorException($message, 0, E_ERROR, null, $node->getLine());
}
}
}
}
private static function isConditional(Node $node)
{
return $node instanceof If_ ||
$node instanceof While_ ||
$node instanceof Do_ ||
$node instanceof Switch_;
}
}