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 +2813 -0
- brownfield_events.py +58 -0
- build_ast_graph.py +3081 -0
- chunk_heuristics.py +62 -0
- graph_enrich.py +1681 -0
- index_common.py +10 -0
- java_codebase_rag/__init__.py +1 -0
- java_codebase_rag/cli.py +761 -0
- java_codebase_rag/cli_progress.py +52 -0
- java_codebase_rag/config.py +327 -0
- java_codebase_rag/pipeline.py +189 -0
- java_codebase_rag-0.1.0.dist-info/METADATA +818 -0
- java_codebase_rag-0.1.0.dist-info/RECORD +27 -0
- java_codebase_rag-0.1.0.dist-info/WHEEL +5 -0
- java_codebase_rag-0.1.0.dist-info/entry_points.txt +3 -0
- java_codebase_rag-0.1.0.dist-info/licenses/LICENSE +21 -0
- java_codebase_rag-0.1.0.dist-info/top_level.txt +17 -0
- java_index_flow_lancedb.py +398 -0
- java_index_v1_common.py +33 -0
- java_ontology.py +446 -0
- kuzu_queries.py +1989 -0
- mcp_hints.py +748 -0
- mcp_v2.py +1957 -0
- path_filtering.py +472 -0
- pr_analysis.py +534 -0
- search_lancedb.py +1075 -0
- server.py +578 -0
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
|