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,400 @@
|
|
|
1
|
+
"""JavaScript / TypeScript / TSX extractor.
|
|
2
|
+
|
|
3
|
+
One extractor drives three grammars. Module paths are dotted (path minus
|
|
4
|
+
extension, ``/`` -> ``.``) and relative imports are pre-expanded to dotted
|
|
5
|
+
project modules here, so the shared (Python-oriented) resolver treats them
|
|
6
|
+
uniformly. ``this`` is handled as a self-receiver by the resolver.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
12
|
+
|
|
13
|
+
from entrygraph.extract.base import FileContext, node_text, span_of, truncate
|
|
14
|
+
from entrygraph.extract.ir import (
|
|
15
|
+
FileExtraction,
|
|
16
|
+
RawImport,
|
|
17
|
+
RawReexport,
|
|
18
|
+
RawReference,
|
|
19
|
+
RawSymbol,
|
|
20
|
+
)
|
|
21
|
+
from entrygraph.kinds import SymbolKind
|
|
22
|
+
from entrygraph.parsing.queries import captures, load_query_for
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
25
|
+
from tree_sitter import Node, Tree
|
|
26
|
+
|
|
27
|
+
_SCOPE_TYPES = frozenset(
|
|
28
|
+
{"function_declaration", "method_definition", "class_declaration",
|
|
29
|
+
"function_expression", "arrow_function"}
|
|
30
|
+
)
|
|
31
|
+
_NAMED_SCOPES = frozenset({"function_declaration", "method_definition", "class_declaration"})
|
|
32
|
+
_SRC_ROOTS = ("src", "lib", "app")
|
|
33
|
+
_EXPORT_PARENTS = frozenset({"export_statement"})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class JavaScriptExtractor:
|
|
37
|
+
language_ids: ClassVar[tuple[str, ...]] = ("javascript", "typescript", "tsx")
|
|
38
|
+
|
|
39
|
+
def module_path_for(self, repo_relative_path: str) -> tuple[str, bool]:
|
|
40
|
+
parts = repo_relative_path.split("/")
|
|
41
|
+
if parts[0] in _SRC_ROOTS and len(parts) > 1:
|
|
42
|
+
parts = parts[1:]
|
|
43
|
+
stem = parts[-1]
|
|
44
|
+
for ext in (".d.ts", ".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".jsx"):
|
|
45
|
+
if stem.endswith(ext):
|
|
46
|
+
stem = stem[: -len(ext)]
|
|
47
|
+
break
|
|
48
|
+
is_package = stem in ("index", "mod")
|
|
49
|
+
parts = parts[:-1] if is_package else [*parts[:-1], stem]
|
|
50
|
+
return ".".join(p for p in parts if p) or "_root", is_package
|
|
51
|
+
|
|
52
|
+
def extract(self, tree: "Tree", ctx: FileContext) -> FileExtraction:
|
|
53
|
+
root = tree.root_node
|
|
54
|
+
out = FileExtraction(
|
|
55
|
+
path=ctx.path, language=ctx.language, module_path=ctx.module_path,
|
|
56
|
+
parse_ok=not root.has_error, error_count=1 if root.has_error else 0,
|
|
57
|
+
)
|
|
58
|
+
self._definitions(root, ctx, out)
|
|
59
|
+
self._imports(root, ctx, out)
|
|
60
|
+
self._calls(root, ctx, out)
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
# ---------------- definitions ----------------
|
|
64
|
+
|
|
65
|
+
def _definitions(self, root, ctx, out) -> None:
|
|
66
|
+
caps = captures(load_query_for(ctx.language, "javascript", "definitions"), root)
|
|
67
|
+
|
|
68
|
+
for node in caps.get("def.function", []):
|
|
69
|
+
self._add_callable(node, ctx, out, SymbolKind.FUNCTION)
|
|
70
|
+
for node in caps.get("def.method", []):
|
|
71
|
+
self._add_callable(node, ctx, out, SymbolKind.METHOD)
|
|
72
|
+
|
|
73
|
+
for node in caps.get("def.class", []):
|
|
74
|
+
name_node = node.child_by_field_name("name")
|
|
75
|
+
if name_node is None:
|
|
76
|
+
continue
|
|
77
|
+
name = node_text(name_node)
|
|
78
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
79
|
+
bases, interfaces = self._heritage(node)
|
|
80
|
+
out.symbols.append(
|
|
81
|
+
RawSymbol(kind=SymbolKind.CLASS, name=name, qualified_name=qname,
|
|
82
|
+
span=span_of(node), parent_qualified_name=parent_q,
|
|
83
|
+
signature=self._signature(node), bases=bases,
|
|
84
|
+
decorators=self._decorators(node),
|
|
85
|
+
is_exported=self._exported(node))
|
|
86
|
+
)
|
|
87
|
+
for base in bases:
|
|
88
|
+
out.references.append(
|
|
89
|
+
RawReference(kind="inherit", callee_text=base,
|
|
90
|
+
callee_name=base.rsplit(".", 1)[-1], receiver_text=None,
|
|
91
|
+
span=span_of(node), caller_qualified_name=qname)
|
|
92
|
+
)
|
|
93
|
+
for iface in interfaces: # TS `implements C, D`
|
|
94
|
+
out.references.append(
|
|
95
|
+
RawReference(kind="implement", callee_text=iface,
|
|
96
|
+
callee_name=iface.rsplit(".", 1)[-1], receiver_text=None,
|
|
97
|
+
span=span_of(node), caller_qualified_name=qname)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
for node in caps.get("def.var", []):
|
|
101
|
+
scope = self._nearest_named_scope(node)
|
|
102
|
+
if scope in ("function_declaration", "method_definition",
|
|
103
|
+
"function_expression", "arrow_function"):
|
|
104
|
+
continue # locals aren't symbols
|
|
105
|
+
name_node = node.child_by_field_name("name")
|
|
106
|
+
value = node.child_by_field_name("value")
|
|
107
|
+
if name_node is None:
|
|
108
|
+
continue
|
|
109
|
+
# a `const x = () => {}` / function expression is a callable symbol
|
|
110
|
+
if value is not None and value.type in ("arrow_function", "function_expression"):
|
|
111
|
+
self._add_named_callable(node, name_node, ctx, out)
|
|
112
|
+
continue
|
|
113
|
+
name = node_text(name_node)
|
|
114
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
115
|
+
out.symbols.append(
|
|
116
|
+
RawSymbol(kind=SymbolKind.VARIABLE, name=name, qualified_name=qname,
|
|
117
|
+
span=span_of(node), parent_qualified_name=parent_q,
|
|
118
|
+
signature=truncate(node_text(node)), is_exported=self._exported(node))
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def _add_callable(self, node, ctx, out, kind) -> None:
|
|
122
|
+
name_node = node.child_by_field_name("name")
|
|
123
|
+
if name_node is None:
|
|
124
|
+
return
|
|
125
|
+
self._add_named_callable(node, name_node, ctx, out, kind)
|
|
126
|
+
|
|
127
|
+
def _add_named_callable(self, node, name_node, ctx, out, kind=SymbolKind.FUNCTION) -> None:
|
|
128
|
+
name = node_text(name_node)
|
|
129
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
130
|
+
if kind is SymbolKind.FUNCTION and self._nearest_named_scope(node) == "class_declaration":
|
|
131
|
+
kind = SymbolKind.METHOD
|
|
132
|
+
out.symbols.append(
|
|
133
|
+
RawSymbol(kind=kind, name=name, qualified_name=qname, span=span_of(node),
|
|
134
|
+
parent_qualified_name=parent_q, signature=self._signature(node),
|
|
135
|
+
decorators=self._decorators(node), is_exported=self._exported(node))
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# ---------------- imports ----------------
|
|
139
|
+
|
|
140
|
+
def _imports(self, root, ctx, out) -> None:
|
|
141
|
+
caps = captures(load_query_for(ctx.language, "javascript", "imports"), root)
|
|
142
|
+
for node in caps.get("import", []):
|
|
143
|
+
source = node.child_by_field_name("source")
|
|
144
|
+
if source is None:
|
|
145
|
+
continue
|
|
146
|
+
module = self._resolve_module(node_text(source).strip("'\"`"), ctx)
|
|
147
|
+
clause = next((c for c in node.named_children if c.type == "import_clause"), None)
|
|
148
|
+
if clause is None:
|
|
149
|
+
out.imports.append(RawImport(module=module, imported_name=None,
|
|
150
|
+
alias=module.split(".")[-1], span=span_of(node)))
|
|
151
|
+
continue
|
|
152
|
+
for child in clause.named_children:
|
|
153
|
+
if child.type == "identifier": # default import
|
|
154
|
+
# Bind the alias to the module itself (CommonJS/esModuleInterop
|
|
155
|
+
# semantics) so `import cp from 'child_process'; cp.exec()`
|
|
156
|
+
# canonicalizes to child_process.exec, not .default.exec.
|
|
157
|
+
out.imports.append(RawImport(module=module, imported_name=None,
|
|
158
|
+
alias=node_text(child), span=span_of(node)))
|
|
159
|
+
elif child.type == "namespace_import":
|
|
160
|
+
ident = child.named_children[-1]
|
|
161
|
+
out.imports.append(RawImport(module=module, imported_name="*",
|
|
162
|
+
alias=node_text(ident), span=span_of(node)))
|
|
163
|
+
elif child.type == "named_imports":
|
|
164
|
+
for spec in child.named_children:
|
|
165
|
+
if spec.type != "import_specifier":
|
|
166
|
+
continue
|
|
167
|
+
name_node = spec.child_by_field_name("name")
|
|
168
|
+
alias_node = spec.child_by_field_name("alias")
|
|
169
|
+
if name_node is None:
|
|
170
|
+
continue
|
|
171
|
+
name = node_text(name_node)
|
|
172
|
+
out.imports.append(
|
|
173
|
+
RawImport(module=module, imported_name=name,
|
|
174
|
+
alias=node_text(alias_node) if alias_node else name,
|
|
175
|
+
span=span_of(node))
|
|
176
|
+
)
|
|
177
|
+
for node in caps.get("export.from", []):
|
|
178
|
+
self._reexport(node, ctx, out)
|
|
179
|
+
|
|
180
|
+
for imp in out.imports:
|
|
181
|
+
out.framework_signals.append(("import", imp.module.split(".")[0] if "." in imp.module else imp.module))
|
|
182
|
+
|
|
183
|
+
def _reexport(self, node, ctx, out) -> None:
|
|
184
|
+
"""`export { X as Y } from "./m"` / `export * from "./m"` -> RawReexport."""
|
|
185
|
+
source = node.child_by_field_name("source")
|
|
186
|
+
if source is None:
|
|
187
|
+
return
|
|
188
|
+
spec = node_text(source).strip("'\"`")
|
|
189
|
+
module = self._resolve_module(spec, ctx)
|
|
190
|
+
is_relative = spec.startswith(".")
|
|
191
|
+
clause = next((c for c in node.named_children if c.type == "export_clause"), None)
|
|
192
|
+
if clause is None: # `export * from "..."`
|
|
193
|
+
out.reexports.append(RawReexport(module=module, exported_name=None, alias=None,
|
|
194
|
+
span=span_of(node), is_star=True,
|
|
195
|
+
is_relative=is_relative))
|
|
196
|
+
return
|
|
197
|
+
for spec_node in clause.named_children:
|
|
198
|
+
if spec_node.type != "export_specifier":
|
|
199
|
+
continue
|
|
200
|
+
name_node = spec_node.child_by_field_name("name")
|
|
201
|
+
alias_node = spec_node.child_by_field_name("alias")
|
|
202
|
+
if name_node is None:
|
|
203
|
+
continue
|
|
204
|
+
out.reexports.append(
|
|
205
|
+
RawReexport(module=module, exported_name=node_text(name_node),
|
|
206
|
+
alias=node_text(alias_node) if alias_node else None,
|
|
207
|
+
span=span_of(node), is_relative=is_relative)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _resolve_module(self, spec: str, ctx: FileContext) -> str:
|
|
211
|
+
if not spec.startswith("."):
|
|
212
|
+
return spec # package import: keep as written (external)
|
|
213
|
+
parts = ctx.module_path.split(".")
|
|
214
|
+
parts = parts if ctx.is_package else parts[:-1]
|
|
215
|
+
for segment in spec.split("/"):
|
|
216
|
+
if segment in ("", "."):
|
|
217
|
+
continue
|
|
218
|
+
if segment == "..":
|
|
219
|
+
parts = parts[:-1]
|
|
220
|
+
else:
|
|
221
|
+
seg = segment
|
|
222
|
+
for ext in (".ts", ".tsx", ".js", ".jsx"):
|
|
223
|
+
if seg.endswith(ext):
|
|
224
|
+
seg = seg[: -len(ext)]
|
|
225
|
+
if seg not in ("index", "mod"):
|
|
226
|
+
parts.append(seg)
|
|
227
|
+
return ".".join(p for p in parts if p) or "_root"
|
|
228
|
+
|
|
229
|
+
# ---------------- calls ----------------
|
|
230
|
+
|
|
231
|
+
def _calls(self, root, ctx, out) -> None:
|
|
232
|
+
caps = captures(load_query_for(ctx.language, "javascript", "calls"), root)
|
|
233
|
+
for node in caps.get("call", []):
|
|
234
|
+
fn = node.child_by_field_name("function")
|
|
235
|
+
if fn is None:
|
|
236
|
+
continue
|
|
237
|
+
args = node.child_by_field_name("arguments")
|
|
238
|
+
caller = self._caller(node, ctx)
|
|
239
|
+
if fn.type == "identifier":
|
|
240
|
+
callee_text, callee_name, receiver = node_text(fn), node_text(fn), None
|
|
241
|
+
elif fn.type == "member_expression":
|
|
242
|
+
obj = fn.child_by_field_name("object")
|
|
243
|
+
prop = fn.child_by_field_name("property")
|
|
244
|
+
if obj is None or prop is None:
|
|
245
|
+
continue
|
|
246
|
+
callee_text, callee_name, receiver = node_text(fn), node_text(prop), node_text(obj)
|
|
247
|
+
elif fn.type == "subscript_expression":
|
|
248
|
+
# obj[name]() — computed member call, target unknowable
|
|
249
|
+
self._emit_dynamic_call(node, fn, caller, out)
|
|
250
|
+
self._emit_callbacks(args, caller, out)
|
|
251
|
+
continue
|
|
252
|
+
else:
|
|
253
|
+
continue
|
|
254
|
+
out.references.append(
|
|
255
|
+
RawReference(kind="call", callee_text=callee_text, callee_name=callee_name,
|
|
256
|
+
receiver_text=receiver, span=span_of(node),
|
|
257
|
+
caller_qualified_name=caller,
|
|
258
|
+
arg_count=len(args.named_children) if args else 0,
|
|
259
|
+
arg_preview=truncate(node_text(args)) if args else None)
|
|
260
|
+
)
|
|
261
|
+
self._emit_callbacks(args, caller, out)
|
|
262
|
+
|
|
263
|
+
def _emit_dynamic_call(self, node, fn, caller, out) -> None:
|
|
264
|
+
args = node.child_by_field_name("arguments")
|
|
265
|
+
out.references.append(
|
|
266
|
+
RawReference(kind="dynamic_call", callee_text=node_text(fn),
|
|
267
|
+
callee_name="<computed>", receiver_text=None, span=span_of(node),
|
|
268
|
+
caller_qualified_name=caller,
|
|
269
|
+
arg_count=len(args.named_children) if args else 0,
|
|
270
|
+
arg_preview=truncate(node_text(args)) if args else None)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _emit_callbacks(self, args, caller, out) -> None:
|
|
274
|
+
"""Bare identifier / simple member args passed to a call (`setTimeout(fn)`)."""
|
|
275
|
+
if args is None:
|
|
276
|
+
return
|
|
277
|
+
for arg in args.named_children:
|
|
278
|
+
if arg.type == "identifier":
|
|
279
|
+
name = node_text(arg)
|
|
280
|
+
elif arg.type == "member_expression":
|
|
281
|
+
prop = arg.child_by_field_name("property")
|
|
282
|
+
if prop is None:
|
|
283
|
+
continue
|
|
284
|
+
name = node_text(prop)
|
|
285
|
+
else:
|
|
286
|
+
continue
|
|
287
|
+
out.references.append(
|
|
288
|
+
RawReference(kind="callback", callee_text=node_text(arg), callee_name=name,
|
|
289
|
+
receiver_text=None, span=span_of(arg), caller_qualified_name=caller)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# ---------------- helpers ----------------
|
|
293
|
+
|
|
294
|
+
def _scope_chain(self, node) -> list[str]:
|
|
295
|
+
parts, current = [], node.parent
|
|
296
|
+
while current is not None:
|
|
297
|
+
if current.type in _NAMED_SCOPES:
|
|
298
|
+
name = current.child_by_field_name("name")
|
|
299
|
+
if name is not None:
|
|
300
|
+
parts.append(node_text(name))
|
|
301
|
+
current = current.parent
|
|
302
|
+
parts.reverse()
|
|
303
|
+
return parts
|
|
304
|
+
|
|
305
|
+
def _nearest_named_scope(self, node) -> str | None:
|
|
306
|
+
current = node.parent
|
|
307
|
+
while current is not None:
|
|
308
|
+
if current.type in _SCOPE_TYPES:
|
|
309
|
+
return current.type
|
|
310
|
+
current = current.parent
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
def _qualify(self, node, name, ctx) -> tuple[str, str | None]:
|
|
314
|
+
chain = self._scope_chain(node)
|
|
315
|
+
parent_q = ".".join([ctx.module_path, *chain]) if chain else None
|
|
316
|
+
return ".".join([ctx.module_path, *chain, name]), parent_q
|
|
317
|
+
|
|
318
|
+
def _caller(self, node, ctx) -> str | None:
|
|
319
|
+
current = node.parent
|
|
320
|
+
while current is not None:
|
|
321
|
+
if current.type in ("function_declaration", "method_definition"):
|
|
322
|
+
name = current.child_by_field_name("name")
|
|
323
|
+
if name is not None:
|
|
324
|
+
return self._qualify(current, node_text(name), ctx)[0]
|
|
325
|
+
if current.type == "variable_declarator":
|
|
326
|
+
value = current.child_by_field_name("value")
|
|
327
|
+
if value is not None and value.type in ("arrow_function", "function_expression"):
|
|
328
|
+
name = current.child_by_field_name("name")
|
|
329
|
+
if name is not None:
|
|
330
|
+
return self._qualify(current, node_text(name), ctx)[0]
|
|
331
|
+
current = current.parent
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
def _signature(self, node) -> str:
|
|
335
|
+
return truncate(node_text(node).split("\n", 1)[0].rstrip("{").strip(), 120)
|
|
336
|
+
|
|
337
|
+
def _decorators(self, node) -> list[str]:
|
|
338
|
+
"""Raw text of `decorator` nodes immediately preceding `node`.
|
|
339
|
+
|
|
340
|
+
Decorators are siblings that appear just before their target — class
|
|
341
|
+
decorators inside the enclosing `export_statement`/`program`, method
|
|
342
|
+
decorators inside the `class_body`. A non-decorator sibling resets the
|
|
343
|
+
run, so each target only picks up its own decorators.
|
|
344
|
+
"""
|
|
345
|
+
parent = node.parent
|
|
346
|
+
if parent is None:
|
|
347
|
+
return []
|
|
348
|
+
decs: list[str] = []
|
|
349
|
+
for child in parent.named_children:
|
|
350
|
+
if child.start_byte == node.start_byte and child.end_byte == node.end_byte:
|
|
351
|
+
return decs
|
|
352
|
+
if child.type == "decorator":
|
|
353
|
+
decs.append(node_text(child))
|
|
354
|
+
else:
|
|
355
|
+
decs = []
|
|
356
|
+
return decs
|
|
357
|
+
|
|
358
|
+
def _heritage(self, node) -> tuple[list[str], list[str]]:
|
|
359
|
+
"""Return (extends bases, implements interfaces).
|
|
360
|
+
|
|
361
|
+
Plain JS puts identifiers directly under `class_heritage`; TS wraps them
|
|
362
|
+
in `extends_clause` / `implements_clause`.
|
|
363
|
+
"""
|
|
364
|
+
heritage = next((c for c in node.named_children if c.type == "class_heritage"), None)
|
|
365
|
+
if heritage is None:
|
|
366
|
+
return [], []
|
|
367
|
+
bases: list[str] = []
|
|
368
|
+
interfaces: list[str] = []
|
|
369
|
+
|
|
370
|
+
def _names(container) -> list[str]:
|
|
371
|
+
out = []
|
|
372
|
+
for child in container.named_children:
|
|
373
|
+
if child.type in ("identifier", "member_expression", "type_identifier",
|
|
374
|
+
"generic_type", "nested_type_identifier"):
|
|
375
|
+
text = node_text(child).split("<", 1)[0].strip()
|
|
376
|
+
if text:
|
|
377
|
+
out.append(text)
|
|
378
|
+
return out
|
|
379
|
+
|
|
380
|
+
saw_clause = False
|
|
381
|
+
for child in heritage.named_children:
|
|
382
|
+
if child.type == "extends_clause":
|
|
383
|
+
saw_clause = True
|
|
384
|
+
bases.extend(_names(child))
|
|
385
|
+
elif child.type == "implements_clause":
|
|
386
|
+
saw_clause = True
|
|
387
|
+
interfaces.extend(_names(child))
|
|
388
|
+
if not saw_clause: # plain JS: identifiers directly under class_heritage
|
|
389
|
+
bases.extend(_names(heritage))
|
|
390
|
+
return bases, interfaces
|
|
391
|
+
|
|
392
|
+
def _exported(self, node) -> bool:
|
|
393
|
+
current = node.parent
|
|
394
|
+
while current is not None:
|
|
395
|
+
if current.type in _EXPORT_PARENTS:
|
|
396
|
+
return True
|
|
397
|
+
if current.type in ("program", "statement_block", "class_body"):
|
|
398
|
+
break
|
|
399
|
+
current = current.parent
|
|
400
|
+
return False
|