Consider side effects when inserting statements before other statements

This commit is contained in:
Daniil Gentili 2020-10-04 17:40:48 +02:00
parent af55021140
commit 1265dd87ff
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
7 changed files with 323 additions and 226 deletions

View File

@ -10,7 +10,8 @@
"phpunit/phpunit": "^7 | ^8 | ^9",
"amphp/php-cs-fixer-config": "dev-master",
"composer/composer": "^1|^2",
"haydenpierce/class-finder": "^0.4.2"
"haydenpierce/class-finder": "^0.4.2",
"vimeo/psalm": "dev-master"
},
"license": "MIT",
"authors": [{

View File

@ -17,6 +17,7 @@ use PhpParser\Node\Expr\Cast\Bool_;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Ternary;
use PhpParser\Node\Expr\Variable;
@ -50,7 +51,9 @@ class Context
*/
public function __construct()
{
/** @var SplStack<Node> */
$this->parents = new SplStack;
/** @var SplStack<VariableContext> */
$this->variables = new SplStack;
}
/**
@ -146,13 +149,13 @@ class Context
/**
* Insert nodes before node.
*
* @param Node $node Node before which to insert nodes
* @param Node ...$nodes Nodes to insert
* @param Node $node Node before which to insert nodes
* @param Node ...$insert Nodes to insert
* @return void
*/
public function insertBefore(Node $node, Node ...$nodes): void
public function insertBefore(Node $node, Node ...$insert): void
{
if (empty($nodes)) {
if (empty($insert)) {
return;
}
$found = false;
@ -165,106 +168,105 @@ class Context
if (!$found) {
throw new \RuntimeException('Node is not a part of the current AST stack!');
}
$this->insertBeforeParent($parent, $nodes);
}
/**
* Insert nodes before node.
*
* @param Node $node Node before which to insert nodes
* @param Node[] $nodes Nodes to insert
* @return void
*/
private function insertBeforeParent(Node $parent, array $nodes): void
{
$subNode = $parent->getAttribute('currentNode');
if ($subNode === 'stmts') {
$subNodeIndex = $parent->getAttribute('currentNodeIndex');
\array_splice($parent->{$subNode}, $subNodeIndex, 0, $nodes);
/** @var string */
$nodeKey = $parent->getAttribute('currentNode');
if ($nodeKey === 'stmts') {
/** @var int */
$nodeKeyIndex = $parent->getAttribute('currentNodeIndex');
\array_splice($parent->{$nodeKey}, $nodeKeyIndex, 0, $insert);
$skips = $parent->getAttribute('skipNodes', []);
$skips []= $subNodeIndex+\count($nodes);
$skips []= $nodeKeyIndex+\count($insert);
$parent->setAttribute('skipNodes', $skips);
$parent->setAttribute('currentNodeIndex', $subNodeIndex - 1);
} else { // Cannot insert, is not in a statement
$curNode = &$parent->{$subNode};
if ($curNode instanceof BooleanOr && $subNode === 'right') {
$result = $this->getVariable();
$nodes = new If_(
$curNode->left,
[
'stmts' => [
new Assign($result, BuilderHelpers::normalizeValue(true))
],
'else' => [
...$nodes,
new Assign($result, new Bool_($curNode->right))
]
$parent->setAttribute('currentNodeIndex', $nodeKeyIndex - 1);
return; // Done, inserted!
}
// Cannot insert, parent is not a statement
$node = &$parent->{$nodeKey};
// If we insert before a conditional branch of a conditional expression,
// make sure the conditional branch has no side effects;
// if it does, turn the entire conditional expression into an if, and bubble it up
//
// Unless we want to go crazy, do not consider side effect evaluation order for stuff like function call arguments, maths and so on.
//
if ($node instanceof BooleanOr && $nodeKey === 'right' && Tools::hasSideEffects($node->right)) {
$result = $this->getVariable();
$insert = new If_(
$node->left,
[
'stmts' => [
new Assign($result, BuilderHelpers::normalizeValue(true))
],
'else' => [
...$insert,
new Assign($result, new Bool_($node->right))
]
);
$curNode = $result;
} elseif ($curNode instanceof BooleanAnd && $subNode === 'right') {
$result = $this->getVariable();
$nodes = new If_(
$curNode->left,
[
'stmts' => [
...$nodes,
new Assign($result, new Bool_($curNode->right))
],
'else' => [
new Assign($result, BuilderHelpers::normalizeValue(false))
]
]
);
$node = $result;
} elseif ($node instanceof BooleanAnd && $nodeKey === 'right' && Tools::hasSideEffects($node->right)) {
$result = $this->getVariable();
$insert = new If_(
$node->left,
[
'stmts' => [
...$insert,
new Assign($result, new Bool_($node->right))
],
'else' => [
new Assign($result, BuilderHelpers::normalizeValue(false))
]
);
$curNode = $result;
} elseif ($curNode instanceof Ternary && $subNode !== 'cond') {
$result = $this->getVariable();
if (!$curNode->if) { // ?:
$nodes = new If_(
new BooleanNot(
new Assign($result, $curNode->cond)
),
[
'stmts' => [
...$nodes,
new Assign($result, $curNode->else)
]
]
);
} else {
$nodes = new If_(
$curNode->cond,
[
'stmts' => [
...$subNode === 'if' ? $nodes : [],
new Assign($result, $curNode->if)
],
'else' => [
...$subNode === 'else' ? $nodes : [],
new Assign($result, $curNode->else)
]
]
);
}
$curNode = $result;
} elseif ($subNode instanceof Coalesce && $subNode === 'right') {
$result = $this->getVariable();
$nodes = new If_(
Plugin::call(
'is_null',
new Assign($result, $curNode->left)
]
);
$node = $result;
} elseif ($node instanceof Ternary && $nodeKey !== 'cond' && (Tools::hasSideEffects($node->if) || Tools::hasSideEffects($node->else))) {
$result = $this->getVariable();
if (!$node->if) { // ?:
$insert = new If_(
new BooleanNot(
new Assign($result, $node->cond)
),
[
'stmts' => [
...$nodes,
new Assign($result, $curNode->right)
...$insert,
new Assign($result, $node->else)
]
]
);
} else {
$insert = new If_(
$node->cond,
[
'stmts' => [
...$nodeKey === 'if' ? $insert : [],
new Assign($result, $node->if)
],
'else' => [
...$nodeKey === 'else' ? $insert : [],
new Assign($result, $node->else)
]
]
);
$curNode = $result;
} elseif ($subNode instanceof FuncCall) {
}
$this->insertBefore($parent, $nodes);
$node = $result;
} elseif ($node instanceof Coalesce && $nodeKey === 'right' && Tools::hasSideEffects($node->right)) {
$result = $this->getVariable();
$insert = new If_(
Plugin::call(
'is_null',
new Assign($result, $node->left)
),
[
'stmts' => [
...$insert,
new Assign($result, $node->right)
]
]
);
$node = $result;
}
$this->insertBefore($parent, $insert);
}
/**
* Insert nodes after node.

View File

@ -3,18 +3,10 @@
namespace Phabel;
use Phabel\PluginGraph\PackageContext;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Expression;
use PhpParser\ParserFactory;
use ReflectionClass;
/**
* Plugin.
@ -22,7 +14,7 @@ use ReflectionClass;
* @author Daniil Gentili <daniil@daniil.it>
* @license MIT
*/
abstract class Plugin implements PluginInterface
abstract class Plugin extends Tools implements PluginInterface
{
/**
* Configuration array.
@ -84,82 +76,6 @@ abstract class Plugin implements PluginInterface
{
return true;
}
/**
* Replace node of one type with another.
*
* @param Node $node Original node
* @param string $class Class of new node
* @param array $propertyMap Property map between old and new objects
*
* @psalm-param class-string<Node> $class Class of new node
* @psalm-param array<string, string> $propertyMap Property map between old and new objects
*
* @return Node
*/
public static function replaceType(Node $node, string $class, array $propertyMap = []): Node
{
if ($propertyMap) {
/** @var Node */
$nodeNew = (new ReflectionClass($class))->newInstanceWithoutConstructor();
foreach ($propertyMap as $old => $new) {
$nodeNew->{$new} = $node->{$old};
}
$nodeNew->setAttributes($node->getAttributes());
return $nodeNew;
}
return new $class(
...\array_map(fn (string $name) => $node->{$name}, $node->getSubNodeNames()),
$node->getAttributes()
);
}
/**
* Replace type in-place.
*
* @param Node &$node Original node
* @param string $class Class of new node
* @param array $propertyMap Property map between old and new objects
*
* @psalm-param class-string<Node> $class Class of new node
* @psalm-param array<string, string> $propertyMap Property map between old and new objects
*
* @param-out Node &$node
*
* @return void
*/
public static function replaceTypeInPlace(Node &$node, string $class, array $propertyMap = []): void
{
$node = self::replaceType($node, $class, $propertyMap);
}
/**
* Create variable assignment.
*
* @param Variable $name Variable
* @param Expr $expression Expression
*
* @return Expression
*/
public static function assign(Variable $name, Expr $expression): Expression
{
return new Expression(
new Assign(
$name,
$expression
)
);
}
/**
* Call function.
*
* @param class-string|array{0: class-string, 1: string}|callable-string $name Function name
* @param Expr|Arg ...$parameters Parameters
*
* @return FuncCall|StaticCall
*/
public static function call($name, ...$parameters)
{
$parameters = \array_map(fn ($data) => $data instanceof Arg ? $data : new Arg($data), $parameters);
return \is_array($name) ? new StaticCall(new Name($name[0]), $name[1], $parameters) : new FuncCall(new Name($name), $parameters);
}
/**
* Call polyfill function from current plugin.
*
@ -172,48 +88,6 @@ abstract class Plugin implements PluginInterface
{
return self::call([static::class, $name], ...$parameters);
}
/**
* Call method of object.
*
* @param Expr $name Object name
* @param string $method Method
* @param Expr|Arg ...$parameters Parameters
*
* @return MethodCall
*/
public static function callMethod(Expr $name, string $method, ...$parameters): MethodCall
{
$parameters = \array_map(fn ($data) => $data instanceof Arg ? $data : new Arg($data), $parameters);
return new MethodCall($name, $method, $parameters);
}
/**
* Convert array, int or other literal to node.
*
* @param mixed $data Data to convert
*
* @return Node
*/
public static function toLiteral($data): Node
{
return self::toNode(\var_export($data, true));
}
/**
* Convert code to node.
*
* @param string $code Code
*
* @memoize $code
*
* @return Node
*/
public static function toNode(string $code): Node
{
$res = (new ParserFactory)->create(ParserFactory::PREFER_PHP7)->parse('<?php '.$code);
if ($res === null || empty($res) || !$res[0] instanceof Expression || !isset($res[0]->expr)) {
throw new \RuntimeException('Invalid code was provided!');
}
return $res[0]->expr;
}
/**
* {@inheritDoc}
*/

View File

@ -4,6 +4,7 @@ namespace Phabel\PluginGraph;
use Phabel\Plugin;
use Phabel\PluginInterface;
use SplQueue;
/**
* Graph API wrapper.
@ -54,7 +55,7 @@ class Graph
*
* @return SplQueue<SplQueue<Plugin>>
*/
public function flatten(): \SplQueue
public function flatten(): SplQueue
{
return $this->graph->flatten();
}

View File

@ -107,11 +107,11 @@ class GraphInternal
*
* @return SplQueue<SplQueue<Plugin>>
*/
public function flatten(): \SplQueue
public function flatten(): SplQueue
{
if (!$this->plugins) {
/** @var SplQueue<SplQueue<Plugin>> */
return new \SplQueue;
return new SplQueue;
}
if ($this->unlinkedNodes->count()) {
foreach ($this->unlinkedNodes as $node) {

219
src/Tools.php Normal file
View File

@ -0,0 +1,219 @@
<?php
namespace Phabel;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\AssignOp;
use PhpParser\Node\Expr\AssignRef;
use PhpParser\Node\Expr\Cast\String_;
use PhpParser\Node\Expr\Clone_;
use PhpParser\Node\Expr\Eval_;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Include_;
use PhpParser\Node\Expr\List_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\NullsafeMethodCall;
use PhpParser\Node\Expr\NullsafePropertyFetch;
use PhpParser\Node\Expr\PostDec;
use PhpParser\Node\Expr\PostInc;
use PhpParser\Node\Expr\PreDec;
use PhpParser\Node\Expr\PreInc;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\Expr\YieldFrom;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Expression;
use PhpParser\ParserFactory;
use ReflectionClass;
/**
* Various tools.
*
* @author Daniil Gentili <daniil@daniil.it>
* @license MIT
*/
abstract class Tools
{
/**
* Replace node of one type with another.
*
* @param Node $node Original node
* @param string $class Class of new node
* @param array $propertyMap Property map between old and new objects
*
* @psalm-param class-string<Node> $class Class of new node
* @psalm-param array<string, string> $propertyMap Property map between old and new objects
*
* @return Node
*/
public static function replaceType(Node $node, string $class, array $propertyMap = []): Node
{
if ($propertyMap) {
$nodeNew = (new ReflectionClass($class))->newInstanceWithoutConstructor();
foreach ($propertyMap as $old => $new) {
$nodeNew->{$new} = $node->{$old};
}
$nodeNew->setAttributes($node->getAttributes());
return $nodeNew;
}
return new $class(
...\array_map(fn (string $name) => $node->{$name}, $node->getSubNodeNames()),
$node->getAttributes()
);
}
/**
* Replace type in-place.
*
* @param Node &$node Original node
* @param string $class Class of new node
* @param array $propertyMap Property map between old and new objects
*
* @psalm-param class-string<Node> $class Class of new node
* @psalm-param array<string, string> $propertyMap Property map between old and new objects
*
* @param-out Node &$node
*
* @return void
*/
public static function replaceTypeInPlace(Node &$node, string $class, array $propertyMap = []): void
{
$node = self::replaceType($node, $class, $propertyMap);
}
/**
* Create variable assignment.
*
* @param Variable $name Variable
* @param Expr $expression Expression
*
* @return Expression
*/
public static function assign(Variable $name, Expr $expression): Expression
{
return new Expression(
new Assign(
$name,
$expression
)
);
}
/**
* Call function.
*
* @param array{0: class-string, 1: string}|callable-string $name Function name
* @param Expr|Arg ...$parameters Parameters
*
* @return FuncCall|StaticCall
*/
public static function call($name, ...$parameters)
{
$parameters = \array_map(fn ($data) => $data instanceof Arg ? $data : new Arg($data), $parameters);
return \is_array($name)
? new StaticCall(new Name($name[0]), $name[1], $parameters)
: new FuncCall(new Name($name), $parameters);
}
/**
* Call method of object.
*
* @param Expr $name Object name
* @param string $method Method
* @param Expr|Arg ...$parameters Parameters
*
* @return MethodCall
*/
public static function callMethod(Expr $name, string $method, ...$parameters): MethodCall
{
$parameters = \array_map(fn ($data) => $data instanceof Arg ? $data : new Arg($data), $parameters);
return new MethodCall($name, $method, $parameters);
}
/**
* Convert array, int or other literal to node.
*
* @param mixed $data Data to convert
*
* @return Node
*/
public static function toLiteral($data): Node
{
return self::toNode(\var_export($data, true));
}
/**
* Convert code to node.
*
* @param string $code Code
*
* @memoize $code
*
* @return Node
*/
public static function toNode(string $code): Node
{
$res = (new ParserFactory)->create(ParserFactory::PREFER_PHP7)->parse('<?php '.$code);
if ($res === null || empty($res) || !$res[0] instanceof Expression || !isset($res[0]->expr)) {
throw new \RuntimeException('Invalid code was provided!');
}
return $res[0]->expr;
}
/**
* Check if this node or any child node have any side effects (like calling other methods, or assigning variables).
*
* @param ?Expr $node Node
*
* @return bool
*/
public static function hasSideEffects(?Expr $node): bool
{
if (!$node) {
return false;
}
if ($node->hasAttribute('hasSideEffects')
|| $node instanceof String_ // __toString
|| $node instanceof ArrayDimFetch // offsetSet/offsetGet
|| $node instanceof Assign
|| $node instanceof AssignOp
|| $node instanceof AssignRef
|| $node instanceof Clone_ // __clone
|| $node instanceof Eval_
|| $node instanceof FuncCall
|| $node instanceof Include_
|| $node instanceof List_ // offsetGet/offsetSet
|| $node instanceof New_
|| $node instanceof NullsafeMethodCall
|| $node instanceof NullsafePropertyFetch
|| $node instanceof PostDec
|| $node instanceof PostInc
|| $node instanceof PreDec
|| $node instanceof PreInc
|| $node instanceof PropertyFetch
|| $node instanceof StaticCall
|| $node instanceof Yield_
|| $node instanceof YieldFrom
) {
$node->setAttribute('hasSideEffects', true);
return true;
}
foreach ($node->getAttributes() as $name) {
if ($node->{$name} instanceof Expr) {
if (self::hasSideEffects($node->{$name})) {
$node->setAttribute('hasSideEffects', true);
return true;
}
} elseif (\is_array($node->{$name})) {
foreach ($node->{$name} as $var) {
if ($var instanceof Expr && self::hasSideEffects($var)) {
$node->setAttribute('hasSideEffects', true);
return true;
}
}
}
}
return false;
}
}

View File

@ -28,9 +28,9 @@ class Traverser
/**
* Plugin queue for specific package.
*
* @return SplQueue<SplQueue<Plugin>>
* @return SplQueue<SplQueue<Plugin>>|null
*/
private SplQueue $packageQueue;
private ?SplQueue $packageQueue;
/**
* Generate traverser from basic plugin instances.
*
@ -114,7 +114,7 @@ class Traverser
} elseif (!$reducedQueue->count()) {
return;
}
$ast = new RootNode($this->parser->parse(\file_get_contents($file)));
$ast = new RootNode($this->parser->parse(\file_get_contents($file)) ?? []);
$this->traverseAst($ast, $reducedQueue);
}
/**