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