packages/sql-catalog/src/Core/Analysis/Derivation/ModifiedNames.php

1<?php
2
3declare(strict_types=1);
4
5namespace SqlCatalog\Core\Analysis\Derivation;
6
7use PhpParser\Node;
8use PhpParser\Node\Expr;
9use PhpParser\Node\Stmt;
10use SqlCatalog\Core\Analysis\Derivation\Objects\ObjectEffects;
11use SqlCatalog\Core\Analysis\Effect\ReferenceEffects;
12use SqlCatalog\Core\Analysis\Effect\WriteEffects;
13use WeakMap;
14
15/**
16 * The names a statement may assign, wherever inside it the assignment is written.
17 *
18 * Walking back over a statement that assigns none of the names still being
19 * looked for learns nothing, so this is what decides whether a branch has to be
20 * split into its arms at all. The answer is remembered per statement, because
21 * the same statement is walked back over once for every path that crosses it.
22 *
23 * @visibility root
24 */
25final class ModifiedNames
26{
27    /**
28     * @var WeakMap<Node, array<string, true>>
29     */
30    private WeakMap $remembered;
31
32    private FreeNames $names;
33
34    private ?ReferenceEffects $references = null;
35
36    /**
37     * Builds the reader over the naming the rest of the derivation uses.
38     */
39    public function __construct(?FreeNames $names = null, private readonly ?ObjectEffects $objects = null)
40    {
41        $this->names = $names ?? new FreeNames();
42        $this->remembered = new WeakMap();
43    }
44
45    /**
46     * Whether the node may assign any of the given names.
47     *
48     * @param array<string, true> $wanted
49     */
50    public function touches(Node $node, array $wanted): bool
51    {
52        $written = $this->of($node);
53
54        return $wanted !== [] && (isset($written[WriteEffects::ALL]) || array_intersect_key($written, $wanted) !== []);
55    }
56
57    /**
58     * Every name the node may assign.
59     *
60     * @return array<string, true>
61     */
62    public function of(Node $node): array
63    {
64        $known = $this->remembered[$node] ?? null;
65        if ($known !== null) {
66            return $known;
67        }
68        $names = $this->collect($node);
69        $this->remembered[$node] = $names;
70
71        return $names;
72    }
73
74    /**
75     * Every name the node may assign, worked out from scratch.
76     *
77     * @return array<string, true>
78     */
79    public function collect(Node $node): array
80    {
81        if ($node instanceof Stmt\Function_ || $node instanceof Stmt\ClassLike || $node instanceof Expr\Closure) {
82            return [];
83        }
84        $names = $this->own($node);
85        $this->references ??= new ReferenceEffects();
86        $names += $this->references->affected($node, $names);
87        foreach (get_object_vars($node) as $sub) {
88            foreach (is_array($sub) ? $sub : [$sub] as $child) {
89                if ($child instanceof Node) {
90                    $names += $this->of($child);
91                }
92            }
93        }
94
95        return $names;
96    }
97
98    /**
99     * The names the node itself assigns, leaving aside what its children assign.
100     *
101     * @return array<string, true>
102     */
103    public function own(Node $node): array
104    {
105        if ($node instanceof Expr\Assign || $node instanceof Expr\AssignOp || $node instanceof Expr\AssignRef) {
106            return $this->targets($node->var) + (new WriteEffects())->own($node);
107        }
108        if ($node instanceof Expr\PreInc || $node instanceof Expr\PostInc
109            || $node instanceof Expr\PreDec || $node instanceof Expr\PostDec) {
110            return $this->targets($node->var);
111        }
112        if ($node instanceof Stmt\Global_ || $node instanceof Stmt\Unset_) {
113            return $this->all($node->vars);
114        }
115        if ($node instanceof Stmt\Static_) {
116            $names = [];
117            foreach ($node->vars as $static) {
118                $names += $this->targets($static->var);
119            }
120
121            return $names;
122        }
123        if ($node instanceof Stmt\Foreach_) {
124            return $this->targets($node->valueVar) + ($node->keyVar === null ? [] : $this->targets($node->keyVar));
125        }
126        if ($node instanceof Stmt\Catch_ && $node->var !== null) {
127            return $this->targets($node->var);
128        }
129
130        $written = (new WriteEffects())->own($node)
131            + ($node instanceof Expr\CallLike ? ($this->objects?->writes($node) ?? []) : []);
132        $this->references ??= new ReferenceEffects();
133
134        return $written + ($node instanceof Expr\CallLike ? $this->references->affected($node, $written) : []);
135    }
136
137    /**
138     * Whether object calls are retained as whole expression steps.
139     */
140    public function tracksObjects(): bool
141    {
142        return $this->objects !== null;
143    }
144
145    /**
146     * The names written by assigning to the given target.
147     *
148     * @return array<string, true>
149     */
150    public function targets(Node $target): array
151    {
152        if ($target instanceof Expr\ArrayDimFetch && $this->objects !== null) {
153            return $this->targets($target->var);
154        }
155        if ($target instanceof Expr\PropertyFetch && $this->objects !== null && $this->names->propertyName($target) === null) {
156            return $this->objects->writes($target);
157        }
158        $base = $this->baseName($target);
159        if ($base !== null) {
160            return [$base => true];
161        }
162        if ($target instanceof Expr\List_ || $target instanceof Expr\Array_) {
163            $names = [];
164            foreach ($target->items as $item) {
165                if ($item !== null) {
166                    $names += $this->targets($item->value);
167                }
168            }
169
170            return $names;
171        }
172
173        return [];
174    }
175
176    /**
177     * The name an assignment target writes into, looking through element writes.
178     *
179     * `$sql['where'][] = …` writes into `sql`; `$this->parts[] = …` writes into
180     * `this->parts`. Anything else, such as a variable variable, has no name.
181     */
182    public function baseName(Node $target): ?string
183    {
184        if ($target instanceof Expr\ArrayDimFetch) {
185            return $this->baseName($target->var);
186        }
187        if ($target instanceof Expr\Variable) {
188            return is_string($target->name) ? $target->name : null;
189        }
190
191        return $this->names->propertyName($target);
192    }
193
194    /**
195     * The names written by each of the given targets.
196     *
197     * @param array<array-key, Node> $targets
198     * @return array<string, true>
199     */
200    public function all(array $targets): array
201    {
202        $names = [];
203        foreach ($targets as $target) {
204            $names += $this->targets($target);
205        }
206
207        return $names;
208    }
209}
210