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