java-codebase-rag 0.11.2__py3-none-any.whl → 0.12.1__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 (73) hide show
  1. java_codebase_rag-0.12.1.dist-info/METADATA +35 -0
  2. java_codebase_rag-0.12.1.dist-info/RECORD +4 -0
  3. {java_codebase_rag-0.11.2.dist-info → java_codebase_rag-0.12.1.dist-info}/WHEEL +1 -1
  4. java_codebase_rag/_fdlimit.py +0 -56
  5. java_codebase_rag/_stdio.py +0 -32
  6. java_codebase_rag/_version.py +0 -35
  7. java_codebase_rag/absence/__init__.py +0 -0
  8. java_codebase_rag/absence/absence_diagnosis.py +0 -700
  9. java_codebase_rag/absence/absence_types.py +0 -124
  10. java_codebase_rag/absence/absence_vocab.py +0 -460
  11. java_codebase_rag/analysis/__init__.py +0 -0
  12. java_codebase_rag/analysis/pr_analysis.py +0 -563
  13. java_codebase_rag/analysis/resolve_service.py +0 -740
  14. java_codebase_rag/ast/__init__.py +0 -0
  15. java_codebase_rag/ast/ast_java.py +0 -2825
  16. java_codebase_rag/ast/brownfield_events.py +0 -58
  17. java_codebase_rag/ast/chunk_heuristics.py +0 -62
  18. java_codebase_rag/cli.py +0 -1215
  19. java_codebase_rag/cli_format.py +0 -85
  20. java_codebase_rag/cli_progress.py +0 -94
  21. java_codebase_rag/config.py +0 -833
  22. java_codebase_rag/eval/__init__.py +0 -1
  23. java_codebase_rag/eval/ground_truth.py +0 -100
  24. java_codebase_rag/eval/metrics.py +0 -107
  25. java_codebase_rag/eval/runner.py +0 -556
  26. java_codebase_rag/graph/__init__.py +0 -0
  27. java_codebase_rag/graph/build_ast_graph.py +0 -4471
  28. java_codebase_rag/graph/graph_enrich.py +0 -1937
  29. java_codebase_rag/graph/graph_types.py +0 -224
  30. java_codebase_rag/graph/java_ontology.py +0 -465
  31. java_codebase_rag/graph/ladybug_queries.py +0 -2213
  32. java_codebase_rag/graph/path_filtering.py +0 -477
  33. java_codebase_rag/index/__init__.py +0 -0
  34. java_codebase_rag/index/java_index_flow_lancedb.py +0 -734
  35. java_codebase_rag/index/java_index_v1_common.py +0 -33
  36. java_codebase_rag/install_data/__init__.py +0 -0
  37. java_codebase_rag/install_data/agents/explorer-rag-cli.md +0 -108
  38. java_codebase_rag/install_data/agents/explorer-rag-enhanced.md +0 -152
  39. java_codebase_rag/install_data/skills/explore-codebase/SKILL.md +0 -165
  40. java_codebase_rag/install_data/skills/explore-codebase-cli/SKILL.md +0 -107
  41. java_codebase_rag/installer.py +0 -2188
  42. java_codebase_rag/jrag.py +0 -4531
  43. java_codebase_rag/jrag_envelope.py +0 -1107
  44. java_codebase_rag/jrag_hints.py +0 -204
  45. java_codebase_rag/jrag_render.py +0 -926
  46. java_codebase_rag/lance_optimize.py +0 -264
  47. java_codebase_rag/mcp/__init__.py +0 -0
  48. java_codebase_rag/mcp/mcp_hints.py +0 -932
  49. java_codebase_rag/mcp/mcp_v2.py +0 -1916
  50. java_codebase_rag/mcp/server.py +0 -884
  51. java_codebase_rag/pipeline.py +0 -531
  52. java_codebase_rag/progress.py +0 -570
  53. java_codebase_rag/read_payloads.py +0 -781
  54. java_codebase_rag/search/__init__.py +0 -0
  55. java_codebase_rag/search/index_common.py +0 -10
  56. java_codebase_rag/search/search_lancedb.py +0 -1296
  57. java_codebase_rag/search/search_lexical.py +0 -449
  58. java_codebase_rag/search/search_scoring.py +0 -523
  59. java_codebase_rag/watch/__init__.py +0 -0
  60. java_codebase_rag/watch/client.py +0 -230
  61. java_codebase_rag/watch/daemon.py +0 -396
  62. java_codebase_rag/watch/lock.py +0 -201
  63. java_codebase_rag/watch/paths.py +0 -76
  64. java_codebase_rag/watch/protocol.py +0 -122
  65. java_codebase_rag/watch/server.py +0 -273
  66. java_codebase_rag/watch/warm.py +0 -105
  67. java_codebase_rag/watch/watcher.py +0 -370
  68. java_codebase_rag-0.11.2.dist-info/METADATA +0 -331
  69. java_codebase_rag-0.11.2.dist-info/RECORD +0 -71
  70. java_codebase_rag-0.11.2.dist-info/entry_points.txt +0 -4
  71. java_codebase_rag-0.11.2.dist-info/licenses/LICENSE +0 -21
  72. java_codebase_rag-0.11.2.dist-info/top_level.txt +0 -1
  73. /java_codebase_rag/__init__.py → /java_codebase_rag-0.12.1.dist-info/top_level.txt +0 -0
