seam-code 0.3.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.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
"""TypeScript/JavaScript symbol and edge extraction from tree-sitter ASTs.
|
|
2
|
+
|
|
3
|
+
LAYER: imports from graph_common (leaf), graph_scope_infer (leaf), field_access (leaf),
|
|
4
|
+
signatures (leaf) — never from graph.py.
|
|
5
|
+
|
|
6
|
+
LAYERING:
|
|
7
|
+
graph_common (leaf — no seam deps)
|
|
8
|
+
graph_scope_infer (leaf — Python+TS receiver-type inference)
|
|
9
|
+
field_access (leaf — field-access read/write classification + field symbols)
|
|
10
|
+
↑
|
|
11
|
+
graph_typescript (this file — TS/JS symbol/edge extraction)
|
|
12
|
+
↑
|
|
13
|
+
graph.py (dispatcher; imports this module's public extractors)
|
|
14
|
+
|
|
15
|
+
WHY split from graph.py: graph.py exceeded 1000 lines. TypeScript/JS extraction is a
|
|
16
|
+
coherent leaf unit split following the Phase 9 precedent.
|
|
17
|
+
|
|
18
|
+
Contract:
|
|
19
|
+
- Accept a tree-sitter Node + filepath.
|
|
20
|
+
- Return a list — never raise, never return None.
|
|
21
|
+
- Edges carry confidence='INFERRED' by default.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from tree_sitter import Node
|
|
27
|
+
|
|
28
|
+
import seam.config as config
|
|
29
|
+
from seam.indexer.field_access import (
|
|
30
|
+
collect_field_symbols_typescript,
|
|
31
|
+
extract_field_accesses_typescript,
|
|
32
|
+
)
|
|
33
|
+
from seam.indexer.graph_common import (
|
|
34
|
+
Comment,
|
|
35
|
+
Edge,
|
|
36
|
+
Symbol,
|
|
37
|
+
_base_type_name,
|
|
38
|
+
_block_comment_lines,
|
|
39
|
+
_find_enclosing_function,
|
|
40
|
+
_make_symbol,
|
|
41
|
+
_match_marker,
|
|
42
|
+
_text,
|
|
43
|
+
)
|
|
44
|
+
from seam.indexer.graph_scope_infer import (
|
|
45
|
+
_TS_SELF_NAMES,
|
|
46
|
+
collect_composition_types_typescript,
|
|
47
|
+
collect_param_types_typescript,
|
|
48
|
+
record_ts_local_types,
|
|
49
|
+
record_ts_param_types,
|
|
50
|
+
resolve_receiver_type,
|
|
51
|
+
scan_class_fields_typescript,
|
|
52
|
+
)
|
|
53
|
+
from seam.indexer.signatures import extract_node_fields
|
|
54
|
+
|
|
55
|
+
# ── JSDoc helper ───────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ts_jsdoc(symbol_node: Node) -> str | None:
|
|
59
|
+
"""Extract leading JSDoc comment from the previous sibling node.
|
|
60
|
+
|
|
61
|
+
tree-sitter emits /** ... */ blocks as 'comment' nodes immediately
|
|
62
|
+
before the declaration. Only /** blocks qualify as JSDoc.
|
|
63
|
+
"""
|
|
64
|
+
prev = symbol_node.prev_sibling
|
|
65
|
+
if prev is None or prev.type != "comment":
|
|
66
|
+
return None
|
|
67
|
+
comment_text = _text(prev)
|
|
68
|
+
if not comment_text.startswith("/**"):
|
|
69
|
+
return None
|
|
70
|
+
return comment_text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── TypeScript/JavaScript extraction ──────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _extract_symbols_typescript(root: Node, filepath: Path) -> list[Symbol]:
|
|
77
|
+
"""Walk a TypeScript/TSX AST and extract all symbol types.
|
|
78
|
+
|
|
79
|
+
A3 Slice 2: when SEAM_FIELD_ACCESS_EDGES='on', also emits kind='field' symbols
|
|
80
|
+
for each class using collect_field_symbols_typescript (field declarations +
|
|
81
|
+
constructor this.x= + parameter properties).
|
|
82
|
+
"""
|
|
83
|
+
symbols: list[Symbol] = []
|
|
84
|
+
file_str = str(filepath)
|
|
85
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
86
|
+
|
|
87
|
+
def _walk(node: Node, class_name: str | None = None) -> None:
|
|
88
|
+
if node.type == "function_declaration":
|
|
89
|
+
name_node = node.child_by_field_name("name")
|
|
90
|
+
if name_node:
|
|
91
|
+
name = _text(name_node)
|
|
92
|
+
kind = "method" if class_name else "function"
|
|
93
|
+
qualified = f"{class_name}.{name}" if class_name else name
|
|
94
|
+
doc = _ts_jsdoc(node)
|
|
95
|
+
fields = extract_node_fields(
|
|
96
|
+
node,
|
|
97
|
+
"typescript",
|
|
98
|
+
qualified_name=qualified,
|
|
99
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
100
|
+
)
|
|
101
|
+
symbols.append(
|
|
102
|
+
_make_symbol(
|
|
103
|
+
qualified,
|
|
104
|
+
kind,
|
|
105
|
+
file_str,
|
|
106
|
+
node,
|
|
107
|
+
doc,
|
|
108
|
+
signature=fields["signature"],
|
|
109
|
+
decorators=fields["decorators"],
|
|
110
|
+
is_exported=fields["is_exported"],
|
|
111
|
+
visibility=fields["visibility"],
|
|
112
|
+
qualified_name=qualified,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
body = node.child_by_field_name("body")
|
|
116
|
+
if body:
|
|
117
|
+
for child in body.children:
|
|
118
|
+
_walk(child, None)
|
|
119
|
+
|
|
120
|
+
elif node.type == "method_definition":
|
|
121
|
+
name_node = node.child_by_field_name("name")
|
|
122
|
+
if name_node:
|
|
123
|
+
name = _text(name_node)
|
|
124
|
+
qualified = f"{class_name}.{name}" if class_name else name
|
|
125
|
+
doc = _ts_jsdoc(node)
|
|
126
|
+
fields = extract_node_fields(
|
|
127
|
+
node,
|
|
128
|
+
"typescript",
|
|
129
|
+
qualified_name=qualified,
|
|
130
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
131
|
+
)
|
|
132
|
+
symbols.append(
|
|
133
|
+
_make_symbol(
|
|
134
|
+
qualified,
|
|
135
|
+
"method",
|
|
136
|
+
file_str,
|
|
137
|
+
node,
|
|
138
|
+
doc,
|
|
139
|
+
signature=fields["signature"],
|
|
140
|
+
decorators=fields["decorators"],
|
|
141
|
+
is_exported=fields["is_exported"],
|
|
142
|
+
visibility=fields["visibility"],
|
|
143
|
+
qualified_name=qualified,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
elif node.type == "class_declaration":
|
|
148
|
+
name_node = node.child_by_field_name("name")
|
|
149
|
+
if name_node:
|
|
150
|
+
cls_name = _text(name_node)
|
|
151
|
+
doc = _ts_jsdoc(node)
|
|
152
|
+
fields = extract_node_fields(
|
|
153
|
+
node,
|
|
154
|
+
"typescript",
|
|
155
|
+
qualified_name=cls_name,
|
|
156
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
157
|
+
)
|
|
158
|
+
symbols.append(
|
|
159
|
+
_make_symbol(
|
|
160
|
+
cls_name,
|
|
161
|
+
"class",
|
|
162
|
+
file_str,
|
|
163
|
+
node,
|
|
164
|
+
doc,
|
|
165
|
+
signature=fields["signature"],
|
|
166
|
+
decorators=fields["decorators"],
|
|
167
|
+
is_exported=fields["is_exported"],
|
|
168
|
+
visibility=fields["visibility"],
|
|
169
|
+
qualified_name=cls_name,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# A3 Slice 2: emit field symbols for this TS class.
|
|
174
|
+
# Mirrors the Python approach: collect (qualified_field, line) pairs
|
|
175
|
+
# from field declarations, constructor assignments, and param properties.
|
|
176
|
+
# Dedup is handled by collect_field_symbols_typescript.
|
|
177
|
+
# WHY Symbol(...) not _make_symbol: _make_symbol derives start/end lines
|
|
178
|
+
# from the node start_point, but field lines come from the collector.
|
|
179
|
+
# Using Symbol directly (as graph_python.py does) avoids a sentinel node.
|
|
180
|
+
if field_access_on and cls_name:
|
|
181
|
+
for qualified_field, field_line in collect_field_symbols_typescript(
|
|
182
|
+
node, cls_name
|
|
183
|
+
):
|
|
184
|
+
symbols.append(Symbol(
|
|
185
|
+
name=qualified_field,
|
|
186
|
+
kind="field",
|
|
187
|
+
file=file_str,
|
|
188
|
+
start_line=field_line,
|
|
189
|
+
end_line=field_line,
|
|
190
|
+
docstring=None,
|
|
191
|
+
signature=None,
|
|
192
|
+
decorators=[],
|
|
193
|
+
is_exported=None,
|
|
194
|
+
visibility=None,
|
|
195
|
+
qualified_name=qualified_field,
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
body = node.child_by_field_name("body")
|
|
199
|
+
if body:
|
|
200
|
+
for child in body.children:
|
|
201
|
+
_walk(child, cls_name)
|
|
202
|
+
|
|
203
|
+
elif node.type == "interface_declaration":
|
|
204
|
+
name_node = node.child_by_field_name("name")
|
|
205
|
+
if name_node:
|
|
206
|
+
name = _text(name_node)
|
|
207
|
+
doc = _ts_jsdoc(node)
|
|
208
|
+
fields = extract_node_fields(
|
|
209
|
+
node,
|
|
210
|
+
"typescript",
|
|
211
|
+
qualified_name=name,
|
|
212
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
213
|
+
)
|
|
214
|
+
symbols.append(
|
|
215
|
+
_make_symbol(
|
|
216
|
+
name,
|
|
217
|
+
"interface",
|
|
218
|
+
file_str,
|
|
219
|
+
node,
|
|
220
|
+
doc,
|
|
221
|
+
signature=fields["signature"],
|
|
222
|
+
decorators=fields["decorators"],
|
|
223
|
+
is_exported=fields["is_exported"],
|
|
224
|
+
visibility=fields["visibility"],
|
|
225
|
+
qualified_name=name,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
elif node.type == "type_alias_declaration":
|
|
230
|
+
name_node = node.child_by_field_name("name")
|
|
231
|
+
if name_node:
|
|
232
|
+
name = _text(name_node)
|
|
233
|
+
doc = _ts_jsdoc(node)
|
|
234
|
+
fields = extract_node_fields(
|
|
235
|
+
node,
|
|
236
|
+
"typescript",
|
|
237
|
+
qualified_name=name,
|
|
238
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
239
|
+
)
|
|
240
|
+
symbols.append(
|
|
241
|
+
_make_symbol(
|
|
242
|
+
name,
|
|
243
|
+
"type",
|
|
244
|
+
file_str,
|
|
245
|
+
node,
|
|
246
|
+
doc,
|
|
247
|
+
signature=fields["signature"],
|
|
248
|
+
decorators=fields["decorators"],
|
|
249
|
+
is_exported=fields["is_exported"],
|
|
250
|
+
visibility=fields["visibility"],
|
|
251
|
+
qualified_name=name,
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
else:
|
|
256
|
+
for child in node.children:
|
|
257
|
+
_walk(child, class_name)
|
|
258
|
+
|
|
259
|
+
for child in root.children:
|
|
260
|
+
_walk(child, None)
|
|
261
|
+
|
|
262
|
+
return symbols
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _extract_edges_typescript(root: Node, filepath: Path) -> list[Edge]:
|
|
266
|
+
"""Extract import and call edges from a TypeScript/TSX AST.
|
|
267
|
+
|
|
268
|
+
Import heuristic:
|
|
269
|
+
- default import X → target = 'X'
|
|
270
|
+
- named import { X, Y } → one edge per import_specifier (real name)
|
|
271
|
+
- aliased import { a as b } → target = 'a' (real name, not alias)
|
|
272
|
+
- namespace import * as ns → target = 'ns'
|
|
273
|
+
|
|
274
|
+
Call heuristic (Tier B B4 enhanced):
|
|
275
|
+
- identifier call_expression → source/target edge (bare, no receiver)
|
|
276
|
+
- member_expression call (obj.method()) → when SEAM_TYPE_INFERENCE=on and the
|
|
277
|
+
receiver type is known in scope, target = 'Type.method'; else bare 'method'.
|
|
278
|
+
- Scope: class field pre-scan (order-independent) + per-function param/local types.
|
|
279
|
+
|
|
280
|
+
B6: new_expression → 'instantiates' edges.
|
|
281
|
+
"""
|
|
282
|
+
edges: list[Edge] = []
|
|
283
|
+
file_str = str(filepath)
|
|
284
|
+
file_stem = filepath.stem
|
|
285
|
+
emit_inheritance = config.SEAM_INHERITANCE_EDGES == "on"
|
|
286
|
+
type_inference_on = config.SEAM_TYPE_INFERENCE == "on"
|
|
287
|
+
composition_on = config.SEAM_COMPOSITION_EDGES == "on"
|
|
288
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
289
|
+
param_edges_on = config.SEAM_PARAM_EDGES == "on"
|
|
290
|
+
|
|
291
|
+
def _emit_ts_inheritance(node: Node) -> None:
|
|
292
|
+
name_node = node.child_by_field_name("name")
|
|
293
|
+
if name_node is None:
|
|
294
|
+
return
|
|
295
|
+
src_name = _text(name_node)
|
|
296
|
+
line = node.start_point[0] + 1
|
|
297
|
+
|
|
298
|
+
def _emit_from_clause(clause: Node, kind: str) -> None:
|
|
299
|
+
for c in clause.named_children:
|
|
300
|
+
target = _base_type_name(c)
|
|
301
|
+
if target:
|
|
302
|
+
edges.append(Edge(
|
|
303
|
+
source=src_name,
|
|
304
|
+
target=target,
|
|
305
|
+
kind=kind,
|
|
306
|
+
file=file_str,
|
|
307
|
+
line=line,
|
|
308
|
+
confidence="INFERRED",
|
|
309
|
+
receiver=None,
|
|
310
|
+
))
|
|
311
|
+
|
|
312
|
+
for child in node.children:
|
|
313
|
+
if child.type == "class_heritage":
|
|
314
|
+
for clause in child.children:
|
|
315
|
+
if clause.type == "extends_clause":
|
|
316
|
+
_emit_from_clause(clause, "extends")
|
|
317
|
+
elif clause.type == "implements_clause":
|
|
318
|
+
_emit_from_clause(clause, "implements")
|
|
319
|
+
elif child.type == "extends_type_clause":
|
|
320
|
+
_emit_from_clause(child, "extends")
|
|
321
|
+
|
|
322
|
+
def _emit_import_edges_ts(node: Node) -> None:
|
|
323
|
+
line = node.start_point[0] + 1
|
|
324
|
+
clause = None
|
|
325
|
+
for child in node.children:
|
|
326
|
+
if child.type == "import_clause":
|
|
327
|
+
clause = child
|
|
328
|
+
break
|
|
329
|
+
if not clause:
|
|
330
|
+
return
|
|
331
|
+
for clause_child in clause.children:
|
|
332
|
+
if clause_child.type == "identifier":
|
|
333
|
+
edges.append(Edge(
|
|
334
|
+
source=file_stem,
|
|
335
|
+
target=_text(clause_child),
|
|
336
|
+
kind="import",
|
|
337
|
+
file=file_str,
|
|
338
|
+
line=line,
|
|
339
|
+
confidence="INFERRED",
|
|
340
|
+
receiver=None,
|
|
341
|
+
))
|
|
342
|
+
elif clause_child.type == "namespace_import":
|
|
343
|
+
for ns_child in clause_child.children:
|
|
344
|
+
if ns_child.type == "identifier":
|
|
345
|
+
edges.append(Edge(
|
|
346
|
+
source=file_stem,
|
|
347
|
+
target=_text(ns_child),
|
|
348
|
+
kind="import",
|
|
349
|
+
file=file_str,
|
|
350
|
+
line=line,
|
|
351
|
+
confidence="INFERRED",
|
|
352
|
+
receiver=None,
|
|
353
|
+
))
|
|
354
|
+
break
|
|
355
|
+
elif clause_child.type == "named_imports":
|
|
356
|
+
for spec in clause_child.children:
|
|
357
|
+
if spec.type == "import_specifier":
|
|
358
|
+
name_node = spec.child_by_field_name("name")
|
|
359
|
+
if name_node is None and spec.children:
|
|
360
|
+
name_node = spec.children[0]
|
|
361
|
+
if name_node:
|
|
362
|
+
edges.append(Edge(
|
|
363
|
+
source=file_stem,
|
|
364
|
+
target=_text(name_node),
|
|
365
|
+
kind="import",
|
|
366
|
+
file=file_str,
|
|
367
|
+
line=line,
|
|
368
|
+
confidence="INFERRED",
|
|
369
|
+
receiver=None,
|
|
370
|
+
))
|
|
371
|
+
|
|
372
|
+
def _emit_ts_instantiates(node: Node, class_name: str | None, language: str) -> None:
|
|
373
|
+
"""Emit an instantiates edge from a TS/JS new_expression node."""
|
|
374
|
+
try:
|
|
375
|
+
source = _find_enclosing_function(node, language)
|
|
376
|
+
if source is None:
|
|
377
|
+
return
|
|
378
|
+
for child in node.children:
|
|
379
|
+
if child.type == "identifier":
|
|
380
|
+
type_name = _text(child)
|
|
381
|
+
if type_name:
|
|
382
|
+
edges.append(Edge(
|
|
383
|
+
source=source,
|
|
384
|
+
target=type_name,
|
|
385
|
+
kind="instantiates",
|
|
386
|
+
file=file_str,
|
|
387
|
+
line=node.start_point[0] + 1,
|
|
388
|
+
confidence="INFERRED",
|
|
389
|
+
receiver=None,
|
|
390
|
+
))
|
|
391
|
+
return
|
|
392
|
+
except Exception: # noqa: BLE001
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
def _emit_call_edge_ts(
|
|
396
|
+
node: Node,
|
|
397
|
+
class_name: str | None,
|
|
398
|
+
var_types: dict[str, str],
|
|
399
|
+
language: str,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Emit a call edge for a TS/JS call_expression node.
|
|
402
|
+
|
|
403
|
+
TS/JS does NOT need a PascalCase bare-call → 'instantiates' check here
|
|
404
|
+
because construction uses `new Foo(...)` (new_expression), handled separately
|
|
405
|
+
by _emit_ts_instantiates. Python has no 'new' keyword, so it must heuristic
|
|
406
|
+
on PascalCase; TS/JS has an explicit AST node for it.
|
|
407
|
+
|
|
408
|
+
receiver_text is stored in the edge even when the target was already qualified
|
|
409
|
+
— same reasoning as Python: preserves raw text for debugging and future passes.
|
|
410
|
+
"""
|
|
411
|
+
func_child = node.child_by_field_name("function")
|
|
412
|
+
callee_node: Node | None = None
|
|
413
|
+
receiver_text: str | None = None
|
|
414
|
+
|
|
415
|
+
if func_child and func_child.type == "identifier":
|
|
416
|
+
callee_node = func_child
|
|
417
|
+
elif func_child and func_child.type == "member_expression":
|
|
418
|
+
# member_expression: object.property() — 'property' is the callee name.
|
|
419
|
+
prop = func_child.child_by_field_name("property")
|
|
420
|
+
obj = func_child.child_by_field_name("object")
|
|
421
|
+
if prop is not None and prop.type == "property_identifier":
|
|
422
|
+
callee_node = prop
|
|
423
|
+
if obj is not None:
|
|
424
|
+
receiver_text = _text(obj)
|
|
425
|
+
|
|
426
|
+
if callee_node is None:
|
|
427
|
+
return
|
|
428
|
+
source = _find_enclosing_function(node, language)
|
|
429
|
+
if source is None:
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
method_name = _text(callee_node)
|
|
433
|
+
target = method_name
|
|
434
|
+
|
|
435
|
+
# B4: qualify target to 'Type.method' when the receiver type is known.
|
|
436
|
+
# When resolve_receiver_type returns None (unknown, optional, generic,
|
|
437
|
+
# or chained), keep the bare method name — conservatism contract.
|
|
438
|
+
if type_inference_on and receiver_text is not None:
|
|
439
|
+
resolved_type = resolve_receiver_type(
|
|
440
|
+
receiver_text, class_name, var_types, _TS_SELF_NAMES
|
|
441
|
+
)
|
|
442
|
+
if resolved_type is not None:
|
|
443
|
+
target = f"{resolved_type}.{method_name}"
|
|
444
|
+
|
|
445
|
+
edges.append(Edge(
|
|
446
|
+
source=source,
|
|
447
|
+
target=target,
|
|
448
|
+
kind="call",
|
|
449
|
+
file=file_str,
|
|
450
|
+
line=node.start_point[0] + 1,
|
|
451
|
+
confidence="INFERRED",
|
|
452
|
+
receiver=receiver_text,
|
|
453
|
+
))
|
|
454
|
+
|
|
455
|
+
def _walk_ts_function_body(
|
|
456
|
+
func_node: Node,
|
|
457
|
+
class_name: str | None,
|
|
458
|
+
class_fields: dict[str, str],
|
|
459
|
+
language: str,
|
|
460
|
+
) -> None:
|
|
461
|
+
# Copy class_fields into a fresh per-function scope (Layer 2).
|
|
462
|
+
# WHY copy not reference: param/local bindings must not bleed between methods.
|
|
463
|
+
# class_fields is immutable from the caller's perspective (Layer 1 pre-scan).
|
|
464
|
+
var_types: dict[str, str] = dict(class_fields)
|
|
465
|
+
record_ts_param_types(func_node, var_types)
|
|
466
|
+
|
|
467
|
+
# 'uses' edges: this function references plain user types as parameters.
|
|
468
|
+
# source_fn derived exactly like the field-access block below (named function →
|
|
469
|
+
# qualified Class.method; anonymous arrow/expression → enclosing-function lookup).
|
|
470
|
+
if param_edges_on:
|
|
471
|
+
uses_source: str | None
|
|
472
|
+
uses_name_node = func_node.child_by_field_name("name")
|
|
473
|
+
if uses_name_node is not None:
|
|
474
|
+
uses_fn = _text(uses_name_node)
|
|
475
|
+
uses_source = f"{class_name}.{uses_fn}" if class_name else uses_fn
|
|
476
|
+
else:
|
|
477
|
+
uses_source = _find_enclosing_function(func_node, language)
|
|
478
|
+
if uses_source is not None:
|
|
479
|
+
for ptype, pline in collect_param_types_typescript(func_node):
|
|
480
|
+
edges.append(Edge(
|
|
481
|
+
source=uses_source,
|
|
482
|
+
target=ptype,
|
|
483
|
+
kind="uses",
|
|
484
|
+
file=file_str,
|
|
485
|
+
line=pline,
|
|
486
|
+
confidence="INFERRED",
|
|
487
|
+
receiver=None,
|
|
488
|
+
))
|
|
489
|
+
|
|
490
|
+
body = func_node.child_by_field_name("body")
|
|
491
|
+
if body is None:
|
|
492
|
+
return
|
|
493
|
+
for stmt in body.children:
|
|
494
|
+
# Accumulate local type bindings BEFORE emitting call edges for this
|
|
495
|
+
# statement so that a call on the same line as a declaration can still
|
|
496
|
+
# see that declaration (e.g. `const e = new Engine(); e.start()`).
|
|
497
|
+
record_ts_local_types(stmt, var_types)
|
|
498
|
+
_walk_ts_stmt(stmt, class_name, var_types, class_fields, language)
|
|
499
|
+
|
|
500
|
+
# A3 Slice 2: emit reads/writes edges for field accesses in this function body.
|
|
501
|
+
# Done AFTER the main stmt walk so var_types is fully populated (record_ts_local_types
|
|
502
|
+
# runs incrementally during the stmt walk above). WHY separate pass: fully-populated
|
|
503
|
+
# var_types gives higher-quality receiver resolution than an interleaved walk.
|
|
504
|
+
# Mirrors graph_python.py's identical two-pass design for extract_field_accesses_python.
|
|
505
|
+
if field_access_on:
|
|
506
|
+
source_fn: str | None = None
|
|
507
|
+
name_node = func_node.child_by_field_name("name")
|
|
508
|
+
if name_node is not None:
|
|
509
|
+
fn_name = _text(name_node)
|
|
510
|
+
source_fn = f"{class_name}.{fn_name}" if class_name else fn_name
|
|
511
|
+
else:
|
|
512
|
+
# Anonymous function (arrow/expression) — use enclosing-function lookup.
|
|
513
|
+
source_fn = _find_enclosing_function(func_node, language)
|
|
514
|
+
if source_fn is not None and body is not None:
|
|
515
|
+
for _src, target_field, mode, fa_line in extract_field_accesses_typescript(
|
|
516
|
+
body, source_fn, class_name, var_types
|
|
517
|
+
):
|
|
518
|
+
edges.append(Edge(
|
|
519
|
+
source=source_fn,
|
|
520
|
+
target=target_field,
|
|
521
|
+
kind=mode,
|
|
522
|
+
file=file_str,
|
|
523
|
+
line=fa_line,
|
|
524
|
+
confidence="INFERRED",
|
|
525
|
+
receiver=None,
|
|
526
|
+
))
|
|
527
|
+
|
|
528
|
+
def _walk_ts_stmt(
|
|
529
|
+
node: Node,
|
|
530
|
+
class_name: str | None,
|
|
531
|
+
var_types: dict[str, str],
|
|
532
|
+
class_fields: dict[str, str],
|
|
533
|
+
language: str,
|
|
534
|
+
) -> None:
|
|
535
|
+
if node.type == "import_statement":
|
|
536
|
+
_emit_import_edges_ts(node)
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
# interface_declaration: emit inheritance here (not in _walk_ts_class).
|
|
540
|
+
# class_declaration: inheritance is emitted inside _walk_ts_class — do NOT emit here
|
|
541
|
+
# to avoid double-emitting when a class is defined inside a function body.
|
|
542
|
+
if emit_inheritance and node.type == "interface_declaration":
|
|
543
|
+
_emit_ts_inheritance(node)
|
|
544
|
+
|
|
545
|
+
if node.type == "call_expression":
|
|
546
|
+
_emit_call_edge_ts(node, class_name, var_types, language)
|
|
547
|
+
|
|
548
|
+
if node.type == "new_expression":
|
|
549
|
+
_emit_ts_instantiates(node, class_name, language)
|
|
550
|
+
|
|
551
|
+
if node.type in ("function_declaration", "arrow_function", "function_expression"):
|
|
552
|
+
_walk_ts_function_body(node, class_name, class_fields, language)
|
|
553
|
+
return
|
|
554
|
+
if node.type == "method_definition":
|
|
555
|
+
_walk_ts_function_body(node, class_name, class_fields, language)
|
|
556
|
+
return
|
|
557
|
+
if node.type == "class_declaration":
|
|
558
|
+
_walk_ts_class(node, language)
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
for child in node.children:
|
|
562
|
+
_walk_ts_stmt(child, class_name, var_types, class_fields, language)
|
|
563
|
+
|
|
564
|
+
def _walk_ts_class(class_node: Node, language: str) -> None:
|
|
565
|
+
cls_name_node = class_node.child_by_field_name("name")
|
|
566
|
+
cls_name = _text(cls_name_node) if cls_name_node else None
|
|
567
|
+
|
|
568
|
+
if emit_inheritance and cls_name:
|
|
569
|
+
_emit_ts_inheritance(class_node)
|
|
570
|
+
|
|
571
|
+
class_fields: dict[str, str] = {}
|
|
572
|
+
if type_inference_on and cls_name:
|
|
573
|
+
class_fields = scan_class_fields_typescript(class_node)
|
|
574
|
+
|
|
575
|
+
# Slice #77: emit composition (holds) edges for this TS class.
|
|
576
|
+
# Mirrors the Python approach: collect deduped (type, line) pairs and emit
|
|
577
|
+
# one holds edge per unique held type. The collector handles dedup so both
|
|
578
|
+
# a field declaration and a ctor parameter of the same type produce one edge.
|
|
579
|
+
if composition_on and cls_name:
|
|
580
|
+
for held_type, held_line in collect_composition_types_typescript(class_node):
|
|
581
|
+
edges.append(Edge(
|
|
582
|
+
source=cls_name,
|
|
583
|
+
target=held_type,
|
|
584
|
+
kind="holds",
|
|
585
|
+
file=file_str,
|
|
586
|
+
line=held_line,
|
|
587
|
+
confidence="INFERRED",
|
|
588
|
+
receiver=None,
|
|
589
|
+
))
|
|
590
|
+
|
|
591
|
+
body = class_node.child_by_field_name("body")
|
|
592
|
+
if body is None:
|
|
593
|
+
return
|
|
594
|
+
for child in body.children:
|
|
595
|
+
if child.type == "method_definition":
|
|
596
|
+
_walk_ts_function_body(child, cls_name, class_fields, language)
|
|
597
|
+
elif child.type in ("public_field_definition", "field_definition"):
|
|
598
|
+
pass
|
|
599
|
+
elif child.type == "class_declaration":
|
|
600
|
+
_walk_ts_class(child, language)
|
|
601
|
+
else:
|
|
602
|
+
_walk_ts_stmt(child, cls_name, class_fields, class_fields, language)
|
|
603
|
+
|
|
604
|
+
def _walk_ts_toplevel(node: Node, language: str) -> None:
|
|
605
|
+
if node.type == "import_statement":
|
|
606
|
+
_emit_import_edges_ts(node)
|
|
607
|
+
elif node.type == "class_declaration":
|
|
608
|
+
_walk_ts_class(node, language)
|
|
609
|
+
elif emit_inheritance and node.type == "interface_declaration":
|
|
610
|
+
_emit_ts_inheritance(node)
|
|
611
|
+
body = node.child_by_field_name("body")
|
|
612
|
+
if body:
|
|
613
|
+
for child in body.children:
|
|
614
|
+
_walk_ts_stmt(child, None, {}, {}, language)
|
|
615
|
+
elif node.type in ("function_declaration", "function_expression"):
|
|
616
|
+
_walk_ts_function_body(node, None, {}, language)
|
|
617
|
+
elif node.type == "lexical_declaration":
|
|
618
|
+
for child in node.children:
|
|
619
|
+
if child.type == "variable_declarator":
|
|
620
|
+
val = child.child_by_field_name("value")
|
|
621
|
+
if val and val.type in ("arrow_function", "function_expression"):
|
|
622
|
+
_walk_ts_function_body(val, None, {}, language)
|
|
623
|
+
else:
|
|
624
|
+
for child in node.children:
|
|
625
|
+
_walk_ts_toplevel(child, language)
|
|
626
|
+
|
|
627
|
+
lang = "typescript"
|
|
628
|
+
for child in root.children:
|
|
629
|
+
_walk_ts_toplevel(child, lang)
|
|
630
|
+
|
|
631
|
+
return edges
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _extract_comments_typescript(root: Node, filepath: Path) -> list[Comment]:
|
|
635
|
+
"""Walk a TypeScript/JS AST and collect matched semantic comment nodes.
|
|
636
|
+
|
|
637
|
+
Handles both // line comments and /* */ block comments.
|
|
638
|
+
"""
|
|
639
|
+
comments: list[Comment] = []
|
|
640
|
+
|
|
641
|
+
def _walk(node: Node) -> None:
|
|
642
|
+
if node.type == "comment":
|
|
643
|
+
raw = _text(node)
|
|
644
|
+
base_row = node.start_point[0] + 1
|
|
645
|
+
if raw.startswith("/*"):
|
|
646
|
+
for offset, body in _block_comment_lines(raw):
|
|
647
|
+
result = _match_marker(body)
|
|
648
|
+
if result is not None:
|
|
649
|
+
marker, text = result
|
|
650
|
+
comments.append(Comment(marker=marker, text=text, line=base_row + offset))
|
|
651
|
+
else:
|
|
652
|
+
body = raw.lstrip("/").strip() if raw.startswith("//") else raw.strip()
|
|
653
|
+
result = _match_marker(body)
|
|
654
|
+
if result is not None:
|
|
655
|
+
marker, text = result
|
|
656
|
+
comments.append(Comment(marker=marker, text=text, line=base_row))
|
|
657
|
+
for child in node.children:
|
|
658
|
+
_walk(child)
|
|
659
|
+
|
|
660
|
+
for child in root.children:
|
|
661
|
+
_walk(child)
|
|
662
|
+
|
|
663
|
+
return comments
|