adl-toolkit 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/INSTALL_TOOLKIT.md +163 -0
  2. package/README.md +269 -2
  3. package/adl_drift/__init__.py +6 -0
  4. package/adl_drift/cli/__init__.py +1 -0
  5. package/adl_drift/cli/commands.py +993 -0
  6. package/adl_drift/cli/main.py +744 -0
  7. package/adl_drift/cortex/__init__.py +1 -0
  8. package/adl_drift/cortex/embeddings.py +96 -0
  9. package/adl_drift/cortex/memory.py +1749 -0
  10. package/adl_drift/cortex/patterns.py +89 -0
  11. package/adl_drift/cortex/telemetry.py +192 -0
  12. package/adl_drift/detectors/__init__.py +1 -0
  13. package/adl_drift/detectors/async_patterns.py +115 -0
  14. package/adl_drift/detectors/base.py +121 -0
  15. package/adl_drift/detectors/config.py +122 -0
  16. package/adl_drift/detectors/data_access.py +128 -0
  17. package/adl_drift/detectors/decorators.py +122 -0
  18. package/adl_drift/detectors/dynamic_patterns.py +260 -0
  19. package/adl_drift/detectors/errors.py +134 -0
  20. package/adl_drift/detectors/imports.py +132 -0
  21. package/adl_drift/detectors/logging_patterns.py +108 -0
  22. package/adl_drift/detectors/routes.py +257 -0
  23. package/adl_drift/detectors/security.py +236 -0
  24. package/adl_drift/detectors/taint.py +406 -0
  25. package/adl_drift/detectors/tests.py +95 -0
  26. package/adl_drift/detectors/type_conventions.py +81 -0
  27. package/adl_drift/graph/__init__.py +1 -0
  28. package/adl_drift/graph/builder.py +2670 -0
  29. package/adl_drift/graph/cfg.py +461 -0
  30. package/adl_drift/graph/cluster.py +40 -0
  31. package/adl_drift/graph/edges.py +86 -0
  32. package/adl_drift/graph/node.py +90 -0
  33. package/adl_drift/harness/__init__.py +10 -0
  34. package/adl_drift/harness/setup.py +832 -0
  35. package/adl_drift/harness/skills/adl-drift.md +25 -0
  36. package/adl_drift/harness/skills/drift-antigravity.md +19 -0
  37. package/adl_drift/harness/skills/drift-generic.md +19 -0
  38. package/adl_drift/harness/watch.py +84 -0
  39. package/adl_drift/lang/__init__.py +8 -0
  40. package/adl_drift/lang/base.py +464 -0
  41. package/adl_drift/lang/java/__init__.py +6 -0
  42. package/adl_drift/lang/java/backend.py +97 -0
  43. package/adl_drift/lang/java/cfg.py +1424 -0
  44. package/adl_drift/lang/java/config.py +707 -0
  45. package/adl_drift/lang/java/detectors/__init__.py +29 -0
  46. package/adl_drift/lang/java/detectors/additional.py +445 -0
  47. package/adl_drift/lang/java/detectors/base.py +154 -0
  48. package/adl_drift/lang/java/detectors/data_access.py +148 -0
  49. package/adl_drift/lang/java/detectors/imports.py +53 -0
  50. package/adl_drift/lang/java/detectors/routes.py +159 -0
  51. package/adl_drift/lang/java/detectors/security.py +386 -0
  52. package/adl_drift/lang/java/detectors/tests.py +74 -0
  53. package/adl_drift/lang/java/extractor.py +582 -0
  54. package/adl_drift/lang/java/ids.py +99 -0
  55. package/adl_drift/lang/java/parser.py +49 -0
  56. package/adl_drift/lang/java/resolution/__init__.py +1 -0
  57. package/adl_drift/lang/java/resolution/constants.py +1619 -0
  58. package/adl_drift/lang/java/resolution/constructors.py +278 -0
  59. package/adl_drift/lang/java/resolution/context.py +305 -0
  60. package/adl_drift/lang/java/resolution/dispatch.py +540 -0
  61. package/adl_drift/lang/java/resolution/functional.py +1318 -0
  62. package/adl_drift/lang/java/resolution/generated.py +95 -0
  63. package/adl_drift/lang/java/resolution/model.py +153 -0
  64. package/adl_drift/lang/java/resolution/orchestration.py +866 -0
  65. package/adl_drift/lang/java/resolution/overloads.py +1275 -0
  66. package/adl_drift/lang/java/resolution/scopes.py +1111 -0
  67. package/adl_drift/lang/java/resolution/types.py +2947 -0
  68. package/adl_drift/lang/java/resolver.py +1401 -0
  69. package/adl_drift/lang/java/scopes.py +69 -0
  70. package/adl_drift/lang/java/taint.py +500 -0
  71. package/adl_drift/lang/java/type_provenance.py +342 -0
  72. package/adl_drift/lang/python/__init__.py +8 -0
  73. package/adl_drift/lang/python/backend.py +94 -0
  74. package/adl_drift/lang/python/cfg.py +537 -0
  75. package/adl_drift/lang/python/extractor.py +454 -0
  76. package/adl_drift/lang/python/parser.py +41 -0
  77. package/adl_drift/lang/python/resolver.py +1789 -0
  78. package/adl_drift/lang/python/taint.py +365 -0
  79. package/adl_drift/lang/registry.py +117 -0
  80. package/adl_drift/parser/__init__.py +1 -0
  81. package/adl_drift/parser/cache.py +38 -0
  82. package/adl_drift/parser/core.py +33 -0
  83. package/adl_drift/parser/extractor.py +17 -0
  84. package/adl_drift/parser/literal_eval.py +326 -0
  85. package/adl_drift/scanner/__init__.py +1 -0
  86. package/adl_drift/scanner/discovery.py +338 -0
  87. package/adl_drift/scanner/incremental.py +261 -0
  88. package/adl_drift/scanner/parallel.py +224 -0
  89. package/adl_drift/server/__init__.py +1 -0
  90. package/adl_drift/server/handlers.py +2849 -0
  91. package/adl_drift/server/mcp_server.py +127 -0
  92. package/adl_drift/server/mcp_stdio.py +335 -0
  93. package/adl_drift/server/protocol.py +71 -0
  94. package/adl_drift/server/static/index.html +696 -0
  95. package/adl_drift/server/web.py +350 -0
  96. package/adl_drift/utils/__init__.py +1 -0
  97. package/adl_drift/utils/config.py +77 -0
  98. package/adl_drift/utils/fswalk.py +52 -0
  99. package/adl_drift/utils/types.py +345 -0
  100. package/bin/adl-drift.mjs +35 -0
  101. package/bin/adl-toolkit.mjs +488 -0
  102. package/package.json +29 -4
  103. package/pyproject.toml +49 -0
  104. package/scripts/install_toolkit.py +3584 -0
