diff --git a/src/Context.php b/src/Context.php index 507b8f2..f89a99a 100644 --- a/src/Context.php +++ b/src/Context.php @@ -3,7 +3,18 @@ namespace Phabel; use PhpParser\Node; -use PhpParser\Node\Expr\Isset_; +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; +use PhpParser\Node\Expr\Closure; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\FunctionLike; +use PhpParser\Node\Param; use SplStack; /** @@ -19,41 +30,94 @@ class Context */ public SplStack $parents; /** - * Whether we're inside an isset expression + * Declared variables stack. + * + * @var SplStack */ - public bool $insideIsset = false; + public SplStack $variables; /** - * Constructor + * Constructor. */ public function __construct() { $this->parents = new SplStack; + $this->variables = new SplStack; } /** - * Push node + * 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; + if ($node instanceof RootNode) { + $this->variables->push(new VariableContext); + } + 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); + } + } + } + if ($node instanceof ArrowFunction) { + $this->variables->top()->addVars($variables); + } else { + $this->variables->push(new VariableContext($variables)); + } + } 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); + } + } } } /** - * Pop node + * Pop node. * * @return void */ public function pop(): void { - if ($this->parents->pop() instanceof Isset_) { - $this->insideIsset = false; + $popped = $this->parents->pop(); + if ($popped instanceof RootNode || ($popped instanceof FunctionLike && !$popped instanceof ArrowFunction)) { + $this->variables->pop(); } } + /** + * Return new unoccupied variable. + * + * @return Variable + */ + public function getVariable(): Variable + { + return new Variable($this->parents->top()->getVar()); + } /** * Insert nodes before node. * diff --git a/src/Plugin/NestedExpressionFixer.php b/src/Plugin/NestedExpressionFixer.php index a1eebdd..2543261 100644 --- a/src/Plugin/NestedExpressionFixer.php +++ b/src/Plugin/NestedExpressionFixer.php @@ -2,23 +2,31 @@ namespace Phabel\Plugin; +use Phabel\Context; use Phabel\Plugin; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; +use PhpParser\Node\Expr\Assign; +use PhpParser\Node\Expr\BinaryOp\BooleanOr; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Expr\StaticPropertyFetch; +use PhpParser\Node\Expr\Ternary; +use PhpParser\Node\Scalar\LNumber; class NestedExpressionFixer extends Plugin { - public function enter(Expr $expr): void + public function leave(Expr $expr, Context $context): ?Expr { /** @var array, true>> */ $subNodes = $this->getConfig($class = \get_class($expr), false); if (!$subNodes) { - return; + return null; } foreach ($subNodes as $key => $types) { /** @var Expr $value */ @@ -34,9 +42,19 @@ class NestedExpressionFixer extends Plugin case Instanceof_::class: $value = self::callPoly('returnMe', $value); break; + case ClassConstFetch::class: + // For all the following expressions, wrapping in a ternary breaks return-by-ref + case StaticCall::class: + case StaticPropertyFetch::class: case FuncCall::class: - $expr->var = self::callPoly('returnMe', $expr->var); - break; + return new Ternary( + new BooleanOr( + new Assign($value = $context->getVariable(), $value), + new LNumber(1) + ), + $expr, + new LNumber(0) + ); } } } diff --git a/src/Target/Php71/ArrayList.php b/src/Target/Php71/ArrayList.php index 3def2bc..358a772 100644 --- a/src/Target/Php71/ArrayList.php +++ b/src/Target/Php71/ArrayList.php @@ -14,7 +14,7 @@ use PhpParser\Node\Stmt\Foreach_; class ArrayList extends Plugin { /** - * Caled when entering Foreach_ node. + * Called when entering Foreach_ node. * * @param Foreach_ $node Node * diff --git a/src/Target/Php71/ListKey.php b/src/Target/Php71/ListKey.php index 063c23f..4333906 100644 --- a/src/Target/Php71/ListKey.php +++ b/src/Target/Php71/ListKey.php @@ -27,7 +27,7 @@ class ListKey extends Plugin return; } [$node->valueVar, $array] = $this->splitList($node->valueVar); - $node->expr = self::callPoly('destructure', $array, $node->expr); + $node->expr = self::callPoly('destructureForeach', $array, $node->expr); } /** * Parse list assignment with custom keys. @@ -56,23 +56,23 @@ class ListKey extends Plugin return isset($list->items[0]->key); } /** - * Split keyed list into new list assignment and array to pass to destructure function. + * Split keyed list into a new list assignment and array to pass to destructure function. * * @param List_ $list Keyed list * * @return array{0: List_, 1: Array_} */ - private function splitList(List_ $list): array + private static function splitList(List_ $list): array { $newList = []; $keys = []; - $key = 0; // Technically list assignment does not support mixed keys, but we need this for nested assignments + $key = 0; // Technically a list assignment does not support mixed keys, but we need this for nested assignments foreach ($list->items as $item) { if ($item) { $curKey = $item->key ?? $key++; $item->key = null; if ($item->value instanceof List_) { - [$item->value, $keys[$curKey]] = $this->splitList($list); + [$item->value, $keys[$curKey]] = self::splitList($list); } else { $keys[$curKey] = null; } @@ -85,6 +85,22 @@ class ListKey extends Plugin $keys = BuilderHelpers::normalizeValue($keys); return [new List_($newList), $keys]; } + /** + * Destructure array of arrays + * + * @param array $keys Custom keys + * @param array $array Array + * + * @psalm-param array $keys Custom keys + * + * @return \Generator + */ + public static function destructureForeach(array $keys, array $array): \Generator + { + foreach ($array as $value) { + yield self::destructure($keys, $value); + } + } /** * Destructure array. * @@ -98,11 +114,11 @@ class ListKey extends Plugin public static function destructure(array $keys, array $array): array { $res = []; - foreach ($keys as $key => $type) { - if ($type === null) { + foreach ($keys as $key => $subKeys) { + if ($subKeys === null) { $res[] = $array[$key]; } else { - $res[] = self::destructure($type, $array[$key]); + $res[] = self::destructure($subKeys, $array[$key]); } } return $res; diff --git a/src/VariableContext.php b/src/VariableContext.php new file mode 100644 index 0000000..262316e --- /dev/null +++ b/src/VariableContext.php @@ -0,0 +1,86 @@ + + */ + private array $variables; + /** + * Custom variable counter. + */ + private int $counter = 0; + /** + * Constructor. + * + * @param array $variables Initial variables + */ + public function __construct(array $variables = []) + { + $this->variables = $variables; + } + /** + * Add variable. + * + * @param string $var Variable + * + * @return void + */ + public function addVar(string $var): void + { + $this->variables[$var] = true; + } + /** + * Add variables. + * + * @param array $vars Variables + * + * @return void + */ + public function addVars(array $vars): void + { + $this->variables += $vars; + } + /** + * Remove variable. + * + * @param string $var Variable name + * + * @return void + */ + public function removeVar(string $var): void + { + unset($this->variables[$var]); + } + /** + * Check if variable is present. + * + * @param string $var + * @return boolean + */ + public function hasVar(string $var): bool + { + return isset($this->variables[$var]); + } + /** + * Get unused variable name. + * + * @return string + */ + public function getVar(): string + { + do { + $var = 'phabel'.$this->counter; + $this->counter++; + } while (isset($this->variables[$var])); + $this->variables[$var] = true; + return $var; + } +}