Create isset expression fixer

This commit is contained in:
Daniil Gentili 2020-08-30 20:27:43 +02:00
parent fb9dac3d95
commit e33da7edc8
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
6 changed files with 243 additions and 280 deletions

View File

@ -3,6 +3,7 @@
namespace Phabel;
use PhpParser\Node;
use PhpParser\Node\Expr\Isset_;
use SplStack;
/**
@ -17,10 +18,42 @@ class Context
* @var SplStack<Node>
*/
public SplStack $parents;
/**
* Whether we're inside an isset expression
*/
public bool $insideIsset = false;
/**
* Constructor
*/
public function __construct()
{
$this->parents = new SplStack;
}
/**
* Push node
*
* @param Node $node Node
*
* @return void
*/
public function push(Node $node): void
{
$this->parents->push($node);
if ($node instanceof Isset_) {
$this->insideIsset = true;
}
}
/**
* Pop node
*
* @return void
*/
public function pop(): void
{
if ($this->parents->pop() instanceof Isset_) {
$this->insideIsset = false;
}
}
/**
* Insert nodes before node.
*

View File

@ -0,0 +1,181 @@
<?php
namespace Phabel\Plugin;
use Phabel\Plugin;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\Isset_;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Expr\Ternary;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\VarLikeIdentifier;
use ReflectionClass;
use ReflectionClassConstant;
use ReflectionException;
use ReflectionProperty;
class IssetExpressionFixer extends Plugin
{
/**
* Recursively extract bottom ArrayDimFetch
*
* @param Node $var
* @return Node
*/
private static function &extractWorkVar(Node &$var): Node
{
if ($var instanceof ArrayDimFetch && $var->var instanceof ArrayDimFetch) {
return self::extractWorkVar($var->var);
}
return $var;
}
/**
* Wrap boolean isset check.
*
* @param Expr $node Node
*
* @return ArrayDimFetch
*/
private static function wrapBoolean(Expr $node): ArrayDimFetch
{
return new ArrayDimFetch(
self::callPoly(
'returnMe',
new Ternary(
$node,
self::toLiteral([0]),
self::toLiteral([]),
)
),
new LNumber(0)
);
}
public function enter(Isset_ $isset): void
{
foreach ($isset->vars as $key => &$var) {
/** @var array<string, array<class-string<Expr>, true>> */
$subNodes = $this->getConfig(\get_class($var), false)
if (!$subNodes) {
continue;
}
$workVar = $this->extractWorkVar($var);
$needsFixing = false;
foreach ($subNodes as $key => $types) {
if (isset($types[\get_class($workVar->{$key})])) {
$needsFixing = true;
break;
}
}
if (!$needsFixing) {
continue;
}
switch (\get_class($workVar)) {
case ArrayDimFetch::class:
case PropertyFetch::class:
$workVar->var = self::callPoly('returnMe', $workVar->var);
break;
case StaticPropertyFetch::class:
$workVar = $this->wrapBoolean(self::callPoly(
'staticExists',
$workVar->class,
$workVar->name instanceof VarLikeIdentifier ? new String_($workVar->name->name) : $workVar->name,
new LNumber(1)
));
break;
case ClassConstFetch::class:
$workVar = $this->wrapBoolean(self::callPoly(
'staticExists',
$workVar->class,
new String_($workVar->name->name),
new LNumber(0)
));
break;
}
}
}
/**
* Returns the data provided.
*
* @param mixed $data Data
*
* @return mixed
*
* @template T
*
* @psalm-param T $data data
*
* @psalm-return T
*/
public static function returnMe($data)
{
return $data;
}
/**
* Get name of class.
*
* @param class-string|object $class Class
*
* @return class-string
*/
public static function getClass($class): string
{
return \is_string($class) ? $class : \get_class($class);
}
/**
* Check if static property is set
*
* @param class-string|object $class Class
* @param string $property Property name
* @param boolean $propertyOrConstant Whether to fetch the property or the constant
*
* @return boolean
*/
public static function staticExists($class, string $property, bool $propertyOrConstant): bool
{
$reflectionClass = new ReflectionClass($class);
$class = new self::getClass($class);
if ($propertyOrConstant) {
try {
$reflection = $reflectionClass->getProperty($property);
} catch (ReflectionException $e) {
return false;
}
} else if (PHP_VERSION_ID >= 70100) {
try {
$reflection = new ReflectionClassConstant($class, $property);
} catch (ReflectionException $e) {
return false;
}
} else {
return isset($reflectionClass->getConstants()[$property];
}
$classCaller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? '';
$allowProtected = false;
$allowPrivate = false;
if ($classCaller) {
if ($class === $classCaller) {
$allowProtected = $allowPrivate = true;
} else if ($reflectionClass->isSubclassOf($classCaller) || (new ReflectionClass($classCaller))->isSubclassOf($class)) {
$allowProtected = true;
}
}
if ($reflection->isPrivate()) {
return $allowPrivate ? $reflection->getValue() !== null : false;
}
if ($reflection->isProtected()) {
return $allowProtected ? $reflection->getValue() !== null : false;
}
return $reflection->getValue() !== null;
}
}

