phabel/src/Context.php

320 lines
10 KiB
PHP
Raw Permalink Normal View History

2020-08-03 21:31:32 +02:00
<?php
namespace Phabel;
2020-09-05 20:52:54 +02:00
use PhpParser\BuilderHelpers;
2020-11-01 20:44:11 +01:00
use PhpParser\ErrorHandler\Throwing;
use PhpParser\NameContext;
2020-08-14 20:40:01 +02:00
use PhpParser\Node;
2020-09-01 12:52:44 +02:00
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\AssignOp;
use PhpParser\Node\Expr\AssignRef;
2020-09-05 20:52:54 +02:00
use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
use PhpParser\Node\Expr\BinaryOp\BooleanOr;
use PhpParser\Node\Expr\BinaryOp\Coalesce;
use PhpParser\Node\Expr\BooleanNot;
use PhpParser\Node\Expr\Cast\Bool_;
2020-09-01 12:52:44 +02:00
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
2020-09-05 20:52:54 +02:00
use PhpParser\Node\Expr\Ternary;
2020-09-01 12:52:44 +02:00
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Param;
2020-09-05 20:52:54 +02:00
use PhpParser\Node\Stmt\If_;
2020-11-01 20:44:11 +01:00
use PhpParser\NodeVisitor\NameResolver;
2020-08-14 14:43:29 +02:00
use SplStack;
2020-08-13 18:30:12 +02:00
/**
2020-09-05 22:35:30 +02:00
* AST Context.
*
2020-08-13 18:30:12 +02:00
* @author Daniil Gentili <daniil@daniil.it>
2020-08-14 14:43:29 +02:00
* @license MIT
2020-08-13 18:30:12 +02:00
*/
2020-08-03 21:31:32 +02:00
class Context
{
2020-08-14 14:43:29 +02:00
/**
2020-08-14 20:40:01 +02:00
* Parent nodes stack.
*
2020-08-14 14:43:29 +02:00
* @var SplStack<Node>
*/
public SplStack $parents;
2020-08-30 20:27:43 +02:00
/**
2020-09-01 12:52:44 +02:00
* Declared variables stack.
*
* @var SplStack<VariableContext>
2020-08-30 20:27:43 +02:00
*/
2020-09-01 12:52:44 +02:00
public SplStack $variables;
2020-11-01 20:44:11 +01:00
/**
* Name resolver.
*
* @var NameResolver
*/
public NameResolver $nameResolver;
2020-08-30 20:27:43 +02:00
/**
2020-09-01 12:52:44 +02:00
* Constructor.
2020-08-30 20:27:43 +02:00
*/
2020-08-14 14:43:29 +02:00
public function __construct()
{
/** @var SplStack<Node> */
2020-08-14 14:43:29 +02:00
$this->parents = new SplStack;
/** @var SplStack<VariableContext> */
2020-09-01 12:52:44 +02:00
$this->variables = new SplStack;
2020-11-01 20:44:11 +01:00
$this->nameResolver = new NameResolver(new Throwing, ['replaceNodes' => false]);
$this->nameResolver->beforeTraverse([]);
2020-08-14 14:43:29 +02:00
}
2020-08-30 20:27:43 +02:00
/**
2020-09-01 12:52:44 +02:00
* Push node.
2020-08-30 20:27:43 +02:00
*
* @param Node $node Node
2020-09-01 12:52:44 +02:00
*
2020-08-30 20:27:43 +02:00
* @return void
*/
public function push(Node $node): void
{
$this->parents->push($node);
2020-09-01 12:52:44 +02:00
if ($node instanceof RootNode) {
$this->variables->push(new VariableContext);
2020-11-01 20:44:11 +01:00
} else {
$this->nameResolver->enterNode($node);
2020-09-01 12:52:44 +02:00
}
if ($node instanceof FunctionLike) {
$variables = \array_fill_keys(
\array_map(
fn (Param $param): string => $param->var->name,
$node->getParams()
),
true
);
if ($node instanceof Closure) {
foreach ($node->uses as $use) {
$variables[$use->var->name] = true;
if ($use->byRef) {
$this->variables->top()->addVar($use->var->name);
}
}
2020-09-01 17:36:13 +02:00
} elseif ($node instanceof ArrowFunction) {
$variables += $this->variables->top()->getVars();
2020-09-01 12:52:44 +02:00
}
2020-09-01 17:36:13 +02:00
$this->variables->push(new VariableContext($variables));
2020-09-01 12:52:44 +02:00
} elseif ($node instanceof Assign || $node instanceof AssignOp || $node instanceof AssignRef) {
do {
$node = $node->var;
} while ($node instanceof ArrayDimFetch && $node->var instanceof ArrayDimFetch);
if ($node instanceof Variable && \is_string($node->name)) {
$this->variables->top()->addVar($node->name);
}
} elseif ($node instanceof MethodCall || $node instanceof StaticCall || $node instanceof FuncCall) {
// Cover reference parameters
foreach ($node->args as $argument) {
$argument = $argument->value;
do {
$argument = $argument->var;
} while ($argument instanceof ArrayDimFetch && $argument->var instanceof ArrayDimFetch);
if ($argument instanceof Variable && \is_string($argument->name)) {
$this->variables->top()->addVar($argument->name);
2020-09-01 14:31:23 +02:00
}
2020-09-01 12:52:44 +02:00
}
2020-08-30 20:27:43 +02:00
}
}
/**
2020-09-01 12:52:44 +02:00
* Pop node.
2020-08-30 20:27:43 +02:00
*
* @return void
*/
public function pop(): void
{
2020-09-01 12:52:44 +02:00
$popped = $this->parents->pop();
if ($popped instanceof RootNode || ($popped instanceof FunctionLike && !$popped instanceof ArrowFunction)) {
$this->variables->pop();
2020-08-30 20:27:43 +02:00
}
}
2020-09-01 12:52:44 +02:00
/**
* Return new unoccupied variable.
*
* @return Variable
*/
public function getVariable(): Variable
{
return new Variable($this->parents->top()->getVar());
}
2020-08-14 20:40:01 +02:00
/**
2020-09-05 20:52:54 +02:00
* Get child currently being iterated on.
2020-08-14 20:40:01 +02:00
*
* @param Node $node
2020-09-05 20:52:54 +02:00
* @return Node
*/
public static function getCurrentChild(Node $node): Node
{
if (!$subNode = $node->getAttribute('currentNode')) {
throw new \RuntimeException('Node is not a part of the current AST stack!');
}
$child = $node->{$subNode};
if ($index = $node->getAttribute('currentNodeIndex')) {
return $child[$index];
}
return $child;
}
/**
* Insert nodes before node.
*
* @param Node $node Node before which to insert nodes
* @param Node ...$insert Nodes to insert
2020-08-14 20:40:01 +02:00
* @return void
*/
public function insertBefore(Node $node, Node ...$insert): void
2020-08-14 20:40:01 +02:00
{
if (empty($insert)) {
2020-08-23 20:54:56 +02:00
return;
}
2020-09-05 20:52:54 +02:00
$found = false;
foreach ($this->parents as $parent) {
if ($this->getCurrentChild($parent) === $node) {
$found = true;
break;
}
}
if (!$found) {
throw new \RuntimeException('Node is not a part of the current AST stack!');
}
/** @var string */
$nodeKey = $parent->getAttribute('currentNode');
if ($nodeKey === 'stmts') {
/** @var int */
$nodeKeyIndex = $parent->getAttribute('currentNodeIndex');
\array_splice($parent->{$nodeKey}, $nodeKeyIndex, 0, $insert);
2020-09-05 20:52:54 +02:00
$skips = $parent->getAttribute('skipNodes', []);
$skips []= $nodeKeyIndex+\count($insert);
2020-09-05 20:52:54 +02:00
$parent->setAttribute('skipNodes', $skips);
$parent->setAttribute('currentNodeIndex', $nodeKeyIndex - 1);
return; // Done, inserted!
}
2020-10-05 11:03:24 +02:00
// Cannot insert, parent is not a statement
$node = &$parent->{$nodeKey};
// If we insert before a conditional branch of a conditional expression,
2020-10-05 11:03:24 +02:00
// make sure the conditional branch has no side effects;
// if it does, turn the entire conditional expression into an if, and bubble it up
2020-10-05 11:03:24 +02:00
//
// 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))
]
]
);
$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))
]
]
);
$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)
),
2020-09-05 20:52:54 +02:00
[
'stmts' => [
...$insert,
new Assign($result, $node->else)
2020-09-05 20:52:54 +02:00
]
]
);
} else {
$insert = new If_(
$node->cond,
2020-09-05 20:52:54 +02:00
[
'stmts' => [
...$nodeKey === 'if' ? $insert : [],
new Assign($result, $node->if)
2020-09-05 20:52:54 +02:00
],
'else' => [
...$nodeKey === 'else' ? $insert : [],
new Assign($result, $node->else)
2020-09-05 20:52:54 +02:00
]
]
);
}
$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;
2020-09-05 20:52:54 +02:00
}
$this->insertBefore($parent, $insert);
2020-08-14 20:40:01 +02:00
}
/**
* Insert nodes after node.
*
2020-09-05 20:52:54 +02:00
* @param Node $node Node ater which to insert nodes
* @param Node ...$nodes Nodes to insert
2020-08-14 20:40:01 +02:00
* @return void
*/
public function insertAfter(Node $node, Node ...$nodes): void
{
2020-09-05 20:52:54 +02:00
if (empty($nodes)) {
return;
}
$found = false;
foreach ($this->parents as $parent) {
if ($this->getCurrentChild($parent) === $node) {
$found = true;
break;
}
}
if (!$found) {
throw new \RuntimeException('Node is not a part of the current AST stack!');
}
$subNode = $parent->getAttribute('currentNode');
$subNodeIndex = $parent->getAttribute('currentNodeIndex');
\array_splice($parent->{$subNode}, $subNodeIndex+1, 0, $nodes);
2020-08-14 20:40:01 +02:00
}
2020-11-01 20:44:11 +01:00
/**
2020-11-04 12:00:16 +01:00
* Gets name context.
2020-11-01 20:44:11 +01:00
*
* @return NameContext
*/
public function getNameContext(): NameContext
{
return $this->nameResolver->getNameContext();
}
2020-08-09 13:49:19 +02:00
}