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,902 @@
|
|
|
1
|
+
"""Scope-inference module for receiver-type resolution in Python and TypeScript/JS.
|
|
2
|
+
|
|
3
|
+
LAYER: leaf — imports from graph_common (leaf) and stdlib only. Never imports from
|
|
4
|
+
graph.py, graph_swift, or any other seam module with side effects.
|
|
5
|
+
|
|
6
|
+
LAYERING:
|
|
7
|
+
graph_common (leaf — no seam deps)
|
|
8
|
+
↑
|
|
9
|
+
graph_scope_infer (this file — pure receiver-type inference, no Edge construction)
|
|
10
|
+
↑
|
|
11
|
+
graph.py (_extract_edges_python / _extract_edges_typescript use these helpers)
|
|
12
|
+
|
|
13
|
+
WHY a separate module:
|
|
14
|
+
1. graph.py would exceed the 1000-line limit if this logic were inlined.
|
|
15
|
+
2. The scope-inference cluster (pre-scan + per-function lookup + resolve) is a coherent
|
|
16
|
+
leaf unit — no Edge construction, no AST walker orchestration — so it belongs in a
|
|
17
|
+
dedicated leaf, mirroring the graph_swift_infer.py precedent from Phase 10.
|
|
18
|
+
|
|
19
|
+
CONSERVATISM CONTRACT (identical to graph_swift_infer):
|
|
20
|
+
NEVER emit a wrong edge. resolve_receiver_type returns None on ANY uncertainty:
|
|
21
|
+
- Optionals: Foo | None, Foo?, Optional[Foo] → None
|
|
22
|
+
- Containers: list[T], List[T], dict[K,V], Tuple[...] → None
|
|
23
|
+
- Generics: Array<T>, Set<T>, Map<K,V> → None
|
|
24
|
+
- Unknown: identifiers not found in the field/param/local scope → None
|
|
25
|
+
- Chained: a.b.c() where the 'b' field has no known type → None
|
|
26
|
+
Callers that receive None keep the raw bare target (never fabricate a type).
|
|
27
|
+
|
|
28
|
+
Two-layer scope model (order-independent):
|
|
29
|
+
Layer 1 — class-level pre-scan: field/property type bindings gathered for the whole
|
|
30
|
+
class body BEFORE walking methods. This is essential for DI'd stored properties
|
|
31
|
+
that are declared after the method that uses them.
|
|
32
|
+
Layer 2 — per-function scope: parameter and local-variable type bindings accumulated
|
|
33
|
+
during the function-body walk. Function scope is fresh for each function.
|
|
34
|
+
|
|
35
|
+
Self/this/cls normalization:
|
|
36
|
+
self / cls → enclosing class name (Python)
|
|
37
|
+
this / super → enclosing class name (TypeScript/JS) for 'super': same class because
|
|
38
|
+
we only know the type at declaration, not the base class; refuse if enclosing unknown.
|
|
39
|
+
|
|
40
|
+
All public functions never raise (backstop try/except in each body).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import logging
|
|
44
|
+
|
|
45
|
+
from tree_sitter import Node
|
|
46
|
+
|
|
47
|
+
from seam.indexer.graph_common import _text
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
# ── Self/this normalized receivers ────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
# Python: 'self' and 'cls' are conventional but not enforced — treat any of these
|
|
54
|
+
# as "enclosing type" without caring about exact identifier.
|
|
55
|
+
_PY_SELF_NAMES: frozenset[str] = frozenset({"self", "cls"})
|
|
56
|
+
|
|
57
|
+
# TypeScript/JS: 'this' always refers to the enclosing class instance.
|
|
58
|
+
# 'super' is deliberately excluded — we can't statically know the base class.
|
|
59
|
+
_TS_SELF_NAMES: frozenset[str] = frozenset({"this"})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── Shared: resolve a receiver expression to a type name ─────────────────────
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_receiver_type(
|
|
66
|
+
receiver_text: str,
|
|
67
|
+
class_name: str | None,
|
|
68
|
+
var_types: dict[str, str],
|
|
69
|
+
self_names: frozenset[str],
|
|
70
|
+
) -> str | None:
|
|
71
|
+
"""Resolve a receiver expression to its declared type name.
|
|
72
|
+
|
|
73
|
+
This is the core lookup that maps a receiver string (as captured by the edge
|
|
74
|
+
extractor) to a type name using the current scope state (class fields + params +
|
|
75
|
+
locals) — exactly mirroring _resolve_navigation_target in graph_swift_infer.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
receiver_text: Raw receiver text captured from the AST (e.g. 'self', 'client',
|
|
79
|
+
'this', 'a.b'). Multi-part receivers (containing '.') resolve
|
|
80
|
+
only if they start with a self/this alias followed by a known
|
|
81
|
+
class field.
|
|
82
|
+
class_name: Name of the enclosing class (or None for module-level functions).
|
|
83
|
+
var_types: scope map — name → type for all in-scope bindings (class fields
|
|
84
|
+
merged with per-function params and locals).
|
|
85
|
+
self_names: Set of receiver strings that alias 'self' (e.g. {'self','cls'}
|
|
86
|
+
for Python, {'this'} for TypeScript).
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The resolved type name string (e.g. 'Client'), or None when the type cannot
|
|
90
|
+
be determined with confidence. None → caller keeps the bare target.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
if not receiver_text:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Multi-part receiver: only handle 'self.field' / 'this.field' patterns.
|
|
97
|
+
# a.b.method() where 'a' is not a self-alias → refuse (cross-class chain).
|
|
98
|
+
if "." in receiver_text:
|
|
99
|
+
return _resolve_chained(receiver_text, class_name, var_types, self_names)
|
|
100
|
+
|
|
101
|
+
# Plain identifier — check self/this alias first.
|
|
102
|
+
if receiver_text in self_names:
|
|
103
|
+
return class_name # May be None (module-level scope)
|
|
104
|
+
|
|
105
|
+
# Look up in the scope map (class fields + params + locals).
|
|
106
|
+
return var_types.get(receiver_text) # None if not in scope
|
|
107
|
+
|
|
108
|
+
except Exception as exc: # noqa: BLE001
|
|
109
|
+
logger.debug("resolve_receiver_type: failed for %r: %r", receiver_text, exc)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _resolve_chained(
|
|
114
|
+
receiver_text: str,
|
|
115
|
+
class_name: str | None,
|
|
116
|
+
var_types: dict[str, str],
|
|
117
|
+
self_names: frozenset[str],
|
|
118
|
+
) -> str | None:
|
|
119
|
+
"""Resolve a dotted receiver like 'self.field' or 'this.repo'.
|
|
120
|
+
|
|
121
|
+
Only ONE level of chaining is supported:
|
|
122
|
+
self.field / this.field → look up field's type from var_types
|
|
123
|
+
Everything else (self.a.b, unknown.field, chained member expressions) → None.
|
|
124
|
+
|
|
125
|
+
This mirrors _resolve_navigation_target's self.prop.method handling in Swift:
|
|
126
|
+
only the self.<prop> form is resolvable since we only track class-level field types.
|
|
127
|
+
"""
|
|
128
|
+
parts = receiver_text.split(".", 1)
|
|
129
|
+
if len(parts) != 2:
|
|
130
|
+
return None
|
|
131
|
+
head, field = parts
|
|
132
|
+
if head not in self_names:
|
|
133
|
+
# Foreign chain (a.b, not self.b) → refuse — would need cross-class field typing.
|
|
134
|
+
return None
|
|
135
|
+
return var_types.get(field) # None if the field has no known type
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ── Python: plain-type extraction ────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _py_plain_type_from_annotation(ann_node: Node) -> str | None:
|
|
142
|
+
"""Extract a plain type name from a Python type annotation node.
|
|
143
|
+
|
|
144
|
+
Conservative: returns the type name ONLY for bare identifiers. Refuses:
|
|
145
|
+
- Union types (X | Y, Optional[X]) → node type contains '|' or subscript
|
|
146
|
+
- Generic subscripts (list[T], List[T], dict[K,V])
|
|
147
|
+
- String-quoted annotations (deferred — we'd need to eval the string)
|
|
148
|
+
- Any complex expression
|
|
149
|
+
|
|
150
|
+
WHY refuse optionals and generics: `foo: Optional[Client]` means `foo` could be
|
|
151
|
+
None; emitting `Client.method()` on `foo.method()` would be wrong when foo=None.
|
|
152
|
+
`foo: list[Client]` means `foo` is a container — `foo.method()` resolves on list,
|
|
153
|
+
not Client. The conservatism contract requires NEVER emitting a wrong edge, so
|
|
154
|
+
any type that could be None or that wraps another type is refused entirely.
|
|
155
|
+
|
|
156
|
+
The node passed here is whatever tree-sitter calls the 'type' field, or
|
|
157
|
+
an 'annotation' node child. We inspect its grammar type:
|
|
158
|
+
identifier → plain bare name → accept
|
|
159
|
+
type → nested type node → recurse one level
|
|
160
|
+
subscript → generic subscript (list[Foo]) → refuse
|
|
161
|
+
binary_operator → X | Y union → refuse
|
|
162
|
+
attribute → dotted type (pkg.Foo) → refuse
|
|
163
|
+
string → deferred annotation → refuse (conservative)
|
|
164
|
+
"""
|
|
165
|
+
if ann_node is None:
|
|
166
|
+
return None
|
|
167
|
+
t = ann_node.type
|
|
168
|
+
if t == "identifier":
|
|
169
|
+
name = _text(ann_node)
|
|
170
|
+
# Reject built-in type names that are not user classes.
|
|
171
|
+
if name in _PY_BUILTIN_TYPES:
|
|
172
|
+
return None
|
|
173
|
+
return name or None
|
|
174
|
+
if t == "type":
|
|
175
|
+
# Nested type wrapper — recurse into its first child.
|
|
176
|
+
for child in ann_node.children:
|
|
177
|
+
if child.type not in (":", "->"):
|
|
178
|
+
return _py_plain_type_from_annotation(child)
|
|
179
|
+
return None
|
|
180
|
+
# All other shapes (subscript, binary_operator, attribute, string, etc.) → refuse.
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Python built-in type names that are NOT user classes. Seeing one of these as
|
|
185
|
+
# the annotation means we must NOT try to resolve .method() calls on it.
|
|
186
|
+
_PY_BUILTIN_TYPES: frozenset[str] = frozenset({
|
|
187
|
+
"int", "float", "str", "bytes", "bool", "None", "object",
|
|
188
|
+
"list", "dict", "tuple", "set", "frozenset",
|
|
189
|
+
"List", "Dict", "Tuple", "Set", "FrozenSet", "Optional", "Union",
|
|
190
|
+
"Any", "Type", "Callable", "Generator", "Iterator", "Iterable",
|
|
191
|
+
"Sequence", "Mapping", "MutableMapping", "MutableSequence",
|
|
192
|
+
"ClassVar", "Final", "Literal", "TypeVar",
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _py_constructor_class(value_node: Node) -> str | None:
|
|
197
|
+
"""If value_node is a 'ClassName(...)' constructor call, return 'ClassName', else None.
|
|
198
|
+
|
|
199
|
+
A Python constructor call is a call node whose function child is a bare identifier.
|
|
200
|
+
Conservative: refuses dotted constructors (pkg.Foo()), subscripts (Foo[T]()), etc.
|
|
201
|
+
"""
|
|
202
|
+
if value_node.type != "call":
|
|
203
|
+
return None
|
|
204
|
+
func = value_node.child_by_field_name("function")
|
|
205
|
+
if func is None:
|
|
206
|
+
return None
|
|
207
|
+
if func.type == "identifier":
|
|
208
|
+
name = _text(func)
|
|
209
|
+
if name and name not in _PY_BUILTIN_TYPES:
|
|
210
|
+
return name
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ── Python: field pre-scan ────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def scan_class_fields_python(class_node: Node) -> dict[str, str]:
|
|
218
|
+
"""Pre-scan a Python class_definition body for field-level type bindings.
|
|
219
|
+
|
|
220
|
+
WHY a pre-scan: methods may reference a field before its declaration in source
|
|
221
|
+
order. Pre-scanning the whole class body gives an order-independent type map.
|
|
222
|
+
|
|
223
|
+
Captures:
|
|
224
|
+
field: Type (annotated class variable, no value)
|
|
225
|
+
field: Type = ... (annotated class variable with value)
|
|
226
|
+
field = ClassName() (plain assignment with constructor; conservative)
|
|
227
|
+
|
|
228
|
+
Returns name → type dict for direct class body members only (not nested classes).
|
|
229
|
+
Never raises.
|
|
230
|
+
"""
|
|
231
|
+
out: dict[str, str] = {}
|
|
232
|
+
try:
|
|
233
|
+
body = class_node.child_by_field_name("body")
|
|
234
|
+
if body is None:
|
|
235
|
+
return out
|
|
236
|
+
for stmt in body.children:
|
|
237
|
+
_py_scan_field_stmt(stmt, out)
|
|
238
|
+
except Exception as exc: # noqa: BLE001
|
|
239
|
+
logger.debug("scan_class_fields_python: failed: %r", exc)
|
|
240
|
+
return out
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _py_scan_field_stmt(stmt: Node, out: dict[str, str]) -> None:
|
|
244
|
+
"""Record a single class body statement's field binding if it has a plain type.
|
|
245
|
+
|
|
246
|
+
Handles:
|
|
247
|
+
expression_statement → assignment with 'type' field (field: Type or field: Type = val)
|
|
248
|
+
expression_statement → assignment without 'type' field (field = ClassName())
|
|
249
|
+
|
|
250
|
+
NOTE: Python tree-sitter represents `x: Type` and `x: Type = val` as `assignment`
|
|
251
|
+
nodes (not `annotated_assignment`), with the annotation stored in the 'type' field.
|
|
252
|
+
The `annotated_assignment` node type exists in older grammars but in practice the
|
|
253
|
+
current Python grammar uses `assignment` with an optional `type` field.
|
|
254
|
+
We handle both for robustness.
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
if stmt.type == "expression_statement" and stmt.children:
|
|
258
|
+
inner = stmt.children[0]
|
|
259
|
+
if inner.type in ("assignment", "annotated_assignment"):
|
|
260
|
+
# Check for type annotation field first (covers x: Type and x: Type = val)
|
|
261
|
+
ann = inner.child_by_field_name("type")
|
|
262
|
+
if ann is not None:
|
|
263
|
+
_py_record_annotated_assignment(inner, out)
|
|
264
|
+
else:
|
|
265
|
+
# Plain assignment — check if RHS is a constructor call.
|
|
266
|
+
_py_record_constructor_assignment(inner, out)
|
|
267
|
+
except Exception as exc: # noqa: BLE001
|
|
268
|
+
logger.debug("_py_scan_field_stmt: failed: %r", exc)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _py_record_annotated_assignment(node: Node, out: dict[str, str]) -> None:
|
|
272
|
+
"""Record binding from `name: Type` or `name: Type = value`.
|
|
273
|
+
|
|
274
|
+
Handles both `annotated_assignment` and `assignment` nodes with a `type` field.
|
|
275
|
+
tree-sitter Python uses `assignment` nodes (with optional `type` field) for:
|
|
276
|
+
field: Type → assignment{left=identifier, type=type}
|
|
277
|
+
field: Type = val → assignment{left=identifier, type=type, right=...}
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
left = node.child_by_field_name("left")
|
|
281
|
+
ann = node.child_by_field_name("type")
|
|
282
|
+
if left is None or ann is None:
|
|
283
|
+
return
|
|
284
|
+
# Only bare identifier targets (no 'self.x: T' — that's an attribute)
|
|
285
|
+
if left.type != "identifier":
|
|
286
|
+
return
|
|
287
|
+
name = _text(left)
|
|
288
|
+
if not name:
|
|
289
|
+
return
|
|
290
|
+
type_name = _py_plain_type_from_annotation(ann)
|
|
291
|
+
if type_name:
|
|
292
|
+
out[name] = type_name
|
|
293
|
+
except Exception as exc: # noqa: BLE001
|
|
294
|
+
logger.debug("_py_record_annotated_assignment: failed: %r", exc)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _py_record_constructor_assignment(node: Node, out: dict[str, str]) -> None:
|
|
298
|
+
"""Record binding from `name = ClassName()`.
|
|
299
|
+
|
|
300
|
+
tree-sitter Python: assignment has left + right children.
|
|
301
|
+
"""
|
|
302
|
+
try:
|
|
303
|
+
left = node.child_by_field_name("left")
|
|
304
|
+
right = node.child_by_field_name("right")
|
|
305
|
+
if left is None or right is None:
|
|
306
|
+
return
|
|
307
|
+
if left.type != "identifier":
|
|
308
|
+
return
|
|
309
|
+
name = _text(left)
|
|
310
|
+
if not name:
|
|
311
|
+
return
|
|
312
|
+
cls = _py_constructor_class(right)
|
|
313
|
+
if cls:
|
|
314
|
+
out[name] = cls
|
|
315
|
+
except Exception as exc: # noqa: BLE001
|
|
316
|
+
logger.debug("_py_record_constructor_assignment: failed: %r", exc)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# ── Python: per-function scope ────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def record_py_param_types(func_node: Node, var_types: dict[str, str]) -> None:
|
|
323
|
+
"""Bind each function parameter's name → declared type into var_types.
|
|
324
|
+
|
|
325
|
+
Handles:
|
|
326
|
+
def f(self, x: Foo, y: Bar) → binds x→Foo, y→Bar
|
|
327
|
+
def f(cls, x: Foo) → binds x→Foo (cls itself is handled by _PY_SELF_NAMES)
|
|
328
|
+
|
|
329
|
+
Conservative: only plain type annotations bind. Optional / generic / union → skip.
|
|
330
|
+
Never raises.
|
|
331
|
+
"""
|
|
332
|
+
try:
|
|
333
|
+
params_node = func_node.child_by_field_name("parameters")
|
|
334
|
+
if params_node is None:
|
|
335
|
+
return
|
|
336
|
+
for param in params_node.children:
|
|
337
|
+
_py_record_single_param(param, var_types)
|
|
338
|
+
except Exception as exc: # noqa: BLE001
|
|
339
|
+
logger.debug("record_py_param_types: failed: %r", exc)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _py_record_single_param(param: Node, var_types: dict[str, str]) -> None:
|
|
343
|
+
"""Record name → type for a single parameter node.
|
|
344
|
+
|
|
345
|
+
tree-sitter Python parameter shapes:
|
|
346
|
+
identifier (positional, no annotation)
|
|
347
|
+
typed_parameter (positional with annotation: x: Foo)
|
|
348
|
+
default_parameter (positional with default: x=val — no annotation)
|
|
349
|
+
typed_default_parameter (positional with annotation+default: x: Foo = val)
|
|
350
|
+
list_splat_pattern / dict_splat_pattern (*args/**kwargs — skip)
|
|
351
|
+
"""
|
|
352
|
+
try:
|
|
353
|
+
t = param.type
|
|
354
|
+
if t == "typed_parameter":
|
|
355
|
+
# children: identifier, ':', type
|
|
356
|
+
names = [c for c in param.children if c.type == "identifier"]
|
|
357
|
+
ann = param.child_by_field_name("type")
|
|
358
|
+
if names and ann:
|
|
359
|
+
pname = _text(names[0])
|
|
360
|
+
if pname and pname not in _PY_SELF_NAMES:
|
|
361
|
+
type_name = _py_plain_type_from_annotation(ann)
|
|
362
|
+
if type_name:
|
|
363
|
+
var_types[pname] = type_name
|
|
364
|
+
elif t == "typed_default_parameter":
|
|
365
|
+
name_node = param.child_by_field_name("name")
|
|
366
|
+
ann = param.child_by_field_name("type")
|
|
367
|
+
if name_node and ann:
|
|
368
|
+
pname = _text(name_node)
|
|
369
|
+
if pname and pname not in _PY_SELF_NAMES:
|
|
370
|
+
type_name = _py_plain_type_from_annotation(ann)
|
|
371
|
+
if type_name:
|
|
372
|
+
var_types[pname] = type_name
|
|
373
|
+
except Exception as exc: # noqa: BLE001
|
|
374
|
+
logger.debug("_py_record_single_param: failed: %r", exc)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def record_py_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
|
|
378
|
+
"""Record type bindings from a local statement (annotated assignment or constructor).
|
|
379
|
+
|
|
380
|
+
Handles two patterns inside function bodies:
|
|
381
|
+
x: Foo = ... (assignment with type field — Python grammar for annotated vars)
|
|
382
|
+
x = Foo() (assignment whose RHS is a constructor call)
|
|
383
|
+
|
|
384
|
+
NOTE: Python tree-sitter uses `assignment` nodes with a `type` field for `x: Foo`
|
|
385
|
+
patterns (not the `annotated_assignment` node type). We check for `type` field first.
|
|
386
|
+
|
|
387
|
+
Called incrementally during the function-body walk so later code in the same
|
|
388
|
+
function can resolve vars defined earlier. Never raises.
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
if stmt_node.type == "expression_statement" and stmt_node.children:
|
|
392
|
+
inner = stmt_node.children[0]
|
|
393
|
+
if inner.type in ("assignment", "annotated_assignment"):
|
|
394
|
+
ann = inner.child_by_field_name("type")
|
|
395
|
+
if ann is not None:
|
|
396
|
+
_py_record_annotated_assignment(inner, var_types)
|
|
397
|
+
else:
|
|
398
|
+
_py_record_constructor_assignment(inner, var_types)
|
|
399
|
+
except Exception as exc: # noqa: BLE001
|
|
400
|
+
logger.debug("record_py_local_types: failed: %r", exc)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ── TypeScript/JS: plain-type extraction ──────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _ts_plain_type_from_annotation(type_node: Node) -> str | None:
|
|
407
|
+
"""Extract a plain user type name from a TypeScript type annotation.
|
|
408
|
+
|
|
409
|
+
Conservative: only accepts a bare type_identifier. Refuses:
|
|
410
|
+
- Union types (string | null, Foo | Bar) → union_type node
|
|
411
|
+
- Generic types (Array<T>, Map<K,V>) → generic_type node
|
|
412
|
+
- Array types (T[]) → array_type node (or predefined_type)
|
|
413
|
+
- Intersection, tuple, function types → other nodes
|
|
414
|
+
- Predefined/primitive types → predefined_type (string, number, etc.)
|
|
415
|
+
|
|
416
|
+
WHY refuse union types: `foo: Client | null` means `foo` may be null; binding
|
|
417
|
+
Client and emitting `Client.method()` would be incorrect half the time at runtime.
|
|
418
|
+
WHY refuse generics: `Array<Client>` or `Map<K,V>` are container types — calling
|
|
419
|
+
`.method()` on them resolves on the container, not the element type.
|
|
420
|
+
Both refusals are required by the conservatism contract: prefer no edge over a
|
|
421
|
+
wrong edge.
|
|
422
|
+
|
|
423
|
+
The node passed here is a type_annotation node or a direct type child.
|
|
424
|
+
"""
|
|
425
|
+
if type_node is None:
|
|
426
|
+
return None
|
|
427
|
+
t = type_node.type
|
|
428
|
+
|
|
429
|
+
# type_annotation wraps the actual type — skip the ':' and recurse.
|
|
430
|
+
if t == "type_annotation":
|
|
431
|
+
for child in type_node.children:
|
|
432
|
+
if child.type not in (":", "=>"):
|
|
433
|
+
result = _ts_plain_type_from_annotation(child)
|
|
434
|
+
if result:
|
|
435
|
+
return result
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
# A bare type identifier (e.g. `Client`, `Parser`, `Engine`)
|
|
439
|
+
if t == "type_identifier":
|
|
440
|
+
name = _text(type_node)
|
|
441
|
+
if name and name not in _TS_BUILTIN_TYPES:
|
|
442
|
+
return name
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
# All other node types (union_type, generic_type, array_type, predefined_type,
|
|
446
|
+
# intersection_type, tuple_type, function_type, undefined_type, literal_type) → refuse.
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# TypeScript built-in / primitive type names that are not user classes.
|
|
451
|
+
_TS_BUILTIN_TYPES: frozenset[str] = frozenset({
|
|
452
|
+
"string", "number", "boolean", "void", "any", "unknown", "never", "object",
|
|
453
|
+
"symbol", "bigint", "null", "undefined",
|
|
454
|
+
"Array", "Map", "Set", "WeakMap", "WeakSet", "WeakRef",
|
|
455
|
+
"Promise", "Function", "Object", "Error",
|
|
456
|
+
"String", "Number", "Boolean", "Symbol", "BigInt",
|
|
457
|
+
"Readonly", "Record", "Partial", "Required", "Pick", "Omit",
|
|
458
|
+
"Exclude", "Extract", "NonNullable", "ReturnType", "InstanceType",
|
|
459
|
+
"Parameters", "ConstructorParameters",
|
|
460
|
+
"ReadonlyArray", "ReadonlyMap", "ReadonlySet",
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _ts_constructor_class(value_node: Node) -> str | None:
|
|
465
|
+
"""If value_node is `new ClassName(...)`, return 'ClassName', else None.
|
|
466
|
+
|
|
467
|
+
TypeScript new_expression: `new Foo(...)` has a 'constructor' field that is a
|
|
468
|
+
type_identifier (plain class name) or a member_expression (namespace.Foo — refuse).
|
|
469
|
+
Conservative: only accepts plain type_identifier.
|
|
470
|
+
"""
|
|
471
|
+
if value_node.type != "new_expression":
|
|
472
|
+
return None
|
|
473
|
+
constructor = value_node.child_by_field_name("constructor")
|
|
474
|
+
if constructor is None:
|
|
475
|
+
return None
|
|
476
|
+
if constructor.type == "identifier":
|
|
477
|
+
name = _text(constructor)
|
|
478
|
+
if name and name not in _TS_BUILTIN_TYPES:
|
|
479
|
+
return name
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# ── TypeScript: class field pre-scan ─────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def scan_class_fields_typescript(class_node: Node) -> dict[str, str]:
|
|
487
|
+
"""Pre-scan a TS class_declaration body for field-level type bindings.
|
|
488
|
+
|
|
489
|
+
WHY pre-scan: same reason as Python — methods may reference a field declared
|
|
490
|
+
below them. Pre-scanning the class body gives an order-independent field map.
|
|
491
|
+
|
|
492
|
+
Captures:
|
|
493
|
+
field: Type; (public_field_definition or field_definition)
|
|
494
|
+
field: Type = new Foo(); (field with initializer — take annotation type)
|
|
495
|
+
|
|
496
|
+
Returns name → type dict for direct class members only (not nested classes).
|
|
497
|
+
Never raises.
|
|
498
|
+
"""
|
|
499
|
+
out: dict[str, str] = {}
|
|
500
|
+
try:
|
|
501
|
+
body = class_node.child_by_field_name("body")
|
|
502
|
+
if body is None:
|
|
503
|
+
return out
|
|
504
|
+
for child in body.children:
|
|
505
|
+
_ts_scan_field_member(child, out)
|
|
506
|
+
except Exception as exc: # noqa: BLE001
|
|
507
|
+
logger.debug("scan_class_fields_typescript: failed: %r", exc)
|
|
508
|
+
return out
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _ts_scan_field_member(member: Node, out: dict[str, str]) -> None:
|
|
512
|
+
"""Record a single class body member if it is a field with a plain type annotation.
|
|
513
|
+
|
|
514
|
+
TypeScript grammar field shapes:
|
|
515
|
+
public_field_definition → name field (property_identifier) + type (type_annotation)
|
|
516
|
+
field_definition → same structure (used in some grammars)
|
|
517
|
+
"""
|
|
518
|
+
try:
|
|
519
|
+
if member.type not in ("public_field_definition", "field_definition"):
|
|
520
|
+
return
|
|
521
|
+
name_node = member.child_by_field_name("name")
|
|
522
|
+
type_node = member.child_by_field_name("type")
|
|
523
|
+
if name_node is None or type_node is None:
|
|
524
|
+
return
|
|
525
|
+
field_name = _text(name_node)
|
|
526
|
+
if not field_name:
|
|
527
|
+
return
|
|
528
|
+
type_name = _ts_plain_type_from_annotation(type_node)
|
|
529
|
+
if type_name:
|
|
530
|
+
out[field_name] = type_name
|
|
531
|
+
except Exception as exc: # noqa: BLE001
|
|
532
|
+
logger.debug("_ts_scan_field_member: failed: %r", exc)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ── TypeScript: per-function scope ────────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def record_ts_param_types(func_node: Node, var_types: dict[str, str]) -> None:
|
|
539
|
+
"""Bind each TS/JS function parameter's name → declared type into var_types.
|
|
540
|
+
|
|
541
|
+
Handles:
|
|
542
|
+
required_parameter (x: Foo)
|
|
543
|
+
optional_parameter (x?: Foo)
|
|
544
|
+
|
|
545
|
+
Conservative: only plain type_identifier annotations bind. Never raises.
|
|
546
|
+
"""
|
|
547
|
+
try:
|
|
548
|
+
params_node = func_node.child_by_field_name("parameters")
|
|
549
|
+
if params_node is None:
|
|
550
|
+
return
|
|
551
|
+
for param in params_node.children:
|
|
552
|
+
_ts_record_single_param(param, var_types)
|
|
553
|
+
except Exception as exc: # noqa: BLE001
|
|
554
|
+
logger.debug("record_ts_param_types: failed: %r", exc)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _ts_record_single_param(param: Node, var_types: dict[str, str]) -> None:
|
|
558
|
+
"""Record name → type for a single TS/JS parameter node."""
|
|
559
|
+
try:
|
|
560
|
+
if param.type not in ("required_parameter", "optional_parameter"):
|
|
561
|
+
return
|
|
562
|
+
name_node = param.child_by_field_name("pattern")
|
|
563
|
+
type_node = param.child_by_field_name("type")
|
|
564
|
+
if name_node is None or type_node is None:
|
|
565
|
+
return
|
|
566
|
+
if name_node.type not in ("identifier", "shorthand_property_identifier_pattern"):
|
|
567
|
+
return
|
|
568
|
+
pname = _text(name_node)
|
|
569
|
+
if not pname or pname in _TS_SELF_NAMES:
|
|
570
|
+
return
|
|
571
|
+
type_name = _ts_plain_type_from_annotation(type_node)
|
|
572
|
+
if type_name:
|
|
573
|
+
var_types[pname] = type_name
|
|
574
|
+
except Exception as exc: # noqa: BLE001
|
|
575
|
+
logger.debug("_ts_record_single_param: failed: %r", exc)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def record_ts_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
|
|
579
|
+
"""Record type bindings from a local TS/JS statement.
|
|
580
|
+
|
|
581
|
+
Handles:
|
|
582
|
+
const x: Foo = ... (lexical_declaration with type_annotation)
|
|
583
|
+
const x = new Foo() (lexical_declaration with new_expression initializer)
|
|
584
|
+
let x: Foo = ... (same — let)
|
|
585
|
+
|
|
586
|
+
Called incrementally during the function-body walk. Never raises.
|
|
587
|
+
"""
|
|
588
|
+
try:
|
|
589
|
+
if stmt_node.type not in ("lexical_declaration", "variable_declaration"):
|
|
590
|
+
return
|
|
591
|
+
for child in stmt_node.children:
|
|
592
|
+
if child.type == "variable_declarator":
|
|
593
|
+
_ts_record_single_declarator(child, var_types)
|
|
594
|
+
except Exception as exc: # noqa: BLE001
|
|
595
|
+
logger.debug("record_ts_local_types: failed: %r", exc)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _ts_record_single_declarator(decl: Node, var_types: dict[str, str]) -> None:
|
|
599
|
+
"""Record one variable_declarator's name → type binding.
|
|
600
|
+
|
|
601
|
+
Two forms:
|
|
602
|
+
const x: Type = ... → 'type' field is type_annotation → use annotation
|
|
603
|
+
const x = new Foo() → 'value' field is new_expression → use constructor name
|
|
604
|
+
Annotation wins over constructor when both present.
|
|
605
|
+
"""
|
|
606
|
+
try:
|
|
607
|
+
name_node = decl.child_by_field_name("name")
|
|
608
|
+
if name_node is None or name_node.type not in ("identifier",):
|
|
609
|
+
return
|
|
610
|
+
var_name = _text(name_node)
|
|
611
|
+
if not var_name:
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
# Prefer explicit type annotation.
|
|
615
|
+
type_node = decl.child_by_field_name("type")
|
|
616
|
+
if type_node is not None:
|
|
617
|
+
type_name = _ts_plain_type_from_annotation(type_node)
|
|
618
|
+
if type_name:
|
|
619
|
+
var_types[var_name] = type_name
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
# Fall back to constructor-call inference.
|
|
623
|
+
value_node = decl.child_by_field_name("value")
|
|
624
|
+
if value_node is not None:
|
|
625
|
+
cls = _ts_constructor_class(value_node)
|
|
626
|
+
if cls:
|
|
627
|
+
var_types[var_name] = cls
|
|
628
|
+
except Exception as exc: # noqa: BLE001
|
|
629
|
+
logger.debug("_ts_record_single_declarator: failed: %r", exc)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
# ── Slice #77: Composition (holds) collectors ────────────────────────────────
|
|
633
|
+
#
|
|
634
|
+
# These functions return deduped (held_type_name, line) pairs for a class node.
|
|
635
|
+
# They REUSE the existing scan_class_fields_* helpers (Layer 1 field pre-scan) for
|
|
636
|
+
# the stored-field half, and add a constructor/init-parameter pass using the SAME
|
|
637
|
+
# plain-type extractors (_py_plain_type_from_annotation, _ts_plain_type_from_annotation).
|
|
638
|
+
#
|
|
639
|
+
# CONSERVATISM CONTRACT (same as receiver-type inference):
|
|
640
|
+
# NEVER emit a wrong type. Only plain user-type identifiers are accepted.
|
|
641
|
+
# Optionals, containers, generics, primitives, and dotted types are all refused.
|
|
642
|
+
# The existing scan_class_fields_* helpers already apply this filter for fields.
|
|
643
|
+
# Constructor/init params add the same filter on top.
|
|
644
|
+
#
|
|
645
|
+
# WHY no is_builtin() post-filter here (unlike graph_scope_infer_ext*.py):
|
|
646
|
+
# This file is a LEAF module — it may only import from graph_common and stdlib.
|
|
647
|
+
# Importing seam.analysis.builtins would break the layering constraint. The
|
|
648
|
+
# _PY_BUILTIN_TYPES and _TS_BUILTIN_TYPES frozensets (already used by the
|
|
649
|
+
# plain-type helpers) serve the same authoritative-filter role in a leaf-safe way.
|
|
650
|
+
#
|
|
651
|
+
# WHY constructor/__init__ params count as composition but other method params do not:
|
|
652
|
+
# A constructor/init parameter that is stored as a field represents an OWNED
|
|
653
|
+
# dependency — the object holds a reference for its lifetime. Parameters of
|
|
654
|
+
# other methods are transient (passed per-call) and express USE, not composition.
|
|
655
|
+
# Emitting holds edges for non-init params would conflate dependency injection with
|
|
656
|
+
# ordinary function arguments, producing false composition claims.
|
|
657
|
+
#
|
|
658
|
+
# DEDUPE: a type name that appears as BOTH a field and a ctor/init param is emitted
|
|
659
|
+
# only ONCE. Dedupe is per (source, target) — the set returned here is deduplicated.
|
|
660
|
+
#
|
|
661
|
+
# NEVER RAISES: all public functions have a backstop try/except and return [] on error.
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def collect_composition_types_python(class_node: Node) -> list[tuple[str, int]]:
|
|
665
|
+
"""Collect (held_type_name, line) pairs from a Python class_definition node.
|
|
666
|
+
|
|
667
|
+
Two passes:
|
|
668
|
+
1. Field types: reuses scan_class_fields_python (annotated class attrs).
|
|
669
|
+
2. __init__ parameter types: scans parameters of __init__ with plain-type annotations.
|
|
670
|
+
|
|
671
|
+
Deduped: if the same type appears in both field and __init__ param, only one entry
|
|
672
|
+
is returned (first source wins — typically the field declaration wins since the class
|
|
673
|
+
body is scanned first, but dedup uses a set so order doesn't matter for correctness).
|
|
674
|
+
|
|
675
|
+
Returns [] on any error. Never raises.
|
|
676
|
+
"""
|
|
677
|
+
try:
|
|
678
|
+
seen: set[str] = set()
|
|
679
|
+
result: list[tuple[str, int]] = []
|
|
680
|
+
|
|
681
|
+
# Pass 1: class-level field annotations.
|
|
682
|
+
# scan_class_fields_python returns name→type, but we need the LINE too.
|
|
683
|
+
# Walk the body directly to capture line numbers alongside types.
|
|
684
|
+
body = class_node.child_by_field_name("body")
|
|
685
|
+
if body is not None:
|
|
686
|
+
for stmt in body.children:
|
|
687
|
+
try:
|
|
688
|
+
if stmt.type == "expression_statement" and stmt.children:
|
|
689
|
+
inner = stmt.children[0]
|
|
690
|
+
if inner.type in ("assignment", "annotated_assignment"):
|
|
691
|
+
ann = inner.child_by_field_name("type")
|
|
692
|
+
left = inner.child_by_field_name("left")
|
|
693
|
+
if ann is not None and left is not None and left.type == "identifier":
|
|
694
|
+
type_name = _py_plain_type_from_annotation(ann)
|
|
695
|
+
if type_name and type_name not in seen:
|
|
696
|
+
seen.add(type_name)
|
|
697
|
+
result.append((type_name, stmt.start_point[0] + 1))
|
|
698
|
+
except Exception: # noqa: BLE001
|
|
699
|
+
pass
|
|
700
|
+
|
|
701
|
+
# Pass 2: __init__ parameter types.
|
|
702
|
+
if body is not None:
|
|
703
|
+
for stmt in body.children:
|
|
704
|
+
try:
|
|
705
|
+
fn = _py_get_init_function(stmt)
|
|
706
|
+
if fn is None:
|
|
707
|
+
continue
|
|
708
|
+
params = fn.child_by_field_name("parameters")
|
|
709
|
+
if params is None:
|
|
710
|
+
continue
|
|
711
|
+
for param in params.children:
|
|
712
|
+
if param.type not in ("typed_parameter", "typed_default_parameter"):
|
|
713
|
+
continue
|
|
714
|
+
ann = param.child_by_field_name("type")
|
|
715
|
+
if ann is None:
|
|
716
|
+
continue
|
|
717
|
+
type_name = _py_plain_type_from_annotation(ann)
|
|
718
|
+
if type_name and type_name not in seen:
|
|
719
|
+
seen.add(type_name)
|
|
720
|
+
result.append((type_name, param.start_point[0] + 1))
|
|
721
|
+
except Exception: # noqa: BLE001
|
|
722
|
+
pass
|
|
723
|
+
|
|
724
|
+
return result
|
|
725
|
+
except Exception as exc: # noqa: BLE001
|
|
726
|
+
logger.debug("collect_composition_types_python: failed: %r", exc)
|
|
727
|
+
return []
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _py_get_init_function(stmt: Node) -> Node | None:
|
|
731
|
+
"""Return the function_definition node if stmt is a Python __init__ method.
|
|
732
|
+
|
|
733
|
+
Handles plain `def __init__(...)` and `@decorator def __init__(...)`.
|
|
734
|
+
Returns None for all other nodes.
|
|
735
|
+
"""
|
|
736
|
+
if stmt.type == "function_definition":
|
|
737
|
+
name = _text(stmt.child_by_field_name("name") or stmt)
|
|
738
|
+
if name == "__init__":
|
|
739
|
+
return stmt
|
|
740
|
+
elif stmt.type == "decorated_definition":
|
|
741
|
+
inner = stmt.child_by_field_name("definition")
|
|
742
|
+
if inner is not None and inner.type == "function_definition":
|
|
743
|
+
name = _text(inner.child_by_field_name("name") or inner)
|
|
744
|
+
if name == "__init__":
|
|
745
|
+
return inner
|
|
746
|
+
return None
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def collect_composition_types_typescript(class_node: Node) -> list[tuple[str, int]]:
|
|
750
|
+
"""Collect (held_type_name, line) pairs from a TypeScript class_declaration node.
|
|
751
|
+
|
|
752
|
+
Two passes:
|
|
753
|
+
1. Field types: scan public_field_definition/field_definition nodes with a plain
|
|
754
|
+
type_annotation — reuses _ts_plain_type_from_annotation (same as scan_class_fields_ts).
|
|
755
|
+
2. Constructor parameter types: scan required_parameter and optional_parameter nodes
|
|
756
|
+
in the constructor method, including parameter-property forms
|
|
757
|
+
(e.g. constructor(private svc: Service)).
|
|
758
|
+
|
|
759
|
+
Deduped: same type from both field and ctor param → one entry.
|
|
760
|
+
Returns [] on any error. Never raises.
|
|
761
|
+
"""
|
|
762
|
+
try:
|
|
763
|
+
seen: set[str] = set()
|
|
764
|
+
result: list[tuple[str, int]] = []
|
|
765
|
+
|
|
766
|
+
body = class_node.child_by_field_name("body")
|
|
767
|
+
if body is None:
|
|
768
|
+
return result
|
|
769
|
+
|
|
770
|
+
# Pass 1: field declarations.
|
|
771
|
+
for child in body.children:
|
|
772
|
+
try:
|
|
773
|
+
if child.type not in ("public_field_definition", "field_definition"):
|
|
774
|
+
continue
|
|
775
|
+
type_node = child.child_by_field_name("type")
|
|
776
|
+
if type_node is None:
|
|
777
|
+
continue
|
|
778
|
+
type_name = _ts_plain_type_from_annotation(type_node)
|
|
779
|
+
if type_name and type_name not in seen:
|
|
780
|
+
seen.add(type_name)
|
|
781
|
+
result.append((type_name, child.start_point[0] + 1))
|
|
782
|
+
except Exception: # noqa: BLE001
|
|
783
|
+
pass
|
|
784
|
+
|
|
785
|
+
# Pass 2: constructor parameters (including parameter-property DI).
|
|
786
|
+
for child in body.children:
|
|
787
|
+
try:
|
|
788
|
+
if child.type != "method_definition":
|
|
789
|
+
continue
|
|
790
|
+
name_node = child.child_by_field_name("name")
|
|
791
|
+
if name_node is None or _text(name_node) != "constructor":
|
|
792
|
+
continue
|
|
793
|
+
# Found the constructor — scan its parameters.
|
|
794
|
+
params = child.child_by_field_name("parameters")
|
|
795
|
+
if params is None:
|
|
796
|
+
continue
|
|
797
|
+
for param in params.children:
|
|
798
|
+
type_name = _ts_param_plain_type(param)
|
|
799
|
+
if type_name and type_name not in seen:
|
|
800
|
+
seen.add(type_name)
|
|
801
|
+
result.append((type_name, param.start_point[0] + 1))
|
|
802
|
+
except Exception: # noqa: BLE001
|
|
803
|
+
pass
|
|
804
|
+
|
|
805
|
+
return result
|
|
806
|
+
except Exception as exc: # noqa: BLE001
|
|
807
|
+
logger.debug("collect_composition_types_typescript: failed: %r", exc)
|
|
808
|
+
return []
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _ts_param_plain_type(param: Node) -> str | None:
|
|
812
|
+
"""Extract a plain user type from a TS constructor parameter node.
|
|
813
|
+
|
|
814
|
+
Handles:
|
|
815
|
+
required_parameter(x: Type) — normal param
|
|
816
|
+
optional_parameter(x?: Type) — optional param name (still has a type)
|
|
817
|
+
required_parameter with access_modifier — parameter property (private/public/protected)
|
|
818
|
+
e.g. constructor(private svc: Service)
|
|
819
|
+
public_field_definition used as param — some grammars model param-props as fields
|
|
820
|
+
|
|
821
|
+
Conservative: only plain type_identifier annotations bind. Never raises.
|
|
822
|
+
"""
|
|
823
|
+
try:
|
|
824
|
+
# Standard required/optional parameter.
|
|
825
|
+
if param.type in ("required_parameter", "optional_parameter"):
|
|
826
|
+
type_node = param.child_by_field_name("type")
|
|
827
|
+
if type_node is not None:
|
|
828
|
+
return _ts_plain_type_from_annotation(type_node)
|
|
829
|
+
|
|
830
|
+
# Parameter property: tree-sitter models `constructor(private svc: Service)` as
|
|
831
|
+
# a required_parameter whose FIRST child is an accessibility_modifier ("private",
|
|
832
|
+
# "public", "protected"), followed by the identifier and type_annotation.
|
|
833
|
+
# Some grammar versions emit `public_field_definition` inside the parameter list.
|
|
834
|
+
if param.type == "public_field_definition":
|
|
835
|
+
type_node = param.child_by_field_name("type")
|
|
836
|
+
if type_node is not None:
|
|
837
|
+
return _ts_plain_type_from_annotation(type_node)
|
|
838
|
+
|
|
839
|
+
return None
|
|
840
|
+
except Exception: # noqa: BLE001
|
|
841
|
+
return None
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def collect_param_types_python(func_node: Node) -> list[tuple[str, int]]:
|
|
845
|
+
"""Collect (param_type_name, line) pairs from a Python function_definition node.
|
|
846
|
+
|
|
847
|
+
Walks the function's `parameters` and applies _py_plain_type_from_annotation — the
|
|
848
|
+
SAME plain-type extraction used for __init__ holds param collection — so a param
|
|
849
|
+
annotated with a plain user type (x: Service) is captured while Optional[T]/list[T]/
|
|
850
|
+
dict[K,V]/generics/builtins are refused (builtins via _PY_BUILTIN_TYPES inside the
|
|
851
|
+
helper). Deduped within the signature.
|
|
852
|
+
|
|
853
|
+
Returns [] on any error. Never raises.
|
|
854
|
+
"""
|
|
855
|
+
try:
|
|
856
|
+
seen: set[str] = set()
|
|
857
|
+
result: list[tuple[str, int]] = []
|
|
858
|
+
params = func_node.child_by_field_name("parameters")
|
|
859
|
+
if params is None:
|
|
860
|
+
return result
|
|
861
|
+
for param in params.children:
|
|
862
|
+
if param.type not in ("typed_parameter", "typed_default_parameter"):
|
|
863
|
+
continue
|
|
864
|
+
ann = param.child_by_field_name("type")
|
|
865
|
+
if ann is None:
|
|
866
|
+
continue
|
|
867
|
+
type_name = _py_plain_type_from_annotation(ann)
|
|
868
|
+
if type_name and type_name not in seen:
|
|
869
|
+
seen.add(type_name)
|
|
870
|
+
result.append((type_name, param.start_point[0] + 1))
|
|
871
|
+
return result
|
|
872
|
+
except Exception as exc: # noqa: BLE001
|
|
873
|
+
logger.debug("collect_param_types_python: failed: %r", exc)
|
|
874
|
+
return []
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def collect_param_types_typescript(func_node: Node) -> list[tuple[str, int]]:
|
|
878
|
+
"""Collect (param_type_name, line) pairs from a TS/JS function-like node.
|
|
879
|
+
|
|
880
|
+
Works for function_declaration, method_definition, arrow_function, and
|
|
881
|
+
function_expression — all carry a `parameters` (formal_parameters) field. Applies
|
|
882
|
+
_ts_param_plain_type per parameter (same helper as constructor holds), so plain
|
|
883
|
+
user-typed params bind while optionals/generics/builtins are refused. Plain JS has
|
|
884
|
+
no type annotations → returns [] naturally. Deduped within the signature.
|
|
885
|
+
|
|
886
|
+
Returns [] on any error. Never raises.
|
|
887
|
+
"""
|
|
888
|
+
try:
|
|
889
|
+
seen: set[str] = set()
|
|
890
|
+
result: list[tuple[str, int]] = []
|
|
891
|
+
params = func_node.child_by_field_name("parameters")
|
|
892
|
+
if params is None:
|
|
893
|
+
return result
|
|
894
|
+
for param in params.children:
|
|
895
|
+
type_name = _ts_param_plain_type(param)
|
|
896
|
+
if type_name and type_name not in seen:
|
|
897
|
+
seen.add(type_name)
|
|
898
|
+
result.append((type_name, param.start_point[0] + 1))
|
|
899
|
+
return result
|
|
900
|
+
except Exception as exc: # noqa: BLE001
|
|
901
|
+
logger.debug("collect_param_types_typescript: failed: %r", exc)
|
|
902
|
+
return []
|