View File

@ -1,241 +0,0 @@
<?php declare(strict_types=1);
namespace Phabel\Plugin;
use Phabel\Plugin;
use PhpParser\ErrorHandler;
use PhpParser\NameContext;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt;
class NameResolver extends Plugin
{
/** @var NameContext Naming context */
protected $nameContext;
/**
* Constructs a name resolution visitor.
*
* Options:
* * preserveOriginalNames (default false): An "originalName" attribute will be added to
* all name nodes that underwent resolution.
* * replaceNodes (default true): Resolved names are replaced in-place. Otherwise, a
* resolvedName attribute is added. (Names that cannot be statically resolved receive a
* namespacedName attribute, as usual.)
*
* @param ErrorHandler|null $errorHandler Error handler
* @param array $options Options
*/
public function __construct(ErrorHandler $errorHandler = null, array $options = [])
{
$this->nameContext = new NameContext($errorHandler ?? new ErrorHandler\Throwing);
}
/**
* Get name resolution context.
*
* @return NameContext
*/
public function getNameContext(): NameContext
{
return $this->nameContext;
}
public function beforeTraverse(array $nodes)
{
$this->nameContext->startNamespace();
return null;
}
public function enterNode(Node $node)
{
if ($node instanceof Stmt\Namespace_) {
$this->nameContext->startNamespace($node->name);
} elseif ($node instanceof Stmt\Use_) {
foreach ($node->uses as $use) {
$this->addAlias($use, $node->type, null);
}
} elseif ($node instanceof Stmt\GroupUse) {
foreach ($node->uses as $use) {
$this->addAlias($use, $node->type, $node->prefix);
}
} elseif ($node instanceof Stmt\Class_) {
if (null !== $node->extends) {
$node->extends = $this->resolveClassName($node->extends);
}
foreach ($node->implements as &$interface) {
$interface = $this->resolveClassName($interface);
}
if (null !== $node->name) {
$this->addNamespacedName($node);
}
} elseif ($node instanceof Stmt\Interface_) {
foreach ($node->extends as &$interface) {
$interface = $this->resolveClassName($interface);
}
$this->addNamespacedName($node);
} elseif ($node instanceof Stmt\Trait_) {
$this->addNamespacedName($node);
} elseif ($node instanceof Stmt\Function_) {
$this->addNamespacedName($node);
$this->resolveSignature($node);
} elseif ($node instanceof Stmt\ClassMethod
|| $node instanceof Expr\Closure
|| $node instanceof Expr\ArrowFunction
) {
$this->resolveSignature($node);
} elseif ($node instanceof Stmt\Property) {
if (null !== $node->type) {
$node->type = $this->resolveType($node->type);
}
} elseif ($node instanceof Stmt\Const_) {
foreach ($node->consts as $const) {
$this->addNamespacedName($const);
}
} elseif ($node instanceof Expr\StaticCall
|| $node instanceof Expr\StaticPropertyFetch
|| $node instanceof Expr\ClassConstFetch
|| $node instanceof Expr\New_
|| $node instanceof Expr\Instanceof_
) {
if ($node->class instanceof Name) {
$node->class = $this->resolveClassName($node->class);
}
} elseif ($node instanceof Stmt\Catch_) {
foreach ($node->types as &$type) {
$type = $this->resolveClassName($type);
}
} elseif ($node instanceof Expr\FuncCall) {
if ($node->name instanceof Name) {
$node->name = $this->resolveName($node->name, Stmt\Use_::TYPE_FUNCTION);
}
} elseif ($node instanceof Expr\ConstFetch) {
$node->name = $this->resolveName($node->name, Stmt\Use_::TYPE_CONSTANT);
} elseif ($node instanceof Stmt\TraitUse) {
foreach ($node->traits as &$trait) {
$trait = $this->resolveClassName($trait);
}
foreach ($node->adaptations as $adaptation) {
if (null !== $adaptation->trait) {
$adaptation->trait = $this->resolveClassName($adaptation->trait);
}
if ($adaptation instanceof Stmt\TraitUseAdaptation\Precedence) {
foreach ($adaptation->insteadof as &$insteadof) {
$insteadof = $this->resolveClassName($insteadof);
}
}
}
}
return null;
}
private function addAlias(Stmt\UseUse $use, $type, Name $prefix = null)
{
// Add prefix for group uses
$name = $prefix ? Name::concat($prefix, $use->name) : $use->name;
// Type is determined either by individual element or whole use declaration
$type |= $use->type;
$this->nameContext->addAlias(
$name,
(string) $use->getAlias(),
$type,
$use->getAttributes()
);
}
/** @param Stmt\Function_|Stmt\ClassMethod|Expr\Closure $node */
private function resolveSignature($node)
{
foreach ($node->params as $param) {
$param->type = $this->resolveType($param->type);
}
$node->returnType = $this->resolveType($node->returnType);
}
private function resolveType($node)
{
if ($node instanceof Name) {
return $this->resolveClassName($node);
}
if ($node instanceof Node\NullableType) {
$node->type = $this->resolveType($node->type);
return $node;
}
if ($node instanceof Node\UnionType) {
foreach ($node->types as &$type) {
$type = $this->resolveType($type);
}
return $node;
}
return $node;
}
/**
* Resolve name, according to name resolver options.
*
* @param Name $name Function or constant name to resolve
* @param int $type One of Stmt\Use_::TYPE_*
*
* @return Name Resolved name, or original name with attribute
*/
protected function resolveName(Name $name, int $type): Name
{
if (!$this->replaceNodes) {
$resolvedName = $this->nameContext->getResolvedName($name, $type);
if (null !== $resolvedName) {
$name->setAttribute('resolvedName', $resolvedName);
} else {
$name->setAttribute('namespacedName', FullyQualified::concat(
$this->nameContext->getNamespace(),
$name,
$name->getAttributes()
));
}
return $name;
}
if ($this->preserveOriginalNames) {
// Save the original name
$originalName = $name;
$name = clone $originalName;
$name->setAttribute('originalName', $originalName);
}
$resolvedName = $this->nameContext->getResolvedName($name, $type);
if (null !== $resolvedName) {
return $resolvedName;
}
// unqualified names inside a namespace cannot be resolved at compile-time
// add the namespaced version of the name as an attribute
$name->setAttribute('namespacedName', FullyQualified::concat(
$this->nameContext->getNamespace(),
$name,
$name->getAttributes()
));
return $name;
}
protected function resolveClassName(Name $name)
{
return $this->resolveName($name, Stmt\Use_::TYPE_NORMAL);
}
protected function addNamespacedName(Node $node)
{
$node->namespacedName = Name::concat(
$this->nameContext->getNamespace(),
(string) $node->name
);
}
}

