entrygraph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- entrygraph/__init__.py +87 -0
- entrygraph/__main__.py +8 -0
- entrygraph/_version.py +24 -0
- entrygraph/api.py +549 -0
- entrygraph/cli/__init__.py +0 -0
- entrygraph/cli/main.py +387 -0
- entrygraph/cli/render.py +136 -0
- entrygraph/data/sinks/csharp.toml +106 -0
- entrygraph/data/sinks/go.toml +87 -0
- entrygraph/data/sinks/java.toml +92 -0
- entrygraph/data/sinks/javascript.toml +112 -0
- entrygraph/data/sinks/lib_javascript.toml +34 -0
- entrygraph/data/sinks/lib_python.toml +39 -0
- entrygraph/data/sinks/php.toml +125 -0
- entrygraph/data/sinks/python.toml +160 -0
- entrygraph/data/sinks/ruby.toml +102 -0
- entrygraph/data/sinks/rust.toml +68 -0
- entrygraph/db/__init__.py +0 -0
- entrygraph/db/engine.py +34 -0
- entrygraph/db/meta.py +70 -0
- entrygraph/db/models.py +176 -0
- entrygraph/db/queries.py +155 -0
- entrygraph/detect/__init__.py +0 -0
- entrygraph/detect/entrypoints/__init__.py +22 -0
- entrygraph/detect/entrypoints/base.py +124 -0
- entrygraph/detect/entrypoints/configs.py +139 -0
- entrygraph/detect/entrypoints/csharp.py +156 -0
- entrygraph/detect/entrypoints/golang.py +158 -0
- entrygraph/detect/entrypoints/java.py +187 -0
- entrygraph/detect/entrypoints/javascript.py +211 -0
- entrygraph/detect/entrypoints/php.py +133 -0
- entrygraph/detect/entrypoints/python.py +335 -0
- entrygraph/detect/entrypoints/ruby.py +147 -0
- entrygraph/detect/entrypoints/rust.py +153 -0
- entrygraph/detect/frameworks.py +369 -0
- entrygraph/detect/manifests.py +234 -0
- entrygraph/detect/taint.py +224 -0
- entrygraph/errors.py +27 -0
- entrygraph/extract/__init__.py +0 -0
- entrygraph/extract/base.py +51 -0
- entrygraph/extract/csharp.py +502 -0
- entrygraph/extract/golang.py +342 -0
- entrygraph/extract/ir.py +105 -0
- entrygraph/extract/java.py +329 -0
- entrygraph/extract/javascript.py +400 -0
- entrygraph/extract/php.py +426 -0
- entrygraph/extract/python.py +390 -0
- entrygraph/extract/registry.py +43 -0
- entrygraph/extract/ruby.py +321 -0
- entrygraph/extract/rust.py +482 -0
- entrygraph/fs/__init__.py +0 -0
- entrygraph/fs/hashing.py +78 -0
- entrygraph/fs/lang.py +134 -0
- entrygraph/fs/walker.py +167 -0
- entrygraph/graph/__init__.py +0 -0
- entrygraph/graph/adjacency.py +146 -0
- entrygraph/graph/cte.py +123 -0
- entrygraph/graph/scoring.py +101 -0
- entrygraph/kinds.py +51 -0
- entrygraph/parsing/__init__.py +0 -0
- entrygraph/parsing/parsers.py +49 -0
- entrygraph/parsing/queries.py +39 -0
- entrygraph/pipeline/__init__.py +0 -0
- entrygraph/pipeline/scanner.py +506 -0
- entrygraph/pipeline/worker.py +49 -0
- entrygraph/pipeline/writer.py +41 -0
- entrygraph/py.typed +0 -0
- entrygraph/queries/csharp/calls.scm +4 -0
- entrygraph/queries/csharp/definitions.scm +29 -0
- entrygraph/queries/csharp/imports.scm +4 -0
- entrygraph/queries/go/calls.scm +2 -0
- entrygraph/queries/go/definitions.scm +24 -0
- entrygraph/queries/go/imports.scm +1 -0
- entrygraph/queries/java/calls.scm +2 -0
- entrygraph/queries/java/definitions.scm +14 -0
- entrygraph/queries/java/imports.scm +2 -0
- entrygraph/queries/javascript/calls.scm +4 -0
- entrygraph/queries/javascript/definitions.scm +4 -0
- entrygraph/queries/javascript/imports.scm +6 -0
- entrygraph/queries/php/calls.scm +8 -0
- entrygraph/queries/php/definitions.scm +24 -0
- entrygraph/queries/php/imports.scm +1 -0
- entrygraph/queries/python/calls.scm +2 -0
- entrygraph/queries/python/definitions.scm +11 -0
- entrygraph/queries/python/imports.scm +2 -0
- entrygraph/queries/ruby/calls.scm +4 -0
- entrygraph/queries/ruby/definitions.scm +20 -0
- entrygraph/queries/ruby/imports.scm +7 -0
- entrygraph/queries/rust/calls.scm +5 -0
- entrygraph/queries/rust/definitions.scm +26 -0
- entrygraph/queries/rust/imports.scm +4 -0
- entrygraph/resolve/__init__.py +0 -0
- entrygraph/resolve/externals.py +61 -0
- entrygraph/resolve/hierarchy.py +152 -0
- entrygraph/resolve/resolver.py +275 -0
- entrygraph/resolve/symbol_table.py +48 -0
- entrygraph/results.py +138 -0
- entrygraph-0.1.0.dist-info/METADATA +204 -0
- entrygraph-0.1.0.dist-info/RECORD +102 -0
- entrygraph-0.1.0.dist-info/WHEEL +4 -0
- entrygraph-0.1.0.dist-info/entry_points.txt +2 -0
- entrygraph-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
r"""PHP extractor: .scm queries harvest nodes, this shaper builds the IR.
|
|
2
|
+
|
|
3
|
+
Module paths are the file's PHP namespace, ``\`` normalized to ``.`` (so
|
|
4
|
+
``namespace App\Http\Controllers;`` -> ``App.Http.Controllers``). A
|
|
5
|
+
namespace-less script (a plain ``index.php`` with no ``namespace`` line) falls
|
|
6
|
+
back to its directory-derived dotted path. PHP has no package concept in the IR
|
|
7
|
+
sense, so ``is_package`` is always False.
|
|
8
|
+
|
|
9
|
+
Qualified names are namespace-scoped and class-nested:
|
|
10
|
+
``App.Http.Controllers.ReportController.store``. The ``\`` separator is
|
|
11
|
+
normalized to ``.`` *everywhere* — qnames, base expressions, and import-map
|
|
12
|
+
values — so externals look like ``php:App.Services.Runner.run`` and the sink
|
|
13
|
+
globs stay dot-based.
|
|
14
|
+
|
|
15
|
+
PHP 8 attributes (``#[Route('/x')]``) are captured on ``RawSymbol.decorators``
|
|
16
|
+
as raw source text and emitted as ``RawReference(kind="decorator")`` — the same
|
|
17
|
+
shape Java uses for annotations — so entrypoint rules can match them.
|
|
18
|
+
``Foo::bar()`` yields receiver ``Foo`` / callee ``bar``; ``$obj->m()`` yields
|
|
19
|
+
receiver ``$obj``. ``include``/``require`` of a variable is modeled as a call
|
|
20
|
+
ref with ``callee_name="include"`` so it can be a sink.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
26
|
+
|
|
27
|
+
from entrygraph.extract.base import FileContext, node_text, span_of, truncate
|
|
28
|
+
from entrygraph.extract.ir import FileExtraction, RawImport, RawReference, RawSymbol
|
|
29
|
+
from entrygraph.kinds import SymbolKind
|
|
30
|
+
from entrygraph.parsing.queries import captures, load_query
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
33
|
+
from tree_sitter import Node, Tree
|
|
34
|
+
|
|
35
|
+
_SCOPE_TYPES = frozenset(
|
|
36
|
+
{"class_declaration", "interface_declaration", "trait_declaration"}
|
|
37
|
+
)
|
|
38
|
+
_FUNC_SCOPES = frozenset({"function_definition", "method_declaration"})
|
|
39
|
+
_SRC_ROOTS = ("src", "lib", "app")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _norm(text: str) -> str:
|
|
43
|
+
r"""Normalize PHP's ``\`` namespace separator to ``.`` and strip leads."""
|
|
44
|
+
return text.replace("\\", ".").strip(".")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PhpExtractor:
|
|
48
|
+
language_ids: ClassVar[tuple[str, ...]] = ("php",)
|
|
49
|
+
|
|
50
|
+
def module_path_for(self, repo_relative_path: str) -> tuple[str, bool]:
|
|
51
|
+
# The authoritative module path is the file's PHP namespace, computed in
|
|
52
|
+
# extract(); this directory-derived fallback is only used for
|
|
53
|
+
# namespace-less scripts. The scanner calls this before parse, so we
|
|
54
|
+
# return the directory form and let extract() override via ctx.
|
|
55
|
+
parts = repo_relative_path.split("/")
|
|
56
|
+
if parts and parts[0] in _SRC_ROOTS and len(parts) > 1:
|
|
57
|
+
parts = parts[1:]
|
|
58
|
+
if parts:
|
|
59
|
+
parts[-1] = parts[-1].removesuffix(".php").removesuffix(".phtml")
|
|
60
|
+
return ".".join(p for p in parts if p) or "_root", False
|
|
61
|
+
|
|
62
|
+
def extract(self, tree: "Tree", ctx: FileContext) -> FileExtraction:
|
|
63
|
+
root = tree.root_node
|
|
64
|
+
namespace = self._namespace(root)
|
|
65
|
+
# The namespace (when present) is the real module path — override the
|
|
66
|
+
# directory-derived ctx.module_path so qnames are namespace-scoped.
|
|
67
|
+
module_path = namespace or ctx.module_path
|
|
68
|
+
out = FileExtraction(
|
|
69
|
+
path=ctx.path,
|
|
70
|
+
language=ctx.language,
|
|
71
|
+
module_path=module_path,
|
|
72
|
+
parse_ok=not root.has_error,
|
|
73
|
+
error_count=1 if root.has_error else 0,
|
|
74
|
+
)
|
|
75
|
+
self._namespace_prefix = module_path
|
|
76
|
+
self._definitions(root, ctx, out)
|
|
77
|
+
self._imports(root, ctx, out)
|
|
78
|
+
self._calls(root, ctx, out)
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
# ---------------- namespace ----------------
|
|
82
|
+
|
|
83
|
+
def _namespace(self, root: "Node") -> str | None:
|
|
84
|
+
caps = captures(load_query("php", "definitions"), root)
|
|
85
|
+
for node in caps.get("namespace", []):
|
|
86
|
+
name = next(
|
|
87
|
+
(c for c in node.named_children if c.type == "namespace_name"), None
|
|
88
|
+
)
|
|
89
|
+
if name is not None:
|
|
90
|
+
return _norm(node_text(name))
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# ---------------- definitions ----------------
|
|
94
|
+
|
|
95
|
+
def _definitions(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
96
|
+
caps = captures(load_query("php", "definitions"), root)
|
|
97
|
+
|
|
98
|
+
for node in caps.get("def.class", []):
|
|
99
|
+
self._add_type(node, ctx, out, SymbolKind.CLASS)
|
|
100
|
+
for node in caps.get("def.interface", []):
|
|
101
|
+
self._add_type(node, ctx, out, SymbolKind.INTERFACE)
|
|
102
|
+
for node in caps.get("def.trait", []):
|
|
103
|
+
self._add_type(node, ctx, out, SymbolKind.CLASS)
|
|
104
|
+
|
|
105
|
+
for node in caps.get("def.function", []):
|
|
106
|
+
self._add_callable(node, ctx, out, SymbolKind.FUNCTION)
|
|
107
|
+
for node in caps.get("def.method", []):
|
|
108
|
+
self._add_callable(node, ctx, out, SymbolKind.METHOD)
|
|
109
|
+
|
|
110
|
+
for node in caps.get("def.const", []):
|
|
111
|
+
for element in node.named_children:
|
|
112
|
+
if element.type != "const_element":
|
|
113
|
+
continue
|
|
114
|
+
name_node = element.child_by_field_name("name") or next(
|
|
115
|
+
(c for c in element.named_children if c.type == "name"), None
|
|
116
|
+
)
|
|
117
|
+
if name_node is None:
|
|
118
|
+
continue
|
|
119
|
+
name = node_text(name_node)
|
|
120
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
121
|
+
out.symbols.append(
|
|
122
|
+
RawSymbol(
|
|
123
|
+
kind=SymbolKind.CONSTANT, name=name, qualified_name=qname,
|
|
124
|
+
span=span_of(node), parent_qualified_name=parent_q,
|
|
125
|
+
signature=truncate(node_text(node)),
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
for node in caps.get("def.property", []):
|
|
130
|
+
if self._nearest_scope(node) is None:
|
|
131
|
+
continue
|
|
132
|
+
modifiers = self._prop_modifiers(node)
|
|
133
|
+
for element in node.named_children:
|
|
134
|
+
if element.type != "property_element":
|
|
135
|
+
continue
|
|
136
|
+
var = next(
|
|
137
|
+
(c for c in element.named_children if c.type == "variable_name"), None
|
|
138
|
+
)
|
|
139
|
+
name_node = var.child_by_field_name("name") if var else None
|
|
140
|
+
if name_node is None and var is not None:
|
|
141
|
+
name_node = next(
|
|
142
|
+
(c for c in var.named_children if c.type == "name"), None
|
|
143
|
+
)
|
|
144
|
+
if name_node is None:
|
|
145
|
+
continue
|
|
146
|
+
name = node_text(name_node)
|
|
147
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
148
|
+
out.symbols.append(
|
|
149
|
+
RawSymbol(
|
|
150
|
+
kind=SymbolKind.FIELD, name=name, qualified_name=qname,
|
|
151
|
+
span=span_of(node), parent_qualified_name=parent_q,
|
|
152
|
+
signature=truncate(node_text(node)), modifiers=modifiers,
|
|
153
|
+
is_exported="public" in modifiers or not modifiers,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _add_type(
|
|
158
|
+
self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind
|
|
159
|
+
) -> None:
|
|
160
|
+
name_node = node.child_by_field_name("name")
|
|
161
|
+
if name_node is None:
|
|
162
|
+
return
|
|
163
|
+
name = node_text(name_node)
|
|
164
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
165
|
+
extends, interfaces = self._supertypes(node)
|
|
166
|
+
out.symbols.append(
|
|
167
|
+
RawSymbol(
|
|
168
|
+
kind=kind, name=name, qualified_name=qname, span=span_of(node),
|
|
169
|
+
parent_qualified_name=parent_q, signature=self._signature(node),
|
|
170
|
+
decorators=self._attributes(node), bases=[*extends, *interfaces],
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
for base in extends:
|
|
174
|
+
out.references.append(
|
|
175
|
+
RawReference(
|
|
176
|
+
kind="inherit", callee_text=base,
|
|
177
|
+
callee_name=base.rsplit(".", 1)[-1], receiver_text=None,
|
|
178
|
+
span=span_of(node), caller_qualified_name=qname,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
for iface in interfaces:
|
|
182
|
+
out.references.append(
|
|
183
|
+
RawReference(
|
|
184
|
+
kind="implement", callee_text=iface,
|
|
185
|
+
callee_name=iface.rsplit(".", 1)[-1], receiver_text=None,
|
|
186
|
+
span=span_of(node), caller_qualified_name=qname,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
self._emit_attribute_refs(node, qname, out)
|
|
190
|
+
|
|
191
|
+
def _add_callable(
|
|
192
|
+
self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind
|
|
193
|
+
) -> None:
|
|
194
|
+
name_node = node.child_by_field_name("name")
|
|
195
|
+
if name_node is None:
|
|
196
|
+
return
|
|
197
|
+
name = node_text(name_node)
|
|
198
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
199
|
+
modifiers = self._method_modifiers(node)
|
|
200
|
+
out.symbols.append(
|
|
201
|
+
RawSymbol(
|
|
202
|
+
kind=kind, name=name, qualified_name=qname, span=span_of(node),
|
|
203
|
+
parent_qualified_name=parent_q, signature=self._signature(node),
|
|
204
|
+
decorators=self._attributes(node), modifiers=modifiers,
|
|
205
|
+
is_exported="private" not in modifiers and "protected" not in modifiers,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
self._emit_attribute_refs(node, qname, out)
|
|
209
|
+
|
|
210
|
+
# ---------------- imports ----------------
|
|
211
|
+
|
|
212
|
+
def _imports(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
213
|
+
caps = captures(load_query("php", "imports"), root)
|
|
214
|
+
for node in caps.get("import", []):
|
|
215
|
+
for clause in node.named_children:
|
|
216
|
+
if clause.type != "namespace_use_clause":
|
|
217
|
+
continue
|
|
218
|
+
qualified = next(
|
|
219
|
+
(c for c in clause.named_children
|
|
220
|
+
if c.type in ("qualified_name", "name")),
|
|
221
|
+
None,
|
|
222
|
+
)
|
|
223
|
+
if qualified is None:
|
|
224
|
+
continue
|
|
225
|
+
full = _norm(node_text(qualified))
|
|
226
|
+
alias_node = clause.child_by_field_name("alias")
|
|
227
|
+
last = full.rsplit(".", 1)[-1]
|
|
228
|
+
alias = node_text(alias_node) if alias_node is not None else last
|
|
229
|
+
out.imports.append(
|
|
230
|
+
RawImport(
|
|
231
|
+
module=full, imported_name=last, alias=alias, span=span_of(node)
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
for imp in out.imports:
|
|
235
|
+
if imp.module:
|
|
236
|
+
out.framework_signals.append(("import", imp.module))
|
|
237
|
+
|
|
238
|
+
# ---------------- calls / attributes ----------------
|
|
239
|
+
|
|
240
|
+
def _calls(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
241
|
+
caps = captures(load_query("php", "calls"), root)
|
|
242
|
+
|
|
243
|
+
for node in caps.get("call", []):
|
|
244
|
+
fn = node.child_by_field_name("function")
|
|
245
|
+
if fn is None:
|
|
246
|
+
continue
|
|
247
|
+
callee_text = _norm(node_text(fn))
|
|
248
|
+
callee_name = callee_text.rsplit(".", 1)[-1]
|
|
249
|
+
receiver = (
|
|
250
|
+
callee_text.rsplit(".", 1)[0] if "." in callee_text else None
|
|
251
|
+
)
|
|
252
|
+
self._emit_call(node, callee_text, callee_name, receiver, ctx, out)
|
|
253
|
+
|
|
254
|
+
for node in caps.get("member_call", []):
|
|
255
|
+
name_node = node.child_by_field_name("name")
|
|
256
|
+
obj = node.child_by_field_name("object")
|
|
257
|
+
if name_node is None or obj is None:
|
|
258
|
+
continue
|
|
259
|
+
callee_name = node_text(name_node)
|
|
260
|
+
receiver = node_text(obj)
|
|
261
|
+
self._emit_call(
|
|
262
|
+
node, f"{receiver}.{callee_name}", callee_name, receiver, ctx, out
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
for node in caps.get("scoped_call", []):
|
|
266
|
+
name_node = node.child_by_field_name("name")
|
|
267
|
+
scope = node.child_by_field_name("scope")
|
|
268
|
+
if name_node is None or scope is None:
|
|
269
|
+
continue
|
|
270
|
+
callee_name = node_text(name_node)
|
|
271
|
+
receiver = _norm(node_text(scope))
|
|
272
|
+
self._emit_call(
|
|
273
|
+
node, f"{receiver}.{callee_name}", callee_name, receiver, ctx, out
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
for node in caps.get("new", []):
|
|
277
|
+
type_node = next(
|
|
278
|
+
(c for c in node.named_children if c.type != "arguments"), None
|
|
279
|
+
)
|
|
280
|
+
if type_node is None:
|
|
281
|
+
continue
|
|
282
|
+
type_text = _norm(node_text(type_node))
|
|
283
|
+
callee_name = type_text.rsplit(".", 1)[-1]
|
|
284
|
+
self._emit_call(node, type_text, callee_name, None, ctx, out)
|
|
285
|
+
|
|
286
|
+
for node in caps.get("include", []):
|
|
287
|
+
# include/require of anything: model as a bare call so it can be a
|
|
288
|
+
# sink. `include $var` -> callee_name="include" with the variable as
|
|
289
|
+
# arg_preview.
|
|
290
|
+
arg = node.named_children[0] if node.named_children else None
|
|
291
|
+
out.references.append(
|
|
292
|
+
RawReference(
|
|
293
|
+
kind="call", callee_text="include", callee_name="include",
|
|
294
|
+
receiver_text=None, span=span_of(node),
|
|
295
|
+
caller_qualified_name=self._caller(node, ctx),
|
|
296
|
+
arg_count=1 if arg is not None else 0,
|
|
297
|
+
arg_preview=truncate(node_text(arg)) if arg is not None else None,
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _emit_call(
|
|
302
|
+
self, node: "Node", callee_text: str, callee_name: str,
|
|
303
|
+
receiver: str | None, ctx: FileContext, out: FileExtraction,
|
|
304
|
+
) -> None:
|
|
305
|
+
args = node.child_by_field_name("arguments")
|
|
306
|
+
out.references.append(
|
|
307
|
+
RawReference(
|
|
308
|
+
kind="call", callee_text=callee_text, callee_name=callee_name,
|
|
309
|
+
receiver_text=receiver, span=span_of(node),
|
|
310
|
+
caller_qualified_name=self._caller(node, ctx),
|
|
311
|
+
arg_count=len(args.named_children) if args is not None else 0,
|
|
312
|
+
arg_preview=truncate(node_text(args)) if args is not None else None,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# ---------------- helpers ----------------
|
|
317
|
+
|
|
318
|
+
def _scope_chain(self, node: "Node") -> list[str]:
|
|
319
|
+
parts: list[str] = []
|
|
320
|
+
current = node.parent
|
|
321
|
+
while current is not None:
|
|
322
|
+
if current.type in _SCOPE_TYPES:
|
|
323
|
+
name = current.child_by_field_name("name")
|
|
324
|
+
if name is not None:
|
|
325
|
+
parts.append(node_text(name))
|
|
326
|
+
current = current.parent
|
|
327
|
+
parts.reverse()
|
|
328
|
+
return parts
|
|
329
|
+
|
|
330
|
+
def _nearest_scope(self, node: "Node") -> str | None:
|
|
331
|
+
current = node.parent
|
|
332
|
+
while current is not None:
|
|
333
|
+
if current.type in _SCOPE_TYPES:
|
|
334
|
+
return current.type
|
|
335
|
+
current = current.parent
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def _qualify(
|
|
339
|
+
self, node: "Node", name: str, ctx: FileContext
|
|
340
|
+
) -> tuple[str, str | None]:
|
|
341
|
+
chain = self._scope_chain(node)
|
|
342
|
+
base = self._namespace_prefix
|
|
343
|
+
prefix_parts = [base, *chain] if base else [*chain]
|
|
344
|
+
parent_q = ".".join(prefix_parts) if prefix_parts else None
|
|
345
|
+
qname = ".".join([*prefix_parts, name])
|
|
346
|
+
return qname, parent_q
|
|
347
|
+
|
|
348
|
+
def _caller(self, node: "Node", ctx: FileContext) -> str | None:
|
|
349
|
+
current = node.parent
|
|
350
|
+
while current is not None:
|
|
351
|
+
if current.type in _FUNC_SCOPES:
|
|
352
|
+
name = current.child_by_field_name("name")
|
|
353
|
+
if name is not None:
|
|
354
|
+
return self._qualify(current, node_text(name), ctx)[0]
|
|
355
|
+
current = current.parent
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
def _signature(self, node: "Node") -> str:
|
|
359
|
+
return truncate(node_text(node).split("{", 1)[0].strip(), 120)
|
|
360
|
+
|
|
361
|
+
def _method_modifiers(self, node: "Node") -> list[str]:
|
|
362
|
+
mods: list[str] = []
|
|
363
|
+
for child in node.children:
|
|
364
|
+
if child.type in ("visibility_modifier", "static_modifier",
|
|
365
|
+
"abstract_modifier", "final_modifier"):
|
|
366
|
+
mods.append(node_text(child))
|
|
367
|
+
return mods
|
|
368
|
+
|
|
369
|
+
def _prop_modifiers(self, node: "Node") -> list[str]:
|
|
370
|
+
return [
|
|
371
|
+
node_text(c) for c in node.named_children
|
|
372
|
+
if c.type in ("visibility_modifier", "static_modifier",
|
|
373
|
+
"readonly_modifier", "final_modifier")
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
def _attribute_lists(self, node: "Node") -> list["Node"]:
|
|
377
|
+
return [c for c in node.named_children if c.type == "attribute_list"]
|
|
378
|
+
|
|
379
|
+
def _attributes(self, node: "Node") -> list[str]:
|
|
380
|
+
# The attribute_list node text already includes the leading `#[ ... ]`.
|
|
381
|
+
return [node_text(attr_list) for attr_list in self._attribute_lists(node)]
|
|
382
|
+
|
|
383
|
+
def _emit_attribute_refs(
|
|
384
|
+
self, node: "Node", owner_qname: str, out: FileExtraction
|
|
385
|
+
) -> None:
|
|
386
|
+
for attr_list in self._attribute_lists(node):
|
|
387
|
+
for group in attr_list.named_children:
|
|
388
|
+
if group.type != "attribute_group":
|
|
389
|
+
continue
|
|
390
|
+
for attr in group.named_children:
|
|
391
|
+
if attr.type != "attribute":
|
|
392
|
+
continue
|
|
393
|
+
name_node = attr.child_by_field_name("name") or next(
|
|
394
|
+
(c for c in attr.named_children
|
|
395
|
+
if c.type in ("name", "qualified_name")),
|
|
396
|
+
None,
|
|
397
|
+
)
|
|
398
|
+
if name_node is None:
|
|
399
|
+
continue
|
|
400
|
+
callee_text = _norm(node_text(name_node))
|
|
401
|
+
out.references.append(
|
|
402
|
+
RawReference(
|
|
403
|
+
kind="decorator", callee_text=callee_text,
|
|
404
|
+
callee_name=callee_text.rsplit(".", 1)[-1],
|
|
405
|
+
receiver_text=(
|
|
406
|
+
callee_text.rsplit(".", 1)[0]
|
|
407
|
+
if "." in callee_text else None
|
|
408
|
+
),
|
|
409
|
+
span=span_of(attr), caller_qualified_name=owner_qname,
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def _supertypes(self, node: "Node") -> tuple[list[str], list[str]]:
|
|
414
|
+
"""Return (extends supertypes, implements interfaces)."""
|
|
415
|
+
extends: list[str] = []
|
|
416
|
+
interfaces: list[str] = []
|
|
417
|
+
for child in node.named_children:
|
|
418
|
+
if child.type == "base_clause":
|
|
419
|
+
for name in child.named_children:
|
|
420
|
+
if name.type in ("name", "qualified_name"):
|
|
421
|
+
extends.append(_norm(node_text(name)))
|
|
422
|
+
elif child.type == "class_interface_clause":
|
|
423
|
+
for name in child.named_children:
|
|
424
|
+
if name.type in ("name", "qualified_name"):
|
|
425
|
+
interfaces.append(_norm(node_text(name)))
|
|
426
|
+
return extends, interfaces
|