@@ -0,0 +1,1424 @@
1
+ """Java CFG construction (P4.6, PROBLEMS.md).
2
+
3
+ `JavaCFGBuilder` is `graph.cfg.CFGBuilderBase`'s Java counterpart to
4
+ `lang/python/cfg.py::PythonCFGBuilder` — walks a `method_declaration`/
5
+ `constructor_declaration`/`compact_constructor_declaration` CST node into
6
+ basic blocks, reusing the shared `CFGBlock`/`CFGDef` bookkeeping and feeding
7
+ the shared `ReachingDefinitions` fixpoint (`graph/cfg.py`) exactly like
8
+ Python does.
9
+
10
+ Minimum statement kinds (codegraph-java-deep-dive.md §10.2), all handled
11
+ below: `local_variable_declaration`, `expression_statement`, `if_statement`,
12
+ `for_statement`, `enhanced_for_statement`, `while_statement`, `do_statement`,
13
+ `switch_expression`/`switch_statement`, `try_statement`,
14
+ `try_with_resources_statement`, `catch_clause`, `finally_clause`,
15
+ `synchronized_statement`, `return_statement`, `throw_statement`,
16
+ `break_statement`, `continue_statement`, `lambda_expression`.
17
+
18
+ Design notes:
19
+ - `return`/`throw` link to the function's synthetic exit block; `break`
20
+ targets the nearest enclosing loop or `switch`'s merge block (the
21
+ statement after it); `continue` targets the nearest enclosing loop's
22
+ condition re-check (a dedicated `update`-entry block for `for`, the
23
+ loop header for `while`/enhanced-`for`/`do`) — each threaded down as
24
+ `exit_b`/`break_target`/`continue_target` through every `_process_*`
25
+ helper, re-pointed at the right block by whichever compound statement
26
+ establishes it. None of these targets are always their "obvious"
27
+ block, though: a `try` with a `finally` (`_process_try`) substitutes
28
+ its own finally block(s) as the target for everything nested inside
29
+ its body/catch handlers, so an abrupt completion of any of the four
30
+ kinds still runs `finally` first — chaining correctly through
31
+ arbitrarily nested `try`/`finally` — before whatever it ultimately
32
+ reaches links onward to the real target.
33
+ - Labeled `break <label>;`/`continue <label>;` targeting an *enclosing
34
+ loop or switch* resolve to that specific statement's own targets, not
35
+ the nearest one — `labeled_statement` records `label -> (break_target,
36
+ continue_target)` into a `labels` map (also threaded through every
37
+ `_process_*` helper, and relayed through an intervening `try`/`finally`
38
+ exactly like `exit_b`/`break_target`/`continue_target` are) as soon as
39
+ it dispatches into the loop/switch it wraps. A label on any *other*
40
+ statement kind (`outer: if (...) { break outer; }` — legal Java, but
41
+ rare) is not supported: `label` is only threaded into the five
42
+ loop/switch handlers, so such a label is silently dropped and any
43
+ `break`/`continue` naming it falls back to the nearest enclosing
44
+ target, same as today.
45
+ - `switch`'s case/rule bodies are each treated as independently reachable
46
+ from the switch entry and merge into one successor block — the same
47
+ conservative "all handlers reachable, no attempt at precise fallthrough
48
+ edges" choice `PythonCFGBuilder._process_try` already makes for
49
+ `except`/`finally` clauses.
50
+ - A `lambda_expression` is an opaque leaf for def/use purposes (no
51
+ descent into its body), matching how `PythonCFGBuilder`/`_collect_uses`
52
+ already treats a nested `function_definition`/`lambda`/`class_definition`
53
+ as a value, not a control-flow site to analyze inline.
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ from typing import Dict, List, Literal, Optional, Tuple, Union
59
+
60
+ from tree_sitter import Node
61
+
62
+ from adl_drift.graph.cfg import CFGBlock, CFGBuilderBase, CFGDef, CFGUse, _node_pos, _text
63
+ from adl_drift.lang.base import CFGConfig
64
+ from adl_drift.lang.java import type_provenance as _tp
65
+ from adl_drift.lang.java.scopes import TYPE_DECLARATIONS
66
+ from adl_drift.lang.java.type_provenance import JavaTypeContext, Provenance, build_type_context
67
+
68
+ __all__ = [
69
+ "TAINT_SOURCES",
70
+ "TAINT_SOURCE_METHODS",
71
+ "TAINT_SOURCE_PARAM_ANNOTATIONS",
72
+ "SINK_METHODS",
73
+ "SINK_METHOD_ARGUMENTS",
74
+ "SINK_CONSTRUCTORS",
75
+ "SINK_CONSTRUCTOR_ARGUMENTS",
76
+ "JavaCFGBuilder",
77
+ "default_java_cfg_config",
78
+ ]
79
+
80
+ # ─── taint sources (codegraph-java-deep-dive.md §10.3) ────────────────────────
81
+
82
+ # Exact dotted-callable text, prefix-matched against a def/argument's full RHS
83
+ # text — same mechanism as Python's TAINT_SOURCES (lang/python/cfg.py).
84
+ TAINT_SOURCES: Tuple[str, ...] = (
85
+ "System.getenv",
86
+ "System.getProperty",
87
+ )
88
+
89
+ # Owner FQN for each `TAINT_SOURCES` entry — a project can declare its own
90
+ # top-level class named `System` in the same file/package, which *does*
91
+ # shadow `java.lang.System`'s implicit import for unqualified references
92
+ # within it (F-04, PROBLEMS.md P4.6); an exact dotted-text match alone can't
93
+ # tell the two apart.
94
+ TAINT_SOURCE_OWNERS: Dict[str, str] = {
95
+ "System.getenv": "java.lang.System",
96
+ "System.getProperty": "java.lang.System",
97
+ }
98
+
99
+ # Final call-target identifier (any receiver) -> source label. Receiver-agnostic
100
+ # because an `HttpServletRequest`/JAX-RS `UriInfo` variable can be named
101
+ # anything, unlike Python's conventionally-named `request` object.
102
+ TAINT_SOURCE_METHODS: Dict[str, str] = {
103
+ "getParameter": "HttpServletRequest.getParameter",
104
+ "getHeader": "HttpServletRequest.getHeader",
105
+ "getQueryString": "HttpServletRequest.getQueryString",
106
+ }
107
+
108
+ # Expected declaring-type FQN(s) for each `TAINT_SOURCE_METHODS` entry (F-04,
109
+ # PROBLEMS.md P4.6) — confirms the call's receiver is plausibly one of these
110
+ # types (via `type_provenance`: declared-variable type + imports) before
111
+ # trusting the simple-name match, so e.g. `Custom.getParameter()` on an
112
+ # unrelated class isn't mistaken for `HttpServletRequest.getParameter`. A
113
+ # method whose receiver can't be confirmed one way or the other (no `import`
114
+ # to check against, or no declared type known for it) still matches — just
115
+ # at a lower confidence (`_owner_provenance` returns `"ambiguous"`) rather
116
+ # than being dropped, to keep recall on unresolved receivers.
117
+ TAINT_SOURCE_METHOD_OWNERS: Dict[str, Tuple[str, ...]] = {
118
+ "getParameter": ("javax.servlet.http.HttpServletRequest", "jakarta.servlet.http.HttpServletRequest"),
119
+ "getHeader": ("javax.servlet.http.HttpServletRequest", "jakarta.servlet.http.HttpServletRequest"),
120
+ "getQueryString": (
121
+ "javax.servlet.http.HttpServletRequest", "jakarta.servlet.http.HttpServletRequest",
122
+ "javax.ws.rs.core.UriInfo", "jakarta.ws.rs.core.UriInfo",
123
+ ),
124
+ }
125
+
126
+ # Annotation simple name on a formal parameter -> source label. These mark a
127
+ # parameter as tainted at function entry regardless of caller (Spring MVC /
128
+ # JAX-RS request binding), unlike an ordinary parameter which only becomes
129
+ # tainted via cross-function propagation.
130
+ TAINT_SOURCE_PARAM_ANNOTATIONS: Dict[str, str] = {
131
+ "RequestParam": "@RequestParam",
132
+ "PathVariable": "@PathVariable",
133
+ "RequestBody": "@RequestBody",
134
+ "QueryParam": "@QueryParam",
135
+ "PathParam": "@PathParam",
136
+ }
137
+
138
+ # Expected FQN(s) for each `TAINT_SOURCE_PARAM_ANNOTATIONS` entry — same
139
+ # provenance role as `TAINT_SOURCE_METHOD_OWNERS`, but for annotations: a
140
+ # project-local `@interface RequestParam {}` (or an import of a *different*
141
+ # `RequestParam`) must not be treated as Spring's.
142
+ TAINT_SOURCE_PARAM_ANNOTATION_OWNERS: Dict[str, Tuple[str, ...]] = {
143
+ "RequestParam": ("org.springframework.web.bind.annotation.RequestParam",),
144
+ "PathVariable": ("org.springframework.web.bind.annotation.PathVariable",),
145
+ "RequestBody": ("org.springframework.web.bind.annotation.RequestBody",),
146
+ "QueryParam": ("javax.ws.rs.QueryParam", "jakarta.ws.rs.QueryParam"),
147
+ "PathParam": ("javax.ws.rs.PathParam", "jakarta.ws.rs.PathParam"),
148
+ }
149
+
150
+ # ─── taint sinks (codegraph-java-deep-dive.md §10.4 / PROBLEMS.md P4.6) ──────
151
+
152
+ # Final call-target identifier (any receiver) -> vulnerability category.
153
+ SINK_METHODS: Dict[str, str] = {
154
+ "executeQuery": "sql_injection",
155
+ "executeUpdate": "sql_injection",
156
+ "execute": "sql_injection",
157
+ "createNativeQuery": "sql_injection",
158
+ # `Connection.prepareStatement(sql)` is the actual injection point for the
159
+ # most common JDBC pattern (`PreparedStatement ps = conn.prepareStatement(
160
+ # taintedSql); ps.executeQuery();`) — that `executeQuery()` call itself
161
+ # takes no argument once parameterized, so without this entry the tainted
162
+ # SQL text passed to `prepareStatement` was never checked against any sink
163
+ # at all (regression fix, PROBLEMS.md P4.6). A tainted SQL *template* is
164
+ # still flagged; a bind value passed only through `setString`/`setInt`/...
165
+ # is not a sink argument and stays unflagged (§10.4's own "prepared
166
+ # statements are not automatically safe" callout, "bound parameter"-safe
167
+ # half is preserved unchanged since those setters aren't in this table).
168
+ "prepareStatement": "sql_injection",
169
+ "readObject": "unsafe_deserialization",
170
+ "eval": "code_injection",
171
+ "exec": "command_injection",
172
+ "command": "command_injection",
173
+ "invoke": "reflection_injection",
174
+ "forName": "reflection_injection",
175
+ }
176
+
177
+ # Expected declaring-type FQN(s) for each `SINK_METHODS` entry — same
178
+ # provenance role as `TAINT_SOURCE_METHOD_OWNERS`, so a business method
179
+ # named `execute`/`invoke`/`exec` on a project-owned class isn't mistaken
180
+ # for the real JDK/framework sink.
181
+ SINK_METHOD_OWNERS: Dict[str, Tuple[str, ...]] = {
182
+ "executeQuery": ("java.sql.Statement", "java.sql.PreparedStatement", "java.sql.CallableStatement"),
183
+ "executeUpdate": ("java.sql.Statement", "java.sql.PreparedStatement", "java.sql.CallableStatement"),
184
+ "execute": ("java.sql.Statement", "java.sql.PreparedStatement", "java.sql.CallableStatement"),
185
+ "createNativeQuery": ("jakarta.persistence.EntityManager", "javax.persistence.EntityManager"),
186
+ "prepareStatement": ("java.sql.Connection",),
187
+ "readObject": ("java.io.ObjectInputStream",),
188
+ "eval": ("javax.script.ScriptEngine",),
189
+ "exec": ("java.lang.Runtime",),
190
+ "command": ("java.lang.ProcessBuilder",),
191
+ "invoke": ("java.lang.reflect.Method",),
192
+ "forName": ("java.lang.Class",),
193
+ }
194
+
195
+ SinkArgumentPolicy = Union[Tuple[int, ...], Literal["all", "receiver"]]
196
+
197
+ SINK_METHOD_ARGUMENTS: Dict[str, SinkArgumentPolicy] = {
198
+ "prepareStatement": (0,),
199
+ "createNativeQuery": (0,),
200
+ "executeQuery": (0,),
201
+ "executeUpdate": (0,),
202
+ "execute": (0,),
203
+ # `readObject()` takes no argument at all — the JDK API is
204
+ # `Object ObjectInputStream.readObject()` — so the attacker-controlled
205
+ # data is the *receiver* (the stream itself), not a positional argument.
206
+ # A positional-args policy ("all"/index tuple) always yields an empty
207
+ # list for this call shape, which `_sink_info` (lang/java/taint.py)
208
+ # then treats as "not a sink" and drops before taint is ever checked
209
+ # (PROBLEMS.md P4.6 F-01 regression) — "receiver" tells `_sink_args` to
210
+ # check the receiver expression for taint instead of any argument.
211
+ "readObject": "receiver",
212
+ "eval": (0,),
213
+ "exec": "all",
214
+ "command": "all",
215
+ "invoke": "all",
216
+ "forName": (0,),
217
+ }
218
+
219
+ SINK_METHOD_EXTERNAL_SUBTYPE_FALLBACKS: frozenset[str] = frozenset({
220
+ "prepareStatement",
221
+ })
222
+
223
+ # Constructed type simple name (`new X(...)`) -> category.
224
+ SINK_CONSTRUCTORS: Dict[str, str] = {
225
+ "ProcessBuilder": "command_injection",
226
+ }
227
+
228
+ SINK_CONSTRUCTOR_OWNERS: Dict[str, Tuple[str, ...]] = {
229
+ "ProcessBuilder": ("java.lang.ProcessBuilder",),
230
+ }
231
+
232
+ SINK_CONSTRUCTOR_ARGUMENTS: Dict[str, SinkArgumentPolicy] = {
233
+ "ProcessBuilder": "all",
234
+ }
235
+
236
+ # ─── literal / structural node-type tables ────────────────────────────────────
237
+
238
+ _LITERAL_TYPES: frozenset = frozenset({
239
+ "decimal_integer_literal", "hex_integer_literal", "octal_integer_literal",
240
+ "binary_integer_literal", "decimal_floating_point_literal",
241
+ "hex_floating_point_literal", "true", "false", "character_literal",
242
+ "string_literal", "null_literal", "text_block",
243
+ })
244
+
245
+ _TERMINATOR_TYPES: frozenset = frozenset({
246
+ "return_statement", "throw_statement", "break_statement", "continue_statement",
247
+ })
248
+
249
+ _SIMPLE_STMT_TYPES: frozenset = frozenset({
250
+ "import_declaration", "package_declaration", "empty_statement", "yield_statement",
251
+ })
252
+
253
+ _METHOD_TYPES = ("method_declaration", "constructor_declaration", "compact_constructor_declaration")
254
+
255
+ _TYPE_DECL_TYPES = (
256
+ "class_declaration", "interface_declaration", "enum_declaration",
257
+ "record_declaration", "annotation_type_declaration",
258
+ )
259
+
260
+
261
+ # ─── helper functions (Java-CST-coupled) ──────────────────────────────────────
262
+
263
+ def _real_children(node: Node) -> List[Node]:
264
+ """Non-extra, non-punctuation children of a `block`/`switch_block`-like node."""
265
+ return [c for c in node.children
266
+ if not c.is_extra and c.type not in ("{", "}", ";", ",")]
267
+
268
+
269
+ def _body_stmts(node: Optional[Node]) -> List[Node]:
270
+ """Statement list for a compound-statement body, whether braced
271
+ (`block`) or a single braceless statement (`if (x) foo();` is legal
272
+ Java)."""
273
+ if node is None:
274
+ return []
275
+ if node.type == "block":
276
+ return _real_children(node)
277
+ return [node]
278
+
279
+
280
+ def _fields(node: Node, field_name: str) -> List[Node]:
281
+ """Every child of `node` bound to `field_name` (`child_by_field_name`
282
+ only returns the first — `int a = 1, b = 2;` has two `declarator`
283
+ fields, `for (int i=0,j=0;;i++,j++)` has two `init`/`update` fields)."""
284
+ out: List[Node] = []
285
+ for i, child in enumerate(node.children):
286
+ if node.field_name_for_child(i) == field_name:
287
+ out.append(child)
288
+ return out
289
+
290
+
291
+ def _first_named_child(node: Node) -> Optional[Node]:
292
+ for child in node.children:
293
+ if child.is_named:
294
+ return child
295
+ return None
296
+
297
+
298
+ def _is_literal(node: Optional[Node]) -> bool:
299
+ """True when node represents a compile-time constant (Java literals)."""
300
+ if node is None:
301
+ return False
302
+ if node.type in _LITERAL_TYPES:
303
+ return True
304
+ if node.type == "unary_expression":
305
+ op = node.child_by_field_name("operator")
306
+ operand = node.child_by_field_name("operand")
307
+ if op is not None and op.type == "-" and operand is not None and operand.type in (
308
+ "decimal_integer_literal", "hex_integer_literal", "octal_integer_literal",
309
+ "binary_integer_literal", "decimal_floating_point_literal",
310
+ "hex_floating_point_literal",
311
+ ):
312
+ return True
313
+ if node.type == "parenthesized_expression" and node.child_count >= 3:
314
+ return _is_literal(node.children[1])
315
+ return False
316
+
317
+
318
+ def _uses_of(node: Optional[Node]) -> List[CFGUse]:
319
+ out: List[CFGUse] = []
320
+ _collect_uses(node, out)
321
+ return out
322
+
323
+
324
+ def _collect_uses(node: Optional[Node], uses: List[CFGUse], _visited: Optional[set] = None) -> None:
325
+ """Walk *node* and append identifier reads to *uses*.
326
+
327
+ Skips:
328
+ - Method/field names (`obj.method(...)`/`obj.field` — only `obj` and
329
+ the arguments are reads)
330
+ - Constructed type names (`new X(...)` — only the arguments are reads)
331
+ - Type positions (`(Foo) x` cast, `x instanceof Foo`, declared types)
332
+ - Annotations
333
+ - Lambda bodies (opaque, matching Python's nested-def treatment)
334
+ """
335
+ if node is None:
336
+ return
337
+ if _visited is None:
338
+ _visited = set()
339
+ nid = id(node)
340
+ if nid in _visited:
341
+ return
342
+ _visited.add(nid)
343
+
344
+ t = node.type
345
+
346
+ if t == "field_access":
347
+ obj = node.child_by_field_name("object")
348
+ _collect_uses(obj, uses, _visited)
349
+ return
350
+
351
+ if t == "method_invocation":
352
+ obj = node.child_by_field_name("object")
353
+ args = node.child_by_field_name("arguments")
354
+ _collect_uses(obj, uses, _visited)
355
+ _collect_uses(args, uses, _visited)
356
+ return
357
+
358
+ if t == "object_creation_expression":
359
+ args = node.child_by_field_name("arguments")
360
+ _collect_uses(args, uses, _visited)
361
+ return
362
+
363
+ if t == "cast_expression":
364
+ _collect_uses(node.child_by_field_name("value"), uses, _visited)
365
+ return
366
+
367
+ if t == "instanceof_expression":
368
+ _collect_uses(node.child_by_field_name("left"), uses, _visited)
369
+ return
370
+
371
+ if t in ("annotation", "marker_annotation"):
372
+ return
373
+
374
+ if t == "lambda_expression":
375
+ # Opaque value — don't descend, matching Python's lambda handling.
376
+ return
377
+
378
+ if t == "this":
379
+ return
380
+
381
+ if t == "identifier":
382
+ txt = _text(node, b"") if node.text is None else (
383
+ node.text.decode("utf-8", errors="replace")
384
+ )
385
+ if txt:
386
+ uses.append(CFGUse(txt, _node_pos(node)))
387
+ return
388
+
389
+ for child in node.children:
390
+ _collect_uses(child, uses, _visited)
391
+
392
+
393
+ def _annotations_of(node: Node, source: bytes) -> List[Tuple[str, int]]:
394
+ """`[(raw_name_text, start_byte), ...]` for every annotation on a
395
+ `formal_parameter`/method/field's `modifiers` child. *raw_name_text*
396
+ keeps the full dotted path exactly as written (`@RequestParam` ->
397
+ `"RequestParam"`, `@org.springframework...RequestParam` -> the whole
398
+ FQN text) — trimming to a simple name here would throw away exactly
399
+ what `type_provenance.annotation_provenance` needs to tell a
400
+ fully-qualified annotation from a simple one that needs an `import` to
401
+ confirm (F-04, PROBLEMS.md P4.6)."""
402
+ modifiers = next((c for c in node.children if c.type == "modifiers"), None)
403
+ if modifiers is None:
404
+ return []
405
+ out: List[Tuple[str, int]] = []
406
+ for child in modifiers.children:
407
+ if child.type not in ("annotation", "marker_annotation"):
408
+ continue
409
+ name_node = child.child_by_field_name("name")
410
+ if name_node is not None:
411
+ out.append((_text(name_node, source).lstrip("@"), child.start_byte))
412
+ return out
413
+
414
+
415
+ def _annotation_owner_verdict(
416
+ raw_name: str, at_byte: int, owners: Optional[Tuple[str, ...]], ctx: Optional[JavaTypeContext],
417
+ ) -> Provenance:
418
+ """`"confirmed"`/`"ambiguous"`/`"rejected"` for whether *raw_name* (an
419
+ annotation as written — possibly fully qualified) denotes one of
420
+ *owners*. No `owners` entry, or no `ctx` (no file-level import data
421
+ available), means "nothing to check against" -> `"ambiguous"` — the
422
+ caller should still treat it as a match, just at reduced confidence."""
423
+ if not owners or ctx is None:
424
+ return "ambiguous"
425
+ verdicts = [
426
+ _tp.annotation_provenance(raw_name, at_byte, ctx.imports, ctx.local_types, fqn)
427
+ for fqn in owners
428
+ ]
429
+ if any(v == "confirmed" for v in verdicts):
430
+ return "confirmed"
431
+ if all(v == "rejected" for v in verdicts):
432
+ return "rejected"
433
+ return "ambiguous"
434
+
435
+
436
+ def _erased_type_name(type_text: str) -> str:
437
+ """Strip generic arguments/array brackets for a constructed-type match
438
+ (`new ProcessBuilder<>(...)`-shaped edge cases, generics on a raw type
439
+ are not legal Java for `new`, but this keeps the match robust to
440
+ whitespace/array suffixes)."""
441
+ text = type_text.strip()
442
+ idx = text.find("<")
443
+ if idx != -1:
444
+ text = text[:idx]
445
+ return text.strip()
446
+
447
+
448
+ def _constructed_receiver_type(obj: Optional[Node], source: bytes) -> str:
449
+ """Simple type name of an inline `new X(...)` receiver
450
+ (`new ObjectInputStream(...).readObject()`), F-07 (PROBLEMS.md P4.6).
451
+ Such a receiver has no declared-variable type and its own text isn't
452
+ identifier-shaped (it's a whole constructor call, not a name), so
453
+ neither of `_receiver_type_candidates`' existing candidate paths ever
454
+ recognized it: `_owner_provenance` saw zero candidates and fell back to
455
+ `"ambiguous"` even when the constructed type is spelled right there in
456
+ the source and exactly matches the sink's expected owner — silently
457
+ downgrading a fully-confirmable sink to reduced confidence."""
458
+ if obj is None or obj.type != "object_creation_expression":
459
+ return ""
460
+ type_node = obj.child_by_field_name("type")
461
+ if type_node is None:
462
+ return ""
463
+ return _erased_type_name(_text(type_node, source))
464
+
465
+
466
+ def _enclosing_type_name(node: Node, source: bytes) -> str:
467
+ """Simple name of the innermost type declaration lexically enclosing
468
+ *node* — the implicit `this` receiver for an *unqualified* call (no
469
+ `object` field at all: `getParameter("cmd")`, `executeQuery(q)`), F-08
470
+ (PROBLEMS.md P4.6). Such a call can only resolve to a method declared on
471
+ `this` or inherited by it, so the type to confirm against a sink/
472
+ source's expected owner is the enclosing class itself — not an absent
473
+ receiver, which previously gave `_receiver_type_candidates` zero
474
+ candidates and made `_owner_provenance` fall back to `"ambiguous"`
475
+ (still matched, just at reduced confidence) for *any* unqualified call,
476
+ including one to a plain, unrelated local method that merely happens to
477
+ share a sink/source method's simple name."""
478
+ parent = node.parent
479
+ while parent is not None:
480
+ if parent.type in TYPE_DECLARATIONS:
481
+ return _text(parent.child_by_field_name("name"), source)
482
+ parent = parent.parent
483
+ return ""
484
+
485
+
486
+ def _receiver_type_candidates(
487
+ receiver: str, ctx: Optional[JavaTypeContext], at_byte: int,
488
+ constructed_type: str = "", enclosing_type: str = "",
489
+ ) -> Tuple[str, ...]:
490
+ """Type-name-shaped candidates for a call's receiver expression, for
491
+ `_owner_provenance`. A bare receiver *identifier* (`request`, `custom`)
492
+ with a known *declared* variable type uses that type as its candidate.
493
+
494
+ Two `Type.staticMethod()`-shaped exceptions, where the receiver text
495
+ itself already spells a type rather than naming a variable:
496
+ - `Runtime.getRuntime()` specifically (`security.py`'s own precedent).
497
+ - Any other bare identifier with *no* declared-variable type that
498
+ looks like a Java type name — starts uppercase, per the PascalCase-
499
+ types/camelCase-variables convention (`Custom.forName(...)`) — F-04,
500
+ PROBLEMS.md P4.6. `type_provenance` naturally rejects a candidate
501
+ that plainly isn't the expected simple name, so offering it
502
+ alongside a genuine declared-variable-type candidate is safe: at
503
+ most one of them can ever match a given owner.
504
+
505
+ A third exception, `constructed_type` (`_constructed_receiver_type`):
506
+ an inline `new X(...)` receiver's own constructed type name — F-07,
507
+ PROBLEMS.md P4.6.
508
+
509
+ A fourth, `enclosing_type` (`_enclosing_type_name`): the implicit `this`
510
+ for a call with no receiver at all — F-08, PROBLEMS.md P4.6. Unlike the
511
+ PascalCase-guess exception above, this is never a guess: the caller only
512
+ passes it once it has actually found the enclosing type declaration."""
513
+ candidates = []
514
+ declared = ""
515
+ if ctx is not None:
516
+ declared = _tp.declared_type_of(receiver, ctx.declared_types, at_byte)
517
+ if declared:
518
+ candidates.append(declared)
519
+ if constructed_type:
520
+ candidates.append(constructed_type)
521
+ if enclosing_type:
522
+ candidates.append(enclosing_type)
523
+ if receiver.endswith(".getRuntime()"):
524
+ candidates.append(receiver[: -len(".getRuntime()")])
525
+ elif not declared and receiver[:1].isupper() and receiver.isidentifier():
526
+ candidates.append(receiver)
527
+ return tuple(candidates)
528
+
529
+
530
+ def _owner_name_signal(simple: str, owners: Tuple[str, ...]) -> bool:
531
+ """True when *simple* (an external, hierarchy-unknown candidate type's
532
+ own simple name) textually suggests one of *owners* — e.g.
533
+ `OracleConnection` for owner `java.sql.Connection` (owner simple name
534
+ `Connection`). Gates `_owner_provenance`'s external-subtype fallback:
535
+ an imported type is only a *plausible* undeclared vendor subtype when
536
+ its own name carries some signal toward the expected owner, not merely
537
+ because it happens to have been imported at all (PROBLEMS.md P4.6 —
538
+ 2026-08-28 regression: `import com.example.QueryBuilder; ...
539
+ builder.prepareStatement(sql)` was flagged `sql_injection` purely
540
+ because `QueryBuilder` was an explicit import, with zero evidence it
541
+ has anything to do with `java.sql.Connection`)."""
542
+ simple_lower = simple.lower()
543
+ for owner in owners:
544
+ owner_simple = owner.rsplit(".", 1)[-1].lower()
545
+ if owner_simple and owner_simple in simple_lower:
546
+ return True
547
+ return False
548
+
549
+
550
+ def _owner_provenance(
551
+ candidates: Tuple[str, ...], at_byte: int, owners: Optional[Tuple[str, ...]],
552
+ ctx: Optional[JavaTypeContext], *, allow_external_subtype_fallback: bool = False,
553
+ ) -> Provenance:
554
+ """`"confirmed"`/`"ambiguous"`/`"rejected"` for whether any of
555
+ *candidates* (a call's receiver text, its declared variable type, ...)
556
+ plausibly denotes one of *owners*. No `owners` entry for this call name,
557
+ or no `ctx`, means "nothing to check against" -> `"ambiguous"`."""
558
+ if not owners or ctx is None:
559
+ return "ambiguous"
560
+ verdicts = [
561
+ _tp.assignability_provenance(cand, ctx.imports, ctx.local_types, ctx.local_supertypes, fqn, at_byte)
562
+ for cand in candidates if cand
563
+ for fqn in owners
564
+ ]
565
+ if any(v == "confirmed" for v in verdicts):
566
+ return "confirmed"
567
+ if allow_external_subtype_fallback and verdicts and all(v == "rejected" for v in verdicts):
568
+ for cand in candidates:
569
+ simple = _tp.simple_type(cand)
570
+ if simple and not _tp.type_declared_at(ctx.local_types, simple, at_byte):
571
+ if _owner_name_signal(simple, owners) and any(
572
+ imported.endswith(f".{simple}") for imported in ctx.imports
573
+ ):
574
+ return "ambiguous"
575
+ if verdicts and all(v == "rejected" for v in verdicts):
576
+ return "rejected"
577
+ return "ambiguous"
578
+
579
+
580
+ def _source_match_for(
581
+ node: Optional[Node], config: CFGConfig, source: bytes, ctx: Optional[JavaTypeContext] = None,
582
+ ) -> Optional[Tuple[str, bool]]:
583
+ """Find a taint-source invocation anywhere inside *node*, not just at its
584
+ root — descends through binary concatenation, parenthesized/ternary/cast
585
+ expressions, nested call receivers/arguments, and array-literal elements
586
+ so a source doesn't have to be the whole RHS to be recognized (`"sh -c "
587
+ + System.getenv("CMD")`, `wrap(System.getenv("D"))`, `cond ?
588
+ System.getenv("A") : "b"`, `new String[] { "sh", System.getenv("D") }`
589
+ — F-09, PROBLEMS.md P4.6 — ...).
590
+
591
+ Returns `(label, confirmed)`: `confirmed` is `True` when the receiver's
592
+ declared type was positively matched against `TAINT_SOURCE_METHOD_
593
+ OWNERS` (F-04, PROBLEMS.md P4.6) — e.g. `Custom.getParameter()` on a
594
+ class unrelated to `HttpServletRequest` is *not* returned at all (its
595
+ receiver type is confirmed to be something else), whereas a receiver
596
+ with no resolvable declared type still matches, just as `confirmed=
597
+ False`, to keep recall on an unresolved receiver. A dotted `TAINT_
598
+ SOURCES` entry (`System.getenv`) is matched against the call's own
599
+ `object.name` text exactly, not as a text prefix — `System.getenvFake(
600
+ ...)` must not match `System.getenv`."""
601
+ if node is None:
602
+ return None
603
+ t = node.type
604
+
605
+ if t == "method_invocation":
606
+ obj = node.child_by_field_name("object")
607
+ name_node = node.child_by_field_name("name")
608
+ if obj is not None and name_node is not None:
609
+ obj_text = _text(obj, source)
610
+ method_name = _text(name_node, source)
611
+ dotted = f"{obj_text}.{method_name}"
612
+ src_key = dotted if dotted in config.taint_sources else None
613
+ if src_key is None:
614
+ # Fully-qualified receiver (`java.lang.System.getenv(...)`),
615
+ # not just the short `System.getenv` form the dotted-text
616
+ # equality above catches — F-04 gap (PROBLEMS.md P4.6):
617
+ # `dotted` above never equals the short `TAINT_SOURCES` entry
618
+ # for an FQN call, so it fell through unmatched even though
619
+ # `_owner_provenance`/`type_provenance` already confirm an
620
+ # exact FQN receiver text on their own (`raw_type ==
621
+ # qualified_name` short-circuit). Only trusted when
622
+ # `obj_text` equals the *entire* owner FQN, not a suffix of
623
+ # it, so this can't be fooled by an unrelated `x.y.System`.
624
+ for cand_key in config.taint_sources:
625
+ cand_method = cand_key.rsplit(".", 1)[-1]
626
+ owner = TAINT_SOURCE_OWNERS.get(cand_key)
627
+ if owner and method_name == cand_method and obj_text == owner:
628
+ src_key = cand_key
629
+ break
630
+ if src_key is not None:
631
+ owner = TAINT_SOURCE_OWNERS.get(src_key)
632
+ verdict = _owner_provenance(
633
+ (obj_text,), node.start_byte, (owner,) if owner else None, ctx,
634
+ )
635
+ if verdict != "rejected":
636
+ return src_key, verdict == "confirmed"
637
+ if name_node is not None:
638
+ method_name = _text(name_node, source)
639
+ label = config.taint_source_methods.get(method_name)
640
+ if label:
641
+ receiver = _text(obj, source) if obj is not None else ""
642
+ verdict = _owner_provenance(
643
+ _receiver_type_candidates(
644
+ receiver, ctx, node.start_byte, _constructed_receiver_type(obj, source),
645
+ _enclosing_type_name(node, source) if obj is None else "",
646
+ ),
647
+ node.start_byte, TAINT_SOURCE_METHOD_OWNERS.get(method_name), ctx,
648
+ )
649
+ if verdict != "rejected":
650
+ return label, verdict == "confirmed"
651
+ found = _source_match_for(obj, config, source, ctx)
652
+ if found:
653
+ return found
654
+ return _source_match_for(node.child_by_field_name("arguments"), config, source, ctx)
655
+
656
+ if t == "binary_expression":
657
+ return (
658
+ _source_match_for(node.child_by_field_name("left"), config, source, ctx)
659
+ or _source_match_for(node.child_by_field_name("right"), config, source, ctx)
660
+ )
661
+
662
+ if t == "ternary_expression":
663
+ return (
664
+ _source_match_for(node.child_by_field_name("consequence"), config, source, ctx)
665
+ or _source_match_for(node.child_by_field_name("alternative"), config, source, ctx)
666
+ )
667
+
668
+ if t == "parenthesized_expression":
669
+ return _source_match_for(_first_named_child(node), config, source, ctx)
670
+
671
+ if t == "cast_expression":
672
+ return _source_match_for(node.child_by_field_name("value"), config, source, ctx)
673
+
674
+ if t == "object_creation_expression":
675
+ # A source nested inside an inline `new X(...)` receiver/argument
676
+ # (`new ObjectInputStream(new FileInputStream(System.getenv("P")))`)
677
+ # was previously invisible (F-06, PROBLEMS.md P4.6): this function
678
+ # recursed into a call's `object`/`arguments` but had no branch for a
679
+ # constructor call at all, so it fell through to the catch-all
680
+ # `return None` below without ever inspecting the constructor's own
681
+ # arguments. Only matters when the source isn't already bound to a
682
+ # tainted variable first (the common case, `_ids_in`/the tainted-set
683
+ # walk, already covers that) — an inline constructed
684
+ # receiver/argument has no such binding to fall back on.
685
+ return _source_match_for(node.child_by_field_name("arguments"), config, source, ctx)
686
+
687
+ if t == "array_creation_expression":
688
+ # A source nested inside an inline array literal argument (`new
689
+ # ProcessBuilder(new String[] { "sh", "-c", System.getenv("CMD") })`)
690
+ # was previously invisible (F-09, PROBLEMS.md P4.6) for the same
691
+ # reason as F-06 above: no branch here for an array-creation node, so
692
+ # it fell through to `return None` without ever inspecting the
693
+ # array's elements.
694
+ return _source_match_for(node.child_by_field_name("value"), config, source, ctx)
695
+
696
+ if t in ("argument_list", "array_initializer"):
697
+ for child in node.children:
698
+ if child.is_named:
699
+ found = _source_match_for(child, config, source, ctx)
700
+ if found:
701
+ return found
702
+ return None
703
+
704
+ if t == "switch_expression":
705
+ # A source nested inside a `switch` expression's arms (`switch (x) {
706
+ # case 1 -> System.getenv("Q"); default -> "static"; }`, Java 14+)
707
+ # was previously invisible (PROBLEMS.md P4.6) for the same reason as
708
+ # F-06/F-09 above: no branch here for `switch_expression`, so it fell
709
+ # through to `return None` without ever inspecting any arm's value.
710
+ body = node.child_by_field_name("body")
711
+ if body is None:
712
+ return None
713
+ for group in body.children:
714
+ if group.type not in ("switch_rule", "switch_block_statement_group"):
715
+ continue
716
+ for stmt in group.children:
717
+ if not stmt.is_named or stmt.type == "switch_label":
718
+ continue
719
+ found = _switch_arm_match(stmt, config, source, ctx)
720
+ if found:
721
+ return found
722
+ return None
723
+
724
+ return None
725
+
726
+
727
+ def _switch_arm_match(
728
+ stmt: Node, config: CFGConfig, source: bytes, ctx: Optional[JavaTypeContext] = None,
729
+ ) -> Optional[Tuple[str, bool]]:
730
+ """The value a single `switch_rule`/`switch_block_statement_group`
731
+ statement contributes, if any — `expression_statement` (arrow form,
732
+ `case 1 -> System.getenv("Q");`) and `yield_statement` (block-arrow form
733
+ or classic `case 1: yield ...;` form) each wrap exactly the value
734
+ expression; a `block` arm is searched for its own `yield_statement`(s),
735
+ recursing through `if`/`try`/`synchronized` control structures that can
736
+ still contain one (`case 1 -> { if (cond) { yield System.getenv("Q"); }
737
+ yield "safe"; }` — a real false negative found in review: the taint
738
+ source here was previously invisible because this function stopped at
739
+ `if_statement`/`try_statement`/`synchronized_statement` instead of
740
+ looking inside them for a `yield`, and — contrary to what an earlier
741
+ version of this docstring claimed — `lang/java/taint.py`'s identifier
742
+ propagation does NOT independently discover a *new* source call nested
743
+ this way; it only propagates taint between identifiers already known to
744
+ be tainted). A `throw_statement` arm never completes the switch with a
745
+ value, so it's deliberately not recursed into — matching
746
+ `_source_match_for`'s own conservative-for-recall,
747
+ not-conservative-for-nonsense philosophy would mean flagging code paths
748
+ that can't actually produce the tainted value. A nested
749
+ `switch_expression`/`switch_statement` is also not descended into here:
750
+ a `yield` inside it resolves to that inner switch, not this one, so it's
751
+ left alone (its own value, if used, is picked up wherever it's actually
752
+ consumed by `_source_match_for`)."""
753
+ t = stmt.type
754
+ if t in ("expression_statement", "yield_statement"):
755
+ return _source_match_for(_first_named_child(stmt), config, source, ctx)
756
+ if t == "block":
757
+ for child in stmt.children:
758
+ if child.is_named:
759
+ found = _switch_arm_match(child, config, source, ctx)
760
+ if found:
761
+ return found
762
+ return None
763
+ if t == "if_statement":
764
+ for field_name in ("consequence", "alternative"):
765
+ branch = stmt.child_by_field_name(field_name)
766
+ if branch is not None:
767
+ found = _switch_arm_match(branch, config, source, ctx)
768
+ if found:
769
+ return found
770
+ return None
771
+ if t in ("try_statement", "try_with_resources_statement"):
772
+ body = stmt.child_by_field_name("body")
773
+ if body is not None:
774
+ found = _switch_arm_match(body, config, source, ctx)
775
+ if found:
776
+ return found
777
+ for child in stmt.children:
778
+ if child.type != "catch_clause":
779
+ continue
780
+ catch_body = child.child_by_field_name("body")
781
+ if catch_body is not None:
782
+ found = _switch_arm_match(catch_body, config, source, ctx)
783
+ if found:
784
+ return found
785
+ return None
786
+ if t == "synchronized_statement":
787
+ body = stmt.child_by_field_name("body")
788
+ return _switch_arm_match(body, config, source, ctx) if body is not None else None
789
+ return None
790
+
791
+
792
+ def _source_label_for(
793
+ node: Optional[Node], config: CFGConfig, source: bytes, ctx: Optional[JavaTypeContext] = None,
794
+ ) -> Optional[str]:
795
+ """Label-only convenience wrapper around `_source_match_for` for callers
796
+ that don't need the confirmed/ambiguous distinction."""
797
+ match = _source_match_for(node, config, source, ctx)
798
+ return match[0] if match else None
799
+
800
+
801
+ def _collect_param_specs(
802
+ params_node: Optional[Node], source: bytes,
803
+ ) -> List[Tuple[str, List[Tuple[str, int]]]]:
804
+ """`[(param_name, [(raw_annotation_name, start_byte), ...]), ...]` for a
805
+ `formal_parameters` CST node (handles plain `formal_parameter` and
806
+ varargs `spread_parameter`)."""
807
+ if params_node is None:
808
+ return []
809
+ out: List[Tuple[str, List[Tuple[str, int]]]] = []
810
+ for child in params_node.children:
811
+ if child.type not in ("formal_parameter", "spread_parameter"):
812
+ continue
813
+ name_node = child.child_by_field_name("name")
814
+ if name_node is None:
815
+ continue
816
+ out.append((_text(name_node, source), _annotations_of(child, source)))
817
+ return out
818
+
819
+
820
+ def _method_parameters(node: Node) -> Optional[Node]:
821
+ params = node.child_by_field_name("parameters")
822
+ if params is not None or node.type != "compact_constructor_declaration":
823
+ return params
824
+ record = node.parent.parent if node.parent is not None else None
825
+ if record is not None and record.type == "record_declaration":
826
+ return record.child_by_field_name("parameters")
827
+ return None
828
+
829
+
830
+ # ─── CFG builder ──────────────────────────────────────────────────────────────
831
+
832
+ class JavaCFGBuilder(CFGBuilderBase):
833
+ """Converts a method/constructor CST node into a basic-block CFG."""
834
+
835
+ def build(
836
+ self, func_node: Node, source: bytes
837
+ ) -> Tuple[int, int, Dict[int, CFGBlock], List[CFGDef]]:
838
+ self._reset()
839
+
840
+ # Per-compilation-unit import/declared-type provenance data (F-04,
841
+ # PROBLEMS.md P4.6) — computed once per `build()` call (once per
842
+ # method; this builder isn't on a hot per-file loop, see module
843
+ # docstring) and consulted by `_taint_of`/the annotation loop below
844
+ # to confirm a source/sink match's receiver or annotation actually
845
+ # denotes the real JDK/framework type, not a same-named local one.
846
+ root = func_node
847
+ while root.parent is not None:
848
+ root = root.parent
849
+ self._type_ctx = build_type_context(root, source)
850
+
851
+ entry = self._new_block()
852
+ exit_b = self._new_block()
853
+
854
+ params_node = _method_parameters(func_node)
855
+ for name, annotations in _collect_param_specs(params_node, source):
856
+ taint = None
857
+ for raw_name, at_byte in annotations:
858
+ simple = raw_name.rsplit(".", 1)[-1]
859
+ label = self.config.taint_source_param_annotations.get(simple)
860
+ if not label:
861
+ continue
862
+ verdict = _annotation_owner_verdict(
863
+ raw_name, at_byte,
864
+ TAINT_SOURCE_PARAM_ANNOTATION_OWNERS.get(simple), self._type_ctx,
865
+ )
866
+ if verdict == "rejected":
867
+ continue
868
+ taint = label
869
+ break
870
+ d = self._new_def(name, _node_pos(params_node), False, taint)
871
+ entry.events.append(d)
872
+
873
+ body = func_node.child_by_field_name("body")
874
+ if body is not None:
875
+ tail = self._process_stmts(_body_stmts(body), entry, exit_b, source)
876
+ if tail is not None:
877
+ self._link(tail, exit_b)
878
+ else:
879
+ self._link(entry, exit_b)
880
+
881
+ return entry.id, exit_b.id, self._blocks, self._all_defs
882
+
883
+ # ── taint helper ──────────────────────────────────────────────────────
884
+
885
+ def _taint_of(self, node: Optional[Node], source: bytes) -> Optional[str]:
886
+ return _source_label_for(node, self.config, source, self._type_ctx)
887
+
888
+ # ── statement dispatch ────────────────────────────────────────────────
889
+
890
+ def _process_stmts(
891
+ self, stmts: List[Node], cur: CFGBlock, exit_b: CFGBlock, source: bytes,
892
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
893
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None,
894
+ ) -> Optional[CFGBlock]:
895
+ for stmt in stmts:
896
+ if cur is None:
897
+ return None
898
+ cur = self._process_stmt(stmt, cur, exit_b, source, break_target, continue_target, labels)
899
+ return cur
900
+
901
+ def _process_stmt(
902
+ self, stmt: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
903
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
904
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
905
+ ) -> Optional[CFGBlock]:
906
+ t = stmt.type
907
+
908
+ if t == "expression_statement":
909
+ real = [c for c in stmt.children if not c.is_extra and c.type != ";"]
910
+ if len(real) == 1:
911
+ return self._process_stmt(real[0], cur, exit_b, source, break_target, continue_target, labels)
912
+ cur.events.extend(_uses_of(stmt))
913
+ return cur
914
+
915
+ if t == "local_variable_declaration":
916
+ for decl in _fields(stmt, "declarator"):
917
+ name_node = decl.child_by_field_name("name")
918
+ value = decl.child_by_field_name("value")
919
+ if name_node is None:
920
+ continue
921
+ is_lit = _is_literal(value)
922
+ taint = self._taint_of(value, source)
923
+ if value is not None:
924
+ cur.events.extend(_uses_of(value))
925
+ cur.events.append(
926
+ self._new_def(_text(name_node, source), _node_pos(name_node), is_lit, taint)
927
+ )
928
+ return cur
929
+
930
+ if t == "assignment_expression":
931
+ lhs = stmt.child_by_field_name("left")
932
+ op = stmt.child_by_field_name("operator")
933
+ rhs = stmt.child_by_field_name("right")
934
+ op_text = _text(op, source) if op is not None else "="
935
+ if rhs is not None:
936
+ cur.events.extend(_uses_of(rhs))
937
+ if lhs is not None and lhs.type == "identifier":
938
+ name = _text(lhs, source)
939
+ if op_text != "=":
940
+ cur.events.append(CFGUse(name, _node_pos(lhs)))
941
+ is_lit = _is_literal(rhs) if op_text == "=" else False
942
+ taint = self._taint_of(rhs, source) if op_text == "=" else None
943
+ cur.events.append(self._new_def(name, _node_pos(lhs), is_lit, taint))
944
+ return cur
945
+
946
+ if t == "update_expression":
947
+ ident = next((c for c in stmt.children if c.type == "identifier"), None)
948
+ if ident is not None:
949
+ name = _text(ident, source)
950
+ cur.events.append(CFGUse(name, _node_pos(ident)))
951
+ cur.events.append(self._new_def(name, _node_pos(ident), False, None))
952
+ return cur
953
+
954
+ if t in ("return_statement", "throw_statement"):
955
+ expr = _first_named_child(stmt)
956
+ if expr is not None:
957
+ cur.events.extend(_uses_of(expr))
958
+ self._link(cur, exit_b)
959
+ return None
960
+
961
+ if t == "break_statement":
962
+ # A labeled `break <label>;` targets that specific loop/switch's
963
+ # own merge block (via `labels`, see module docstring), not
964
+ # necessarily the nearest one. Unlabeled falls back to
965
+ # `break_target`, and that falls back to `exit_b` only for
966
+ # malformed/unparseable code where no enclosing loop/switch was
967
+ # tracked (a bare `break` outside one is not valid Java).
968
+ label_node = next((c for c in stmt.children if c.type == "identifier"), None)
969
+ target = break_target
970
+ if label_node is not None and labels:
971
+ named = labels.get(_text(label_node, source))
972
+ if named is not None:
973
+ target = named[0]
974
+ self._link(cur, target if target is not None else exit_b)
975
+ return None
976
+
977
+ if t == "continue_statement":
978
+ label_node = next((c for c in stmt.children if c.type == "identifier"), None)
979
+ target = continue_target
980
+ if label_node is not None and labels:
981
+ named = labels.get(_text(label_node, source))
982
+ if named is not None:
983
+ target = named[1]
984
+ self._link(cur, target if target is not None else exit_b)
985
+ return None
986
+
987
+ if t == "assert_statement":
988
+ for child in stmt.children:
989
+ if child.is_named:
990
+ cur.events.extend(_uses_of(child))
991
+ return cur
992
+
993
+ if t in _SIMPLE_STMT_TYPES:
994
+ cur.events.extend(_uses_of(stmt))
995
+ return cur
996
+
997
+ if t == "if_statement":
998
+ return self._process_if(stmt, cur, exit_b, source, break_target, continue_target, labels)
999
+
1000
+ if t == "for_statement":
1001
+ return self._process_for(stmt, cur, exit_b, source, break_target, continue_target, labels, label)
1002
+
1003
+ if t == "enhanced_for_statement":
1004
+ return self._process_enhanced_for(
1005
+ stmt, cur, exit_b, source, break_target, continue_target, labels, label
1006
+ )
1007
+
1008
+ if t == "while_statement":
1009
+ return self._process_while(stmt, cur, exit_b, source, break_target, continue_target, labels, label)
1010
+
1011
+ if t == "do_statement":
1012
+ return self._process_do_while(stmt, cur, exit_b, source, break_target, continue_target, labels, label)
1013
+
1014
+ if t in ("switch_expression", "switch_statement"):
1015
+ return self._process_switch(stmt, cur, exit_b, source, break_target, continue_target, labels, label)
1016
+
1017
+ if t in ("try_statement", "try_with_resources_statement"):
1018
+ return self._process_try(stmt, cur, exit_b, source, break_target, continue_target, labels)
1019
+
1020
+ if t == "synchronized_statement":
1021
+ return self._process_synchronized(stmt, cur, exit_b, source, break_target, continue_target, labels)
1022
+
1023
+ if t == "labeled_statement":
1024
+ # Only threaded into the five loop/switch handlers above (via
1025
+ # their own `label` parameter) — a label on any other statement
1026
+ # kind is dropped; see module docstring.
1027
+ label_node = next((c for c in stmt.children if c.type == "identifier"), None)
1028
+ inner = next((c for c in reversed(stmt.children) if c.is_named), None)
1029
+ if inner is not None:
1030
+ label_text = _text(label_node, source) if label_node is not None else None
1031
+ return self._process_stmt(
1032
+ inner, cur, exit_b, source, break_target, continue_target, labels, label_text
1033
+ )
1034
+ return cur
1035
+
1036
+ if t == "block":
1037
+ return self._process_stmts(_real_children(stmt), cur, exit_b, source, break_target, continue_target, labels)
1038
+
1039
+ if t in _TYPE_DECL_TYPES:
1040
+ # Local/nested type declaration — opaque, no defs/uses of its own.
1041
+ return cur
1042
+
1043
+ # ── fallback: conservative — collect all identifiers as uses ──────
1044
+ cur.events.extend(_uses_of(stmt))
1045
+ return cur
1046
+
1047
+ # ── compound statement helpers ────────────────────────────────────────
1048
+
1049
+ def _process_if(
1050
+ self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
1051
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
1052
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None,
1053
+ ) -> Optional[CFGBlock]:
1054
+ cond = node.child_by_field_name("condition")
1055
+ if cond is not None:
1056
+ cur.events.extend(_uses_of(cond))
1057
+
1058
+ merge = self._new_block()
1059
+
1060
+ then_body = node.child_by_field_name("consequence")
1061
+ if then_body is not None:
1062
+ then_b = self._new_block()
1063
+ self._link(cur, then_b)
1064
+ tail = self._process_stmts(
1065
+ _body_stmts(then_body), then_b, exit_b, source, break_target, continue_target, labels
1066
+ )
1067
+ if tail is not None:
1068
+ self._link(tail, merge)
1069
+ else:
1070
+ self._link(cur, merge)
1071
+
1072
+ alt = node.child_by_field_name("alternative")
1073
+ if alt is None:
1074
+ self._link(cur, merge)
1075
+ elif alt.type == "if_statement":
1076
+ alt_b = self._new_block()
1077
+ self._link(cur, alt_b)
1078
+ tail = self._process_stmt(alt, alt_b, exit_b, source, break_target, continue_target, labels)
1079
+ if tail is not None:
1080
+ self._link(tail, merge)
1081
+ else:
1082
+ alt_b = self._new_block()
1083
+ self._link(cur, alt_b)
1084
+ tail = self._process_stmts(
1085
+ _body_stmts(alt), alt_b, exit_b, source, break_target, continue_target, labels
1086
+ )
1087
+ if tail is not None:
1088
+ self._link(tail, merge)
1089
+
1090
+ return merge
1091
+
1092
+ def _process_for(
1093
+ self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
1094
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
1095
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
1096
+ ) -> Optional[CFGBlock]:
1097
+ # init runs once, unconditionally, before the header — not part of
1098
+ # the loop body, so it keeps whatever break/continue target this
1099
+ # `for` statement itself is nested in (irrelevant in valid Java: an
1100
+ # `init` clause can't contain a bare `break`/`continue`).
1101
+ for init in _fields(node, "init"):
1102
+ cur = self._process_stmt(init, cur, exit_b, source, break_target, continue_target, labels)
1103
+ if cur is None:
1104
+ return None
1105
+
1106
+ header = self._new_block()
1107
+ body_b = self._new_block()
1108
+ # `continue`'s target is a dedicated block preceding the `update`
1109
+ # clause (not the body's own tail block) — Java's `for`-loop
1110
+ # `continue` runs `update` before the condition is re-checked, and
1111
+ # a `continue` may fire before the body's tail is known (e.g. from
1112
+ # inside a nested `if`).
1113
+ update_b = self._new_block()
1114
+ after = self._new_block()
1115
+ self._link(cur, header)
1116
+
1117
+ cond = node.child_by_field_name("condition")
1118
+ if cond is not None:
1119
+ header.events.extend(_uses_of(cond))
1120
+
1121
+ self._link(header, body_b)
1122
+ self._link(header, after)
1123
+
1124
+ # If this `for` is itself directly labeled (`outer: for (...) {}`),
1125
+ # `break outer;`/`continue outer;` anywhere inside its body —
1126
+ # including past a nested loop — must resolve to *this* loop's own
1127
+ # targets, not whatever loop most closely encloses them.
1128
+ body_labels = {**(labels or {}), label: (after, update_b)} if label else (labels or {})
1129
+
1130
+ body = node.child_by_field_name("body")
1131
+ tail = self._process_stmts(_body_stmts(body), body_b, exit_b, source, after, update_b, body_labels)
1132
+ if tail is not None:
1133
+ self._link(tail, update_b)
1134
+
1135
+ upd_tail = update_b
1136
+ for upd in _fields(node, "update"):
1137
+ upd_tail = self._process_stmt(upd, upd_tail, exit_b, source, break_target, continue_target, labels)
1138
+ if upd_tail is None:
1139
+ break
1140
+ if upd_tail is not None:
1141
+ self._link(upd_tail, header) # back-edge
1142
+
1143
+ return after
1144
+
1145
+ def _process_enhanced_for(
1146
+ self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
1147
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
1148
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
1149
+ ) -> Optional[CFGBlock]:
1150
+ header = self._new_block()
1151
+ body_b = self._new_block()
1152
+ after = self._new_block()
1153
+ self._link(cur, header)
1154
+
1155
+ value = node.child_by_field_name("value")
1156
+ if value is not None:
1157
+ header.events.extend(_uses_of(value))
1158
+
1159
+ name_node = node.child_by_field_name("name")
1160
+ if name_node is not None:
1161
+ taint = self._taint_of(value, source)
1162
+ header.events.append(
1163
+ self._new_def(_text(name_node, source), _node_pos(name_node), False, taint)
1164
+ )
1165
+
1166
+ self._link(header, body_b)
1167
+ self._link(header, after)
1168
+
1169
+ # `continue` re-fetches the next element — there's no `update`
1170
+ # clause, so it targets the header directly.
1171
+ body_labels = {**(labels or {}), label: (after, header)} if label else (labels or {})
1172
+
1173
+ body = node.child_by_field_name("body")
1174
+ tail = self._process_stmts(_body_stmts(body), body_b, exit_b, source, after, header, body_labels)
1175
+ if tail is not None:
1176
+ self._link(tail, header) # back-edge
1177
+
1178
+ return after
1179
+
1180
+ def _process_while(
1181
+ self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
1182
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
1183
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
1184
+ ) -> Optional[CFGBlock]:
1185
+ header = self._new_block()
1186
+ body_b = self._new_block()
1187
+ after = self._new_block()
1188
+ self._link(cur, header)
1189
+
1190
+ cond = node.child_by_field_name("condition")
1191
+ if cond is not None:
1192
+ header.events.extend(_uses_of(cond))
1193
+
1194
+ self._link(header, body_b)
1195
+ self._link(header, after)
1196
+
1197
+ # `continue` jumps straight to the condition re-check.
1198
+ body_labels = {**(labels or {}), label: (after, header)} if label else (labels or {})
1199
+
1200
+ body = node.child_by_field_name("body")
1201
+ tail = self._process_stmts(_body_stmts(body), body_b, exit_b, source, after, header, body_labels)
1202
+ if tail is not None:
1203
+ self._link(tail, header) # back-edge
1204
+
1205
+ return after
1206
+
1207
+ def _process_do_while(
1208
+ self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
1209
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
1210
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
1211
+ ) -> Optional[CFGBlock]:
1212
+ body_b = self._new_block()
1213
+ header = self._new_block()
1214
+ after = self._new_block()
1215
+ self._link(cur, body_b) # body always runs at least once
1216
+
1217
+ # `continue` in a `do`-loop still goes to the condition check
1218
+ # (`header`), which here comes *after* the body syntactically.
1219
+ body_labels = {**(labels or {}), label: (after, header)} if label else (labels or {})
1220
+
1221
+ body = node.child_by_field_name("body")
1222
+ tail = self._process_stmts(_body_stmts(body), body_b, exit_b, source, after, header, body_labels)
1223
+ if tail is not None:
1224
+ self._link(tail, header)
1225
+
1226
+ cond = node.child_by_field_name("condition")
1227
+ if cond is not None:
1228
+ header.events.extend(_uses_of(cond))
1229
+
1230
+ self._link(header, body_b) # back-edge
1231
+ self._link(header, after)
1232
+
1233
+ return after
1234
+
1235
+ def _process_switch(
1236
+ self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
1237
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
1238
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
1239
+ ) -> Optional[CFGBlock]:
1240
+ cond = node.child_by_field_name("condition")
1241
+ if cond is not None:
1242
+ cur.events.extend(_uses_of(cond))
1243
+
1244
+ merge = self._new_block()
1245
+ switch_body = node.child_by_field_name("body")
1246
+ groups: List[Node] = []
1247
+ if switch_body is not None:
1248
+ groups = [c for c in switch_body.children
1249
+ if c.type in ("switch_block_statement_group", "switch_rule")]
1250
+
1251
+ # Conservative fallback: no case (including `default`, if present) is
1252
+ # guaranteed to execute from a static analysis's point of view, so a
1253
+ # pre-switch def must still be considered reaching the merge block —
1254
+ # same "extra reachable pair is safe, a missed one is not" philosophy
1255
+ # `PythonCFGBuilder._process_if`'s no-else fallthrough edge follows.
1256
+ self._link(cur, merge)
1257
+
1258
+ # `break` inside a case targets this switch's own merge block (the
1259
+ # statement after the switch) — a plain, unlabeled `continue`,
1260
+ # though, isn't caught by the switch at all; it still refers to
1261
+ # whatever loop lexically encloses the switch, so `continue_target`
1262
+ # passes through unchanged. If this switch is itself labeled
1263
+ # (`outer: switch (...) {}`), `break outer;`/`continue outer;` from
1264
+ # deeper inside must resolve to *this* switch's targets specifically.
1265
+ body_labels = {**(labels or {}), label: (merge, continue_target)} if label else (labels or {})
1266
+
1267
+ for group in groups:
1268
+ group_b = self._new_block()
1269
+ self._link(cur, group_b) # conservative: every case reachable
1270
+ stmts = [c for c in group.children
1271
+ if c.is_named and c.type != "switch_label"]
1272
+ tail = self._process_stmts(stmts, group_b, exit_b, source, merge, continue_target, body_labels)
1273
+ if tail is not None:
1274
+ self._link(tail, merge)
1275
+
1276
+ return merge
1277
+
1278
+ def _process_try(
1279
+ self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
1280
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
1281
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None,
1282
+ ) -> Optional[CFGBlock]:
1283
+ merge = self._new_block()
1284
+
1285
+ finally_clause = next((c for c in node.children if c.type == "finally_clause"), None)
1286
+ fin_body = None
1287
+ if finally_clause is not None:
1288
+ fin_body = next((c for c in finally_clause.children if c.type == "block"), None)
1289
+
1290
+ # An active `finally` intercepts every abrupt completion (`return`,
1291
+ # `throw`, `break`, `continue`) from the try body and catch handlers
1292
+ # before it actually reaches whichever target that completion was
1293
+ # headed for (function exit, an enclosing loop/switch's break point,
1294
+ # an enclosing loop's continue point). Each distinct target that's
1295
+ # actually active here gets its own copy of the finally body — the
1296
+ # continuations differ (chasing `exit_b` vs. `break_target` vs.
1297
+ # `continue_target`), and this also chains correctly through
1298
+ # arbitrarily nested `try`/`finally`, since a target this method
1299
+ # itself received may already be *another* enclosing try's own
1300
+ # relay block. A `return`/`break`/`continue` written directly inside
1301
+ # the finally body itself is unaffected — it still resolves against
1302
+ # this try's own `exit_b`/`break_target`/`continue_target`, per
1303
+ # normal lexical scoping, not the relayed one.
1304
+ def _relay(target: Optional[CFGBlock]) -> Optional[CFGBlock]:
1305
+ if target is None or fin_body is None:
1306
+ return target
1307
+ relay_b = self._new_block()
1308
+ tail = self._process_stmts(
1309
+ _real_children(fin_body), relay_b, exit_b, source, break_target, continue_target, labels
1310
+ )
1311
+ if tail is not None:
1312
+ self._link(tail, target)
1313
+ return relay_b
1314
+
1315
+ abrupt_exit = _relay(exit_b)
1316
+ abrupt_break = _relay(break_target)
1317
+ abrupt_continue = _relay(continue_target)
1318
+ # A labeled break/continue escaping this try must run `finally`
1319
+ # too, same as the unlabeled/exit cases above — relay every active
1320
+ # label's own (break, continue) target pair the same way.
1321
+ abrupt_labels = (
1322
+ {name: (_relay(brk), _relay(cont)) for name, (brk, cont) in labels.items()}
1323
+ if labels else labels
1324
+ )
1325
+
1326
+ pre = cur
1327
+ resources = node.child_by_field_name("resources")
1328
+ if resources is not None:
1329
+ res_b = self._new_block()
1330
+ self._link(cur, res_b)
1331
+ for res in resources.children:
1332
+ if res.type != "resource":
1333
+ continue
1334
+ name_node = res.child_by_field_name("name")
1335
+ value = res.child_by_field_name("value")
1336
+ if value is not None:
1337
+ res_b.events.extend(_uses_of(value))
1338
+ if name_node is not None:
1339
+ taint = self._taint_of(value, source)
1340
+ res_b.events.append(
1341
+ self._new_def(_text(name_node, source), _node_pos(name_node), False, taint)
1342
+ )
1343
+ pre = res_b
1344
+
1345
+ body = node.child_by_field_name("body")
1346
+ if body is not None:
1347
+ try_b = self._new_block()
1348
+ self._link(pre, try_b)
1349
+ tail = self._process_stmts(
1350
+ _real_children(body), try_b, abrupt_exit, source, abrupt_break, abrupt_continue, abrupt_labels
1351
+ )
1352
+ if tail is not None:
1353
+ self._link(tail, merge)
1354
+ else:
1355
+ self._link(pre, merge)
1356
+
1357
+ for child in node.children:
1358
+ if child.type != "catch_clause":
1359
+ continue
1360
+ exc_b = self._new_block()
1361
+ self._link(pre, exc_b) # conservative: reachable from try entry
1362
+ param = next(
1363
+ (c for c in child.children if c.type == "catch_formal_parameter"), None
1364
+ )
1365
+ if param is not None:
1366
+ name_node = param.child_by_field_name("name")
1367
+ if name_node is not None:
1368
+ exc_b.events.append(
1369
+ self._new_def(_text(name_node, source), _node_pos(name_node), False, None)
1370
+ )
1371
+ exc_body = child.child_by_field_name("body")
1372
+ if exc_body is not None:
1373
+ tail = self._process_stmts(
1374
+ _real_children(exc_body), exc_b, abrupt_exit, source, abrupt_break, abrupt_continue,
1375
+ abrupt_labels,
1376
+ )
1377
+ if tail is not None:
1378
+ self._link(tail, merge)
1379
+ else:
1380
+ self._link(exc_b, merge)
1381
+
1382
+ if fin_body is not None:
1383
+ fin_b = self._new_block()
1384
+ self._link(merge, fin_b)
1385
+ tail = self._process_stmts(
1386
+ _real_children(fin_body), fin_b, exit_b, source, break_target, continue_target, labels
1387
+ )
1388
+ if tail is not None:
1389
+ merge = tail
1390
+
1391
+ return merge
1392
+
1393
+ def _process_synchronized(
1394
+ self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
1395
+ break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
1396
+ labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None,
1397
+ ) -> Optional[CFGBlock]:
1398
+ lock = next((c for c in node.children if c.type == "parenthesized_expression"), None)
1399
+ if lock is not None:
1400
+ cur.events.extend(_uses_of(lock))
1401
+ body = node.child_by_field_name("body")
1402
+ return self._process_stmts(_body_stmts(body), cur, exit_b, source, break_target, continue_target, labels)
1403
+
1404
+
1405
+ def default_java_cfg_config() -> CFGConfig:
1406
+ """The real Java `CFGConfig` — returned by `JavaBackend.cfg_config()`."""
1407
+ from adl_drift.lang.java.taint import default_java_taint_strategy
1408
+
1409
+ return CFGConfig(
1410
+ taint_sources=TAINT_SOURCES,
1411
+ taint_source_methods=dict(TAINT_SOURCE_METHODS),
1412
+ taint_source_param_annotations=dict(TAINT_SOURCE_PARAM_ANNOTATIONS),
1413
+ sink_methods=dict(SINK_METHODS),
1414
+ sink_constructors=dict(SINK_CONSTRUCTORS),
1415
+ cfg_builder_factory=lambda config: JavaCFGBuilder(config),
1416
+ taint_strategy_factory=lambda config: default_java_taint_strategy(config),
1417
+ )
1418
+
1419
+
1420
+ def analyze_function(func_node: Node, source: bytes, function_id: str):
1421
+ """Convenience wrapper mirroring `graph.cfg.analyze_function`, pinned to
1422
+ Java's `CFGConfig` — used directly by Java CFG unit tests."""
1423
+ from adl_drift.graph.cfg import analyze_function as _analyze_function
1424
+ return _analyze_function(func_node, source, function_id, default_java_cfg_config())