packages/sql-catalog/src/Core/Analysis/Derivation/SourceTree.php
1<?php
2
3declare(strict_types=1);
4
5namespace SqlCatalog\Core\Analysis\Derivation;
6
7use PhpParser\Node;
8use PhpParser\Node\FunctionLike;
9use PhpParser\Node\Stmt;
10use SqlCatalog\Core\Php\ParsedFile;
11use WeakMap;
12
13/**
14 * Where a node sits in the source: which statement list holds it, and which file.
15 *
16 * Walking back from a call means walking back through the statement lists it
17 * is nested in, so every statement has to be able to say which list it sits in
18 * and at what position. A statement inside a body says so through its parent;
19 * a statement at the top of a file has no parent, and is found here instead.
20 *
21 * @visibility root
22 */
23final class SourceTree
24{
25 /**
26 * @var WeakMap<Stmt, array{string, int}>
27 */
28 private WeakMap $roots;
29
30 /**
31 * @var array<string, list<Stmt>>
32 */
33 private array $files = [];
34
35 /**
36 * Records the top level of every file.
37 *
38 * @param list<ParsedFile> $files
39 */
40 public function __construct(array $files = [])
41 {
42 $this->roots = new WeakMap();
43 foreach ($files as $file) {
44 $this->files[$file->path] = $file->statements;
45 foreach ($file->statements as $index => $statement) {
46 $this->roots[$statement] = [$file->path, $index];
47 }
48 }
49 }
50
51 /**
52 * The statement a node is part of, or the function-like whose expression body holds it.
53 *
54 * An arrow function has no statements, so a call written in one stops at
55 * the arrow function itself rather than at the statement that defines it:
56 * the arrow function's parameters are its own, not the enclosing body's.
57 */
58 public function anchorOf(Node $node): Stmt|FunctionLike|null
59 {
60 $current = $node;
61 while ($current instanceof Node) {
62 if ($current !== $node && $current instanceof FunctionLike) {
63 return $current;
64 }
65 if ($current instanceof Stmt && $this->isListed($current)) {
66 return $current;
67 }
68 $parent = $current->getAttribute('parent');
69 $current = $parent instanceof Node ? $parent : null;
70 }
71
72 return null;
73 }
74
75 /**
76 * Whether a statement sits in a list of statements that run one after another.
77 *
78 * The arms of a conditional, a switch or a try are statements too, but
79 * they sit in the statement that owns them rather than in a list that runs.
80 */
81 public function isListed(Stmt $statement): bool
82 {
83 if ($statement instanceof Stmt\ElseIf_ || $statement instanceof Stmt\Else_ || $statement instanceof Stmt\Case_
84 || $statement instanceof Stmt\Catch_ || $statement instanceof Stmt\Finally_) {
85 return false;
86 }
87 $parent = $statement->getAttribute('parent');
88
89 return !$parent instanceof Stmt\ClassLike;
90 }
91
92 /**
93 * The node that owns the list a statement sits in, the list, and the statement's position in it.
94 *
95 * @return array{Node|null, list<Stmt>, int}|null
96 */
97 public function locate(Stmt $statement): ?array
98 {
99 $root = $this->roots[$statement] ?? null;
100 if ($root !== null) {
101 return [null, $this->files[$root[0]], $root[1]];
102 }
103 $parent = $statement->getAttribute('parent');
104 if (!$parent instanceof Node) {
105 return null;
106 }
107 $list = $this->listOf($parent);
108 $index = array_search($statement, $list, true);
109
110 return is_int($index) ? [$parent, $list, $index] : null;
111 }
112
113 /**
114 * The statements a node runs one after another, or an empty list when it runs none.
115 *
116 * @return list<Stmt>
117 */
118 public function listOf(Node $parent): array
119 {
120 if ($parent instanceof FunctionLike) {
121 return array_values($parent->getStmts() ?? []);
122 }
123 if ($parent instanceof Stmt\If_ || $parent instanceof Stmt\ElseIf_ || $parent instanceof Stmt\Else_
124 || $parent instanceof Stmt\While_ || $parent instanceof Stmt\Do_ || $parent instanceof Stmt\For_
125 || $parent instanceof Stmt\Foreach_ || $parent instanceof Stmt\Case_ || $parent instanceof Stmt\TryCatch
126 || $parent instanceof Stmt\Catch_ || $parent instanceof Stmt\Finally_ || $parent instanceof Stmt\Block
127 || $parent instanceof Stmt\Namespace_) {
128 return array_values($parent->stmts);
129 }
130 if ($parent instanceof Stmt\Declare_) {
131 return array_values($parent->stmts ?? []);
132 }
133
134 return [];
135 }
136
137 /**
138 * The file a node is written in, or an empty string when it is not in any recorded file.
139 */
140 public function fileOf(Node $node): string
141 {
142 $current = $node;
143 while ($current instanceof Node) {
144 if ($current instanceof Stmt) {
145 $root = $this->roots[$current] ?? null;
146 if ($root !== null) {
147 return $root[0];
148 }
149 }
150 $parent = $current->getAttribute('parent');
151 $current = $parent instanceof Node ? $parent : null;
152 }
153
154 return '';
155 }
156
157 /**
158 * The function-like body a node is written in, or null when it is written at the top of a file.
159 */
160 public function bodyOf(Node $node): ?FunctionLike
161 {
162 $parent = $node->getAttribute('parent');
163 while ($parent instanceof Node) {
164 if ($parent instanceof FunctionLike) {
165 return $parent;
166 }
167 $parent = $parent->getAttribute('parent');
168 }
169
170 return null;
171 }
172}
173