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