java-codebase-rag 0.1.0__py3-none-any.whl

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