@@ -1,2825 +0,0 @@
1
- """Deterministic Java AST extraction on top of tree-sitter.
2
-
3
- Produces a typed, stable view of a single .java compilation unit:
4
- package, imports, and a tree of TypeDecl (class/interface/enum/record/annotation)
5
- with their annotations, fields, methods, and nested types. Anonymous classes
6
- (`new T() { … }`) become synthetic nested TypeDecl rows (`<anon:startByte>`) so
7
- their method bodies own call-site lists (see `propose/completed/CALL-GRAPH-PROPOSE.md` §4.1).
8
-
9
- The output is deliberately language-model friendly (simple names, no tree-sitter
10
- Nodes leak through) so downstream graph / chunk-enrichment code can stay pure
11
- Python with no tree-sitter dependency.
12
- """
13
- from __future__ import annotations
14
-
15
- import posixpath
16
- import sys
17
- import threading
18
- from dataclasses import dataclass, field
19
- from typing import Iterable
20
-
21
- import tree_sitter_java as _ts_java
22
-
23
- from java_codebase_rag.ast.brownfield_events import (
24
- emit_brownfield_exclusivity_shadowing,
25
- emit_brownfield_method_string_literal,
26
- )
27
- from tree_sitter import Language, Node, Parser
28
-
29
- __all__ = [
30
- "AnnotationRef",
31
- "CallSite",
32
- "FieldDecl",
33
- "FileImports",
34
- "ParamDecl",
35
- "MethodDecl",
36
- "OutgoingCallDecl",
37
- "RouteDecl",
38
- "ROUTE_META_ANNOTATION_NAMES",
39
- "CODEBASE_ROUTE_ANNOTATIONS",
40
- "CODEBASE_HTTP_CLIENT_ANNOTATIONS",
41
- "CODEBASE_PRODUCER_ANNOTATIONS",
42
- "TypeDecl",
43
- "JavaFileAst",
44
- "parse_java",
45
- "infer_role",
46
- "infer_role_for_type",
47
- "infer_capabilities_for_type",
48
- "ROLE_ANNOTATIONS",
49
- "ONTOLOGY_VERSION",
50
- "_METHOD_ANN_TO_CAPABILITY",
51
- "_TYPE_ANN_TO_CAPABILITY",
52
- "_INJECTED_TYPES_TO_CAPABILITY",
53
- "_SUPERTYPE_TO_CAPABILITY",
54
- ]
55
-
56
- # Name suffixes that strongly indicate a passive data-carrier type when the
57
- # type is not annotated as a Spring/JPA component. Kept conservative on
58
- # purpose: a single clear suffix match is enough, but only when role
59
- # inference would otherwise return OTHER (so @Service FooRequest stays a
60
- # SERVICE). Checked case-sensitively against the simple type name.
61
- _DTO_NAME_SUFFIXES: tuple[str, ...] = (
62
- "Dto", "DTO",
63
- "Request", "Response",
64
- "Payload", "Model",
65
- "Event", "Message",
66
- "Body", "Form",
67
- "Command", "Query",
68
- "Record", "View",
69
- )
70
-
71
- # Lombok value / builder annotations typical of DTO-style types. Presence of
72
- # any one of these promotes an otherwise role-less type to DTO.
73
- _DTO_LOMBOK_ANNOTATIONS: frozenset[str] = frozenset({
74
- "Data", "Value", "Builder",
75
- "Getter", "Setter",
76
- "EqualsAndHashCode", "ToString",
77
- })
78
-
79
- # Phase 5: HTTP_CALLS + ASYNC_CALLS (B2b); Phase 6: cross-service resolution mode on GraphMeta;
80
- # Phase 7: FEIGN_CLIENT role -> CLIENT + HTTP_CLIENT capability vocabulary cleanup;
81
- # Phase 8: first-class Client node + DECLARES_CLIENT relation, separating outbound declarations from Route.
82
- # Phase 9: `@CodebaseAsyncRoute` replaces same-method built-in `@KafkaListener` routes in graph composition.
83
- # Phase 10: `@CodebaseHttpClient` rename + `CodebaseHttpMethod` enum; inbound HTTP layer-C replaces built-in rows.
84
- # Phase 11: `EDGE_SCHEMA` in `java_ontology.py` (canonical edge navigation schema; v14 re-index).
85
- # Phase 12: CALLS `callee_declaring_role`, supertype-walk dedup, pass3 unresolved counters (v15 re-index).
86
- # Bumps whenever extraction / enrichment semantics change.
87
- # Phase 13: Symbol.search_text + LadybugDB FTS (Okapi BM25) index for lexical search (v19 re-index).
88
- ONTOLOGY_VERSION = 19
89
-
90
- ROLE_ANNOTATIONS: dict[str, str] = {
91
- # Spring Web
92
- "RestController": "CONTROLLER",
93
- "Controller": "CONTROLLER",
94
- # Spring stereotypes
95
- "Service": "SERVICE",
96
- "Repository": "REPOSITORY",
97
- "Component": "COMPONENT",
98
- "Configuration": "CONFIG",
99
- # Persistence
100
- "Entity": "ENTITY",
101
- "MappedSuperclass": "ENTITY",
102
- "Embeddable": "ENTITY",
103
- # Remoting / messaging
104
- "FeignClient": "CLIENT",
105
- # Mappers
106
- "Mapper": "MAPPER",
107
- }
108
-
109
- _INJECT_FIELD_ANNOTATIONS = frozenset({"Autowired", "Inject", "Resource"})
110
- _LOMBOK_RAC = frozenset({"RequiredArgsConstructor", "AllArgsConstructor"})
111
-
112
- # ---------- capability detector tables ----------
113
-
114
- _METHOD_ANN_TO_CAPABILITY: dict[str, str] = {
115
- "KafkaListener": "MESSAGE_LISTENER",
116
- "RabbitListener": "MESSAGE_LISTENER",
117
- "JmsListener": "MESSAGE_LISTENER",
118
- "SqsListener": "MESSAGE_LISTENER",
119
- "EventListener": "MESSAGE_LISTENER",
120
- "StreamListener": "MESSAGE_LISTENER",
121
- "Scheduled": "SCHEDULED_TASK",
122
- "ExceptionHandler": "EXCEPTION_HANDLER",
123
- }
124
-
125
- _TYPE_ANN_TO_CAPABILITY: dict[str, str] = {
126
- "ControllerAdvice": "EXCEPTION_HANDLER",
127
- "RestControllerAdvice": "EXCEPTION_HANDLER",
128
- "FeignClient": "HTTP_CLIENT",
129
- }
130
-
131
- _INJECTED_TYPES_TO_CAPABILITY: dict[str, str] = {
132
- "KafkaTemplate": "MESSAGE_PRODUCER",
133
- "RabbitTemplate": "MESSAGE_PRODUCER",
134
- "JmsTemplate": "MESSAGE_PRODUCER",
135
- "StreamBridge": "MESSAGE_PRODUCER",
136
- "ApplicationEventPublisher": "MESSAGE_PRODUCER",
137
- }
138
-
139
- _SUPERTYPE_TO_CAPABILITY: dict[str, str] = {
140
- "Job": "SCHEDULED_TASK",
141
- }
142
-
143
- _ROUTE_HTTP_MAPPING_NAMES = frozenset({
144
- "RequestMapping",
145
- "GetMapping",
146
- "PostMapping",
147
- "PutMapping",
148
- "DeleteMapping",
149
- "PatchMapping",
150
- })
151
-
152
- # Seeds for `collect_annotation_meta_chain` so custom @interface meta-annotations
153
- # (e.g. @AcmeGet meta-@GetMapping) resolve in Layer A (see graph_enrich._meta_builtins).
154
- ROUTE_META_ANNOTATION_NAMES: frozenset[str] = _ROUTE_HTTP_MAPPING_NAMES | frozenset({
155
- "KafkaListener",
156
- "RabbitListener",
157
- "JmsListener",
158
- "StreamListener",
159
- })
160
-
161
- CODEBASE_ROUTE_ANNOTATIONS: frozenset[str] = frozenset({
162
- "CodebaseHttpRoute",
163
- "CodebaseHttpRoutes",
164
- "CodebaseAsyncRoute",
165
- "CodebaseAsyncRoutes",
166
- })
167
- CODEBASE_HTTP_CLIENT_ANNOTATIONS: frozenset[str] = frozenset(
168
- {"CodebaseHttpClient", "CodebaseHttpClients"}
169
- )
170
-
171
- # Framework annotations bypassed when `@CodebaseHttpRoute` / `@CodebaseHttpClient` wins (verbose INFO).
172
- _BROWNFIELD_SHADOWABLE_HTTP_FRAMEWORK_METHOD_ANNOTATIONS: frozenset[str] = (
173
- _ROUTE_HTTP_MAPPING_NAMES
174
- | frozenset({
175
- "GET",
176
- "POST",
177
- "PUT",
178
- "PATCH",
179
- "DELETE",
180
- "HEAD",
181
- "OPTIONS",
182
- })
183
- )
184
- CODEBASE_PRODUCER_ANNOTATIONS: frozenset[str] = frozenset({"CodebaseProducer", "CodebaseProducers"})
185
-
186
- _ROUTE_ASYNC_METHOD_NAMES = frozenset({
187
- "KafkaListener",
188
- "RabbitListener",
189
- "JmsListener",
190
- "StreamListener",
191
- })
192
-
193
- _TYPE_KINDS = {
194
- "class_declaration": "class",
195
- "interface_declaration": "interface",
196
- "enum_declaration": "enum",
197
- "record_declaration": "record",
198
- "annotation_type_declaration": "annotation",
199
- }
200
-
201
- # For `new Super() { }` when `Super` is not declared in the same compilation unit:
202
- # treat these simple names as interfaces (implements) vs classes (extends).
203
- _ANON_SUPER_AS_INTERFACE: frozenset[str] = frozenset({
204
- "Runnable", "Callable", "Comparable", "Iterable", "Iterator", "AutoCloseable",
205
- "Closeable", "Flushable", "Readable", "Appendable", "Cloneable", "Serializable",
206
- "Externalizable", "InvocationHandler", "ThreadFactory", "PrivilegedAction",
207
- "PrivilegedExceptionAction", "Comparator", "Consumer", "BiConsumer", "Supplier",
208
- "Function", "BiFunction", "UnaryOperator", "BinaryOperator", "Predicate",
209
- "BiPredicate", "IntConsumer", "LongConsumer", "DoubleConsumer", "IntFunction",
210
- "LongFunction", "DoubleFunction", "IntPredicate", "LongPredicate", "DoublePredicate",
211
- "IntSupplier", "LongSupplier", "DoubleSupplier", "ToIntFunction", "ToLongFunction",
212
- "ToDoubleFunction", "Stream", "BaseStream", "Collector", "Observer", "Observable",
213
- "List", "Set", "Map", "Queue", "Deque", "Collection", "EventListener",
214
- "ActionListener", "MouseListener", "KeyListener", "WindowListener", "RowMapper",
215
- "ResultSetExtractor", "PreparedStatementCreator", "CallableStatementCallback",
216
- })
217
-
218
-
219
- # tree-sitter's ``Parser`` mutates internal state during ``parse()`` and is NOT
220
- # thread-safe, so each OS thread gets its own instance. ``parse_java`` is called
221
- # concurrently from worker threads when indexing runs with cocoindex's inflight
222
- # parallelism — both directly (java_index_flow_lancedb.py: process_java_file
223
- # offloads parse+enrich to asyncio.to_thread) and transitively
224
- # (graph_enrich.collect_annotation_meta_chain -> _collect_annotation_decl_index
225
- # -> parse_java, reached from enrich_chunk). The ``Language`` is immutable and
226
- # shared; per-thread ``Parser`` construction is lazy and cheap (once per thread),
227
- # which also preserves parse parallelism instead of serializing it.
228
- _parser_tls = threading.local()
229
-
230
-
231
- def _parser() -> Parser:
232
- p = getattr(_parser_tls, "parser", None)
233
- if p is None:
234
- _parser_tls.parser = p = Parser(Language(_ts_java.language()))
235
- return p
236
-
237
-
238
- # ---------- dataclasses ----------
239
-
240
-
241
- @dataclass
242
- class AnnotationRef:
243
- name: str # simple (last segment); e.g. "RestController"
244
- qualified: str # raw source text, e.g. "org.springframework.web.bind.annotation.RestController"
245
- arguments: dict[str, str] = field(default_factory=dict)
246
- # Argument origin by key: "enum" | "string".
247
- argument_kinds: dict[str, str] = field(default_factory=dict)
248
- # Populated for `@CodebaseCapabilities({@CodebaseCapability("a"), ...})` — inner values.
249
- container_capability_values: tuple[str, ...] = field(default_factory=tuple)
250
- # Entry-aligned with `container_capability_values`; each value is "enum" | "string".
251
- container_capability_kinds: tuple[str, ...] = field(default_factory=tuple)
252
-
253
-
254
- @dataclass
255
- class FieldDecl:
256
- name: str
257
- type_name: str # simple name, generics + arrays stripped
258
- type_raw: str # original text
259
- modifiers: list[str] = field(default_factory=list)
260
- annotations: list[AnnotationRef] = field(default_factory=list)
261
- start_byte: int = 0
262
- end_byte: int = 0
263
- start_line: int = 0
264
- end_line: int = 0
265
-
266
-
267
- @dataclass
268
- class ParamDecl:
269
- name: str
270
- type_name: str
271
- type_raw: str
272
- annotations: list[AnnotationRef] = field(default_factory=list)
273
-
274
-
275
- @dataclass
276
- class FileImports:
277
- """Per-compilation-unit import maps used by call-site resolution."""
278
-
279
- explicit: dict[str, str] = field(default_factory=dict) # SimpleType -> type FQN
280
- static_methods: dict[str, str] = field(default_factory=dict) # simple method name -> "pkg.Type.method"
281
- static_wildcards: list[str] = field(default_factory=list) # type FQNs for `import static T.*`
282
-
283
-
284
- @dataclass
285
- class CallSite:
286
- """A single static call site inside a method or constructor body."""
287
-
288
- caller_fqn: str # type_fqn#signature (matches Symbol.fqn for method nodes)
289
- receiver_expr: str # raw receiver text; "" for bare calls
290
- callee_simple: str # method name or "<init>"
291
- arg_count: int # -1 for method references (unknown)
292
- is_static_call: bool
293
- is_constructor: bool
294
- in_lambda: bool
295
- line: int
296
- byte: int
297
- chained_method_reference: bool = False # true for ``expr::name`` where expr is a call chain
298
-
299
-
300
- @dataclass
301
- class MethodDecl:
302
- name: str
303
- return_type: str # simple name; "" for constructors / void kept as "void"
304
- is_constructor: bool
305
- parameters: list[ParamDecl] = field(default_factory=list)
306
- modifiers: list[str] = field(default_factory=list)
307
- annotations: list[AnnotationRef] = field(default_factory=list)
308
- signature: str = "" # "name(T1,T2)"
309
- start_byte: int = 0
310
- end_byte: int = 0
311
- start_line: int = 0
312
- end_line: int = 0
313
- call_sites: list[CallSite] = field(default_factory=list)
314
- # Ordered (name, simple_type_name) from `local_variable_declaration` in body.
315
- local_vars: list[tuple[str, str]] = field(default_factory=list)
316
- routes: list["RouteDecl"] = field(default_factory=list)
317
- outgoing_calls: list["OutgoingCallDecl"] = field(default_factory=list)
318
-
319
-
320
- @dataclass
321
- class RouteDecl:
322
- """Extracted route declaration anchored on a method (B2a).
323
-
324
- `method_fqn` matches graph Symbol.fqn (`type.pkg.Type#name(T1,T2)`).
325
- """
326
-
327
- method_fqn: str
328
- method_sig: str
329
- kind: str
330
- framework: str
331
- http_method: str
332
- path: str
333
- topic: str
334
- broker: str
335
- feign_name: str
336
- feign_url: str
337
- resolution_strategy: str
338
- confidence: float
339
- resolved: bool
340
- filename: str
341
- start_line: int
342
- end_line: int
343
- # brownfield / B2a composition (graph_enrich.resolve_routes_for_method); not a graph column.
344
- route_source_layer: str = "builtin"
345
-
346
-
347
- @dataclass
348
- class OutgoingCallDecl:
349
- method_fqn: str
350
- method_sig: str
351
- client_kind: str
352
- channel: str
353
- feign_target_name: str
354
- feign_target_url: str
355
- path_template_call: str
356
- method_call: str
357
- topic_call: str
358
- broker_call: str
359
- raw_uri: str
360
- raw_topic: str
361
- resolution_strategy: str
362
- confidence_base: float
363
- resolved: bool
364
- filename: str
365
- start_line: int
366
- end_line: int
367
-
368
-
369
- @dataclass
370
- class TypeDecl:
371
- name: str
372
- kind: str
373
- fqn: str
374
- modifiers: list[str] = field(default_factory=list)
375
- annotations: list[AnnotationRef] = field(default_factory=list)
376
- extends: list[str] = field(default_factory=list) # simple names
377
- implements: list[str] = field(default_factory=list)
378
- fields: list[FieldDecl] = field(default_factory=list)
379
- methods: list[MethodDecl] = field(default_factory=list)
380
- nested: list["TypeDecl"] = field(default_factory=list)
381
- start_byte: int = 0
382
- end_byte: int = 0
383
- start_line: int = 0
384
- end_line: int = 0
385
- outer_fqn: str | None = None # None for top-level
386
- capabilities: list[str] = field(default_factory=list)
387
-
388
-
389
- @dataclass
390
- class JavaFileAst:
391
- package: str
392
- imports: list[str] # raw, as written (may include ".*" suffix)
393
- wildcard_imports: list[str] # e.g. "java.util"
394
- explicit_imports: dict[str, str] # "List" -> "java.util.List"
395
- top_level_types: list[TypeDecl]
396
- all_types: list[TypeDecl] # flat, includes nested
397
- parse_error: bool = False
398
- source_bytes: int = 0
399
- file_imports: FileImports = field(default_factory=FileImports)
400
- routes_skipped_unresolved: int = 0
401
-
402
-
403
- @dataclass
404
- class _ParseCtx:
405
- routes_skipped_unresolved: int = 0
406
- verbose: bool = False
407
-
408
-
409
- # ---------- helpers ----------
410
-
411
-
412
- def _txt(node: Node, src: bytes) -> str:
413
- return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
414
-
415
-
416
- def _annotation_name(node: Node, src: bytes) -> tuple[str, str]:
417
- """Extract ('simple', 'qualified-as-written') from an annotation node."""
418
- name_node = node.child_by_field_name("name")
419
- qualified = _txt(name_node, src) if name_node is not None else _txt(node, src).lstrip("@").split("(", 1)[0]
420
- simple = qualified.rsplit(".", 1)[-1]
421
- return simple, qualified
422
-
423
-
424
- def _string_literal_value(node: Node, src: bytes) -> str | None:
425
- if node.type != "string_literal":
426
- return None
427
- for ch in node.children:
428
- if ch.type == "string_fragment":
429
- return _txt(ch, src)
430
- return None
431
-
432
-
433
- def _annotation_value(
434
- node: Node, src: bytes
435
- ) -> tuple[str | None, str | None]:
436
- """Extract annotation value and its kind.
437
-
438
- Returns `(value, kind)` where kind is one of "enum" / "string".
439
- Enum-like expressions are normalized to the terminal constant name:
440
- `CodebaseRoleKind.SERVICE` -> `SERVICE`.
441
- """
442
- if node.type == "element_value" and node.named_children:
443
- return _annotation_value(node.named_children[0], src)
444
-
445
- sval = _string_literal_value(node, src)
446
- if sval is not None:
447
- return sval, "string"
448
-
449
- if node.type in ("identifier", "scoped_identifier", "field_access"):
450
- raw = _txt(node, src).strip()
451
- if not raw:
452
- return None, None
453
- return raw.rsplit(".", 1)[-1], "enum"
454
-
455
- return None, None
456
-
457
-
458
- def _parse_annotation_argument_list(
459
- alist: Node, src: bytes
460
- ) -> tuple[dict[str, str], dict[str, str]]:
461
- """Map argument names to normalized enum/string values and value kinds."""
462
- out: dict[str, str] = {}
463
- kinds: dict[str, str] = {}
464
- for ch in alist.named_children:
465
- if ch.type == "element_value_pair":
466
- key_node = ch.child_by_field_name("key")
467
- val_node = ch.child_by_field_name("value")
468
- if key_node is None:
469
- ids = [c for c in ch.children if c.type == "identifier"]
470
- key_node = ids[0] if ids else None
471
- if val_node is None:
472
- for c in reversed(ch.named_children):
473
- if c is not key_node:
474
- val_node = c
475
- break
476
- if key_node is None or val_node is None:
477
- continue
478
- key = _txt(key_node, src)
479
- val, kind = _annotation_value(val_node, src)
480
- if val is not None and kind is not None:
481
- out[key] = val
482
- kinds[key] = kind
483
- else:
484
- v, kind = _annotation_value(ch, src)
485
- if v is not None and kind is not None and "value" not in out:
486
- out["value"] = v
487
- kinds["value"] = kind
488
- return out, kinds
489
-
490
-
491
- def _codebase_capability_values_from_array(
492
- ann_node: Node, src: bytes
493
- ) -> tuple[tuple[str, ...], tuple[str, ...]]:
494
- found: list[str] = []
495
- kinds: list[str] = []
496
-
497
- def visit(n: Node) -> None:
498
- if n.type == "annotation":
499
- name_node = n.child_by_field_name("name")
500
- n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
501
- if n_simple == "CodebaseCapability":
502
- for c in n.children:
503
- if c.type == "annotation_argument_list":
504
- m, mk = _parse_annotation_argument_list(c, src)
505
- v = m.get("value")
506
- k = mk.get("value")
507
- if v is not None and k is not None:
508
- found.append(v)
509
- kinds.append(k)
510
- for c in n.children:
511
- visit(c)
512
-
513
- visit(ann_node)
514
- return tuple(found), tuple(kinds)
515
-
516
-
517
- def _parse_annotation_ref_node(node: Node, src: bytes) -> AnnotationRef:
518
- """Build `AnnotationRef` for a `marker_annotation` or `annotation` node."""
519
- simple, qualified = _annotation_name(node, src)
520
- t = node.type
521
- if t == "marker_annotation":
522
- return AnnotationRef(name=simple, qualified=qualified, arguments={})
523
-
524
- args: dict[str, str] = {}
525
- arg_kinds: dict[str, str] = {}
526
- container: tuple[str, ...] = ()
527
- container_kinds: tuple[str, ...] = ()
528
- alist = node.child_by_field_name("arguments")
529
- if alist is None:
530
- for ch in node.children:
531
- if ch.type == "annotation_argument_list":
532
- alist = ch
533
- break
534
- if alist is not None:
535
- args, arg_kinds = _parse_annotation_argument_list(alist, src)
536
- if simple == "CodebaseCapabilities" and alist is not None:
537
- container, container_kinds = _codebase_capability_values_from_array(node, src)
538
- return AnnotationRef(
539
- name=simple,
540
- qualified=qualified,
541
- arguments=args,
542
- argument_kinds=arg_kinds,
543
- container_capability_values=container,
544
- container_capability_kinds=container_kinds,
545
- )
546
-
547
-
548
- _MODIFIER_KEYWORDS = frozenset({
549
- "public",
550
- "private",
551
- "protected",
552
- "static",
553
- "final",
554
- "abstract",
555
- "default",
556
- "synchronized",
557
- "native",
558
- "transient",
559
- "volatile",
560
- "strictfp",
561
- "sealed",
562
- "non-sealed",
563
- })
564
-
565
-
566
- def _find_modifiers_child(parent: Node) -> Node | None:
567
- """tree-sitter-java exposes `modifiers` as an unnamed-field child node."""
568
- for ch in parent.children:
569
- if ch.type == "modifiers":
570
- return ch
571
- return None
572
-
573
-
574
- def _collect_annotations_and_modifiers(
575
- parent: Node, src: bytes
576
- ) -> tuple[list[str], list[AnnotationRef]]:
577
- """Extract modifiers + annotations from the `modifiers` sibling of `parent`."""
578
- mods_node = _find_modifiers_child(parent)
579
- if mods_node is None:
580
- return [], []
581
- mods: list[str] = []
582
- anns: list[AnnotationRef] = []
583
- for child in mods_node.children:
584
- t = child.type
585
- if t in ("marker_annotation", "annotation"):
586
- anns.append(_parse_annotation_ref_node(child, src))
587
- elif t in _MODIFIER_KEYWORDS:
588
- mods.append(t)
589
- return mods, anns
590
-
591
-
592
- def _strip_type_to_simple(type_node: Node, src: bytes) -> str:
593
- """Reduce any type expression to its head simple name.
594
-
595
- Examples:
596
- List<String> -> List
597
- java.util.List<Foo> -> List
598
- Map<String,Integer>[][] -> Map
599
- String[] -> String
600
- void -> void
601
- int -> int
602
- """
603
- t = type_node.type
604
- if t == "generic_type":
605
- # first child is the name part (type_identifier | scoped_type_identifier)
606
- for ch in type_node.children:
607
- if ch.type in ("type_identifier", "scoped_type_identifier"):
608
- return _strip_type_to_simple(ch, src)
609
- return _txt(type_node, src).split("<", 1)[0].rsplit(".", 1)[-1]
610
- if t == "array_type":
611
- elem = type_node.child_by_field_name("element") or (type_node.named_children[0] if type_node.named_children else None)
612
- if elem is not None:
613
- return _strip_type_to_simple(elem, src)
614
- return _txt(type_node, src).split("[", 1)[0]
615
- if t == "scoped_type_identifier":
616
- return _txt(type_node, src).rsplit(".", 1)[-1]
617
- if t == "type_identifier":
618
- return _txt(type_node, src)
619
- # primitive / void / anything else: return raw
620
- return _txt(type_node, src)
621
-
622
-
623
- def _collect_type_list(node: Node, src: bytes) -> list[str]:
624
- """Turn an `interface_type_list` / `type_list` / single type into simple names."""
625
- out: list[str] = []
626
- if node.type in ("type_list", "interface_type_list", "super_interfaces", "extends_interfaces"):
627
- for ch in node.named_children:
628
- if ch.type == "type_list" or ch.type == "interface_type_list":
629
- out.extend(_collect_type_list(ch, src))
630
- else:
631
- out.append(_strip_type_to_simple(ch, src))
632
- return out
633
- return [_strip_type_to_simple(node, src)]
634
-
635
-
636
- def _extends_of(type_node: Node, src: bytes) -> list[str]:
637
- out: list[str] = []
638
- # class: superclass field; interface: extends_interfaces field
639
- sc = type_node.child_by_field_name("superclass")
640
- if sc is not None:
641
- # `superclass` node has children like `extends` + type
642
- for ch in sc.named_children:
643
- out.append(_strip_type_to_simple(ch, src))
644
- ei = type_node.child_by_field_name("interfaces")
645
- # for interface declarations, the extends clause uses field "extends" via `extends_interfaces`
646
- if ei is None:
647
- for ch in type_node.children:
648
- if ch.type in ("extends_interfaces",):
649
- for sub in ch.named_children:
650
- if sub.type in ("type_list", "interface_type_list"):
651
- out.extend(_collect_type_list(sub, src))
652
- else:
653
- out.append(_strip_type_to_simple(sub, src))
654
- return out
655
-
656
-
657
- def _implements_of(type_node: Node, src: bytes) -> list[str]:
658
- out: list[str] = []
659
- si = type_node.child_by_field_name("interfaces")
660
- if si is not None:
661
- for ch in si.named_children:
662
- if ch.type in ("type_list", "interface_type_list"):
663
- out.extend(_collect_type_list(ch, src))
664
- else:
665
- out.append(_strip_type_to_simple(ch, src))
666
- return out
667
- # fallback for grammars exposing super_interfaces unnamed
668
- for ch in type_node.children:
669
- if ch.type == "super_interfaces":
670
- for sub in ch.named_children:
671
- if sub.type in ("type_list", "interface_type_list"):
672
- out.extend(_collect_type_list(sub, src))
673
- else:
674
- out.append(_strip_type_to_simple(sub, src))
675
- return out
676
-
677
-
678
- def _iter_body_members(body: Node) -> Iterable[Node]:
679
- if body is None:
680
- return []
681
- return [c for c in body.named_children]
682
-
683
-
684
- def _pre_scan_declared_type_kinds(root: Node, src: bytes) -> dict[str, str]:
685
- """Map simple type name -> kind for declarations in this CU (for anonymous super)."""
686
- out: dict[str, str] = {}
687
-
688
- def visit(n: Node) -> None:
689
- t = n.type
690
- if t in _TYPE_KINDS:
691
- nn = n.child_by_field_name("name")
692
- if nn is not None:
693
- nm = _txt(nn, src)
694
- if nm and nm != "<anon>":
695
- out[nm] = _TYPE_KINDS[t]
696
- for c in n.children:
697
- visit(c)
698
-
699
- visit(root)
700
- return out
701
-
702
-
703
- def _anonymous_extends_implements(
704
- super_simple: str, kind_by_simple: dict[str, str],
705
- ) -> tuple[list[str], list[str]]:
706
- """Java: anonymous extends class X, or extends Object + implements I."""
707
- k = kind_by_simple.get(super_simple)
708
- if k == "interface":
709
- return [], [super_simple]
710
- if k in ("class", "enum", "record"):
711
- return [super_simple], []
712
- if super_simple in _ANON_SUPER_AS_INTERFACE:
713
- return [], [super_simple]
714
- return [super_simple], []
715
-
716
-
717
- def _parse_type_body_into_decl(
718
- body: Node,
719
- src: bytes,
720
- *,
721
- package: str,
722
- fqn: str,
723
- kind: str,
724
- extends: list[str],
725
- implements: list[str],
726
- modifiers: list[str],
727
- annotations: list[AnnotationRef],
728
- kind_by_simple: dict[str, str],
729
- start_byte: int,
730
- end_byte: int,
731
- start_line: int,
732
- end_line: int,
733
- outer_fqn: str | None,
734
- enclosing_type_node: Node | None,
735
- file_rel: str,
736
- ctx: _ParseCtx,
737
- ) -> TypeDecl:
738
- """Shared member parsing for named types and synthetic anonymous classes."""
739
- fields: list[FieldDecl] = []
740
- methods: list[MethodDecl] = []
741
- nested: list[TypeDecl] = []
742
- anon_nested: list[TypeDecl] = []
743
- for ch in _iter_body_members(body):
744
- if ch.type == "field_declaration":
745
- fields.extend(_parse_field(ch, src))
746
- elif ch.type == "method_declaration":
747
- m, anons = _parse_method(
748
- ch, src, is_constructor=False, type_fqn=fqn,
749
- package=package, kind_by_simple=kind_by_simple,
750
- ctx=ctx,
751
- enclosing_type_node=enclosing_type_node,
752
- type_kind=kind,
753
- type_anns=annotations,
754
- file_rel=file_rel,
755
- )
756
- methods.append(m)
757
- anon_nested.extend(anons)
758
- elif ch.type == "constructor_declaration":
759
- m, anons = _parse_method(
760
- ch, src, is_constructor=True, type_fqn=fqn,
761
- package=package, kind_by_simple=kind_by_simple,
762
- ctx=ctx,
763
- enclosing_type_node=enclosing_type_node,
764
- type_kind=kind,
765
- type_anns=annotations,
766
- file_rel=file_rel,
767
- )
768
- methods.append(m)
769
- anon_nested.extend(anons)
770
- elif ch.type in _TYPE_KINDS:
771
- nested.append(
772
- _parse_type(
773
- ch, src,
774
- package=package, outer_fqn=fqn, kind_by_simple=kind_by_simple,
775
- file_rel=file_rel, ctx=ctx,
776
- ),
777
- )
778
- nested.extend(anon_nested)
779
-
780
- ann_names_set = {a.name for a in annotations}
781
- if (
782
- kind in ("class", "enum")
783
- and not any(m.is_constructor for m in methods)
784
- and not (_LOMBOK_RAC & ann_names_set)
785
- ):
786
- default_ctor_sig = "<init>()"
787
- methods.append(
788
- MethodDecl(
789
- name="<init>",
790
- return_type="",
791
- is_constructor=True,
792
- parameters=[],
793
- modifiers=[],
794
- annotations=[],
795
- signature=default_ctor_sig,
796
- start_byte=start_byte,
797
- end_byte=start_byte,
798
- start_line=start_line,
799
- end_line=start_line,
800
- call_sites=[],
801
- local_vars=[],
802
- )
803
- )
804
-
805
- name = fqn.rsplit(".", 1)[-1]
806
- type_decl = TypeDecl(
807
- name=name,
808
- kind=kind,
809
- fqn=fqn,
810
- modifiers=modifiers,
811
- annotations=annotations,
812
- extends=extends,
813
- implements=implements,
814
- fields=fields,
815
- methods=methods,
816
- nested=nested,
817
- start_byte=start_byte,
818
- end_byte=end_byte,
819
- start_line=start_line,
820
- end_line=end_line,
821
- outer_fqn=outer_fqn,
822
- )
823
- type_decl.capabilities = infer_capabilities_for_type(type_decl)
824
- return type_decl
825
-
826
-
827
- def _parse_synthetic_anonymous_type(
828
- object_creation: Node,
829
- class_body: Node,
830
- src: bytes,
831
- *,
832
- package: str,
833
- host_type_fqn: str,
834
- kind_by_simple: dict[str, str],
835
- file_rel: str,
836
- ctx: _ParseCtx,
837
- ) -> TypeDecl:
838
- label = f"<anon:{object_creation.start_byte}>"
839
- fqn = f"{host_type_fqn}.{label}"
840
- type_node = object_creation.child_by_field_name("type")
841
- super_simple = _strip_type_to_simple(type_node, src) if type_node is not None else "Object"
842
- extends, implements = _anonymous_extends_implements(super_simple, kind_by_simple)
843
- return _parse_type_body_into_decl(
844
- class_body,
845
- src,
846
- package=package,
847
- fqn=fqn,
848
- kind="class",
849
- extends=extends,
850
- implements=implements,
851
- modifiers=[],
852
- annotations=[],
853
- kind_by_simple=kind_by_simple,
854
- start_byte=object_creation.start_byte,
855
- end_byte=object_creation.end_byte,
856
- start_line=object_creation.start_point[0] + 1,
857
- end_line=object_creation.end_point[0] + 1,
858
- outer_fqn=host_type_fqn,
859
- enclosing_type_node=None,
860
- file_rel=file_rel,
861
- ctx=ctx,
862
- )
863
-
864
-
865
- def _extract_anonymous_types_in_subtree(
866
- root: Node,
867
- src: bytes,
868
- *,
869
- package: str,
870
- host_type_fqn: str,
871
- kind_by_simple: dict[str, str],
872
- file_rel: str,
873
- ctx: _ParseCtx,
874
- ) -> list[TypeDecl]:
875
- """Find every `new T() { }` with class_body under root; skip bodies (parsed separately)."""
876
- found: list[TypeDecl] = []
877
-
878
- def visit(n: Node) -> None:
879
- if n.type == "object_creation_expression":
880
- class_body: Node | None = None
881
- for ch in n.named_children:
882
- if ch.type == "class_body":
883
- class_body = ch
884
- break
885
- if class_body is not None:
886
- found.append(
887
- _parse_synthetic_anonymous_type(
888
- n, class_body, src,
889
- package=package, host_type_fqn=host_type_fqn, kind_by_simple=kind_by_simple,
890
- file_rel=file_rel,
891
- ctx=ctx,
892
- )
893
- )
894
- for ch in n.named_children:
895
- if ch.type == "class_body":
896
- continue
897
- visit(ch)
898
- return
899
- for ch in n.children:
900
- visit(ch)
901
-
902
- visit(root)
903
- return found
904
-
905
-
906
- def _import_declaration_is_static(node: Node, src: bytes) -> bool:
907
- for c in node.children:
908
- if c.type == "static" and _txt(c, src) == "static":
909
- return True
910
- return False
911
-
912
-
913
- def _arg_list_count(arg_list: Node | None) -> int:
914
- if arg_list is None:
915
- return 0
916
- return len(arg_list.named_children)
917
-
918
-
919
- def _infer_static_method_invocation(obj: Node | None, src: bytes) -> bool:
920
- """Heuristic: ClassName.method() vs instance.method().
921
-
922
- TODO: Uppercase ``identifier`` receivers are treated as types; the graph
923
- builder may override via the per-method scope table when the name is a local.
924
- """
925
- if obj is None:
926
- return False
927
- if obj.type in ("type_identifier", "scoped_type_identifier"):
928
- return True
929
- if obj.type == "this" or obj.type == "super":
930
- return False
931
- if obj.type == "identifier":
932
- name = _txt(obj, src)
933
- return len(name) > 0 and name[0].isupper()
934
- return False
935
-
936
-
937
- def _collect_local_vars(body: Node, src: bytes) -> list[tuple[str, str]]:
938
- """Declaration order: (variable name, head simple type name)."""
939
- out: list[tuple[str, str]] = []
940
- if body is None:
941
- return out
942
-
943
- def visit(n: Node) -> None:
944
- if n.type == "local_variable_declaration":
945
- type_node = n.child_by_field_name("type")
946
- if type_node is None:
947
- return
948
- t_simple = _strip_type_to_simple(type_node, src)
949
- for ch in n.named_children:
950
- if ch.type != "variable_declarator":
951
- continue
952
- name_node = ch.child_by_field_name("name")
953
- if name_node is not None:
954
- out.append((_txt(name_node, src), t_simple))
955
- return
956
- for c in n.children:
957
- visit(c)
958
-
959
- visit(body)
960
- return out
961
-
962
-
963
- def _collect_call_sites(
964
- body: Node,
965
- src: bytes,
966
- *,
967
- caller_fqn: str,
968
- in_lambda: bool,
969
- ) -> list[CallSite]:
970
- """Walk a block body and collect CallSite records (attributed to caller_fqn)."""
971
- out: list[CallSite] = []
972
-
973
- def add_site(
974
- *,
975
- receiver_expr: str,
976
- callee_simple: str,
977
- arg_count: int,
978
- is_static_call: bool,
979
- is_constructor: bool,
980
- line: int,
981
- byte: int,
982
- lam: bool,
983
- chained_method_reference: bool = False,
984
- ) -> None:
985
- out.append(
986
- CallSite(
987
- caller_fqn=caller_fqn,
988
- receiver_expr=receiver_expr,
989
- callee_simple=callee_simple,
990
- arg_count=arg_count,
991
- is_static_call=is_static_call,
992
- is_constructor=is_constructor,
993
- in_lambda=lam,
994
- chained_method_reference=chained_method_reference,
995
- line=line,
996
- byte=byte,
997
- )
998
- )
999
-
1000
- def visit(n: Node, lam: bool) -> None:
1001
- t = n.type
1002
- if t == "lambda_expression":
1003
- body_node = n.child_by_field_name("body")
1004
- if body_node is not None:
1005
- visit(body_node, True)
1006
- return
1007
- if t == "object_creation_expression":
1008
- type_node = n.child_by_field_name("type")
1009
- args = n.child_by_field_name("arguments")
1010
- if type_node is not None:
1011
- recv = _txt(type_node, src)
1012
- line = n.start_point[0] + 1
1013
- add_site(
1014
- receiver_expr=recv,
1015
- callee_simple="<init>",
1016
- arg_count=_arg_list_count(args),
1017
- is_static_call=False,
1018
- is_constructor=True,
1019
- line=line,
1020
- byte=n.start_byte,
1021
- lam=lam,
1022
- )
1023
- # Anonymous `new T() { }` bodies are indexed as synthetic nested types;
1024
- # do not attribute their call sites to this caller_fqn (D3).
1025
- for ch in n.named_children:
1026
- if ch.type == "class_body":
1027
- continue
1028
- visit(ch, lam)
1029
- return
1030
- if t == "method_invocation":
1031
- obj = n.child_by_field_name("object")
1032
- name_node = n.child_by_field_name("name")
1033
- callee = _txt(name_node, src) if name_node is not None else ""
1034
- args = n.child_by_field_name("arguments")
1035
- argc = _arg_list_count(args)
1036
- line = n.start_point[0] + 1
1037
- if obj is None:
1038
- recv = ""
1039
- static_call = False
1040
- else:
1041
- recv = _txt(obj, src)
1042
- static_call = _infer_static_method_invocation(obj, src)
1043
- add_site(
1044
- receiver_expr=recv,
1045
- callee_simple=callee,
1046
- arg_count=argc,
1047
- is_static_call=static_call,
1048
- is_constructor=False,
1049
- line=line,
1050
- byte=n.start_byte,
1051
- lam=lam,
1052
- )
1053
- for ch in n.children:
1054
- visit(ch, lam)
1055
- return
1056
- if t == "method_reference":
1057
- parts = [c for c in n.children if c.type != "::"]
1058
- if not parts:
1059
- for ch in n.children:
1060
- visit(ch, lam)
1061
- return
1062
- name_node = parts[-1]
1063
- if name_node.type != "identifier":
1064
- for ch in n.children:
1065
- visit(ch, lam)
1066
- return
1067
- name_id = _txt(name_node, src)
1068
- qual = parts[0] if len(parts) >= 2 else None
1069
- recv = _txt(qual, src) if qual is not None else ""
1070
- chained = qual is not None and qual.type == "method_invocation"
1071
- add_site(
1072
- receiver_expr=recv,
1073
- callee_simple=name_id,
1074
- arg_count=-1,
1075
- is_static_call=False,
1076
- is_constructor=False,
1077
- line=n.start_point[0] + 1,
1078
- byte=n.start_byte,
1079
- lam=lam,
1080
- chained_method_reference=chained,
1081
- )
1082
- for ch in n.children:
1083
- visit(ch, lam)
1084
- return
1085
- if t == "explicit_constructor_invocation":
1086
- is_super = any(c.type == "super" for c in n.children)
1087
- recv = "super" if is_super else "this"
1088
- args = n.child_by_field_name("arguments")
1089
- if args is None:
1090
- for c in n.named_children:
1091
- if c.type == "argument_list":
1092
- args = c
1093
- break
1094
- add_site(
1095
- receiver_expr=recv,
1096
- callee_simple="<init>",
1097
- arg_count=_arg_list_count(args),
1098
- is_static_call=False,
1099
- is_constructor=True,
1100
- line=n.start_point[0] + 1,
1101
- byte=n.start_byte,
1102
- lam=lam,
1103
- )
1104
- return
1105
- for ch in n.children:
1106
- visit(ch, lam)
1107
-
1108
- visit(body, in_lambda)
1109
- return out
1110
-
1111
-
1112
- def _unwrap_element_value(node: Node) -> Node:
1113
- if node.type == "element_value" and node.named_children:
1114
- return _unwrap_element_value(node.named_children[0])
1115
- return node
1116
-
1117
-
1118
- def _record_route_skip(ctx: _ParseCtx) -> None:
1119
- ctx.routes_skipped_unresolved += 1
1120
-
1121
-
1122
- def _string_value_atoms(val: Node, src: bytes, ctx: _ParseCtx) -> list[tuple[str, str, float, bool]]:
1123
- """String-like values: (raw_text, resolution_strategy, confidence, resolved).
1124
-
1125
- Ladder (PR-A2): literal string without ``${`` → ``annotation`` / 1.0 / resolved;
1126
- string containing ``${`` → ``spel`` / 0.85 / unresolved; anything else
1127
- (identifier, binary expr, …) → ``constant_ref`` / 0.7 / unresolved.
1128
- """
1129
- val = _unwrap_element_value(val)
1130
- if val.type == "string_literal":
1131
- s = _string_literal_value(val, src)
1132
- if s is None:
1133
- _record_route_skip(ctx)
1134
- return []
1135
- if "${" in s:
1136
- return [(s, "spel", 0.85, False)]
1137
- return [(s, "annotation", 1.0, True)]
1138
- if val.type in ("array_initializer", "element_value_array_initializer"):
1139
- out: list[tuple[str, str, float, bool]] = []
1140
- for ch in val.named_children:
1141
- out.extend(_string_value_atoms(ch, src, ctx))
1142
- return out
1143
- raw = _txt(val, src).strip()
1144
- if not raw:
1145
- return []
1146
- return [(raw, "constant_ref", 0.7, False)]
1147
-
1148
-
1149
- def _literal_strings_from_route_arg(val: Node, src: bytes, ctx: _ParseCtx) -> list[str]:
1150
- """Literal-only slice (for @FeignClient name/url/path where SpEL is not modelled)."""
1151
- return [a[0] for a in _string_value_atoms(val, src, ctx) if a[3]]
1152
-
1153
-
1154
- def _annotation_kv_nodes(ann: Node, src: bytes) -> tuple[dict[str, Node], Node | None]:
1155
- pairs: dict[str, Node] = {}
1156
- positional: Node | None = None
1157
- alist = ann.child_by_field_name("arguments")
1158
- if alist is None:
1159
- for ch in ann.children:
1160
- if ch.type == "annotation_argument_list":
1161
- alist = ch
1162
- break
1163
- if alist is None:
1164
- return pairs, positional
1165
- for ch in alist.named_children:
1166
- if ch.type == "element_value_pair":
1167
- key_node = ch.child_by_field_name("key")
1168
- val_node = ch.child_by_field_name("value")
1169
- if key_node is None:
1170
- ids = [c for c in ch.children if c.type == "identifier"]
1171
- key_node = ids[0] if ids else None
1172
- if val_node is None:
1173
- for c in reversed(ch.named_children):
1174
- if c is not key_node:
1175
- val_node = c
1176
- break
1177
- if key_node is None or val_node is None:
1178
- continue
1179
- pairs[_txt(key_node, src)] = val_node
1180
- else:
1181
- positional = ch
1182
- return pairs, positional
1183
-
1184
-
1185
- def _extract_http_methods_from_arg(val: Node, src: bytes) -> list[str]:
1186
- val = _unwrap_element_value(val)
1187
- if val.type == "array_initializer":
1188
- out: list[str] = []
1189
- for ch in val.named_children:
1190
- out.extend(_extract_http_methods_from_arg(ch, src))
1191
- return out
1192
- raw = _txt(val, src).strip()
1193
- if not raw:
1194
- return []
1195
- simple = raw.rsplit(".", 1)[-1]
1196
- return [simple.upper()] if simple else []
1197
-
1198
-
1199
- def _paths_and_methods_from_mapping_ann(
1200
- ann: Node,
1201
- src: bytes,
1202
- simple_name: str,
1203
- ctx: _ParseCtx,
1204
- ) -> tuple[list[tuple[str, str, float, bool]], list[str]]:
1205
- pairs, positional = _annotation_kv_nodes(ann, src)
1206
- path_atoms: list[tuple[str, str, float, bool]] = []
1207
- had_explicit_path_arg = False
1208
- if "path" in pairs:
1209
- had_explicit_path_arg = True
1210
- path_atoms.extend(_string_value_atoms(pairs["path"], src, ctx))
1211
- elif "value" in pairs:
1212
- had_explicit_path_arg = True
1213
- path_atoms.extend(_string_value_atoms(pairs["value"], src, ctx))
1214
- elif positional is not None:
1215
- had_explicit_path_arg = True
1216
- path_atoms.extend(_string_value_atoms(positional, src, ctx))
1217
-
1218
- if simple_name == "GetMapping":
1219
- methods = ["GET"]
1220
- elif simple_name == "PostMapping":
1221
- methods = ["POST"]
1222
- elif simple_name == "PutMapping":
1223
- methods = ["PUT"]
1224
- elif simple_name == "DeleteMapping":
1225
- methods = ["DELETE"]
1226
- elif simple_name == "PatchMapping":
1227
- methods = ["PATCH"]
1228
- elif simple_name == "RequestMapping":
1229
- methods = (
1230
- _extract_http_methods_from_arg(pairs["method"], src)
1231
- if "method" in pairs
1232
- else [""]
1233
- )
1234
- else:
1235
- methods = [""]
1236
-
1237
- if not path_atoms:
1238
- if had_explicit_path_arg:
1239
- return [], methods
1240
- path_atoms = [("", "annotation", 1.0, True)]
1241
- return path_atoms, methods
1242
-
1243
-
1244
- def _compose_http_paths(class_base: str, method_paths: list[str]) -> list[str]:
1245
- """Join Feign / servlet context paths; always merge when `class_base` is set."""
1246
- class_base = class_base.strip()
1247
- out: list[str] = []
1248
- for mp in method_paths:
1249
- mp = mp.strip()
1250
- if not class_base:
1251
- p = mp if mp else "/"
1252
- elif not mp:
1253
- p = class_base
1254
- else:
1255
- joined = posixpath.normpath(f"{class_base.rstrip('/')}/{mp.lstrip('/')}")
1256
- if not joined.startswith("/"):
1257
- joined = "/" + joined
1258
- p = joined
1259
- out.append(p)
1260
- return out
1261
-
1262
-
1263
- def _merge_http_route_with_class_base(
1264
- class_base: str,
1265
- method_path: str,
1266
- method_strategy: str,
1267
- method_confidence: float,
1268
- method_resolved: bool,
1269
- ) -> tuple[str, str, float, bool]:
1270
- """Compose class-level + method path and derive final strategy/confidence."""
1271
- full = _compose_http_paths(class_base, [method_path])[0]
1272
- # Non-string annotation args stay ``constant_ref`` even if the expression text
1273
- # contains ``${…}`` (e.g. string concat); SpEL applies only to string_literal.
1274
- if method_strategy == "constant_ref":
1275
- return full, "constant_ref", 0.7, False
1276
- if method_strategy == "spel":
1277
- return full, "spel", 0.85, False
1278
- if "${" in full:
1279
- return full, "spel", 0.85, False
1280
- if class_base and "${" in class_base:
1281
- return full, "spel", 0.85, False
1282
- return full, method_strategy, method_confidence, method_resolved
1283
-
1284
-
1285
- def _type_level_request_mapping_base(enclosing_type_node: Node | None, src: bytes, ctx: _ParseCtx) -> str:
1286
- if enclosing_type_node is None:
1287
- return ""
1288
- mods = _find_modifiers_child(enclosing_type_node)
1289
- if mods is None:
1290
- return ""
1291
- for child in mods.children:
1292
- if child.type not in ("marker_annotation", "annotation"):
1293
- continue
1294
- simple, _ = _annotation_name(child, src)
1295
- if simple != "RequestMapping":
1296
- continue
1297
- atoms, _ = _paths_and_methods_from_mapping_ann(child, src, simple, ctx)
1298
- return atoms[0][0] if atoms else ""
1299
- return ""
1300
-
1301
-
1302
- def _kafka_topics_from_ann_node(
1303
- ann: Node, src: bytes, ctx: _ParseCtx,
1304
- ) -> list[tuple[str, str, float, bool]]:
1305
- pairs, positional = _annotation_kv_nodes(ann, src)
1306
- if "topics" in pairs:
1307
- return _string_value_atoms(pairs["topics"], src, ctx)
1308
- if "topicPattern" in pairs:
1309
- _record_route_skip(ctx)
1310
- return []
1311
- if positional is not None:
1312
- return _string_value_atoms(positional, src, ctx)
1313
- return []
1314
-
1315
-
1316
- def _rabbit_queues_from_ann_node(
1317
- ann: Node, src: bytes, ctx: _ParseCtx,
1318
- ) -> list[tuple[str, str, float, bool]]:
1319
- pairs, positional = _annotation_kv_nodes(ann, src)
1320
- if "queues" in pairs:
1321
- return _string_value_atoms(pairs["queues"], src, ctx)
1322
- if "bindings" in pairs:
1323
- _record_route_skip(ctx)
1324
- return []
1325
- if positional is not None:
1326
- return _string_value_atoms(positional, src, ctx)
1327
- return []
1328
-
1329
-
1330
- def _jms_destination_from_ann_node(
1331
- ann: Node, src: bytes, ctx: _ParseCtx,
1332
- ) -> list[tuple[str, str, float, bool]]:
1333
- pairs, positional = _annotation_kv_nodes(ann, src)
1334
- for key in ("destination", "value"):
1335
- if key in pairs:
1336
- return _string_value_atoms(pairs[key], src, ctx)
1337
- if positional is not None:
1338
- return _string_value_atoms(positional, src, ctx)
1339
- return []
1340
-
1341
-
1342
- def _stream_listener_destinations(
1343
- ann: Node, src: bytes, ctx: _ParseCtx,
1344
- ) -> list[tuple[str, str, float, bool]]:
1345
- pairs, positional = _annotation_kv_nodes(ann, src)
1346
- for key in ("value", "name"):
1347
- if key in pairs:
1348
- return _string_value_atoms(pairs[key], src, ctx)
1349
- if positional is not None:
1350
- return _string_value_atoms(positional, src, ctx)
1351
- return []
1352
-
1353
-
1354
- def _collect_type_level_kafka_topics(enclosing_type_node: Node | None, src: bytes, ctx: _ParseCtx) -> list[str]:
1355
- if enclosing_type_node is None:
1356
- return []
1357
- mods = _find_modifiers_child(enclosing_type_node)
1358
- if mods is None:
1359
- return []
1360
- topics: list[str] = []
1361
- for child in mods.children:
1362
- if child.type not in ("marker_annotation", "annotation"):
1363
- continue
1364
- simple, _ = _annotation_name(child, src)
1365
- if simple != "KafkaListener":
1366
- continue
1367
- topics.extend(a[0] for a in _kafka_topics_from_ann_node(child, src, ctx) if a[3])
1368
- return topics
1369
-
1370
-
1371
- def _collect_type_level_rabbit_queues(enclosing_type_node: Node | None, src: bytes, ctx: _ParseCtx) -> list[str]:
1372
- if enclosing_type_node is None:
1373
- return []
1374
- mods = _find_modifiers_child(enclosing_type_node)
1375
- if mods is None:
1376
- return []
1377
- qs: list[str] = []
1378
- for child in mods.children:
1379
- if child.type not in ("marker_annotation", "annotation"):
1380
- continue
1381
- simple, _ = _annotation_name(child, src)
1382
- if simple != "RabbitListener":
1383
- continue
1384
- qs.extend(a[0] for a in _rabbit_queues_from_ann_node(child, src, ctx) if a[3])
1385
- return qs
1386
-
1387
-
1388
- def _enclosing_class_body_reactive(body: Node | None, src: bytes) -> bool:
1389
- if body is None:
1390
- return False
1391
- for ch in body.named_children:
1392
- if ch.type != "method_declaration":
1393
- continue
1394
- ret = ch.child_by_field_name("type")
1395
- if ret is not None and _strip_type_to_simple(ret, src) in ("Mono", "Flux"):
1396
- return True
1397
- formal = ch.child_by_field_name("parameters")
1398
- if formal is None:
1399
- continue
1400
- for p in formal.named_children:
1401
- if p.type not in ("formal_parameter", "spread_parameter"):
1402
- continue
1403
- tnode = p.child_by_field_name("type")
1404
- if tnode is not None and _strip_type_to_simple(tnode, src) in ("Mono", "Flux"):
1405
- return True
1406
- return False
1407
-
1408
-
1409
- def _http_framework_for_mapping(
1410
- *,
1411
- enclosing_body: Node | None,
1412
- src: bytes,
1413
- method_decl: MethodDecl,
1414
- type_ann_names: set[str],
1415
- ) -> str:
1416
- if _enclosing_class_body_reactive(enclosing_body, src):
1417
- return "webflux"
1418
- if method_decl.return_type in ("Mono", "Flux"):
1419
- return "webflux"
1420
- if any(p.type_name in ("Mono", "Flux") for p in method_decl.parameters):
1421
- return "webflux"
1422
- if "RestController" in type_ann_names and _enclosing_class_body_reactive(enclosing_body, src):
1423
- return "webflux"
1424
- return "spring_mvc"
1425
-
1426
-
1427
- def _parse_feign_client_literals(enclosing_type_node: Node | None, src: bytes, ctx: _ParseCtx) -> tuple[str, str, str]:
1428
- """Literal-only `name`, `url`, `path` from @FeignClient on the enclosing type."""
1429
- if enclosing_type_node is None:
1430
- return "", "", ""
1431
- mods = _find_modifiers_child(enclosing_type_node)
1432
- if mods is None:
1433
- return "", "", ""
1434
- for child in mods.children:
1435
- if child.type not in ("marker_annotation", "annotation"):
1436
- continue
1437
- simple, _ = _annotation_name(child, src)
1438
- if simple != "FeignClient":
1439
- continue
1440
- pairs, positional = _annotation_kv_nodes(child, src)
1441
- name_vals = _literal_strings_from_route_arg(pairs["name"], src, ctx) if "name" in pairs else []
1442
- url_vals = _literal_strings_from_route_arg(pairs["url"], src, ctx) if "url" in pairs else []
1443
- path_vals: list[str] = []
1444
- if "path" in pairs:
1445
- path_vals = _literal_strings_from_route_arg(pairs["path"], src, ctx)
1446
- elif "value" in pairs:
1447
- path_vals = _literal_strings_from_route_arg(pairs["value"], src, ctx)
1448
- elif positional is not None:
1449
- path_vals = _literal_strings_from_route_arg(positional, src, ctx)
1450
- name = name_vals[0] if name_vals else ""
1451
- url = url_vals[0] if url_vals else ""
1452
- base_path = path_vals[0] if path_vals else ""
1453
- return name, url, base_path
1454
- return "", "", ""
1455
-
1456
-
1457
- def _method_has_bean_annotation(method_node: Node, src: bytes) -> bool:
1458
- mods = _find_modifiers_child(method_node)
1459
- if mods is None:
1460
- return False
1461
- for child in mods.children:
1462
- if child.type not in ("marker_annotation", "annotation"):
1463
- continue
1464
- simple, _ = _annotation_name(child, src)
1465
- if simple == "Bean":
1466
- return True
1467
- return False
1468
-
1469
-
1470
- def _method_return_simple(method_node: Node, src: bytes) -> str:
1471
- ret = method_node.child_by_field_name("type")
1472
- return _strip_type_to_simple(ret, src) if ret is not None else ""
1473
-
1474
-
1475
- def _iter_method_annotation_nodes(method_node: Node, src: bytes) -> list[tuple[str, Node]]:
1476
- mods = _find_modifiers_child(method_node)
1477
- if mods is None:
1478
- return []
1479
- out: list[tuple[str, Node]] = []
1480
- for child in mods.children:
1481
- if child.type in ("marker_annotation", "annotation"):
1482
- simple, _ = _annotation_name(child, src)
1483
- out.append((simple, child))
1484
- return out
1485
-
1486
-
1487
- def _maybe_emit_brownfield_exclusivity_shadowing(
1488
- method_node: Node,
1489
- src: bytes,
1490
- *,
1491
- ctx: _ParseCtx,
1492
- method_fqn: str,
1493
- file_rel: str,
1494
- type_anns: list[AnnotationRef],
1495
- ) -> None:
1496
- """INFO when brownfield HTTP route/client co-exists with shadowable framework annotations."""
1497
- if not ctx.verbose:
1498
- return
1499
- method_anns = _iter_method_annotation_nodes(method_node, src)
1500
- has_bf_route = any(s in ("CodebaseHttpRoute", "CodebaseHttpRoutes") for s, _ in method_anns)
1501
- has_bf_client = any(s in ("CodebaseHttpClient", "CodebaseHttpClients") for s, _ in method_anns)
1502
- if not has_bf_route and not has_bf_client:
1503
- return
1504
- shadowed: list[str] = []
1505
- for s, _ in method_anns:
1506
- if s in _BROWNFIELD_SHADOWABLE_HTTP_FRAMEWORK_METHOD_ANNOTATIONS:
1507
- shadowed.append(s)
1508
- type_names = {a.name for a in type_anns}
1509
- if has_bf_client and "FeignClient" in type_names:
1510
- shadowed.append("FeignClient")
1511
- if not shadowed:
1512
- return
1513
- emit_brownfield_exclusivity_shadowing(
1514
- method_fqn=method_fqn,
1515
- file=file_rel,
1516
- shadowed_framework_annotations=sorted(frozenset(shadowed)),
1517
- )
1518
-
1519
-
1520
- def _parse_codebase_http_route_inner_annotation(
1521
- ann: Node,
1522
- src: bytes,
1523
- ctx: _ParseCtx,
1524
- *,
1525
- handler_fqn: str,
1526
- method_sig: str,
1527
- file_rel: str,
1528
- start_line: int,
1529
- end_line: int,
1530
- ) -> list[RouteDecl]:
1531
- """One `@CodebaseHttpRoute(...)` element → `RouteDecl`(s)."""
1532
- pairs, _ = _annotation_kv_nodes(ann, src)
1533
- path_node = pairs.get("path")
1534
- meth_arg = pairs.get("method")
1535
-
1536
- http_method = ""
1537
- if meth_arg is not None:
1538
- mv, mk = _annotation_value(meth_arg, src)
1539
- if mv is not None:
1540
- if mk == "enum":
1541
- http_method = str(mv).upper()
1542
- else:
1543
- http_method = str(mv).strip().upper()
1544
- emit_brownfield_method_string_literal(
1545
- method_fqn=handler_fqn,
1546
- file=file_rel,
1547
- reason="codebase_http_route_method_non_enum",
1548
- )
1549
-
1550
- path_atoms: list[tuple[str, str, float, bool]] = []
1551
- if path_node is not None:
1552
- path_atoms = _string_value_atoms(path_node, src, ctx)
1553
- if not path_atoms or not http_method:
1554
- return []
1555
-
1556
- out: list[RouteDecl] = []
1557
- for raw_path, _strat, conf, res in path_atoms:
1558
- out.append(
1559
- RouteDecl(
1560
- method_fqn=handler_fqn,
1561
- method_sig=method_sig,
1562
- kind="http_endpoint",
1563
- framework="spring_mvc",
1564
- http_method=http_method,
1565
- path=raw_path,
1566
- topic="",
1567
- broker="",
1568
- feign_name="",
1569
- feign_url="",
1570
- resolution_strategy="codebase_route",
1571
- confidence=conf,
1572
- resolved=res,
1573
- filename=file_rel,
1574
- start_line=start_line,
1575
- end_line=end_line,
1576
- route_source_layer="layer_c_source",
1577
- ),
1578
- )
1579
- return out
1580
-
1581
-
1582
- def _inner_annotation_nodes(container_ann: Node, src: bytes, target_simple: str) -> list[Node]:
1583
- """Collect nested ``@<target_simple>`` annotations anywhere under ``container_ann``.
1584
-
1585
- Shared by the four brownfield container walkers — ``CodebaseHttpRoute``,
1586
- ``CodebaseAsyncRoute``, ``CodebaseHttpClient``, ``CodebaseProducer`` — which
1587
- differ only by the target annotation simple name.
1588
- """
1589
- found: list[Node] = []
1590
-
1591
- def visit(n: Node) -> None:
1592
- if n.type == "annotation":
1593
- name_node = n.child_by_field_name("name")
1594
- n_simple = _txt(name_node, src).rsplit(".", 1)[-1] if name_node is not None else ""
1595
- if n_simple == target_simple:
1596
- found.append(n)
1597
- for c in n.children:
1598
- visit(c)
1599
-
1600
- visit(container_ann)
1601
- return found
1602
-
1603
-
1604
- def _parse_codebase_http_client_annotation(
1605
- ann: Node,
1606
- src: bytes,
1607
- ctx: _ParseCtx,
1608
- *,
1609
- method_fqn: str,
1610
- method_sig: str,
1611
- file_rel: str,
1612
- start_line: int,
1613
- end_line: int,
1614
- ) -> OutgoingCallDecl:
1615
- pairs, _ = _annotation_kv_nodes(ann, src)
1616
- client_kind = ""
1617
- if "clientKind" in pairs:
1618
- val, vkind = _annotation_value(pairs["clientKind"], src)
1619
- if val and vkind == "enum":
1620
- kind_val = str(val)
1621
- from java_codebase_rag.graph.java_ontology import VALID_CLIENT_KINDS # deferred: java_ontology imports ast_java
1622
- if kind_val in VALID_CLIENT_KINDS:
1623
- client_kind = kind_val
1624
- else:
1625
- print(
1626
- f"[lancedb-mcp] CodebaseHttpClient: invalid clientKind {kind_val!r} — ignored",
1627
- file=sys.stderr,
1628
- )
1629
- target_service = ""
1630
- if "targetService" in pairs:
1631
- atoms = _string_value_atoms(pairs["targetService"], src, ctx)
1632
- if atoms:
1633
- target_service = atoms[0][0]
1634
- path = ""
1635
- if "path" in pairs:
1636
- atoms = _string_value_atoms(pairs["path"], src, ctx)
1637
- if atoms:
1638
- path = _normalize_call_path(atoms[0][0]) if atoms[0][0] else ""
1639
- method_call = ""
1640
- if "method" in pairs:
1641
- mnode = pairs["method"]
1642
- mv, mk = _annotation_value(mnode, src)
1643
- if mv is not None and mk == "enum":
1644
- method_call = str(mv).upper()
1645
- elif mv is not None:
1646
- method_call = str(mv).strip().upper()
1647
- emit_brownfield_method_string_literal(
1648
- method_fqn=method_fqn,
1649
- file=file_rel,
1650
- reason="codebase_http_client_method_non_enum",
1651
- )
1652
- else:
1653
- atoms = _string_value_atoms(mnode, src, ctx)
1654
- if atoms:
1655
- method_call = atoms[0][0].upper()
1656
- emit_brownfield_method_string_literal(
1657
- method_fqn=method_fqn,
1658
- file=file_rel,
1659
- reason="codebase_http_client_method_non_enum",
1660
- )
1661
- return OutgoingCallDecl(
1662
- method_fqn=method_fqn,
1663
- method_sig=method_sig,
1664
- client_kind=client_kind,
1665
- channel="http",
1666
- feign_target_name=target_service,
1667
- feign_target_url="",
1668
- path_template_call=path,
1669
- method_call=method_call,
1670
- topic_call="",
1671
- broker_call="",
1672
- raw_uri=path,
1673
- raw_topic="",
1674
- resolution_strategy="codebase_client",
1675
- confidence_base=1.0,
1676
- resolved=True,
1677
- filename=file_rel,
1678
- start_line=start_line,
1679
- end_line=end_line,
1680
- )
1681
-
1682
-
1683
- def _parse_codebase_producer_annotation(
1684
- ann: Node,
1685
- src: bytes,
1686
- ctx: _ParseCtx,
1687
- *,
1688
- method_fqn: str,
1689
- method_sig: str,
1690
- file_rel: str,
1691
- start_line: int,
1692
- end_line: int,
1693
- ) -> OutgoingCallDecl:
1694
- pairs, _ = _annotation_kv_nodes(ann, src)
1695
- client_kind = "kafka_send"
1696
- kind_node = pairs.get("producerKind") or pairs.get("clientKind")
1697
- if kind_node is not None:
1698
- val, vkind = _annotation_value(kind_node, src)
1699
- if val and vkind == "enum":
1700
- kind_val = str(val)
1701
- from java_codebase_rag.graph.java_ontology import VALID_PRODUCER_KINDS # deferred: java_ontology imports ast_java
1702
- if kind_val in VALID_PRODUCER_KINDS:
1703
- client_kind = kind_val
1704
- else:
1705
- print(
1706
- f"[lancedb-mcp] CodebaseProducer: invalid producerKind {kind_val!r} — ignored",
1707
- file=sys.stderr,
1708
- )
1709
- topic = ""
1710
- if "topic" in pairs:
1711
- atoms = _string_value_atoms(pairs["topic"], src, ctx)
1712
- if atoms:
1713
- topic = atoms[0][0]
1714
- broker = ""
1715
- return OutgoingCallDecl(
1716
- method_fqn=method_fqn,
1717
- method_sig=method_sig,
1718
- client_kind=client_kind,
1719
- channel="async",
1720
- feign_target_name="",
1721
- feign_target_url="",
1722
- path_template_call="",
1723
- method_call="",
1724
- topic_call=topic,
1725
- broker_call=broker,
1726
- raw_uri="",
1727
- raw_topic=topic,
1728
- resolution_strategy="codebase_producer",
1729
- confidence_base=1.0,
1730
- resolved=True,
1731
- filename=file_rel,
1732
- start_line=start_line,
1733
- end_line=end_line,
1734
- )
1735
-
1736
-
1737
- def _field_types_for_type(type_node: Node | None, src: bytes) -> dict[str, str]:
1738
- out: dict[str, str] = {}
1739
- if type_node is None:
1740
- return out
1741
- body = type_node.child_by_field_name("body")
1742
- if body is None:
1743
- return out
1744
- for ch in body.named_children:
1745
- if ch.type != "field_declaration":
1746
- continue
1747
- tnode = ch.child_by_field_name("type")
1748
- if tnode is None:
1749
- continue
1750
- tname = _strip_type_to_simple(tnode, src)
1751
- for dc in ch.named_children:
1752
- if dc.type != "variable_declarator":
1753
- continue
1754
- nnode = dc.child_by_field_name("name")
1755
- if nnode is not None:
1756
- out[_txt(nnode, src)] = tname
1757
- return out
1758
-
1759
-
1760
- def _collect_plus_literals(node: Node, src: bytes) -> list[str]:
1761
- vals: list[str] = []
1762
- if node.type == "binary_expression":
1763
- left = node.child_by_field_name("left")
1764
- right = node.child_by_field_name("right")
1765
- op = node.child_by_field_name("operator")
1766
- op_txt = _txt(op, src) if op is not None else ""
1767
- if op_txt == "+" and left is not None and right is not None:
1768
- vals.extend(_collect_plus_literals(left, src))
1769
- vals.extend(_collect_plus_literals(right, src))
1770
- return vals
1771
- lit = _string_literal_value(node, src)
1772
- if lit is not None:
1773
- vals.append(lit)
1774
- return vals
1775
-
1776
-
1777
- def _normalize_call_path(raw_path: str) -> str:
1778
- p = (raw_path or "").strip()
1779
- if not p:
1780
- return ""
1781
- if not p.startswith("/"):
1782
- p = "/" + p
1783
- if len(p) > 1:
1784
- p = p.rstrip("/")
1785
- return p
1786
-
1787
-
1788
- def _outgoing_calls_from_codebase_http_client_producer_annotations(
1789
- method_node: Node,
1790
- src: bytes,
1791
- *,
1792
- method_fqn: str,
1793
- method_decl: MethodDecl,
1794
- file_rel: str,
1795
- ctx: _ParseCtx,
1796
- ) -> list[OutgoingCallDecl]:
1797
- """Brownfield @CodebaseHttpClient(s) / @CodebaseProducer(s) on the method itself.
1798
-
1799
- Must run even when the method has no body (interfaces, abstract methods).
1800
- """
1801
- out: list[OutgoingCallDecl] = []
1802
- for simple, ann in _iter_method_annotation_nodes(method_node, src):
1803
- if simple == "CodebaseHttpClient":
1804
- out.append(
1805
- _parse_codebase_http_client_annotation(
1806
- ann,
1807
- src,
1808
- ctx,
1809
- method_fqn=method_fqn,
1810
- method_sig=method_decl.signature,
1811
- file_rel=file_rel,
1812
- start_line=method_decl.start_line,
1813
- end_line=method_decl.end_line,
1814
- ),
1815
- )
1816
- elif simple == "CodebaseHttpClients":
1817
- for inner in _inner_annotation_nodes(ann, src, "CodebaseHttpClient"):
1818
- out.append(
1819
- _parse_codebase_http_client_annotation(
1820
- inner,
1821
- src,
1822
- ctx,
1823
- method_fqn=method_fqn,
1824
- method_sig=method_decl.signature,
1825
- file_rel=file_rel,
1826
- start_line=method_decl.start_line,
1827
- end_line=method_decl.end_line,
1828
- ),
1829
- )
1830
- elif simple == "CodebaseProducer":
1831
- out.append(
1832
- _parse_codebase_producer_annotation(
1833
- ann,
1834
- src,
1835
- ctx,
1836
- method_fqn=method_fqn,
1837
- method_sig=method_decl.signature,
1838
- file_rel=file_rel,
1839
- start_line=method_decl.start_line,
1840
- end_line=method_decl.end_line,
1841
- ),
1842
- )
1843
- elif simple == "CodebaseProducers":
1844
- for inner in _inner_annotation_nodes(ann, src, "CodebaseProducer"):
1845
- out.append(
1846
- _parse_codebase_producer_annotation(
1847
- inner,
1848
- src,
1849
- ctx,
1850
- method_fqn=method_fqn,
1851
- method_sig=method_decl.signature,
1852
- file_rel=file_rel,
1853
- start_line=method_decl.start_line,
1854
- end_line=method_decl.end_line,
1855
- ),
1856
- )
1857
- return out
1858
-
1859
-
1860
- def _collect_outgoing_calls(
1861
- method_node: Node,
1862
- type_node: Node | None,
1863
- src: bytes,
1864
- *,
1865
- ctx: _ParseCtx,
1866
- project_root: str,
1867
- method_decl: MethodDecl,
1868
- type_fqn: str,
1869
- file_rel: str,
1870
- ) -> list[OutgoingCallDecl]:
1871
- del project_root
1872
- from java_codebase_rag.graph.java_ontology import ( # deferred: java_ontology imports ast_java
1873
- CLIENT_KIND_FEIGN_METHOD,
1874
- CLIENT_KIND_REST_TEMPLATE,
1875
- CLIENT_KIND_WEB_CLIENT,
1876
- )
1877
- out: list[OutgoingCallDecl] = []
1878
- method_fqn = f"{type_fqn}#{method_decl.signature}"
1879
- type_mods = _find_modifiers_child(type_node) if type_node is not None else None
1880
- type_ann_names: set[str] = set()
1881
- feign_target_name = ""
1882
- feign_target_url = ""
1883
- feign_base_path = ""
1884
- if type_mods is not None:
1885
- for child in type_mods.children:
1886
- if child.type not in ("marker_annotation", "annotation"):
1887
- continue
1888
- simple, _ = _annotation_name(child, src)
1889
- type_ann_names.add(simple)
1890
- feign_target_name, feign_target_url, feign_base_path = _parse_feign_client_literals(type_node, src, ctx)
1891
- if type_node is not None and type_node.type == "interface_declaration" and "FeignClient" in type_ann_names:
1892
- method_call = ""
1893
- path_template = ""
1894
- for simple, ann_node in _iter_method_annotation_nodes(method_node, src):
1895
- if simple not in _ROUTE_HTTP_MAPPING_NAMES:
1896
- continue
1897
- path_atoms, methods = _paths_and_methods_from_mapping_ann(ann_node, src, simple, ctx)
1898
- if methods:
1899
- method_call = methods[0]
1900
- if path_atoms:
1901
- composed = _compose_http_paths(feign_base_path, [path_atoms[0][0]])[0]
1902
- path_template = _normalize_call_path(composed)
1903
- break
1904
- out.append(
1905
- OutgoingCallDecl(
1906
- method_fqn=method_fqn,
1907
- method_sig=method_decl.signature,
1908
- client_kind=CLIENT_KIND_FEIGN_METHOD,
1909
- channel="http",
1910
- feign_target_name=feign_target_name,
1911
- feign_target_url=feign_target_url,
1912
- path_template_call=path_template,
1913
- method_call=method_call,
1914
- topic_call="",
1915
- broker_call="",
1916
- raw_uri=path_template,
1917
- raw_topic="",
1918
- resolution_strategy="feign_method",
1919
- confidence_base=1.0,
1920
- resolved=True,
1921
- filename=file_rel,
1922
- start_line=method_decl.start_line,
1923
- end_line=method_decl.end_line,
1924
- )
1925
- )
1926
-
1927
- ann_out = _outgoing_calls_from_codebase_http_client_producer_annotations(
1928
- method_node,
1929
- src,
1930
- method_fqn=method_fqn,
1931
- method_decl=method_decl,
1932
- file_rel=file_rel,
1933
- ctx=ctx,
1934
- )
1935
- body = method_node.child_by_field_name("body")
1936
- if body is None:
1937
- out.extend(ann_out)
1938
- return out
1939
- receiver_types: dict[str, str] = {}
1940
- receiver_types.update(_field_types_for_type(type_node, src))
1941
- for p in method_decl.parameters:
1942
- receiver_types[p.name] = p.type_name
1943
- for n, t in method_decl.local_vars:
1944
- receiver_types[n] = t
1945
-
1946
- rest_methods = {
1947
- "getForObject": "GET",
1948
- "getForEntity": "GET",
1949
- "postForEntity": "POST",
1950
- "postForObject": "POST",
1951
- "put": "PUT",
1952
- "delete": "DELETE",
1953
- }
1954
- web_methods = {"get", "post", "put", "delete", "patch"}
1955
-
1956
- def _receiver_type(obj: Node | None) -> str:
1957
- if obj is None:
1958
- return ""
1959
- if obj.type == "identifier":
1960
- return receiver_types.get(_txt(obj, src), "")
1961
- return ""
1962
-
1963
- def visit(n: Node) -> None:
1964
- if n.type == "method_invocation":
1965
- obj = n.child_by_field_name("object")
1966
- name_node = n.child_by_field_name("name")
1967
- args = n.child_by_field_name("arguments")
1968
- mname = _txt(name_node, src) if name_node is not None else ""
1969
- recv_type = _receiver_type(obj)
1970
- recv_txt = _txt(obj, src) if obj is not None else ""
1971
- arg_nodes = args.named_children if args is not None else []
1972
- if recv_type == "RestTemplate" and mname in (set(rest_methods) | {"exchange"}) and arg_nodes:
1973
- first = arg_nodes[0]
1974
- atoms = _string_value_atoms(first, src, ctx)
1975
- method_call = rest_methods.get(mname, "")
1976
- if mname == "exchange" and len(arg_nodes) > 1:
1977
- raw = _txt(arg_nodes[1], src).strip()
1978
- if raw.startswith("HttpMethod."):
1979
- method_call = raw.rsplit(".", 1)[-1].upper()
1980
- path_template = ""
1981
- strategy = "rest_template"
1982
- conf = 0.3
1983
- resolved = False
1984
- raw_uri = _txt(first, src)
1985
- force_unresolved = first.type in ("method_invocation", "lambda_expression", "ternary_expression")
1986
- if atoms:
1987
- val, strat, base_conf, is_resolved = atoms[0]
1988
- path_template = _normalize_call_path(val) if val.startswith("/") else ""
1989
- strategy = "rest_template"
1990
- conf = base_conf
1991
- resolved = is_resolved
1992
- if force_unresolved:
1993
- path_template = ""
1994
- conf = 0.3
1995
- resolved = False
1996
- if first.type == "binary_expression":
1997
- lits = [s for s in _collect_plus_literals(first, src) if s.startswith("/")]
1998
- if lits:
1999
- path_template = _normalize_call_path(lits[-1])
2000
- conf = 0.7
2001
- strategy = "rest_template"
2002
- resolved = False
2003
- out.append(
2004
- OutgoingCallDecl(
2005
- method_fqn=method_fqn,
2006
- method_sig=method_decl.signature,
2007
- client_kind=CLIENT_KIND_REST_TEMPLATE,
2008
- channel="http",
2009
- feign_target_name="",
2010
- feign_target_url="",
2011
- path_template_call=path_template,
2012
- method_call=method_call,
2013
- topic_call="",
2014
- broker_call="",
2015
- raw_uri=raw_uri,
2016
- raw_topic="",
2017
- resolution_strategy=strategy,
2018
- confidence_base=conf,
2019
- resolved=resolved and bool(path_template),
2020
- filename=file_rel,
2021
- start_line=n.start_point[0] + 1,
2022
- end_line=n.end_point[0] + 1,
2023
- )
2024
- )
2025
- elif recv_type == "KafkaTemplate" and mname == "send" and arg_nodes:
2026
- first = arg_nodes[0]
2027
- atoms = _string_value_atoms(first, src, ctx)
2028
- topic = ""
2029
- conf = 0.3
2030
- resolved = False
2031
- if atoms:
2032
- topic, _s, conf, resolved = atoms[0]
2033
- out.append(
2034
- OutgoingCallDecl(
2035
- method_fqn=method_fqn,
2036
- method_sig=method_decl.signature,
2037
- client_kind="kafka_send",
2038
- channel="async",
2039
- feign_target_name="",
2040
- feign_target_url="",
2041
- path_template_call="",
2042
- method_call="",
2043
- topic_call=topic,
2044
- broker_call="",
2045
- raw_uri="",
2046
- raw_topic=_txt(first, src),
2047
- resolution_strategy="kafka_template",
2048
- confidence_base=conf,
2049
- resolved=resolved,
2050
- filename=file_rel,
2051
- start_line=n.start_point[0] + 1,
2052
- end_line=n.end_point[0] + 1,
2053
- )
2054
- )
2055
- elif recv_type == "WebClient" and mname in web_methods:
2056
- out.append(
2057
- OutgoingCallDecl(
2058
- method_fqn=method_fqn,
2059
- method_sig=method_decl.signature,
2060
- client_kind=CLIENT_KIND_WEB_CLIENT,
2061
- channel="http",
2062
- feign_target_name="",
2063
- feign_target_url="",
2064
- path_template_call="",
2065
- method_call=mname.upper(),
2066
- topic_call="",
2067
- broker_call="",
2068
- raw_uri=recv_txt,
2069
- raw_topic="",
2070
- resolution_strategy="unresolved",
2071
- confidence_base=0.3,
2072
- resolved=False,
2073
- filename=file_rel,
2074
- start_line=n.start_point[0] + 1,
2075
- end_line=n.end_point[0] + 1,
2076
- )
2077
- )
2078
- elif recv_type == "StreamBridge" and mname == "send":
2079
- out.append(
2080
- OutgoingCallDecl(
2081
- method_fqn=method_fqn,
2082
- method_sig=method_decl.signature,
2083
- client_kind="stream_bridge_send",
2084
- channel="async",
2085
- feign_target_name="",
2086
- feign_target_url="",
2087
- path_template_call="",
2088
- method_call="",
2089
- topic_call="",
2090
- broker_call="",
2091
- raw_uri="",
2092
- raw_topic=_txt(n, src),
2093
- resolution_strategy="unresolved",
2094
- confidence_base=0.3,
2095
- resolved=False,
2096
- filename=file_rel,
2097
- start_line=n.start_point[0] + 1,
2098
- end_line=n.end_point[0] + 1,
2099
- )
2100
- )
2101
- for c in n.children:
2102
- visit(c)
2103
-
2104
- visit(body)
2105
- out.extend(ann_out)
2106
- return out
2107
-
2108
-
2109
- def _collect_routes(
2110
- method_node: Node,
2111
- enclosing_type_node: Node | None,
2112
- src: bytes,
2113
- *,
2114
- type_fqn: str,
2115
- type_kind: str,
2116
- type_anns: list[AnnotationRef],
2117
- method_decl: MethodDecl,
2118
- signature: str,
2119
- file_rel: str,
2120
- ctx: _ParseCtx,
2121
- ) -> list[RouteDecl]:
2122
- """Extract RouteDecl literals from Spring mapping / messaging annotations.
2123
-
2124
- WebFlux vs Spring MVC: same annotations; framework is ``webflux`` when the
2125
- enclosing type exposes reactive signatures (Mono/Flux) — otherwise
2126
- ``spring_mvc`` (PR-A1 plan).
2127
- """
2128
- routes: list[RouteDecl] = []
2129
- handler_fqn = f"{type_fqn}#{signature}"
2130
- type_ann_names = {a.name for a in type_anns}
2131
- enclosing_body = enclosing_type_node.child_by_field_name("body") if enclosing_type_node else None
2132
-
2133
- ann_nodes = _iter_method_annotation_nodes(method_node, src)
2134
-
2135
- # --- Spring Cloud Stream-style @Bean handler ---
2136
- if _method_has_bean_annotation(method_node, src):
2137
- ret_simple = _method_return_simple(method_node, src)
2138
- if ret_simple in ("Function", "Consumer", "Supplier"):
2139
- routes.append(
2140
- RouteDecl(
2141
- method_fqn=handler_fqn,
2142
- method_sig=signature,
2143
- kind="stream_binding",
2144
- framework="stream",
2145
- http_method="",
2146
- path="",
2147
- topic="",
2148
- broker="",
2149
- feign_name="",
2150
- feign_url="",
2151
- resolution_strategy="annotation",
2152
- confidence=1.0,
2153
- resolved=True,
2154
- filename=file_rel,
2155
- start_line=method_decl.start_line,
2156
- end_line=method_decl.end_line,
2157
- )
2158
- )
2159
-
2160
- class_kafka_topics = _collect_type_level_kafka_topics(enclosing_type_node, src, ctx)
2161
- class_rabbit_queues = _collect_type_level_rabbit_queues(enclosing_type_node, src, ctx)
2162
-
2163
- # --- Messaging annotations on method ---
2164
- for simple, node in ann_nodes:
2165
- if simple == "KafkaListener":
2166
- topic_atoms = _kafka_topics_from_ann_node(node, src, ctx)
2167
- if not topic_atoms and class_kafka_topics:
2168
- topic_atoms = [(t, "annotation", 1.0, True) for t in class_kafka_topics]
2169
- for tp, strat, conf, res in topic_atoms:
2170
- routes.append(
2171
- RouteDecl(
2172
- method_fqn=handler_fqn,
2173
- method_sig=signature,
2174
- kind="kafka_topic",
2175
- framework="kafka",
2176
- http_method="",
2177
- path="",
2178
- topic=tp,
2179
- broker="",
2180
- feign_name="",
2181
- feign_url="",
2182
- resolution_strategy=strat,
2183
- confidence=conf,
2184
- resolved=res,
2185
- filename=file_rel,
2186
- start_line=method_decl.start_line,
2187
- end_line=method_decl.end_line,
2188
- )
2189
- )
2190
- elif simple == "RabbitListener":
2191
- queue_atoms = _rabbit_queues_from_ann_node(node, src, ctx)
2192
- if not queue_atoms and class_rabbit_queues:
2193
- queue_atoms = [(q, "annotation", 1.0, True) for q in class_rabbit_queues]
2194
- for q, strat, conf, res in queue_atoms:
2195
- routes.append(
2196
- RouteDecl(
2197
- method_fqn=handler_fqn,
2198
- method_sig=signature,
2199
- kind="rabbit_queue",
2200
- framework="rabbitmq",
2201
- http_method="",
2202
- path="",
2203
- topic=q,
2204
- broker="",
2205
- feign_name="",
2206
- feign_url="",
2207
- resolution_strategy=strat,
2208
- confidence=conf,
2209
- resolved=res,
2210
- filename=file_rel,
2211
- start_line=method_decl.start_line,
2212
- end_line=method_decl.end_line,
2213
- )
2214
- )
2215
- elif simple == "JmsListener":
2216
- for dest, strat, conf, res in _jms_destination_from_ann_node(node, src, ctx):
2217
- routes.append(
2218
- RouteDecl(
2219
- method_fqn=handler_fqn,
2220
- method_sig=signature,
2221
- kind="jms_destination",
2222
- framework="jms",
2223
- http_method="",
2224
- path="",
2225
- topic=dest,
2226
- broker="",
2227
- feign_name="",
2228
- feign_url="",
2229
- resolution_strategy=strat,
2230
- confidence=conf,
2231
- resolved=res,
2232
- filename=file_rel,
2233
- start_line=method_decl.start_line,
2234
- end_line=method_decl.end_line,
2235
- )
2236
- )
2237
- elif simple == "StreamListener":
2238
- for dest, strat, conf, res in _stream_listener_destinations(node, src, ctx):
2239
- routes.append(
2240
- RouteDecl(
2241
- method_fqn=handler_fqn,
2242
- method_sig=signature,
2243
- kind="stream_binding",
2244
- framework="stream",
2245
- http_method="",
2246
- path="",
2247
- topic=dest,
2248
- broker="",
2249
- feign_name="",
2250
- feign_url="",
2251
- resolution_strategy=strat,
2252
- confidence=conf,
2253
- resolved=res,
2254
- filename=file_rel,
2255
- start_line=method_decl.start_line,
2256
- end_line=method_decl.end_line,
2257
- )
2258
- )
2259
-
2260
- # --- HTTP mappings ---
2261
- feign_iface = type_kind == "interface" and _type_has_feign_client(type_anns)
2262
- http_base = _type_level_request_mapping_base(enclosing_type_node, src, ctx)
2263
- feign_name, feign_url, feign_base_path = _parse_feign_client_literals(enclosing_type_node, src, ctx)
2264
-
2265
- for simple, node in ann_nodes:
2266
- if simple not in _ROUTE_HTTP_MAPPING_NAMES:
2267
- continue
2268
- path_atoms, methods = _paths_and_methods_from_mapping_ann(node, src, simple, ctx)
2269
- if not path_atoms:
2270
- continue
2271
- class_path_prefix = http_base if not feign_iface else feign_base_path
2272
- if feign_iface:
2273
- continue
2274
- fw = _http_framework_for_mapping(
2275
- enclosing_body=enclosing_body,
2276
- src=src,
2277
- method_decl=method_decl,
2278
- type_ann_names=type_ann_names,
2279
- )
2280
- kind = "http_endpoint"
2281
- for raw_path, m_strat, m_conf, m_res in path_atoms:
2282
- full_path, f_strat, f_conf, f_res = _merge_http_route_with_class_base(
2283
- class_path_prefix, raw_path, m_strat, m_conf, m_res,
2284
- )
2285
- for hm in methods:
2286
- routes.append(
2287
- RouteDecl(
2288
- method_fqn=handler_fqn,
2289
- method_sig=signature,
2290
- kind=kind,
2291
- framework=fw,
2292
- http_method=hm,
2293
- path=full_path,
2294
- topic="",
2295
- broker="",
2296
- feign_name=feign_name if feign_iface else "",
2297
- feign_url=feign_url if feign_iface else "",
2298
- resolution_strategy=f_strat,
2299
- confidence=f_conf,
2300
- resolved=f_res,
2301
- filename=file_rel,
2302
- start_line=method_decl.start_line,
2303
- end_line=method_decl.end_line,
2304
- )
2305
- )
2306
-
2307
- # --- @CodebaseHttpRoute / @CodebaseHttpRoutes + @CodebaseAsyncRoute(s) ---
2308
- for simple, node in ann_nodes:
2309
- if simple == "CodebaseHttpRoute":
2310
- routes.extend(
2311
- _parse_codebase_http_route_inner_annotation(
2312
- node,
2313
- src,
2314
- ctx,
2315
- handler_fqn=handler_fqn,
2316
- method_sig=signature,
2317
- file_rel=file_rel,
2318
- start_line=method_decl.start_line,
2319
- end_line=method_decl.end_line,
2320
- ),
2321
- )
2322
- elif simple == "CodebaseHttpRoutes":
2323
- for inner in _inner_annotation_nodes(node, src, "CodebaseHttpRoute"):
2324
- routes.extend(
2325
- _parse_codebase_http_route_inner_annotation(
2326
- inner,
2327
- src,
2328
- ctx,
2329
- handler_fqn=handler_fqn,
2330
- method_sig=signature,
2331
- file_rel=file_rel,
2332
- start_line=method_decl.start_line,
2333
- end_line=method_decl.end_line,
2334
- ),
2335
- )
2336
- elif simple in ("CodebaseAsyncRoute", "CodebaseAsyncRoutes"):
2337
- nodes = [node]
2338
- if simple == "CodebaseAsyncRoutes":
2339
- nodes = list(_inner_annotation_nodes(node, src, "CodebaseAsyncRoute"))
2340
- for ann in nodes:
2341
- pairs, _ = _annotation_kv_nodes(ann, src)
2342
- topic_node = pairs.get("topic")
2343
- if topic_node is None:
2344
- continue
2345
- topic_atoms = _string_value_atoms(topic_node, src, ctx)
2346
- for topic, strat, conf, res in topic_atoms:
2347
- routes.append(
2348
- RouteDecl(
2349
- method_fqn=handler_fqn,
2350
- method_sig=signature,
2351
- kind="kafka_topic",
2352
- framework="kafka",
2353
- http_method="",
2354
- path="",
2355
- topic=topic,
2356
- broker="",
2357
- feign_name="",
2358
- feign_url="",
2359
- resolution_strategy=strat,
2360
- confidence=conf,
2361
- resolved=res,
2362
- filename=file_rel,
2363
- start_line=method_decl.start_line,
2364
- end_line=method_decl.end_line,
2365
- route_source_layer="layer_c_source",
2366
- )
2367
- )
2368
-
2369
- return routes
2370
-
2371
-
2372
- def _type_has_feign_client(type_anns: list[AnnotationRef]) -> bool:
2373
- return any(a.name == "FeignClient" for a in type_anns)
2374
-
2375
-
2376
- def _parse_params(formal: Node | None, src: bytes) -> list[ParamDecl]:
2377
- if formal is None:
2378
- return []
2379
- params: list[ParamDecl] = []
2380
- for ch in formal.named_children:
2381
- if ch.type not in ("formal_parameter", "spread_parameter"):
2382
- continue
2383
- _, p_anns = _collect_annotations_and_modifiers(ch, src)
2384
- type_node = ch.child_by_field_name("type")
2385
- name_node = ch.child_by_field_name("name")
2386
- if type_node is None or name_node is None:
2387
- continue
2388
- params.append(
2389
- ParamDecl(
2390
- name=_txt(name_node, src),
2391
- type_name=_strip_type_to_simple(type_node, src),
2392
- type_raw=_txt(type_node, src),
2393
- annotations=p_anns,
2394
- )
2395
- )
2396
- return params
2397
-
2398
-
2399
- def _parse_method(
2400
- node: Node,
2401
- src: bytes,
2402
- *,
2403
- is_constructor: bool,
2404
- type_fqn: str,
2405
- package: str,
2406
- kind_by_simple: dict[str, str],
2407
- ctx: _ParseCtx,
2408
- enclosing_type_node: Node | None,
2409
- type_kind: str,
2410
- type_anns: list[AnnotationRef],
2411
- file_rel: str,
2412
- ) -> tuple[MethodDecl, list[TypeDecl]]:
2413
- mods, anns = _collect_annotations_and_modifiers(node, src)
2414
- name_node = node.child_by_field_name("name")
2415
- name = _txt(name_node, src) if name_node is not None else ("<init>" if is_constructor else "<method>")
2416
- ret_node = node.child_by_field_name("type")
2417
- if is_constructor:
2418
- return_type = ""
2419
- else:
2420
- return_type = _strip_type_to_simple(ret_node, src) if ret_node is not None else ""
2421
- params = _parse_params(node.child_by_field_name("parameters"), src)
2422
- sig_params = ",".join(p.type_name for p in params)
2423
- signature = f"{name}({sig_params})"
2424
- m = MethodDecl(
2425
- name=name,
2426
- return_type=return_type,
2427
- is_constructor=is_constructor,
2428
- parameters=params,
2429
- modifiers=mods,
2430
- annotations=anns,
2431
- signature=signature,
2432
- start_byte=node.start_byte,
2433
- end_byte=node.end_byte,
2434
- start_line=node.start_point[0] + 1,
2435
- end_line=node.end_point[0] + 1,
2436
- )
2437
- caller_fqn = f"{type_fqn}#{signature}"
2438
- anon_nested: list[TypeDecl] = []
2439
- body = node.child_by_field_name("body")
2440
- if body is not None:
2441
- m.local_vars = _collect_local_vars(body, src)
2442
- sites = _collect_call_sites(body, src, caller_fqn=caller_fqn, in_lambda=False)
2443
- if is_constructor:
2444
- had_explicit = any(
2445
- s.callee_simple == "<init>" and s.receiver_expr in ("this", "super") for s in sites
2446
- )
2447
- if not had_explicit:
2448
- sites.append(
2449
- CallSite(
2450
- caller_fqn=caller_fqn,
2451
- receiver_expr="super",
2452
- callee_simple="<init>",
2453
- arg_count=0,
2454
- is_static_call=False,
2455
- is_constructor=True,
2456
- in_lambda=False,
2457
- line=m.start_line,
2458
- byte=m.start_byte,
2459
- )
2460
- )
2461
- m.call_sites = sites
2462
- anon_nested = _extract_anonymous_types_in_subtree(
2463
- body, src, package=package, host_type_fqn=type_fqn, kind_by_simple=kind_by_simple,
2464
- file_rel=file_rel,
2465
- ctx=ctx,
2466
- )
2467
- if not is_constructor:
2468
- m.routes = _collect_routes(
2469
- node,
2470
- enclosing_type_node,
2471
- src,
2472
- type_fqn=type_fqn,
2473
- type_kind=type_kind,
2474
- type_anns=type_anns,
2475
- method_decl=m,
2476
- signature=signature,
2477
- file_rel=file_rel,
2478
- ctx=ctx,
2479
- )
2480
- m.outgoing_calls = _collect_outgoing_calls(
2481
- node,
2482
- enclosing_type_node,
2483
- src,
2484
- ctx=ctx,
2485
- project_root="",
2486
- method_decl=m,
2487
- type_fqn=type_fqn,
2488
- file_rel=file_rel,
2489
- )
2490
- _maybe_emit_brownfield_exclusivity_shadowing(
2491
- node,
2492
- src,
2493
- ctx=ctx,
2494
- method_fqn=caller_fqn,
2495
- file_rel=file_rel,
2496
- type_anns=type_anns,
2497
- )
2498
- return m, anon_nested
2499
-
2500
-
2501
- def _parse_field(node: Node, src: bytes) -> list[FieldDecl]:
2502
- """A single `field_declaration` may declare multiple variables."""
2503
- mods, anns = _collect_annotations_and_modifiers(node, src)
2504
- type_node = node.child_by_field_name("type")
2505
- if type_node is None:
2506
- return []
2507
- type_simple = _strip_type_to_simple(type_node, src)
2508
- type_raw = _txt(type_node, src)
2509
- out: list[FieldDecl] = []
2510
- for ch in node.named_children:
2511
- if ch.type != "variable_declarator":
2512
- continue
2513
- name_node = ch.child_by_field_name("name")
2514
- if name_node is None:
2515
- continue
2516
- out.append(
2517
- FieldDecl(
2518
- name=_txt(name_node, src),
2519
- type_name=type_simple,
2520
- type_raw=type_raw,
2521
- modifiers=list(mods),
2522
- annotations=list(anns),
2523
- start_byte=node.start_byte,
2524
- end_byte=node.end_byte,
2525
- start_line=node.start_point[0] + 1,
2526
- end_line=node.end_point[0] + 1,
2527
- )
2528
- )
2529
- return out
2530
-
2531
-
2532
- def _parse_type(
2533
- node: Node,
2534
- src: bytes,
2535
- *,
2536
- package: str,
2537
- outer_fqn: str | None,
2538
- kind_by_simple: dict[str, str],
2539
- file_rel: str,
2540
- ctx: _ParseCtx,
2541
- ) -> TypeDecl:
2542
- kind = _TYPE_KINDS[node.type]
2543
- name_node = node.child_by_field_name("name")
2544
- name = _txt(name_node, src) if name_node is not None else "<anon>"
2545
- if outer_fqn:
2546
- fqn = f"{outer_fqn}.{name}"
2547
- elif package:
2548
- fqn = f"{package}.{name}"
2549
- else:
2550
- fqn = name
2551
-
2552
- mods, anns = _collect_annotations_and_modifiers(node, src)
2553
-
2554
- extends = _extends_of(node, src)
2555
- implements = _implements_of(node, src)
2556
-
2557
- body = node.child_by_field_name("body")
2558
- if body is None:
2559
- td = TypeDecl(
2560
- name=name,
2561
- kind=kind,
2562
- fqn=fqn,
2563
- modifiers=mods,
2564
- annotations=anns,
2565
- extends=extends,
2566
- implements=implements,
2567
- fields=[],
2568
- methods=[],
2569
- nested=[],
2570
- start_byte=node.start_byte,
2571
- end_byte=node.end_byte,
2572
- start_line=node.start_point[0] + 1,
2573
- end_line=node.end_point[0] + 1,
2574
- outer_fqn=outer_fqn,
2575
- )
2576
- td.capabilities = infer_capabilities_for_type(td)
2577
- return td
2578
- return _parse_type_body_into_decl(
2579
- body,
2580
- src,
2581
- package=package,
2582
- fqn=fqn,
2583
- kind=kind,
2584
- extends=extends,
2585
- implements=implements,
2586
- modifiers=mods,
2587
- annotations=anns,
2588
- kind_by_simple=kind_by_simple,
2589
- start_byte=node.start_byte,
2590
- end_byte=node.end_byte,
2591
- start_line=node.start_point[0] + 1,
2592
- end_line=node.end_point[0] + 1,
2593
- outer_fqn=outer_fqn,
2594
- enclosing_type_node=node,
2595
- file_rel=file_rel,
2596
- ctx=ctx,
2597
- )
2598
-
2599
-
2600
- def _flatten(types: list[TypeDecl]) -> list[TypeDecl]:
2601
- out: list[TypeDecl] = []
2602
- stack = list(types)
2603
- while stack:
2604
- t = stack.pop()
2605
- out.append(t)
2606
- stack.extend(t.nested)
2607
- return out
2608
-
2609
-
2610
- # ---------- public API ----------
2611
-
2612
-
2613
- def parse_java(source: bytes | str, *, filename: str = "", verbose: bool = False) -> JavaFileAst:
2614
- """Parse a Java file into a JavaFileAst. Never raises on invalid source."""
2615
- if isinstance(source, str):
2616
- src = source.encode("utf-8", errors="replace")
2617
- else:
2618
- src = source
2619
-
2620
- ctx = _ParseCtx(verbose=verbose)
2621
- empty = JavaFileAst(
2622
- package="",
2623
- imports=[],
2624
- wildcard_imports=[],
2625
- explicit_imports={},
2626
- top_level_types=[],
2627
- all_types=[],
2628
- parse_error=False,
2629
- source_bytes=len(src),
2630
- file_imports=FileImports(),
2631
- routes_skipped_unresolved=0,
2632
- )
2633
-
2634
- if not src:
2635
- return empty
2636
-
2637
- try:
2638
- tree = _parser().parse(src)
2639
- except Exception:
2640
- empty.parse_error = True
2641
- return empty
2642
-
2643
- root = tree.root_node
2644
- package = ""
2645
- imports: list[str] = []
2646
- wildcard_imports: list[str] = []
2647
- explicit_imports: dict[str, str] = {}
2648
- static_methods: dict[str, str] = {}
2649
- static_wildcards: list[str] = []
2650
- top_types: list[TypeDecl] = []
2651
-
2652
- for child in root.named_children:
2653
- t = child.type
2654
- if t == "package_declaration":
2655
- for c in child.named_children:
2656
- if c.type in ("scoped_identifier", "identifier"):
2657
- package = _txt(c, src)
2658
- break
2659
- elif t == "import_declaration":
2660
- is_static = _import_declaration_is_static(child, src)
2661
- has_wild = any(c.type == "asterisk" for c in child.children)
2662
- ident_node = None
2663
- for c in child.named_children:
2664
- if c.type in ("scoped_identifier", "identifier"):
2665
- ident_node = c
2666
- break
2667
- if ident_node is None:
2668
- continue
2669
- ident = _txt(ident_node, src)
2670
- if is_static:
2671
- if has_wild:
2672
- static_wildcards.append(ident)
2673
- imports.append(f"import static {ident}.*")
2674
- else:
2675
- simple = ident.rsplit(".", 1)[-1]
2676
- static_methods[simple] = ident
2677
- imports.append(f"import static {ident}")
2678
- continue
2679
- if has_wild:
2680
- wildcard_imports.append(ident)
2681
- imports.append(f"{ident}.*")
2682
- else:
2683
- imports.append(ident)
2684
- simple = ident.rsplit(".", 1)[-1]
2685
- explicit_imports[simple] = ident
2686
-
2687
- file_imports = FileImports(
2688
- explicit=explicit_imports,
2689
- static_methods=static_methods,
2690
- static_wildcards=static_wildcards,
2691
- )
2692
- kind_by_simple = _pre_scan_declared_type_kinds(root, src)
2693
- file_rel = filename
2694
- for child in root.named_children:
2695
- if child.type in _TYPE_KINDS:
2696
- top_types.append(
2697
- _parse_type(
2698
- child, src,
2699
- package=package, outer_fqn=None, kind_by_simple=kind_by_simple,
2700
- file_rel=file_rel,
2701
- ctx=ctx,
2702
- ),
2703
- )
2704
-
2705
- all_types = _flatten(top_types)
2706
- return JavaFileAst(
2707
- package=package,
2708
- imports=imports,
2709
- wildcard_imports=wildcard_imports,
2710
- explicit_imports=explicit_imports,
2711
- top_level_types=top_types,
2712
- all_types=all_types,
2713
- parse_error=root.has_error,
2714
- source_bytes=len(src),
2715
- file_imports=file_imports,
2716
- routes_skipped_unresolved=ctx.routes_skipped_unresolved,
2717
- )
2718
-
2719
-
2720
- def infer_role(annotation_names: Iterable[str]) -> str:
2721
- """Map a set of simple annotation names to a single role. First hit wins."""
2722
- for ann in annotation_names:
2723
- role = ROLE_ANNOTATIONS.get(ann)
2724
- if role:
2725
- return role
2726
- return "OTHER"
2727
-
2728
-
2729
- def _type_injects_messaging(type_decl: "TypeDecl") -> bool:
2730
- """True when the type injects a messaging template via field or constructor."""
2731
- for fld in type_decl.fields:
2732
- if fld.type_name in _INJECTED_TYPES_TO_CAPABILITY:
2733
- return True
2734
- for method in type_decl.methods:
2735
- if method.is_constructor:
2736
- for p in method.parameters:
2737
- if p.type_name in _INJECTED_TYPES_TO_CAPABILITY:
2738
- return True
2739
- return False
2740
-
2741
-
2742
- def infer_role_for_type(type_decl: "TypeDecl") -> str:
2743
- """Role inference that also detects DTO-like passive data carriers.
2744
-
2745
- Applied only when annotation-based inference yields OTHER, so an
2746
- explicitly-stereotyped class (e.g. @Service FooRequest) keeps its role.
2747
- A type is considered DTO when *any* of the following hold:
2748
-
2749
- * kind is `record` (Java records are value carriers by definition);
2750
- * a Lombok value/getter/setter annotation is present (`@Data`, etc.);
2751
- * the simple name ends with a known DTO suffix (`Dto`, `Request`, ...).
2752
-
2753
- Used to down-rank DTOs in behavioural search; schema-focused queries can
2754
- still fetch them via explicit `role=DTO` or by turning the weight off.
2755
- """
2756
- ann_names = [a.name for a in type_decl.annotations]
2757
- base = infer_role(ann_names)
2758
- if base != "OTHER":
2759
- return base
2760
-
2761
- if type_decl.kind == "record":
2762
- return "DTO"
2763
-
2764
- ann_set = set(ann_names)
2765
- if ann_set & _DTO_LOMBOK_ANNOTATIONS:
2766
- return "DTO"
2767
-
2768
- name = type_decl.name or ""
2769
- for suffix in _DTO_NAME_SUFFIXES:
2770
- if name.endswith(suffix) and name != suffix:
2771
- return "DTO"
2772
-
2773
- # Types injecting messaging templates are outbound callers (CLIENT role),
2774
- # symmetric with CONTROLLER covering both HTTP and messaging inbound.
2775
- if _type_injects_messaging(type_decl):
2776
- return "CLIENT"
2777
-
2778
- return "OTHER"
2779
-
2780
-
2781
- def infer_capabilities_for_type(type_decl: "TypeDecl") -> list[str]:
2782
- """Aggregate type-level capabilities. Stable, sorted, deduplicated.
2783
-
2784
- Pure function: derives capabilities from the parsed AST only. Does
2785
- not consult external configuration; brownfield overrides are merged
2786
- later in `graph_enrich.py` so this stays free of I/O.
2787
- """
2788
- caps: set[str] = set()
2789
-
2790
- for ann in type_decl.annotations:
2791
- cap = _TYPE_ANN_TO_CAPABILITY.get(ann.name)
2792
- if cap:
2793
- caps.add(cap)
2794
-
2795
- for method in type_decl.methods:
2796
- for ann in method.annotations:
2797
- cap = _METHOD_ANN_TO_CAPABILITY.get(ann.name)
2798
- if cap:
2799
- caps.add(cap)
2800
-
2801
- for fld in type_decl.fields:
2802
- cap = _INJECTED_TYPES_TO_CAPABILITY.get(fld.type_name)
2803
- if cap:
2804
- caps.add(cap)
2805
- for method in type_decl.methods:
2806
- if method.is_constructor:
2807
- for p in method.parameters:
2808
- cap = _INJECTED_TYPES_TO_CAPABILITY.get(p.type_name)
2809
- if cap:
2810
- caps.add(cap)
2811
-
2812
- for sup in (*type_decl.extends, *type_decl.implements):
2813
- cap = _SUPERTYPE_TO_CAPABILITY.get(sup)
2814
- if cap:
2815
- caps.add(cap)
2816
-
2817
- return sorted(caps)
2818
-
2819
-
2820
- def injection_annotation_names() -> frozenset[str]:
2821
- return _INJECT_FIELD_ANNOTATIONS
2822
-
2823
-
2824
- def lombok_required_args_annotations() -> frozenset[str]:
2825
- return _LOMBOK_RAC