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.
Files changed (109) hide show
  1. seam/__init__.py +3 -0
  2. seam/_data/schema.sql +225 -0
  3. seam/_web/assets/index-BL_tqprR.js +216 -0
  4. seam/_web/assets/index-GTKUhVyD.css +1 -0
  5. seam/_web/index.html +13 -0
  6. seam/analysis/__init__.py +14 -0
  7. seam/analysis/affected.py +254 -0
  8. seam/analysis/builtins.py +966 -0
  9. seam/analysis/byte_budget.py +217 -0
  10. seam/analysis/changes.py +709 -0
  11. seam/analysis/cluster_naming.py +260 -0
  12. seam/analysis/clustering.py +216 -0
  13. seam/analysis/confidence.py +699 -0
  14. seam/analysis/embeddings.py +195 -0
  15. seam/analysis/flows.py +708 -0
  16. seam/analysis/impact.py +444 -0
  17. seam/analysis/imports.py +994 -0
  18. seam/analysis/imports_ext.py +780 -0
  19. seam/analysis/imports_resolve.py +176 -0
  20. seam/analysis/processes.py +453 -0
  21. seam/analysis/relevance.py +155 -0
  22. seam/analysis/rwr.py +129 -0
  23. seam/analysis/staleness.py +328 -0
  24. seam/analysis/steer.py +282 -0
  25. seam/analysis/synthesis.py +253 -0
  26. seam/analysis/synthesis_channels.py +433 -0
  27. seam/analysis/testpaths.py +103 -0
  28. seam/analysis/traversal.py +470 -0
  29. seam/cli/__init__.py +0 -0
  30. seam/cli/install.py +232 -0
  31. seam/cli/main.py +2602 -0
  32. seam/cli/output.py +137 -0
  33. seam/cli/read.py +244 -0
  34. seam/cli/serve.py +145 -0
  35. seam/config.py +551 -0
  36. seam/indexer/__init__.py +0 -0
  37. seam/indexer/cluster_index.py +425 -0
  38. seam/indexer/db.py +496 -0
  39. seam/indexer/embedding_index.py +183 -0
  40. seam/indexer/field_access.py +536 -0
  41. seam/indexer/field_access_c_cpp.py +643 -0
  42. seam/indexer/field_access_ext.py +708 -0
  43. seam/indexer/field_access_ext2.py +408 -0
  44. seam/indexer/field_access_go_rust.py +737 -0
  45. seam/indexer/field_access_php_swift.py +888 -0
  46. seam/indexer/field_access_ts.py +626 -0
  47. seam/indexer/graph.py +321 -0
  48. seam/indexer/graph_c.py +562 -0
  49. seam/indexer/graph_c_cpp.py +39 -0
  50. seam/indexer/graph_common.py +644 -0
  51. seam/indexer/graph_cpp.py +615 -0
  52. seam/indexer/graph_csharp.py +651 -0
  53. seam/indexer/graph_go.py +723 -0
  54. seam/indexer/graph_go_rust.py +39 -0
  55. seam/indexer/graph_java.py +689 -0
  56. seam/indexer/graph_java_csharp.py +38 -0
  57. seam/indexer/graph_php.py +914 -0
  58. seam/indexer/graph_python.py +628 -0
  59. seam/indexer/graph_ruby.py +748 -0
  60. seam/indexer/graph_rust.py +653 -0
  61. seam/indexer/graph_scope_infer.py +902 -0
  62. seam/indexer/graph_scope_infer_ext.py +723 -0
  63. seam/indexer/graph_scope_infer_ext2.py +992 -0
  64. seam/indexer/graph_swift.py +1014 -0
  65. seam/indexer/graph_swift_infer.py +515 -0
  66. seam/indexer/graph_typescript.py +663 -0
  67. seam/indexer/migrations.py +816 -0
  68. seam/indexer/parser.py +204 -0
  69. seam/indexer/pipeline.py +197 -0
  70. seam/indexer/signatures.py +634 -0
  71. seam/indexer/signatures_ext.py +780 -0
  72. seam/indexer/sync.py +287 -0
  73. seam/indexer/synthesis_index.py +291 -0
  74. seam/indexer/tokenize.py +79 -0
  75. seam/installer/__init__.py +67 -0
  76. seam/installer/claude.py +97 -0
  77. seam/installer/codex.py +94 -0
  78. seam/installer/core.py +127 -0
  79. seam/installer/cursor.py +61 -0
  80. seam/installer/guide.py +110 -0
  81. seam/installer/jsonfile.py +85 -0
  82. seam/installer/markdownfile.py +146 -0
  83. seam/installer/tomlfile.py +72 -0
  84. seam/query/__init__.py +0 -0
  85. seam/query/clusters.py +206 -0
  86. seam/query/comments.py +217 -0
  87. seam/query/context.py +293 -0
  88. seam/query/engine.py +940 -0
  89. seam/query/fts.py +328 -0
  90. seam/query/names.py +470 -0
  91. seam/query/pack.py +433 -0
  92. seam/query/semantic.py +339 -0
  93. seam/query/structure.py +727 -0
  94. seam/server/__init__.py +0 -0
  95. seam/server/graph_api.py +437 -0
  96. seam/server/handler_common.py +323 -0
  97. seam/server/impact_handler.py +615 -0
  98. seam/server/mcp.py +556 -0
  99. seam/server/tools.py +697 -0
  100. seam/server/trace_handler.py +184 -0
  101. seam/server/web.py +922 -0
  102. seam/watcher/__init__.py +0 -0
  103. seam/watcher/__main__.py +56 -0
  104. seam/watcher/daemon.py +237 -0
  105. seam_code-0.3.0.dist-info/METADATA +318 -0
  106. seam_code-0.3.0.dist-info/RECORD +109 -0
  107. seam_code-0.3.0.dist-info/WHEEL +4 -0
  108. seam_code-0.3.0.dist-info/entry_points.txt +2 -0
  109. 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