organizrnginxsonarrradarrplexdashboardcouchpotatonzbgetbookmarkapplication-dashboardmuximuxlandingpagestartpagelandinghtpcserverhomepagesabnzbdheimdallemby
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
393 lines
11 KiB
393 lines
11 KiB
<?php
|
|
|
|
/*
|
|
* This file is part of Psy Shell.
|
|
*
|
|
* (c) 2012-2017 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';
|
|
|
|
protected $checkTraits;
|
|
private $conditionalScopes = 0;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->checkTraits = function_exists('trait_exists');
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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);
|
|
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);
|
|
$this->ensureInterfacesExist($stmt->extends, $stmt);
|
|
}
|
|
|
|
/**
|
|
* Validate a trait definition statement.
|
|
*
|
|
* @param Trait_ $stmt
|
|
*/
|
|
protected function validateTraitStatement(Trait_ $stmt)
|
|
{
|
|
$this->ensureCanDefine($stmt);
|
|
}
|
|
|
|
/**
|
|
* 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'
|
|
&& version_compare(PHP_VERSION, '5.5', '>=')) {
|
|
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
|
|
*/
|
|
protected function ensureCanDefine(Stmt $stmt)
|
|
{
|
|
$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)] = $this->getScopeType($stmt);
|
|
}
|
|
|
|
/**
|
|
* 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 statically called method exists.
|
|
*
|
|
* @throws FatalErrorException
|
|
*
|
|
* @param string $class
|
|
* @param string $name
|
|
* @param Stmt $stmt
|
|
*/
|
|
protected function ensureMethodExists($class, $name, $stmt)
|
|
{
|
|
$this->ensureClassExists($class, $stmt);
|
|
|
|
// let's pretend all calls to self, parent and static are valid
|
|
if (in_array(strtolower($class), array('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 $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.
|
|
*
|
|
* @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), array('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 $this->checkTraits && (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());
|
|
}
|
|
}
|
|
|