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,502 @@
|
|
|
1
|
+
"""C# extractor: .scm queries harvest nodes, this shaper builds the IR.
|
|
2
|
+
|
|
3
|
+
Qualified names come from the enclosing **namespace**, not the file path — C#
|
|
4
|
+
decouples the two. Both classic block namespaces (``namespace Foo { ... }``)
|
|
5
|
+
and C# 10 file-scoped namespaces (``namespace Foo;``) are honoured by walking
|
|
6
|
+
ancestors for ``namespace_declaration`` / ``file_scoped_namespace_declaration``.
|
|
7
|
+
So ``MyApp.Controllers.UsersController.Get`` regardless of where the file lives.
|
|
8
|
+
Files with no namespace (top-level statements) fall back to a directory-derived
|
|
9
|
+
module path.
|
|
10
|
+
|
|
11
|
+
``module_path_for`` returns ``(dotted_dir_module, False)`` — it cannot see the
|
|
12
|
+
namespace (no parse), so it is only a fallback; ``extract`` overrides the
|
|
13
|
+
per-symbol qname prefix with the real namespace when one is present.
|
|
14
|
+
|
|
15
|
+
Attributes (``[HttpGet("/x")]``) are captured on ``RawSymbol.decorators`` as raw
|
|
16
|
+
source text and also emitted as ``RawReference(kind="decorator")`` (mirrors the
|
|
17
|
+
Java annotation handling). Base types / interfaces become
|
|
18
|
+
``RawReference(kind="inherit")`` — C# does not syntactically distinguish a base
|
|
19
|
+
class from implemented interfaces, so every entry in the ``base_list`` is emitted
|
|
20
|
+
as an ``inherit`` ref and stored on ``bases``. ``is_exported`` is ``"public" in
|
|
21
|
+
modifiers``.
|
|
22
|
+
|
|
23
|
+
Partial classes (``partial class Foo`` split across files/blocks) produce the
|
|
24
|
+
same qualified name more than once; the DB is last-writer-wins, which is
|
|
25
|
+
acceptable — members still qualify under the shared type qname.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
31
|
+
|
|
32
|
+
from entrygraph.extract.base import FileContext, node_text, span_of, truncate
|
|
33
|
+
from entrygraph.extract.ir import FileExtraction, RawImport, RawReference, RawSymbol
|
|
34
|
+
from entrygraph.kinds import SymbolKind
|
|
35
|
+
from entrygraph.parsing.queries import captures, load_query
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
38
|
+
from tree_sitter import Node, Tree
|
|
39
|
+
|
|
40
|
+
_NAMESPACE_TYPES = frozenset(
|
|
41
|
+
{"namespace_declaration", "file_scoped_namespace_declaration"}
|
|
42
|
+
)
|
|
43
|
+
_TYPE_SCOPES = frozenset(
|
|
44
|
+
{
|
|
45
|
+
"class_declaration",
|
|
46
|
+
"interface_declaration",
|
|
47
|
+
"struct_declaration",
|
|
48
|
+
"record_declaration",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
_CALLABLE_SCOPES = frozenset(
|
|
52
|
+
{
|
|
53
|
+
"method_declaration",
|
|
54
|
+
"constructor_declaration",
|
|
55
|
+
"local_function_statement",
|
|
56
|
+
"property_declaration",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _looks_like_type_ref(receiver: str) -> bool:
|
|
62
|
+
"""Heuristic: is a member-access receiver a *type* (static call) rather than
|
|
63
|
+
an instance/variable? A type reference is a dotted chain of identifiers whose
|
|
64
|
+
every segment starts with an uppercase letter (``Process``,
|
|
65
|
+
``System.Diagnostics.Process``). Instance receivers are conventionally
|
|
66
|
+
lowercase (``service``, ``cmd``, ``_reports``) or use ``this``/``base``.
|
|
67
|
+
Anything with call/index syntax is not a plain type reference.
|
|
68
|
+
"""
|
|
69
|
+
if not receiver or any(c in receiver for c in "()[]?"):
|
|
70
|
+
return False
|
|
71
|
+
segments = receiver.split(".")
|
|
72
|
+
return all(seg[:1].isupper() and seg[:1].isalpha() for seg in segments)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CSharpExtractor:
|
|
76
|
+
language_ids: ClassVar[tuple[str, ...]] = ("csharp",)
|
|
77
|
+
|
|
78
|
+
def module_path_for(self, repo_relative_path: str) -> tuple[str, bool]:
|
|
79
|
+
# Fallback only: the real qname prefix is the namespace, resolved per
|
|
80
|
+
# symbol in extract(). Directory-derived dotted path, ".cs" dropped.
|
|
81
|
+
path = repo_relative_path.removesuffix(".cs")
|
|
82
|
+
dotted = ".".join(p for p in path.split("/") if p)
|
|
83
|
+
return dotted or "_root", False
|
|
84
|
+
|
|
85
|
+
def extract(self, tree: "Tree", ctx: FileContext) -> FileExtraction:
|
|
86
|
+
root = tree.root_node
|
|
87
|
+
out = FileExtraction(
|
|
88
|
+
path=ctx.path,
|
|
89
|
+
language=ctx.language,
|
|
90
|
+
module_path=ctx.module_path,
|
|
91
|
+
parse_ok=not root.has_error,
|
|
92
|
+
error_count=1 if root.has_error else 0,
|
|
93
|
+
)
|
|
94
|
+
self._definitions(root, ctx, out)
|
|
95
|
+
self._imports(root, ctx, out)
|
|
96
|
+
self._calls(root, ctx, out)
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
# ---------------- definitions ----------------
|
|
100
|
+
|
|
101
|
+
def _definitions(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
102
|
+
caps = captures(load_query("csharp", "definitions"), root)
|
|
103
|
+
|
|
104
|
+
for node in caps.get("def.class", []):
|
|
105
|
+
self._add_type(node, ctx, out, SymbolKind.CLASS)
|
|
106
|
+
for node in caps.get("def.interface", []):
|
|
107
|
+
self._add_type(node, ctx, out, SymbolKind.INTERFACE)
|
|
108
|
+
for node in caps.get("def.struct", []):
|
|
109
|
+
self._add_type(node, ctx, out, SymbolKind.STRUCT)
|
|
110
|
+
for node in caps.get("def.record", []):
|
|
111
|
+
# C# has no dedicated record kind in the IR; treat as a class.
|
|
112
|
+
self._add_type(node, ctx, out, SymbolKind.CLASS)
|
|
113
|
+
|
|
114
|
+
for kind_key, sym_kind in (
|
|
115
|
+
("def.method", SymbolKind.METHOD),
|
|
116
|
+
("def.constructor", SymbolKind.METHOD),
|
|
117
|
+
("def.local_function", SymbolKind.FUNCTION),
|
|
118
|
+
):
|
|
119
|
+
for node in caps.get(kind_key, []):
|
|
120
|
+
self._add_callable(node, ctx, out, sym_kind)
|
|
121
|
+
|
|
122
|
+
for node in caps.get("def.property", []):
|
|
123
|
+
self._add_member(node, ctx, out, SymbolKind.PROPERTY)
|
|
124
|
+
|
|
125
|
+
for node in caps.get("def.field", []):
|
|
126
|
+
self._add_field(node, ctx, out)
|
|
127
|
+
|
|
128
|
+
def _add_type(
|
|
129
|
+
self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind
|
|
130
|
+
) -> None:
|
|
131
|
+
name_node = node.child_by_field_name("name")
|
|
132
|
+
if name_node is None:
|
|
133
|
+
return
|
|
134
|
+
name = node_text(name_node)
|
|
135
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
136
|
+
bases = self._base_types(node)
|
|
137
|
+
modifiers = self._modifiers(node)
|
|
138
|
+
out.symbols.append(
|
|
139
|
+
RawSymbol(
|
|
140
|
+
kind=kind,
|
|
141
|
+
name=name,
|
|
142
|
+
qualified_name=qname,
|
|
143
|
+
span=span_of(node),
|
|
144
|
+
parent_qualified_name=parent_q,
|
|
145
|
+
signature=self._signature(node),
|
|
146
|
+
decorators=self._attributes(node),
|
|
147
|
+
bases=bases,
|
|
148
|
+
modifiers=modifiers,
|
|
149
|
+
is_exported="public" in modifiers,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
for base in bases:
|
|
153
|
+
out.references.append(
|
|
154
|
+
RawReference(
|
|
155
|
+
kind="inherit",
|
|
156
|
+
callee_text=base,
|
|
157
|
+
callee_name=base.rsplit(".", 1)[-1],
|
|
158
|
+
receiver_text=None,
|
|
159
|
+
span=span_of(node),
|
|
160
|
+
caller_qualified_name=qname,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
self._emit_attribute_refs(node, qname, out)
|
|
164
|
+
|
|
165
|
+
def _add_callable(
|
|
166
|
+
self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind
|
|
167
|
+
) -> None:
|
|
168
|
+
name_node = node.child_by_field_name("name")
|
|
169
|
+
if name_node is None:
|
|
170
|
+
return
|
|
171
|
+
name = node_text(name_node)
|
|
172
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
173
|
+
modifiers = self._modifiers(node)
|
|
174
|
+
out.symbols.append(
|
|
175
|
+
RawSymbol(
|
|
176
|
+
kind=kind,
|
|
177
|
+
name=name,
|
|
178
|
+
qualified_name=qname,
|
|
179
|
+
span=span_of(node),
|
|
180
|
+
parent_qualified_name=parent_q,
|
|
181
|
+
signature=self._signature(node),
|
|
182
|
+
decorators=self._attributes(node),
|
|
183
|
+
modifiers=modifiers,
|
|
184
|
+
is_exported="public" in modifiers,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
self._emit_attribute_refs(node, qname, out)
|
|
188
|
+
|
|
189
|
+
def _add_member(
|
|
190
|
+
self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind
|
|
191
|
+
) -> None:
|
|
192
|
+
if self._nearest_type_scope(node) is None:
|
|
193
|
+
return # only type members are symbols
|
|
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._modifiers(node)
|
|
200
|
+
out.symbols.append(
|
|
201
|
+
RawSymbol(
|
|
202
|
+
kind=kind,
|
|
203
|
+
name=name,
|
|
204
|
+
qualified_name=qname,
|
|
205
|
+
span=span_of(node),
|
|
206
|
+
parent_qualified_name=parent_q,
|
|
207
|
+
signature=truncate(node_text(node), 120),
|
|
208
|
+
decorators=self._attributes(node),
|
|
209
|
+
modifiers=modifiers,
|
|
210
|
+
is_exported="public" in modifiers,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
self._emit_attribute_refs(node, qname, out)
|
|
214
|
+
|
|
215
|
+
def _add_field(self, node: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
216
|
+
if self._nearest_type_scope(node) is None:
|
|
217
|
+
return
|
|
218
|
+
declaration = next(
|
|
219
|
+
(c for c in node.named_children if c.type == "variable_declaration"), None
|
|
220
|
+
)
|
|
221
|
+
if declaration is None:
|
|
222
|
+
return
|
|
223
|
+
declarator = next(
|
|
224
|
+
(c for c in declaration.named_children if c.type == "variable_declarator"),
|
|
225
|
+
None,
|
|
226
|
+
)
|
|
227
|
+
if declarator is None:
|
|
228
|
+
return
|
|
229
|
+
name_node = declarator.child_by_field_name("name") or next(
|
|
230
|
+
(c for c in declarator.named_children if c.type == "identifier"), None
|
|
231
|
+
)
|
|
232
|
+
if name_node is None:
|
|
233
|
+
return
|
|
234
|
+
name = node_text(name_node)
|
|
235
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
236
|
+
modifiers = self._modifiers(node)
|
|
237
|
+
is_const = "const" in modifiers or (
|
|
238
|
+
"static" in modifiers and "readonly" in modifiers and name.isupper()
|
|
239
|
+
)
|
|
240
|
+
out.symbols.append(
|
|
241
|
+
RawSymbol(
|
|
242
|
+
kind=SymbolKind.CONSTANT if is_const else SymbolKind.FIELD,
|
|
243
|
+
name=name,
|
|
244
|
+
qualified_name=qname,
|
|
245
|
+
span=span_of(node),
|
|
246
|
+
parent_qualified_name=parent_q,
|
|
247
|
+
signature=truncate(node_text(node), 120),
|
|
248
|
+
decorators=self._attributes(node),
|
|
249
|
+
modifiers=modifiers,
|
|
250
|
+
is_exported="public" in modifiers,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# ---------------- imports ----------------
|
|
255
|
+
|
|
256
|
+
def _imports(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
257
|
+
caps = captures(load_query("csharp", "imports"), root)
|
|
258
|
+
for node in caps.get("import", []):
|
|
259
|
+
self._add_using(node, out)
|
|
260
|
+
for imp in out.imports:
|
|
261
|
+
if imp.module:
|
|
262
|
+
out.framework_signals.append(("import", imp.module))
|
|
263
|
+
|
|
264
|
+
def _add_using(self, node: "Node", out: FileExtraction) -> None:
|
|
265
|
+
# Named children in order; an `=` child marks an alias directive, a
|
|
266
|
+
# `static` keyword marks `using static`.
|
|
267
|
+
children = node.children
|
|
268
|
+
has_alias = any(c.type == "=" for c in children)
|
|
269
|
+
# The dotted namespace / type is the qualified_name or identifier child.
|
|
270
|
+
name_nodes = [
|
|
271
|
+
c
|
|
272
|
+
for c in node.named_children
|
|
273
|
+
if c.type in ("qualified_name", "identifier")
|
|
274
|
+
]
|
|
275
|
+
if not name_nodes:
|
|
276
|
+
return
|
|
277
|
+
if has_alias and len(name_nodes) >= 2:
|
|
278
|
+
alias = node_text(name_nodes[0])
|
|
279
|
+
module = node_text(name_nodes[1])
|
|
280
|
+
imported = module.rsplit(".", 1)[-1]
|
|
281
|
+
out.imports.append(
|
|
282
|
+
RawImport(
|
|
283
|
+
module=module,
|
|
284
|
+
imported_name=imported,
|
|
285
|
+
alias=alias,
|
|
286
|
+
span=span_of(node),
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
return
|
|
290
|
+
module = node_text(name_nodes[-1])
|
|
291
|
+
# Plain / static using: a namespace wildcard import (any type in it is
|
|
292
|
+
# brought into scope), mirror Java's star import.
|
|
293
|
+
out.imports.append(
|
|
294
|
+
RawImport(
|
|
295
|
+
module=module,
|
|
296
|
+
imported_name="*",
|
|
297
|
+
alias="*",
|
|
298
|
+
span=span_of(node),
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# ---------------- calls ----------------
|
|
303
|
+
|
|
304
|
+
def _calls(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
305
|
+
caps = captures(load_query("csharp", "calls"), root)
|
|
306
|
+
|
|
307
|
+
for node in caps.get("call", []):
|
|
308
|
+
self._add_invocation(node, ctx, out)
|
|
309
|
+
for node in caps.get("new", []):
|
|
310
|
+
self._add_object_creation(node, ctx, out)
|
|
311
|
+
|
|
312
|
+
def _add_invocation(self, node: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
313
|
+
fn = node.child_by_field_name("function")
|
|
314
|
+
if fn is None:
|
|
315
|
+
return
|
|
316
|
+
if fn.type == "member_access_expression":
|
|
317
|
+
name_node = fn.child_by_field_name("name")
|
|
318
|
+
obj = fn.child_by_field_name("expression")
|
|
319
|
+
if name_node is None:
|
|
320
|
+
return
|
|
321
|
+
callee_name = node_text(name_node)
|
|
322
|
+
receiver = node_text(obj) if obj is not None else None
|
|
323
|
+
callee_text = node_text(fn)
|
|
324
|
+
# Static-type calls (`Process.Start`, `System.Diagnostics.Process.Start`)
|
|
325
|
+
# cannot be told from instance calls by the grammar, but the receiver
|
|
326
|
+
# of a static call is a *type* — a dotted chain of PascalCase
|
|
327
|
+
# identifiers. Treat those as bare calls so the resolver keeps the
|
|
328
|
+
# full dotted callee (`cs:Process.Start`) instead of collapsing to
|
|
329
|
+
# `cs:*.Start`; instance receivers (`cmd`, `service`) stay set so
|
|
330
|
+
# `cs:*.Method` sink matching still fires.
|
|
331
|
+
if receiver is not None and _looks_like_type_ref(receiver):
|
|
332
|
+
receiver = None
|
|
333
|
+
else:
|
|
334
|
+
callee_name = node_text(fn).rsplit(".", 1)[-1]
|
|
335
|
+
receiver = None
|
|
336
|
+
callee_text = node_text(fn)
|
|
337
|
+
args = node.child_by_field_name("arguments")
|
|
338
|
+
out.references.append(
|
|
339
|
+
RawReference(
|
|
340
|
+
kind="call",
|
|
341
|
+
callee_text=callee_text,
|
|
342
|
+
callee_name=callee_name,
|
|
343
|
+
receiver_text=receiver,
|
|
344
|
+
span=span_of(node),
|
|
345
|
+
caller_qualified_name=self._caller(node, ctx),
|
|
346
|
+
arg_count=len(args.named_children) if args is not None else 0,
|
|
347
|
+
arg_preview=truncate(node_text(args)) if args is not None else None,
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def _add_object_creation(
|
|
352
|
+
self, node: "Node", ctx: FileContext, out: FileExtraction
|
|
353
|
+
) -> None:
|
|
354
|
+
type_node = node.child_by_field_name("type")
|
|
355
|
+
if type_node is None:
|
|
356
|
+
return
|
|
357
|
+
type_text = node_text(type_node).split("<", 1)[0].strip()
|
|
358
|
+
callee_name = type_text.rsplit(".", 1)[-1]
|
|
359
|
+
args = node.child_by_field_name("arguments")
|
|
360
|
+
out.references.append(
|
|
361
|
+
RawReference(
|
|
362
|
+
kind="call",
|
|
363
|
+
callee_text=type_text,
|
|
364
|
+
callee_name=callee_name,
|
|
365
|
+
receiver_text=None,
|
|
366
|
+
span=span_of(node),
|
|
367
|
+
caller_qualified_name=self._caller(node, ctx),
|
|
368
|
+
arg_count=len(args.named_children) if args is not None else 0,
|
|
369
|
+
arg_preview=truncate(node_text(args)) if args is not None else None,
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# ---------------- helpers ----------------
|
|
374
|
+
|
|
375
|
+
def _namespace(self, node: "Node") -> str | None:
|
|
376
|
+
# Block namespaces enclose their members; walk ancestors.
|
|
377
|
+
current = node.parent
|
|
378
|
+
while current is not None:
|
|
379
|
+
if current.type == "namespace_declaration":
|
|
380
|
+
name = current.child_by_field_name("name")
|
|
381
|
+
if name is not None:
|
|
382
|
+
return node_text(name)
|
|
383
|
+
current = current.parent
|
|
384
|
+
# File-scoped namespaces (C# 10, `namespace Foo;`) do NOT nest their
|
|
385
|
+
# members: the declaration is a sibling at compilation_unit level and
|
|
386
|
+
# applies to everything after it. Find it at the root.
|
|
387
|
+
root = node
|
|
388
|
+
while root.parent is not None:
|
|
389
|
+
root = root.parent
|
|
390
|
+
for child in root.children:
|
|
391
|
+
if child.type == "file_scoped_namespace_declaration":
|
|
392
|
+
name = child.child_by_field_name("name")
|
|
393
|
+
if name is not None:
|
|
394
|
+
return node_text(name)
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
def _type_chain(self, node: "Node") -> list[str]:
|
|
398
|
+
parts, current = [], node.parent
|
|
399
|
+
while current is not None:
|
|
400
|
+
if current.type in _TYPE_SCOPES:
|
|
401
|
+
name = current.child_by_field_name("name")
|
|
402
|
+
if name is not None:
|
|
403
|
+
parts.append(node_text(name))
|
|
404
|
+
current = current.parent
|
|
405
|
+
parts.reverse()
|
|
406
|
+
return parts
|
|
407
|
+
|
|
408
|
+
def _base_prefix(self, node: "Node", ctx: FileContext) -> str:
|
|
409
|
+
ns = self._namespace(node)
|
|
410
|
+
return ns if ns is not None else ctx.module_path
|
|
411
|
+
|
|
412
|
+
def _qualify(
|
|
413
|
+
self, node: "Node", name: str, ctx: FileContext
|
|
414
|
+
) -> tuple[str, str | None]:
|
|
415
|
+
prefix = self._base_prefix(node, ctx)
|
|
416
|
+
chain = self._type_chain(node)
|
|
417
|
+
parent_parts = [prefix, *chain]
|
|
418
|
+
parent_q = ".".join(p for p in parent_parts if p) or None
|
|
419
|
+
qname = ".".join(p for p in [*parent_parts, name] if p)
|
|
420
|
+
return qname, parent_q
|
|
421
|
+
|
|
422
|
+
def _nearest_type_scope(self, node: "Node") -> "Node | None":
|
|
423
|
+
current = node.parent
|
|
424
|
+
while current is not None:
|
|
425
|
+
if current.type in _TYPE_SCOPES:
|
|
426
|
+
return current
|
|
427
|
+
current = current.parent
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
def _caller(self, node: "Node", ctx: FileContext) -> str | None:
|
|
431
|
+
current = node.parent
|
|
432
|
+
while current is not None:
|
|
433
|
+
if current.type in _CALLABLE_SCOPES:
|
|
434
|
+
name = current.child_by_field_name("name")
|
|
435
|
+
if name is not None:
|
|
436
|
+
return self._qualify(current, node_text(name), ctx)[0]
|
|
437
|
+
current = current.parent
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
def _signature(self, node: "Node") -> str:
|
|
441
|
+
text = node_text(node)
|
|
442
|
+
for stop in ("{", "=>", ";"):
|
|
443
|
+
text = text.split(stop, 1)[0]
|
|
444
|
+
return truncate(text.strip(), 120)
|
|
445
|
+
|
|
446
|
+
def _modifiers(self, node: "Node") -> list[str]:
|
|
447
|
+
return [node_text(c) for c in node.children if c.type == "modifier"]
|
|
448
|
+
|
|
449
|
+
def _attribute_lists(self, node: "Node") -> list["Node"]:
|
|
450
|
+
return [c for c in node.children if c.type == "attribute_list"]
|
|
451
|
+
|
|
452
|
+
def _attributes(self, node: "Node") -> list[str]:
|
|
453
|
+
return [
|
|
454
|
+
"[" + node_text(attr) + "]"
|
|
455
|
+
for lst in self._attribute_lists(node)
|
|
456
|
+
for attr in lst.named_children
|
|
457
|
+
if attr.type == "attribute"
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
def _emit_attribute_refs(
|
|
461
|
+
self, node: "Node", owner_qname: str, out: FileExtraction
|
|
462
|
+
) -> None:
|
|
463
|
+
for lst in self._attribute_lists(node):
|
|
464
|
+
for attr in lst.named_children:
|
|
465
|
+
if attr.type != "attribute":
|
|
466
|
+
continue
|
|
467
|
+
name_node = attr.child_by_field_name("name") or next(
|
|
468
|
+
(
|
|
469
|
+
c
|
|
470
|
+
for c in attr.named_children
|
|
471
|
+
if c.type in ("identifier", "qualified_name")
|
|
472
|
+
),
|
|
473
|
+
None,
|
|
474
|
+
)
|
|
475
|
+
if name_node is None:
|
|
476
|
+
continue
|
|
477
|
+
callee_text = node_text(name_node)
|
|
478
|
+
out.references.append(
|
|
479
|
+
RawReference(
|
|
480
|
+
kind="decorator",
|
|
481
|
+
callee_text=callee_text,
|
|
482
|
+
callee_name=callee_text.rsplit(".", 1)[-1],
|
|
483
|
+
receiver_text=callee_text.rsplit(".", 1)[0]
|
|
484
|
+
if "." in callee_text
|
|
485
|
+
else None,
|
|
486
|
+
span=span_of(attr),
|
|
487
|
+
caller_qualified_name=owner_qname,
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
def _base_types(self, node: "Node") -> list[str]:
|
|
492
|
+
base_list = next(
|
|
493
|
+
(c for c in node.named_children if c.type == "base_list"), None
|
|
494
|
+
)
|
|
495
|
+
if base_list is None:
|
|
496
|
+
return []
|
|
497
|
+
bases: list[str] = []
|
|
498
|
+
for child in base_list.named_children:
|
|
499
|
+
text = node_text(child).split("<", 1)[0].strip()
|
|
500
|
+
if text:
|
|
501
|
+
bases.append(text)
|
|
502
|
+
return bases
|