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,689 @@
|
|
|
1
|
+
"""Java symbol, edge, and comment extraction from tree-sitter ASTs.
|
|
2
|
+
|
|
3
|
+
LAYER: imports from graph_common (leaf) and graph_scope_infer_ext[2] (leaf) — never from graph.py.
|
|
4
|
+
|
|
5
|
+
LAYERING:
|
|
6
|
+
graph_common (leaf — no seam deps)
|
|
7
|
+
↑
|
|
8
|
+
graph_java (this file)
|
|
9
|
+
↑
|
|
10
|
+
graph_java_csharp (thin re-exporter; graph.py imports from there)
|
|
11
|
+
|
|
12
|
+
WHY split from graph_java_csharp.py: graph_java_csharp.py exceeded 1000 lines after Tier B
|
|
13
|
+
additions. Java and C# are now each large enough to stand alone. Same precedent as
|
|
14
|
+
graph_go.py / graph_rust.py (split from graph_go_rust.py).
|
|
15
|
+
|
|
16
|
+
All extractor functions follow the same contract:
|
|
17
|
+
- Accept a tree-sitter Node + filepath.
|
|
18
|
+
- Return a list — never raise, never return None.
|
|
19
|
+
- Edges carry confidence='INFERRED' by default (whole-index resolution
|
|
20
|
+
at read time handles EXTRACTED/AMBIGUOUS/INFERRED for cross-file edges).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
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_ext import (
|
|
30
|
+
collect_field_symbols_java,
|
|
31
|
+
extract_field_accesses_java,
|
|
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_ext import param_types_via_recorder, resolve_receiver_type_ext
|
|
45
|
+
from seam.indexer.graph_scope_infer_ext2 import (
|
|
46
|
+
_JAVA_SELF_NAMES,
|
|
47
|
+
collect_composition_types_java,
|
|
48
|
+
record_java_local_types,
|
|
49
|
+
record_java_param_types,
|
|
50
|
+
scan_class_fields_java,
|
|
51
|
+
)
|
|
52
|
+
from seam.indexer.signatures import extract_node_fields
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Shared helpers ─────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _node_name(node: Node) -> str | None:
|
|
61
|
+
"""Return text of the 'name' field child, or None if absent."""
|
|
62
|
+
name_node = node.child_by_field_name("name")
|
|
63
|
+
if name_node is None:
|
|
64
|
+
return None
|
|
65
|
+
return _text(name_node)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _java_javadoc(decl_node: Node) -> str | None:
|
|
69
|
+
"""Capture the nearest Javadoc comment for a Java declaration.
|
|
70
|
+
|
|
71
|
+
Walks prev_sibling backwards, skipping intervening line_comment nodes
|
|
72
|
+
(e.g. // HACK: ... between /** */ and the class), to find the nearest
|
|
73
|
+
block_comment that starts with '/**'.
|
|
74
|
+
|
|
75
|
+
WHY skip line_comments: in the fixture pattern
|
|
76
|
+
/** Javadoc */
|
|
77
|
+
// HACK: ... (semantic comment — not the docstring)
|
|
78
|
+
@SomeAnnotation
|
|
79
|
+
public class Foo { }
|
|
80
|
+
the line_comment separates the Javadoc from the declaration but should NOT
|
|
81
|
+
prevent the Javadoc from being attached. Annotations are folded into modifiers
|
|
82
|
+
inside the declaration, so they don't appear as prev_siblings.
|
|
83
|
+
|
|
84
|
+
Stops at non-comment nodes other than line_comment (e.g. another declaration).
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
current = decl_node.prev_sibling
|
|
88
|
+
while current is not None:
|
|
89
|
+
if current.type == "block_comment":
|
|
90
|
+
raw = _text(current)
|
|
91
|
+
if raw.startswith("/**"):
|
|
92
|
+
return _clean_block_comment(raw)
|
|
93
|
+
# Non-Javadoc block comment — stop searching.
|
|
94
|
+
return None
|
|
95
|
+
elif current.type == "line_comment":
|
|
96
|
+
# Skip over line comments (e.g. // HACK: ...) to reach the /** block.
|
|
97
|
+
current = current.prev_sibling
|
|
98
|
+
continue
|
|
99
|
+
else:
|
|
100
|
+
# Hit another declaration or non-comment node — stop.
|
|
101
|
+
return None
|
|
102
|
+
return None
|
|
103
|
+
except Exception: # noqa: BLE001
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _clean_block_comment(raw: str) -> str | None:
|
|
108
|
+
"""Clean a /** ... */ block comment into a plain docstring.
|
|
109
|
+
|
|
110
|
+
Removes /** and */ delimiters and leading ' * ' decoration from each line.
|
|
111
|
+
Returns the joined text, or None if the result is empty.
|
|
112
|
+
"""
|
|
113
|
+
lines: list[str] = []
|
|
114
|
+
for line in raw.splitlines():
|
|
115
|
+
stripped = line.strip()
|
|
116
|
+
if stripped.startswith("/**"):
|
|
117
|
+
stripped = stripped[3:]
|
|
118
|
+
elif stripped.startswith("*/"):
|
|
119
|
+
stripped = stripped[2:]
|
|
120
|
+
elif stripped.startswith("*"):
|
|
121
|
+
stripped = stripped[1:]
|
|
122
|
+
stripped = stripped.strip()
|
|
123
|
+
if stripped:
|
|
124
|
+
lines.append(stripped)
|
|
125
|
+
result = "\n".join(lines)
|
|
126
|
+
return result if result else None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _java_visibility_from_modifiers(mods_node: Node | None) -> str | None:
|
|
130
|
+
"""Extract visibility from a Java modifiers node.
|
|
131
|
+
|
|
132
|
+
Scans children for 'public', 'private', 'protected' keyword nodes.
|
|
133
|
+
Returns None if no access modifier is present (package-private).
|
|
134
|
+
"""
|
|
135
|
+
if mods_node is None:
|
|
136
|
+
return None
|
|
137
|
+
try:
|
|
138
|
+
for child in mods_node.children:
|
|
139
|
+
if child.type in ("public", "private", "protected"):
|
|
140
|
+
return child.type
|
|
141
|
+
except Exception: # noqa: BLE001
|
|
142
|
+
pass
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _java_annotations_from_modifiers(mods_node: Node | None) -> list[str]:
|
|
147
|
+
"""Extract Java annotations (@Service, @Override, etc.) from a modifiers node.
|
|
148
|
+
|
|
149
|
+
Returns verbatim annotation text for each marker_annotation and annotation child.
|
|
150
|
+
"""
|
|
151
|
+
result: list[str] = []
|
|
152
|
+
if mods_node is None:
|
|
153
|
+
return result
|
|
154
|
+
try:
|
|
155
|
+
for child in mods_node.children:
|
|
156
|
+
if child.type in ("marker_annotation", "annotation"):
|
|
157
|
+
text = _text(child).strip()
|
|
158
|
+
if text:
|
|
159
|
+
result.append(text)
|
|
160
|
+
except Exception: # noqa: BLE001
|
|
161
|
+
pass
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ── Java extraction ────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _extract_symbols_java(root: Node, filepath: Path) -> list[Symbol]:
|
|
169
|
+
"""Walk a Java AST and extract class, interface, enum, record, and method symbols.
|
|
170
|
+
|
|
171
|
+
Kind mapping (Phase 9 spec, closed vocabulary):
|
|
172
|
+
class_declaration → class
|
|
173
|
+
interface_declaration → interface
|
|
174
|
+
enum_declaration → type
|
|
175
|
+
record_declaration → class
|
|
176
|
+
method_declaration (inside class) → method (qualified as 'Class.method')
|
|
177
|
+
constructor_declaration (inside class) → method (qualified as 'Class.ClassName')
|
|
178
|
+
field_declaration (inside class, A3 Slice 4) → field (qualified as 'Class.field')
|
|
179
|
+
|
|
180
|
+
WHY top-level-only walk for type declarations: Java allows multiple top-level types
|
|
181
|
+
but methods/constructors are always inside a type body.
|
|
182
|
+
"""
|
|
183
|
+
symbols: list[Symbol] = []
|
|
184
|
+
file_str = str(filepath)
|
|
185
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
186
|
+
|
|
187
|
+
def _walk(node: Node, class_name: str | None = None) -> None:
|
|
188
|
+
"""Recursively walk the AST, tracking class context for method qualification."""
|
|
189
|
+
try:
|
|
190
|
+
ntype = node.type
|
|
191
|
+
|
|
192
|
+
if ntype == "class_declaration":
|
|
193
|
+
name = _node_name(node)
|
|
194
|
+
if name:
|
|
195
|
+
doc = _java_javadoc(node)
|
|
196
|
+
fields = extract_node_fields(
|
|
197
|
+
node,
|
|
198
|
+
"java",
|
|
199
|
+
qualified_name=name,
|
|
200
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
201
|
+
)
|
|
202
|
+
symbols.append(
|
|
203
|
+
_make_symbol(
|
|
204
|
+
name,
|
|
205
|
+
"class",
|
|
206
|
+
file_str,
|
|
207
|
+
node,
|
|
208
|
+
doc,
|
|
209
|
+
signature=fields["signature"],
|
|
210
|
+
decorators=fields["decorators"],
|
|
211
|
+
is_exported=fields["is_exported"],
|
|
212
|
+
visibility=fields["visibility"],
|
|
213
|
+
qualified_name=name,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
# A3 Slice 4: emit field symbols for Java class fields.
|
|
217
|
+
if field_access_on:
|
|
218
|
+
for qual_name, field_line in collect_field_symbols_java(node, name):
|
|
219
|
+
symbols.append(Symbol(
|
|
220
|
+
name=qual_name,
|
|
221
|
+
kind="field",
|
|
222
|
+
file=file_str,
|
|
223
|
+
start_line=field_line,
|
|
224
|
+
end_line=field_line,
|
|
225
|
+
docstring=None,
|
|
226
|
+
signature=None,
|
|
227
|
+
decorators=[],
|
|
228
|
+
is_exported=None,
|
|
229
|
+
visibility=None,
|
|
230
|
+
qualified_name=qual_name,
|
|
231
|
+
))
|
|
232
|
+
body = node.child_by_field_name("body")
|
|
233
|
+
if body is not None:
|
|
234
|
+
for child in body.named_children:
|
|
235
|
+
_walk(child, class_name=name)
|
|
236
|
+
|
|
237
|
+
elif ntype == "interface_declaration":
|
|
238
|
+
name = _node_name(node)
|
|
239
|
+
if name:
|
|
240
|
+
doc = _java_javadoc(node)
|
|
241
|
+
fields = extract_node_fields(
|
|
242
|
+
node,
|
|
243
|
+
"java",
|
|
244
|
+
qualified_name=name,
|
|
245
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
246
|
+
)
|
|
247
|
+
symbols.append(
|
|
248
|
+
_make_symbol(
|
|
249
|
+
name,
|
|
250
|
+
"interface",
|
|
251
|
+
file_str,
|
|
252
|
+
node,
|
|
253
|
+
doc,
|
|
254
|
+
signature=fields["signature"],
|
|
255
|
+
decorators=fields["decorators"],
|
|
256
|
+
is_exported=fields["is_exported"],
|
|
257
|
+
visibility=fields["visibility"],
|
|
258
|
+
qualified_name=name,
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
body = node.child_by_field_name("body")
|
|
262
|
+
if body is not None:
|
|
263
|
+
for child in body.named_children:
|
|
264
|
+
_walk(child, class_name=name)
|
|
265
|
+
|
|
266
|
+
elif ntype == "enum_declaration":
|
|
267
|
+
name = _node_name(node)
|
|
268
|
+
if name:
|
|
269
|
+
doc = _java_javadoc(node)
|
|
270
|
+
fields = extract_node_fields(
|
|
271
|
+
node,
|
|
272
|
+
"java",
|
|
273
|
+
qualified_name=name,
|
|
274
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
275
|
+
)
|
|
276
|
+
symbols.append(
|
|
277
|
+
_make_symbol(
|
|
278
|
+
name,
|
|
279
|
+
"type",
|
|
280
|
+
file_str,
|
|
281
|
+
node,
|
|
282
|
+
doc,
|
|
283
|
+
signature=fields["signature"],
|
|
284
|
+
decorators=fields["decorators"],
|
|
285
|
+
is_exported=fields["is_exported"],
|
|
286
|
+
visibility=fields["visibility"],
|
|
287
|
+
qualified_name=name,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
# WHY recurse: enums can have methods in their body.
|
|
291
|
+
body = node.child_by_field_name("body")
|
|
292
|
+
if body is not None:
|
|
293
|
+
for body_child in body.named_children:
|
|
294
|
+
if body_child.type == "enum_body_declarations":
|
|
295
|
+
for decl in body_child.named_children:
|
|
296
|
+
_walk(decl, class_name=name)
|
|
297
|
+
|
|
298
|
+
elif ntype == "record_declaration":
|
|
299
|
+
name = _node_name(node)
|
|
300
|
+
if name:
|
|
301
|
+
doc = _java_javadoc(node)
|
|
302
|
+
fields = extract_node_fields(
|
|
303
|
+
node,
|
|
304
|
+
"java",
|
|
305
|
+
qualified_name=name,
|
|
306
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
307
|
+
)
|
|
308
|
+
symbols.append(
|
|
309
|
+
_make_symbol(
|
|
310
|
+
name,
|
|
311
|
+
"class",
|
|
312
|
+
file_str,
|
|
313
|
+
node,
|
|
314
|
+
doc,
|
|
315
|
+
signature=fields["signature"],
|
|
316
|
+
decorators=fields["decorators"],
|
|
317
|
+
is_exported=fields["is_exported"],
|
|
318
|
+
visibility=fields["visibility"],
|
|
319
|
+
qualified_name=name,
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
elif (
|
|
324
|
+
ntype in ("method_declaration", "constructor_declaration")
|
|
325
|
+
and class_name is not None
|
|
326
|
+
):
|
|
327
|
+
name = _node_name(node)
|
|
328
|
+
if name:
|
|
329
|
+
qualified = f"{class_name}.{name}"
|
|
330
|
+
doc = _java_javadoc(node)
|
|
331
|
+
fields = extract_node_fields(
|
|
332
|
+
node,
|
|
333
|
+
"java",
|
|
334
|
+
qualified_name=qualified,
|
|
335
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
336
|
+
)
|
|
337
|
+
symbols.append(
|
|
338
|
+
_make_symbol(
|
|
339
|
+
qualified,
|
|
340
|
+
"method",
|
|
341
|
+
file_str,
|
|
342
|
+
node,
|
|
343
|
+
doc,
|
|
344
|
+
signature=fields["signature"],
|
|
345
|
+
decorators=fields["decorators"],
|
|
346
|
+
is_exported=fields["is_exported"],
|
|
347
|
+
visibility=fields["visibility"],
|
|
348
|
+
qualified_name=qualified,
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
else:
|
|
353
|
+
for child in node.named_children:
|
|
354
|
+
_walk(child, class_name)
|
|
355
|
+
|
|
356
|
+
except Exception: # noqa: BLE001
|
|
357
|
+
logger.debug(
|
|
358
|
+
"_extract_symbols_java: unhandled exception for node.type=%r file=%s",
|
|
359
|
+
node.type,
|
|
360
|
+
file_str,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
for child in root.named_children:
|
|
364
|
+
_walk(child)
|
|
365
|
+
|
|
366
|
+
return symbols
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _extract_edges_java(root: Node, filepath: Path) -> list[Edge]:
|
|
370
|
+
"""Extract import and call edges from a Java AST.
|
|
371
|
+
|
|
372
|
+
Import heuristic:
|
|
373
|
+
import java.util.List; → target = 'List' (last segment of scoped_identifier)
|
|
374
|
+
import static java.lang.Math.abs; → target = 'abs' (same rule)
|
|
375
|
+
|
|
376
|
+
Call heuristic:
|
|
377
|
+
method_invocation where 'object' field is absent → bare call → target = name text.
|
|
378
|
+
method_invocation where 'object' field is set → obj.method() → capture receiver.
|
|
379
|
+
|
|
380
|
+
Tier B B5: when SEAM_TYPE_INFERENCE is on, receiver calls are resolved to
|
|
381
|
+
'Type.method' qualified targets using a per-function scope map (class fields +
|
|
382
|
+
params + local variable declarations).
|
|
383
|
+
"""
|
|
384
|
+
edges: list[Edge] = []
|
|
385
|
+
file_str = str(filepath)
|
|
386
|
+
file_stem = filepath.stem
|
|
387
|
+
emit_inheritance = config.SEAM_INHERITANCE_EDGES == "on"
|
|
388
|
+
infer = config.SEAM_TYPE_INFERENCE == "on"
|
|
389
|
+
composition_on = config.SEAM_COMPOSITION_EDGES == "on"
|
|
390
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
391
|
+
param_edges_on = config.SEAM_PARAM_EDGES == "on"
|
|
392
|
+
|
|
393
|
+
def _walk(
|
|
394
|
+
node: Node,
|
|
395
|
+
class_name: str | None,
|
|
396
|
+
class_fields: dict[str, str],
|
|
397
|
+
var_types: dict[str, str],
|
|
398
|
+
) -> None:
|
|
399
|
+
try:
|
|
400
|
+
ntype = node.type
|
|
401
|
+
|
|
402
|
+
if emit_inheritance and ntype in (
|
|
403
|
+
"class_declaration",
|
|
404
|
+
"interface_declaration",
|
|
405
|
+
):
|
|
406
|
+
_handle_java_inheritance(node, file_str, edges)
|
|
407
|
+
|
|
408
|
+
if ntype == "import_declaration":
|
|
409
|
+
_handle_java_import(node, file_str, file_stem, edges)
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
if ntype == "class_declaration":
|
|
413
|
+
new_class_name: str | None = None
|
|
414
|
+
name_node = node.child_by_field_name("name")
|
|
415
|
+
if name_node is not None:
|
|
416
|
+
new_class_name = _text(name_node).strip() or None
|
|
417
|
+
# Slice #79: emit holds edges for Java class fields + ctor params.
|
|
418
|
+
if composition_on and new_class_name:
|
|
419
|
+
_handle_java_class_holds(node, new_class_name, file_str, edges)
|
|
420
|
+
new_fields: dict[str, str] = scan_class_fields_java(node) if infer else {}
|
|
421
|
+
body = node.child_by_field_name("body")
|
|
422
|
+
if body is not None:
|
|
423
|
+
for child in body.named_children:
|
|
424
|
+
_walk(child, new_class_name, new_fields, dict(new_fields))
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
if ntype in ("method_declaration", "constructor_declaration"):
|
|
428
|
+
new_types: dict[str, str] = dict(class_fields)
|
|
429
|
+
if infer:
|
|
430
|
+
record_java_param_types(node, new_types)
|
|
431
|
+
# 'uses' edges: method references plain user types as params.
|
|
432
|
+
if param_edges_on and class_name is not None:
|
|
433
|
+
_m = _node_name(node)
|
|
434
|
+
if _m:
|
|
435
|
+
_src = f"{class_name}.{_m}"
|
|
436
|
+
for ptype, pline in param_types_via_recorder(
|
|
437
|
+
node, record_java_param_types, "java"
|
|
438
|
+
):
|
|
439
|
+
edges.append(Edge(
|
|
440
|
+
source=_src, target=ptype, kind="uses",
|
|
441
|
+
file=file_str, line=pline, confidence="INFERRED", receiver=None,
|
|
442
|
+
))
|
|
443
|
+
body = node.child_by_field_name("body")
|
|
444
|
+
if body is not None:
|
|
445
|
+
# A3 Slice 4: emit reads/writes field-access edges.
|
|
446
|
+
if field_access_on and class_name is not None:
|
|
447
|
+
method_name = _node_name(node)
|
|
448
|
+
if method_name:
|
|
449
|
+
source_fn = f"{class_name}.{method_name}"
|
|
450
|
+
for src, tgt, mode, line in extract_field_accesses_java(
|
|
451
|
+
body, source_fn, class_name, new_types
|
|
452
|
+
):
|
|
453
|
+
edges.append(Edge(
|
|
454
|
+
source=src,
|
|
455
|
+
target=tgt,
|
|
456
|
+
kind=mode,
|
|
457
|
+
file=file_str,
|
|
458
|
+
line=line,
|
|
459
|
+
confidence="INFERRED",
|
|
460
|
+
receiver=None,
|
|
461
|
+
))
|
|
462
|
+
for child in body.named_children:
|
|
463
|
+
_walk(child, class_name, class_fields, new_types)
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
if infer and ntype == "local_variable_declaration":
|
|
467
|
+
record_java_local_types(node, var_types)
|
|
468
|
+
|
|
469
|
+
# Tier B B6: object_creation_expression (new Foo()) → instantiates edge.
|
|
470
|
+
elif ntype == "object_creation_expression":
|
|
471
|
+
type_node = node.child_by_field_name("type")
|
|
472
|
+
if type_node is None:
|
|
473
|
+
type_node = next(
|
|
474
|
+
(c for c in node.children if c.type == "type_identifier"), None
|
|
475
|
+
)
|
|
476
|
+
if type_node is not None:
|
|
477
|
+
type_name = _text(type_node)
|
|
478
|
+
if type_name:
|
|
479
|
+
source = _find_enclosing_function(node, "java")
|
|
480
|
+
if source is not None:
|
|
481
|
+
edges.append(
|
|
482
|
+
Edge(
|
|
483
|
+
source=source,
|
|
484
|
+
target=type_name,
|
|
485
|
+
kind="instantiates",
|
|
486
|
+
file=file_str,
|
|
487
|
+
line=node.start_point[0] + 1,
|
|
488
|
+
confidence="INFERRED",
|
|
489
|
+
receiver=None,
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
for child in node.children:
|
|
493
|
+
_walk(child, class_name, class_fields, var_types)
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
elif ntype == "method_invocation":
|
|
497
|
+
obj = node.child_by_field_name("object")
|
|
498
|
+
name_node = node.child_by_field_name("name")
|
|
499
|
+
if name_node is not None:
|
|
500
|
+
recv_text: str | None = _text(obj) if obj is not None else None
|
|
501
|
+
final_target = _text(name_node)
|
|
502
|
+
if infer and recv_text is not None:
|
|
503
|
+
resolved_type = resolve_receiver_type_ext(
|
|
504
|
+
recv_text, class_name, var_types, _JAVA_SELF_NAMES
|
|
505
|
+
)
|
|
506
|
+
if resolved_type:
|
|
507
|
+
final_target = f"{resolved_type}.{_text(name_node)}"
|
|
508
|
+
source = _find_enclosing_function(node, "java")
|
|
509
|
+
if source is not None:
|
|
510
|
+
edges.append(
|
|
511
|
+
Edge(
|
|
512
|
+
source=source,
|
|
513
|
+
target=final_target,
|
|
514
|
+
kind="call",
|
|
515
|
+
file=file_str,
|
|
516
|
+
line=node.start_point[0] + 1,
|
|
517
|
+
confidence="INFERRED",
|
|
518
|
+
receiver=recv_text,
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
for child in node.children:
|
|
523
|
+
_walk(child, class_name, class_fields, var_types)
|
|
524
|
+
|
|
525
|
+
except Exception: # noqa: BLE001
|
|
526
|
+
logger.debug(
|
|
527
|
+
"_extract_edges_java: unhandled exception for node.type=%r file=%s",
|
|
528
|
+
node.type,
|
|
529
|
+
file_str,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
for child in root.children:
|
|
533
|
+
_walk(child, None, {}, {})
|
|
534
|
+
|
|
535
|
+
return edges
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _handle_java_class_holds(
|
|
539
|
+
class_node: Node, class_name: str, file_str: str, edges: list[Edge]
|
|
540
|
+
) -> None:
|
|
541
|
+
"""Emit holds edges for each plain user-type field/ctor-param in a Java class.
|
|
542
|
+
|
|
543
|
+
Delegates to collect_composition_types_java for (held_type, line) pairs and
|
|
544
|
+
emits one Edge per unique pair. Never raises (backstop try/except).
|
|
545
|
+
"""
|
|
546
|
+
try:
|
|
547
|
+
for held_type, held_line in collect_composition_types_java(class_node):
|
|
548
|
+
edges.append(
|
|
549
|
+
Edge(
|
|
550
|
+
source=class_name,
|
|
551
|
+
target=held_type,
|
|
552
|
+
kind="holds",
|
|
553
|
+
file=file_str,
|
|
554
|
+
line=held_line,
|
|
555
|
+
confidence="INFERRED",
|
|
556
|
+
receiver=None,
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
except Exception as exc: # noqa: BLE001
|
|
560
|
+
logger.debug("_handle_java_class_holds: failed: %r", exc)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _handle_java_inheritance(decl_node: Node, file_str: str, edges: list[Edge]) -> None:
|
|
564
|
+
"""Emit extends/implements edges from a Java class or interface declaration.
|
|
565
|
+
|
|
566
|
+
class_declaration:
|
|
567
|
+
superclass (extends Base) → 'extends' edge subclass→Base
|
|
568
|
+
super_interfaces (implements I, J) → 'implements' edges per type
|
|
569
|
+
interface_declaration:
|
|
570
|
+
extends_interfaces (extends I, J) → 'extends' edges (interface inheritance)
|
|
571
|
+
|
|
572
|
+
Each base name is normalized to a bare type name (generic args stripped).
|
|
573
|
+
String-name-keyed: source=this type's name, target=base name. Never raises.
|
|
574
|
+
"""
|
|
575
|
+
name_node = decl_node.child_by_field_name("name")
|
|
576
|
+
if name_node is None:
|
|
577
|
+
return
|
|
578
|
+
src_name = _text(name_node)
|
|
579
|
+
line = decl_node.start_point[0] + 1
|
|
580
|
+
|
|
581
|
+
def _emit(type_node: Node, kind: str) -> None:
|
|
582
|
+
target = _base_type_name(type_node)
|
|
583
|
+
if target:
|
|
584
|
+
edges.append(
|
|
585
|
+
Edge(
|
|
586
|
+
source=src_name,
|
|
587
|
+
target=target,
|
|
588
|
+
kind=kind,
|
|
589
|
+
file=file_str,
|
|
590
|
+
line=line,
|
|
591
|
+
confidence="INFERRED",
|
|
592
|
+
receiver=None,
|
|
593
|
+
)
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
for child in decl_node.children:
|
|
597
|
+
ctype = child.type
|
|
598
|
+
if ctype == "superclass":
|
|
599
|
+
for sub in child.named_children:
|
|
600
|
+
_emit(sub, "extends")
|
|
601
|
+
elif ctype == "super_interfaces":
|
|
602
|
+
for tl in child.named_children:
|
|
603
|
+
if tl.type == "type_list":
|
|
604
|
+
for t in tl.named_children:
|
|
605
|
+
_emit(t, "implements")
|
|
606
|
+
elif ctype == "extends_interfaces":
|
|
607
|
+
for tl in child.named_children:
|
|
608
|
+
if tl.type == "type_list":
|
|
609
|
+
for t in tl.named_children:
|
|
610
|
+
_emit(t, "extends")
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _handle_java_import(decl_node: Node, file_str: str, file_stem: str, edges: list[Edge]) -> None:
|
|
614
|
+
"""Emit an import edge from a Java import_declaration node.
|
|
615
|
+
|
|
616
|
+
Extracts the last segment of the qualified name:
|
|
617
|
+
import java.util.List → 'List' (scoped_identifier.name field)
|
|
618
|
+
import java.util.* → skipped (asterisk — no single target)
|
|
619
|
+
"""
|
|
620
|
+
line = decl_node.start_point[0] + 1
|
|
621
|
+
if any(child.type == "asterisk" for child in decl_node.children):
|
|
622
|
+
return # Wildcard import — no target to record.
|
|
623
|
+
|
|
624
|
+
for child in decl_node.named_children:
|
|
625
|
+
target = _java_import_last_segment(child)
|
|
626
|
+
if target:
|
|
627
|
+
edges.append(
|
|
628
|
+
Edge(
|
|
629
|
+
source=file_stem,
|
|
630
|
+
target=target,
|
|
631
|
+
kind="import",
|
|
632
|
+
file=file_str,
|
|
633
|
+
line=line,
|
|
634
|
+
confidence="INFERRED",
|
|
635
|
+
receiver=None,
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _java_import_last_segment(node: Node) -> str | None:
|
|
641
|
+
"""Extract the rightmost segment from a Java scoped_identifier or identifier."""
|
|
642
|
+
if node.type == "identifier":
|
|
643
|
+
return _text(node)
|
|
644
|
+
if node.type == "scoped_identifier":
|
|
645
|
+
name_node = node.child_by_field_name("name")
|
|
646
|
+
if name_node is not None:
|
|
647
|
+
return _text(name_node)
|
|
648
|
+
return None
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _extract_comments_java(root: Node, filepath: Path) -> list[Comment]:
|
|
652
|
+
"""Walk a Java AST and extract semantic comment markers (WHY/HACK/NOTE/TODO/FIXME).
|
|
653
|
+
|
|
654
|
+
Java comment node types:
|
|
655
|
+
line_comment — // single-line comments
|
|
656
|
+
block_comment — /* */ and /** */ (Javadoc)
|
|
657
|
+
"""
|
|
658
|
+
comments: list[Comment] = []
|
|
659
|
+
|
|
660
|
+
def _walk(node: Node) -> None:
|
|
661
|
+
try:
|
|
662
|
+
if node.type == "line_comment":
|
|
663
|
+
raw = _text(node)
|
|
664
|
+
base_row = node.start_point[0] + 1
|
|
665
|
+
body = raw.lstrip("/").strip()
|
|
666
|
+
result = _match_marker(body)
|
|
667
|
+
if result is not None:
|
|
668
|
+
marker, text = result
|
|
669
|
+
comments.append(Comment(marker=marker, text=text, line=base_row))
|
|
670
|
+
|
|
671
|
+
elif node.type == "block_comment":
|
|
672
|
+
raw = _text(node)
|
|
673
|
+
base_row = node.start_point[0] + 1
|
|
674
|
+
for offset, body in _block_comment_lines(raw):
|
|
675
|
+
result = _match_marker(body)
|
|
676
|
+
if result is not None:
|
|
677
|
+
marker, text = result
|
|
678
|
+
comments.append(Comment(marker=marker, text=text, line=base_row + offset))
|
|
679
|
+
|
|
680
|
+
for child in node.children:
|
|
681
|
+
_walk(child)
|
|
682
|
+
|
|
683
|
+
except Exception: # noqa: BLE001
|
|
684
|
+
pass
|
|
685
|
+
|
|
686
|
+
for child in root.children:
|
|
687
|
+
_walk(child)
|
|
688
|
+
|
|
689
|
+
return comments
|