View File

View File

@ -125,7 +125,7 @@ class Traverser
public function traverseAst(Node &$node, SplQueue $pluginQueue = null): void
{
$context = new Context;
$context->parents->push($node);
$context->push($node);
foreach ($pluginQueue ?? $this->packageQueue ?? $this->queue as $queue) {
$this->traverseNode($ast, $queue, $context);
}
@ -158,7 +158,7 @@ class Traverser
}
}
}
$context->parents->push($node);
$context->push($node);
foreach ($node->getSubNodeNames() as $name) {
$node->setAttribute('currentNode', $name);
@ -177,7 +177,7 @@ class Traverser
$this->traverseNode($subNode, $plugins, $context);
}
}
$context->parents->pop();
$context->pop();
foreach ($plugins as $plugin) {
foreach (PluginCache::leaveMethods(\get_class($plugin)) as $type => $methods) {
if (!$node instanceof $type) {

View File

@ -1,5 +1,9 @@
<?php
// This file generates an array containing all possible expression nodes, generated using default parameters.
// Then, for each expression that accepts another expression as subnode, it tries to use all the expressions generated in the previous step,
// for each subnode, to test compatibility with various versions of the PHP lexer.
use HaydenPierce\ClassFinder\ClassFinder;
use PhpParser\Builder\Class_;
use PhpParser\Builder\Method;
@ -30,7 +34,7 @@ function format(Node $code): string
->addStmt($code)
->getNode()
)->getNode();
$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
$prettyPrinter = new PhpParser\PrettyPrinter\Standard(['shortArraySyntax' => true]);
return $prettyPrinter->prettyPrintFile([$code]);
}
function readUntilPrompt($resource): string
@ -63,11 +67,10 @@ function checkSyntaxVersion(int $version, string $code): bool
\fputs($pipes[$version][0], $code);
if ($hasPrompt) {
$result = \str_replace(['{', '}'], '', \substr(\preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', readUntilPrompt($pipes[$version][1])), \strlen($code)));
//var_dump($code, "REsult for $version is: " .trim($result));
} else {
$result = readUntilPrompt($pipes[$version][1]);
//var_dump($code, trim($result));
}
//var_dump($code, "Result for $version is: " .trim($result));
$result = \trim($result);
return \strlen($result) === 0;
}
@ -150,12 +153,12 @@ foreach ($expressions as $expr) {
$argTypes[$key] = [false, [Expr::class]];
$arguments[] = new Variable("test");
break;
case Name::class:
$arguments[] = new Name('self');
break;
case Variable::class:
$arguments[] = new Variable("test");
break;
case Name::class:
$arguments[] = new Name('self');
break;
case 'array':
if (\in_array('Expr[]', $types[$param->getName()] ?? [])) {
$argTypes[$key] = [true, [Expr::class]];
@ -209,7 +212,7 @@ foreach ($instanceArgTypes as $class => $argTypes) {
foreach ($types as $type) {
switch ($type) {
case Expr::class:
$possibleValues = array_merge($possibleValues, $exprInstances);
$possibleValues = \array_merge($possibleValues, $exprInstances);
break;
case Name::class:
$possibleValues[] = new Name('self');
@ -217,7 +220,10 @@ foreach ($instanceArgTypes as $class => $argTypes) {
case Variable::class:
$possibleValues[] = new Variable("test");
break;
case Identifier::class;
case Identifier::class:
$possibleValues[] = new Identifier('test');
break;
case VarLikeIdentifier::class:
$possibleValues[] = new Identifier('test');
break;
case StmtClass_::class:
@ -227,12 +233,15 @@ foreach ($instanceArgTypes as $class => $argTypes) {
case 'string':
$possibleValues[] = 'test';
break;
case 'null':
$possibleValues[] = null;
break;
default:
throw new Exception($type);
}
}
foreach ($possibleValues as $arg) {
$subVersion = \max(is_object($arg) ? $versionMap[\get_class($arg)] : 0, $versionMap[$class]);
$subVersion = \max($versionMap[\get_debug_type($arg)] ?? 0, $versionMap[$class]);
$arguments = $baseArgs;
$arguments[$key] = $isArray ? [$arg] : $arg;
@ -240,46 +249,27 @@ foreach ($instanceArgTypes as $class => $argTypes) {
$code = format($prev = new $class(...$arguments));
$curVersion = checkSyntax($code, $subVersion);
if ($curVersion && $curVersion !== $subVersion) {
$result['main'][$curVersion][$class][$name][$arg] = true;
$result['main'][$curVersion][$class][$name][\get_debug_type($arg)] = true;
echo "Min $curVersion for $code\n";
}
$code = format(new Isset_([$prev]));
$curVersion = checkSyntax($code, $subVersion);
if ($curVersion && $curVersion !== $subVersion) {
$result['isset'][$curVersion][$class][$name][\get_class($expr)] = true;
$result['isset'][$curVersion][$class][$name][\get_debug_type($arg)] = true;
echo "Min $curVersion for $code\n";
}
}
}
}
foreach ($instanceArgs as $class => $argumentsOld) {
foreach ($argumentsOld as $key => $argument) {
$name = $instanceArgNames[$class][$key];
if ($argument instanceof Expr || (\is_array($argument) && \count($argument))) {
foreach ($exprInstances as $expr) {
$subVersion = \max($versionMap[\get_class($expr)], $versionMap[$class]);
$arguments = $argumentsOld;
$arguments[$key] = \is_array($argument) ? [$expr] : $expr;
$code = format($prev = new $class(...$arguments));
$curVersion = checkSyntax($code, $subVersion);
if ($curVersion && $curVersion !== $subVersion) {
$result['main'][$curVersion][$class][$name][\get_class($expr)] = true;
echo "Min $curVersion for $code\n";
}
$code = format(new Isset_([$prev]));
$curVersion = checkSyntax($code, $subVersion);
if ($curVersion && $curVersion !== $subVersion) {
$result['isset'][$curVersion][$class][$name][\get_class($expr)] = true;
echo "Min $curVersion for $code\n";
}
}
}
$keys = [];
foreach ($result['isset'] as $version) {
$keys = array_merge_recursive($keys, $version);
}
foreach ($keys as &$values) {
$values = array_keys($values);
}
$allClassesKeys = \array_fill_keys($allClasses, true);
var_dump($keys);
\file_put_contents('result.php', '<?php $result = '.\var_export($result, true).";");