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.
- package/INSTALL_TOOLKIT.md +163 -0
- package/README.md +269 -2
- package/adl_drift/__init__.py +6 -0
- package/adl_drift/cli/__init__.py +1 -0
- package/adl_drift/cli/commands.py +993 -0
- package/adl_drift/cli/main.py +744 -0
- package/adl_drift/cortex/__init__.py +1 -0
- package/adl_drift/cortex/embeddings.py +96 -0
- package/adl_drift/cortex/memory.py +1749 -0
- package/adl_drift/cortex/patterns.py +89 -0
- package/adl_drift/cortex/telemetry.py +192 -0
- package/adl_drift/detectors/__init__.py +1 -0
- package/adl_drift/detectors/async_patterns.py +115 -0
- package/adl_drift/detectors/base.py +121 -0
- package/adl_drift/detectors/config.py +122 -0
- package/adl_drift/detectors/data_access.py +128 -0
- package/adl_drift/detectors/decorators.py +122 -0
- package/adl_drift/detectors/dynamic_patterns.py +260 -0
- package/adl_drift/detectors/errors.py +134 -0
- package/adl_drift/detectors/imports.py +132 -0
- package/adl_drift/detectors/logging_patterns.py +108 -0
- package/adl_drift/detectors/routes.py +257 -0
- package/adl_drift/detectors/security.py +236 -0
- package/adl_drift/detectors/taint.py +406 -0
- package/adl_drift/detectors/tests.py +95 -0
- package/adl_drift/detectors/type_conventions.py +81 -0
- package/adl_drift/graph/__init__.py +1 -0
- package/adl_drift/graph/builder.py +2670 -0
- package/adl_drift/graph/cfg.py +461 -0
- package/adl_drift/graph/cluster.py +40 -0
- package/adl_drift/graph/edges.py +86 -0
- package/adl_drift/graph/node.py +90 -0
- package/adl_drift/harness/__init__.py +10 -0
- package/adl_drift/harness/setup.py +832 -0
- package/adl_drift/harness/skills/adl-drift.md +25 -0
- package/adl_drift/harness/skills/drift-antigravity.md +19 -0
- package/adl_drift/harness/skills/drift-generic.md +19 -0
- package/adl_drift/harness/watch.py +84 -0
- package/adl_drift/lang/__init__.py +8 -0
- package/adl_drift/lang/base.py +464 -0
- package/adl_drift/lang/java/__init__.py +6 -0
- package/adl_drift/lang/java/backend.py +97 -0
- package/adl_drift/lang/java/cfg.py +1424 -0
- package/adl_drift/lang/java/config.py +707 -0
- package/adl_drift/lang/java/detectors/__init__.py +29 -0
- package/adl_drift/lang/java/detectors/additional.py +445 -0
- package/adl_drift/lang/java/detectors/base.py +154 -0
- package/adl_drift/lang/java/detectors/data_access.py +148 -0
- package/adl_drift/lang/java/detectors/imports.py +53 -0
- package/adl_drift/lang/java/detectors/routes.py +159 -0
- package/adl_drift/lang/java/detectors/security.py +386 -0
- package/adl_drift/lang/java/detectors/tests.py +74 -0
- package/adl_drift/lang/java/extractor.py +582 -0
- package/adl_drift/lang/java/ids.py +99 -0
- package/adl_drift/lang/java/parser.py +49 -0
- package/adl_drift/lang/java/resolution/__init__.py +1 -0
- package/adl_drift/lang/java/resolution/constants.py +1619 -0
- package/adl_drift/lang/java/resolution/constructors.py +278 -0
- package/adl_drift/lang/java/resolution/context.py +305 -0
- package/adl_drift/lang/java/resolution/dispatch.py +540 -0
- package/adl_drift/lang/java/resolution/functional.py +1318 -0
- package/adl_drift/lang/java/resolution/generated.py +95 -0
- package/adl_drift/lang/java/resolution/model.py +153 -0
- package/adl_drift/lang/java/resolution/orchestration.py +866 -0
- package/adl_drift/lang/java/resolution/overloads.py +1275 -0
- package/adl_drift/lang/java/resolution/scopes.py +1111 -0
- package/adl_drift/lang/java/resolution/types.py +2947 -0
- package/adl_drift/lang/java/resolver.py +1401 -0
- package/adl_drift/lang/java/scopes.py +69 -0
- package/adl_drift/lang/java/taint.py +500 -0
- package/adl_drift/lang/java/type_provenance.py +342 -0
- package/adl_drift/lang/python/__init__.py +8 -0
- package/adl_drift/lang/python/backend.py +94 -0
- package/adl_drift/lang/python/cfg.py +537 -0
- package/adl_drift/lang/python/extractor.py +454 -0
- package/adl_drift/lang/python/parser.py +41 -0
- package/adl_drift/lang/python/resolver.py +1789 -0
- package/adl_drift/lang/python/taint.py +365 -0
- package/adl_drift/lang/registry.py +117 -0
- package/adl_drift/parser/__init__.py +1 -0
- package/adl_drift/parser/cache.py +38 -0
- package/adl_drift/parser/core.py +33 -0
- package/adl_drift/parser/extractor.py +17 -0
- package/adl_drift/parser/literal_eval.py +326 -0
- package/adl_drift/scanner/__init__.py +1 -0
- package/adl_drift/scanner/discovery.py +338 -0
- package/adl_drift/scanner/incremental.py +261 -0
- package/adl_drift/scanner/parallel.py +224 -0
- package/adl_drift/server/__init__.py +1 -0
- package/adl_drift/server/handlers.py +2849 -0
- package/adl_drift/server/mcp_server.py +127 -0
- package/adl_drift/server/mcp_stdio.py +335 -0
- package/adl_drift/server/protocol.py +71 -0
- package/adl_drift/server/static/index.html +696 -0
- package/adl_drift/server/web.py +350 -0
- package/adl_drift/utils/__init__.py +1 -0
- package/adl_drift/utils/config.py +77 -0
- package/adl_drift/utils/fswalk.py +52 -0
- package/adl_drift/utils/types.py +345 -0
- package/bin/adl-drift.mjs +35 -0
- package/bin/adl-toolkit.mjs +488 -0
- package/package.json +29 -4
- package/pyproject.toml +49 -0
- package/scripts/install_toolkit.py +3584 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
"""Execution-order-preserving orchestration for Java call resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import logging
|
|
7
|
+
import multiprocessing
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
12
|
+
from concurrent.futures.process import BrokenProcessPool
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Set, Tuple
|
|
15
|
+
|
|
16
|
+
from adl_drift.graph.edges import EdgeType, GraphEdge
|
|
17
|
+
from adl_drift.graph.node import GraphNode, NodeType
|
|
18
|
+
from adl_drift.lang.base import ParseResult
|
|
19
|
+
from adl_drift.lang.java.resolution.constants import (
|
|
20
|
+
_ENCLOSING_TYPE_CST_TYPES,
|
|
21
|
+
_INIT_BLOCK_PARENTS,
|
|
22
|
+
)
|
|
23
|
+
from adl_drift.lang.java.resolution.model import ImportContext as _ImportContext
|
|
24
|
+
from adl_drift.utils.types import AnalysisResult
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _ProcessEdgeCollector:
|
|
30
|
+
"""Worker-local edge sink over the parent's read-only graph snapshot."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, base) -> None:
|
|
33
|
+
self._base = base
|
|
34
|
+
self._nodes = base._nodes
|
|
35
|
+
self.graph = base.graph
|
|
36
|
+
# The worker must observe edges it has already emitted. Reading the
|
|
37
|
+
# parent's key set alone would emit duplicate dependency edges from
|
|
38
|
+
# two call sites in the same file and make `edges_added` inaccurate.
|
|
39
|
+
self._edge_keys = set(base._edge_keys)
|
|
40
|
+
self._occurrence_keys = set(base._occurrence_keys)
|
|
41
|
+
self.edges: List[GraphEdge] = []
|
|
42
|
+
self.occurrences: List[GraphEdge] = []
|
|
43
|
+
|
|
44
|
+
def _add_edge(self, edge: GraphEdge) -> None:
|
|
45
|
+
key = (edge.source, edge.target, edge.edge_type)
|
|
46
|
+
if key in self._edge_keys:
|
|
47
|
+
return
|
|
48
|
+
self._edge_keys.add(key)
|
|
49
|
+
self.edges.append(edge)
|
|
50
|
+
|
|
51
|
+
def _record_edge_occurrence(self, edge: GraphEdge) -> None:
|
|
52
|
+
key = (
|
|
53
|
+
edge.source, edge.target, edge.edge_type, edge.file_path,
|
|
54
|
+
edge.start_byte if edge.start_byte is not None else edge.line,
|
|
55
|
+
edge.end_byte if edge.end_byte is not None else edge.column,
|
|
56
|
+
)
|
|
57
|
+
if key in self._occurrence_keys:
|
|
58
|
+
return
|
|
59
|
+
self._occurrence_keys.add(key)
|
|
60
|
+
self.occurrences.append(edge)
|
|
61
|
+
|
|
62
|
+
def __getattr__(self, name: str):
|
|
63
|
+
return getattr(self._base, name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _edge_order(edge: GraphEdge) -> tuple:
|
|
67
|
+
return (
|
|
68
|
+
edge.file_path or "", edge.line if edge.line is not None else -1,
|
|
69
|
+
edge.column if edge.column is not None else -1,
|
|
70
|
+
edge.start_byte if edge.start_byte is not None else -1,
|
|
71
|
+
edge.end_byte if edge.end_byte is not None else -1,
|
|
72
|
+
edge.source, edge.target, edge.edge_type.name,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Set immediately before forking. ParseResult/Tree objects are intentionally
|
|
77
|
+
# not sent through the process queue (they are not pickleable); Linux fork gives
|
|
78
|
+
# workers a copy-on-write read-only view of the completed resolution index.
|
|
79
|
+
_PROCESS_RESOLVER = None
|
|
80
|
+
_PROCESS_PARSES = None
|
|
81
|
+
_PROCESS_RESULTS = None
|
|
82
|
+
_PROCESS_FUNC_IDS = None
|
|
83
|
+
|
|
84
|
+
# A failed pool has not mutated the parent graph: workers return data only.
|
|
85
|
+
# Fall back to the existing sequential path rather than making an optional
|
|
86
|
+
# speed-up a new availability requirement.
|
|
87
|
+
_PROCESS_POOL_FAILURES = (BrokenProcessPool, OSError, RuntimeError)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve_one_forked(file_path: Path):
|
|
91
|
+
resolver = copy.copy(_PROCESS_RESOLVER)
|
|
92
|
+
collector = _ProcessEdgeCollector(_PROCESS_RESOLVER.builder)
|
|
93
|
+
resolver.builder = collector
|
|
94
|
+
resolver._method_cst_cache = {}
|
|
95
|
+
resolver._cst_index_cache = {}
|
|
96
|
+
resolver._method_facts_cache = {}
|
|
97
|
+
resolver._local_scopes_cache = {}
|
|
98
|
+
resolver._declaring_class_cache = {}
|
|
99
|
+
resolver._members_by_name_cache = {}
|
|
100
|
+
resolver._erased_signature_cache = {}
|
|
101
|
+
resolver._param_ctx_for_cache = {}
|
|
102
|
+
resolver._declared_type_memo = {}
|
|
103
|
+
resolver._class_ancestors_cache = {}
|
|
104
|
+
resolver._contains_children_cache = {}
|
|
105
|
+
resolver.fanout_telemetry = {
|
|
106
|
+
"mode": resolver._fanout_mode, "cap": resolver._fanout_cap,
|
|
107
|
+
"candidate_counts": [], "emitted_counts": [], "cap_skipped": 0,
|
|
108
|
+
}
|
|
109
|
+
parse = _PROCESS_PARSES[file_path]
|
|
110
|
+
result = _PROCESS_RESULTS.get(file_path)
|
|
111
|
+
if result is not None:
|
|
112
|
+
resolver._resolve_project_call_sites_sequential(
|
|
113
|
+
{file_path: parse}, {file_path: result}, _PROCESS_FUNC_IDS, 0,
|
|
114
|
+
)
|
|
115
|
+
return collector.edges, collector.occurrences, resolver.fanout_telemetry
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ResolutionOrchestrationMixin:
|
|
119
|
+
def _run_resolution_passes(
|
|
120
|
+
self,
|
|
121
|
+
parses: Dict[Path, ParseResult],
|
|
122
|
+
results: Dict[Path, AnalysisResult],
|
|
123
|
+
) -> int:
|
|
124
|
+
"""Emit CALLS (method_invocation) / INSTANTIATES
|
|
125
|
+
(object_creation_expression) edges for every call site.
|
|
126
|
+
|
|
127
|
+
No AWAITS (Java has no `await` expression) and no P1.8 REFERENCES
|
|
128
|
+
pass here — field/variable *reads* outside of a call are not part of
|
|
129
|
+
this issue's scope (P4.4 is call resolution; a Java REFERENCES pass
|
|
130
|
+
would be an additive follow-up, not required by PROBLEMS.md's "what
|
|
131
|
+
the fix looks like" list for this issue).
|
|
132
|
+
"""
|
|
133
|
+
func_id_by_position, edges_added = self._build_resolution_indexes(
|
|
134
|
+
parses, results,
|
|
135
|
+
)
|
|
136
|
+
return self._resolve_project_call_sites(
|
|
137
|
+
parses, results, func_id_by_position, edges_added,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _build_resolution_indexes(
|
|
141
|
+
self, parses: Dict[Path, ParseResult], results: Dict[Path, AnalysisResult],
|
|
142
|
+
) -> Tuple[Dict[Tuple[str, int, int], str], int]:
|
|
143
|
+
# Per-run caches (see resolver.__init__): the graph's node set and
|
|
144
|
+
# CONTAINS edges are fixed for the whole run, the method-CST index
|
|
145
|
+
# keys on the tree object, and the declared-type memo is armed only
|
|
146
|
+
# at the end of this method once every index it reads exists.
|
|
147
|
+
self._method_cst_cache = {}
|
|
148
|
+
self._cst_index_cache = {}
|
|
149
|
+
self._method_facts_cache = {}
|
|
150
|
+
self._local_scopes_cache = {}
|
|
151
|
+
self._declaring_class_cache = {}
|
|
152
|
+
self._members_by_name_cache = {}
|
|
153
|
+
self._erased_signature_cache = {}
|
|
154
|
+
self._param_ctx_for_cache = {}
|
|
155
|
+
self._declared_type_memo = None
|
|
156
|
+
self._file_import_map_ids = frozenset()
|
|
157
|
+
by_module_symbol: Dict[str, Dict[str, str]] = defaultdict(dict)
|
|
158
|
+
field_types: Dict[str, Dict[str, str]] = defaultdict(dict)
|
|
159
|
+
field_nodes: Dict[str, Dict[str, str]] = defaultdict(dict)
|
|
160
|
+
# Keyed by (file, line, column) — not just (file, line). Two type
|
|
161
|
+
# declarations routinely start on the same *line* (`} class Box<T> {`
|
|
162
|
+
# closing a preceding class, or compact/generated/minified source),
|
|
163
|
+
# and a line-only key silently collides, dropping whichever
|
|
164
|
+
# declaration loses the last-write-wins race (F-02: `Box`'s own
|
|
165
|
+
# `<T>` type parameters attributed to an unrelated class sharing its
|
|
166
|
+
# line, breaking generic narrowing with no warning). Two
|
|
167
|
+
# declarations can never share a start *column* too, so the triple
|
|
168
|
+
# is collision-free.
|
|
169
|
+
class_id_by_position: Dict[Tuple[str, int, int], str] = {}
|
|
170
|
+
for nid, node in self.builder._nodes.items():
|
|
171
|
+
if node.language != "java":
|
|
172
|
+
continue
|
|
173
|
+
if node.node_type == NodeType.CLASS:
|
|
174
|
+
by_module_symbol[node.module][node.name] = nid
|
|
175
|
+
class_id_by_position[(node.file_path, node.line, node.column)] = nid
|
|
176
|
+
elif node.node_type == NodeType.VARIABLE and node.contained_in and node.signature:
|
|
177
|
+
class_id = self._declaring_class_id(nid)
|
|
178
|
+
if class_id is None:
|
|
179
|
+
continue
|
|
180
|
+
field_types[class_id][node.name] = node.signature
|
|
181
|
+
field_nodes[class_id][node.name] = nid
|
|
182
|
+
self._by_module_symbol = by_module_symbol
|
|
183
|
+
self._field_types = field_types
|
|
184
|
+
self._field_nodes = field_nodes
|
|
185
|
+
# P4.12 reverse hierarchy index, built off the now-complete node/edge
|
|
186
|
+
# graph. The `@Bean` factory list also needs `self._file_ctx` (a
|
|
187
|
+
# `@Bean` return type resolves against its declaring file's imports),
|
|
188
|
+
# so it is built lower down, once `file_ctx` exists.
|
|
189
|
+
self._subtypes = self._build_subtype_index()
|
|
190
|
+
|
|
191
|
+
param_type_lists: Dict[str, List[str]] = {}
|
|
192
|
+
# Same (file, line, column) reasoning as `class_id_by_position`
|
|
193
|
+
# above: two methods routinely share a line (`void a() {} void b()
|
|
194
|
+
# {}`), and a line-only key drops one of them from `_iter_method_cst`
|
|
195
|
+
# entirely — not a narrowing miss but the caller's whole call-site
|
|
196
|
+
# scan (and constructor/setter DI indexing) silently skipped.
|
|
197
|
+
func_id_by_position: Dict[Tuple[str, int, int], str] = {}
|
|
198
|
+
file_ctx: Dict[str, _ImportContext] = {}
|
|
199
|
+
static_wildcard_by_file: Dict[str, List[str]] = {}
|
|
200
|
+
for path, ar in results.items():
|
|
201
|
+
mod = self.builder._module_for_result(ar)
|
|
202
|
+
# Each candidate method's parameter types must resolve against
|
|
203
|
+
# *its own declaring file's* imports, not whichever call site
|
|
204
|
+
# happens to be resolving against it right now — see
|
|
205
|
+
# `_ImportContext`. Keyed by file path, not by `mod`: Java's
|
|
206
|
+
# module id is the declared `package`, so several files
|
|
207
|
+
# routinely share one module, each with its own import list.
|
|
208
|
+
file_ctx[str(path)] = _ImportContext(
|
|
209
|
+
mod,
|
|
210
|
+
self.builder._import_map_for(ar, mod),
|
|
211
|
+
self.builder._wildcard_packages_for(ar, mod),
|
|
212
|
+
)
|
|
213
|
+
static_wildcard_by_file[str(path)] = (
|
|
214
|
+
self.builder._static_wildcard_classes_for(ar, mod)
|
|
215
|
+
)
|
|
216
|
+
for sym in ar.symbols:
|
|
217
|
+
if sym.kind != "function":
|
|
218
|
+
continue
|
|
219
|
+
fid = self.builder._symbol_to_id(sym, mod)
|
|
220
|
+
param_type_lists[fid] = list(sym.param_types.values())
|
|
221
|
+
func_id_by_position[
|
|
222
|
+
(str(sym.file_path), sym.position.line, sym.position.column)
|
|
223
|
+
] = fid
|
|
224
|
+
param_type_lists.update(self._ensure_generated_member_stubs())
|
|
225
|
+
self._param_type_lists = param_type_lists
|
|
226
|
+
self._class_id_by_position = class_id_by_position
|
|
227
|
+
# Bug group 4 fix: materialize the synthetic `<init>`/`<clinit>`
|
|
228
|
+
# FUNCTION each class needs *before* `_resolve_project_call_sites`
|
|
229
|
+
# runs, the same way `_ensure_generated_member_stubs` above
|
|
230
|
+
# materializes `values()`/`valueOf()`/record accessors — those are
|
|
231
|
+
# real call *targets* the extractor never emits a Symbol for; these
|
|
232
|
+
# are real call *sources* (a field initializer or `static { }` block
|
|
233
|
+
# invoking something) it never emits one for either, so without a
|
|
234
|
+
# matching FUNCTION node here `_resolve_initializer_call_sites`
|
|
235
|
+
# would have nothing to attribute the edge to.
|
|
236
|
+
self._synthesize_initializer_members(parses, class_id_by_position)
|
|
237
|
+
self._method_type_param_bounds = self._collect_method_type_param_bounds(
|
|
238
|
+
parses, func_id_by_position,
|
|
239
|
+
)
|
|
240
|
+
self._file_ctx = file_ctx
|
|
241
|
+
self._static_wildcard_by_file = static_wildcard_by_file
|
|
242
|
+
declared_ancestors: Dict[Tuple[str, EdgeType], List[str]] = defaultdict(list)
|
|
243
|
+
for path, ar in results.items():
|
|
244
|
+
ctx = file_ctx.get(str(path))
|
|
245
|
+
if ctx is None:
|
|
246
|
+
continue
|
|
247
|
+
for sym in ar.symbols:
|
|
248
|
+
if sym.kind != "class":
|
|
249
|
+
continue
|
|
250
|
+
class_id = class_id_by_position.get(
|
|
251
|
+
(str(sym.file_path), sym.position.line, sym.position.column)
|
|
252
|
+
)
|
|
253
|
+
if class_id is None:
|
|
254
|
+
continue
|
|
255
|
+
for edge_type, parent_names in (
|
|
256
|
+
(EdgeType.INHERITS, sym.superclasses),
|
|
257
|
+
(EdgeType.IMPLEMENTS, sym.implements),
|
|
258
|
+
):
|
|
259
|
+
for parent_name in parent_names:
|
|
260
|
+
parent_id = self._resolve_declared_type(
|
|
261
|
+
parent_name, ctx.module, ctx.import_map,
|
|
262
|
+
ctx.wildcard_packages, class_id,
|
|
263
|
+
)
|
|
264
|
+
if parent_id and parent_id != class_id:
|
|
265
|
+
declared_ancestors[(class_id, edge_type)].append(parent_id)
|
|
266
|
+
self._declared_direct_ancestors = {
|
|
267
|
+
key: list(dict.fromkeys(values))
|
|
268
|
+
for key, values in declared_ancestors.items()
|
|
269
|
+
}
|
|
270
|
+
# `_class_ancestors`/`_contains_children`'s memoization caches must
|
|
271
|
+
# not outlive the graph state they were computed against (see
|
|
272
|
+
# resolution/overloads.py) — reset them here, alongside
|
|
273
|
+
# `_declared_direct_ancestors`, on every resolution pass. This runs
|
|
274
|
+
# after `_ensure_generated_member_stubs()` above, so the reset
|
|
275
|
+
# covers any CONTAINS edges that call added too.
|
|
276
|
+
self._class_ancestors_cache = {}
|
|
277
|
+
self._contains_children_cache = {}
|
|
278
|
+
# Rebuild from the exact resolver hierarchy (plus graph edges) now
|
|
279
|
+
# that the per-file import contexts exist.
|
|
280
|
+
self._subtypes = self._build_subtype_index()
|
|
281
|
+
self._local_annotation_type_names = self._collect_annotation_type_names(parses)
|
|
282
|
+
self._class_type_params = self._collect_class_type_params(
|
|
283
|
+
parses, class_id_by_position,
|
|
284
|
+
)
|
|
285
|
+
self._class_type_param_bounds = self._collect_class_type_param_bounds(
|
|
286
|
+
parses, class_id_by_position,
|
|
287
|
+
)
|
|
288
|
+
self._ancestor_type_args = self._collect_ancestor_type_args(
|
|
289
|
+
results, class_id_by_position, file_ctx,
|
|
290
|
+
)
|
|
291
|
+
self._field_allocation_types = self._build_field_allocation_index(
|
|
292
|
+
parses, func_id_by_position, class_id_by_position,
|
|
293
|
+
)
|
|
294
|
+
# `@Bean` factory beans (P4.12 DI): resolves each factory method's
|
|
295
|
+
# return type against its declaring file's imports, so it must run
|
|
296
|
+
# after `self._file_ctx` is populated above.
|
|
297
|
+
self._bean_method_candidates = self._build_bean_method_candidates()
|
|
298
|
+
self._di_injected_fields = {
|
|
299
|
+
**self._build_constructor_injection_index(
|
|
300
|
+
parses, results, func_id_by_position,
|
|
301
|
+
),
|
|
302
|
+
**self._build_setter_injection_index(
|
|
303
|
+
parses, results, func_id_by_position,
|
|
304
|
+
),
|
|
305
|
+
}
|
|
306
|
+
edge_count_before_callable_fields = len(self.builder._edges)
|
|
307
|
+
self._field_callable_targets = self._index_field_callable_targets(
|
|
308
|
+
parses, class_id_by_position, func_id_by_position,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
self._file_import_map_ids = frozenset(
|
|
312
|
+
id(ctx.import_map) for ctx in self._file_ctx.values()
|
|
313
|
+
)
|
|
314
|
+
self._declared_type_memo = {}
|
|
315
|
+
return (
|
|
316
|
+
func_id_by_position,
|
|
317
|
+
len(self.builder._edges) - edge_count_before_callable_fields,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def _resolve_project_call_sites(
|
|
321
|
+
self, parses: Dict[Path, ParseResult], results: Dict[Path, AnalysisResult],
|
|
322
|
+
func_id_by_position: Dict[Tuple[str, int, int], str], edges_added: int,
|
|
323
|
+
) -> int:
|
|
324
|
+
"""Resolve calls, optionally using a Linux fork process pool.
|
|
325
|
+
|
|
326
|
+
This experimental path is opt-in because it relies on ``fork`` to
|
|
327
|
+
share the non-pickleable Tree-sitter parse trees copy-on-write. Each
|
|
328
|
+
process has private resolver caches and edge buffers; only this main
|
|
329
|
+
process changes the real graph after deterministic sorting.
|
|
330
|
+
"""
|
|
331
|
+
if self._should_process_resolve(parses):
|
|
332
|
+
raw_workers = os.environ.get("DRIFT_JAVA_RESOLVE_WORKERS", "")
|
|
333
|
+
try:
|
|
334
|
+
workers = int(raw_workers) if raw_workers else min(os.cpu_count() or 1, 8)
|
|
335
|
+
except ValueError:
|
|
336
|
+
workers = min(os.cpu_count() or 1, 8)
|
|
337
|
+
workers = min(workers, len(parses))
|
|
338
|
+
if workers > 1:
|
|
339
|
+
global _PROCESS_RESOLVER, _PROCESS_PARSES, _PROCESS_RESULTS, _PROCESS_FUNC_IDS
|
|
340
|
+
_PROCESS_RESOLVER = self
|
|
341
|
+
_PROCESS_PARSES = parses
|
|
342
|
+
_PROCESS_RESULTS = results
|
|
343
|
+
_PROCESS_FUNC_IDS = func_id_by_position
|
|
344
|
+
try:
|
|
345
|
+
context = multiprocessing.get_context("fork")
|
|
346
|
+
with ProcessPoolExecutor(max_workers=workers, mp_context=context) as pool:
|
|
347
|
+
resolved = list(pool.map(_resolve_one_forked, sorted(parses)))
|
|
348
|
+
except _PROCESS_POOL_FAILURES as exc:
|
|
349
|
+
logger.warning(
|
|
350
|
+
"Java resolve process pool unavailable (%s: %s); "
|
|
351
|
+
"falling back to sequential resolution.",
|
|
352
|
+
type(exc).__name__, exc,
|
|
353
|
+
)
|
|
354
|
+
return self._resolve_project_call_sites_sequential(
|
|
355
|
+
parses, results, func_id_by_position, edges_added,
|
|
356
|
+
)
|
|
357
|
+
finally:
|
|
358
|
+
_PROCESS_RESOLVER = _PROCESS_PARSES = _PROCESS_RESULTS = _PROCESS_FUNC_IDS = None
|
|
359
|
+
for edges, occurrences, telemetry in resolved:
|
|
360
|
+
for edge in sorted(occurrences, key=_edge_order):
|
|
361
|
+
self.builder._record_edge_occurrence(edge)
|
|
362
|
+
for edge in sorted(edges, key=_edge_order):
|
|
363
|
+
key = (edge.source, edge.target, edge.edge_type)
|
|
364
|
+
if key in self.builder._edge_keys:
|
|
365
|
+
continue
|
|
366
|
+
self.builder._add_edge(edge)
|
|
367
|
+
# Workers mutate copy-on-write GraphNodes only. Keep
|
|
368
|
+
# the public node-level adjacency sets equivalent to
|
|
369
|
+
# the sequential resolver while the parent remains
|
|
370
|
+
# the sole writer of the real graph.
|
|
371
|
+
self.builder._nodes[edge.source].calls.add(edge.target)
|
|
372
|
+
self.builder._nodes[edge.target].called_by.add(edge.source)
|
|
373
|
+
self.fanout_telemetry["candidate_counts"].extend(telemetry["candidate_counts"])
|
|
374
|
+
self.fanout_telemetry["emitted_counts"].extend(telemetry["emitted_counts"])
|
|
375
|
+
self.fanout_telemetry["cap_skipped"] += telemetry["cap_skipped"]
|
|
376
|
+
return edges_added + sum(len(edges) for edges, _, _ in resolved)
|
|
377
|
+
return self._resolve_project_call_sites_sequential(
|
|
378
|
+
parses, results, func_id_by_position, edges_added,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def _should_process_resolve(parses: Dict[Path, ParseResult]) -> bool:
|
|
383
|
+
"""Whether the opt-in fork implementation is usable for this run.
|
|
384
|
+
|
|
385
|
+
`fork` is deliberately Linux-only. On spawn-only platforms a child
|
|
386
|
+
cannot inherit Tree-sitter trees or the resolver's read-only indexes,
|
|
387
|
+
and serialising that state is a different implementation strategy.
|
|
388
|
+
Never nest this pool inside another multiprocessing worker either.
|
|
389
|
+
"""
|
|
390
|
+
return (
|
|
391
|
+
os.environ.get("DRIFT_JAVA_PROCESS_RESOLVE") == "1"
|
|
392
|
+
and sys.platform.startswith("linux")
|
|
393
|
+
and "fork" in multiprocessing.get_all_start_methods()
|
|
394
|
+
and multiprocessing.parent_process() is None
|
|
395
|
+
and len(parses) >= 12
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def _resolve_project_call_sites_sequential(
|
|
399
|
+
self, parses: Dict[Path, ParseResult], results: Dict[Path, AnalysisResult],
|
|
400
|
+
func_id_by_position: Dict[Tuple[str, int, int], str], edges_added: int,
|
|
401
|
+
) -> int:
|
|
402
|
+
for file_path, parse in parses.items():
|
|
403
|
+
result = results.get(file_path)
|
|
404
|
+
if result is None:
|
|
405
|
+
continue
|
|
406
|
+
module_name = self.builder._module_for_result(result)
|
|
407
|
+
caller_ctx = self._file_ctx[str(file_path)]
|
|
408
|
+
import_map = caller_ctx.import_map
|
|
409
|
+
wildcard_packages = caller_ctx.wildcard_packages
|
|
410
|
+
static_wildcard_classes = self._static_wildcard_by_file.get(str(file_path), [])
|
|
411
|
+
source = parse.source_bytes
|
|
412
|
+
fp_str = str(file_path)
|
|
413
|
+
|
|
414
|
+
for func_cst, caller_id in self._iter_method_cst(
|
|
415
|
+
parse.tree.root_node, fp_str, func_id_by_position
|
|
416
|
+
):
|
|
417
|
+
if caller_id not in self.builder._nodes:
|
|
418
|
+
continue
|
|
419
|
+
caller_node = self.builder._nodes[caller_id]
|
|
420
|
+
enclosing_class_id = self._declaring_class_id(caller_id)
|
|
421
|
+
field_types_own = (
|
|
422
|
+
self._visible_bare_field_types(enclosing_class_id)
|
|
423
|
+
if enclosing_class_id else {}
|
|
424
|
+
)
|
|
425
|
+
body = func_cst.child_by_field_name("body")
|
|
426
|
+
if body is None:
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
own_param_types = dict(
|
|
430
|
+
zip(
|
|
431
|
+
self._param_names(func_cst, source),
|
|
432
|
+
self._param_type_lists.get(caller_id, []),
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
# A method declared in an anonymous/local class may read an
|
|
436
|
+
# effectively-final parameter of its lexical enclosing method.
|
|
437
|
+
# Add only that enclosing method's declared parameters; its
|
|
438
|
+
# locals and fields remain governed by their ordinary scopes.
|
|
439
|
+
captured_param_types = self._captured_param_types(
|
|
440
|
+
func_cst, source, fp_str, func_id_by_position,
|
|
441
|
+
)
|
|
442
|
+
captured_param_types.update(own_param_types)
|
|
443
|
+
own_param_types = captured_param_types
|
|
444
|
+
# P4.12 generic narrowing: `<T extends StripeGateway>` binds a
|
|
445
|
+
# method type parameter to a concrete upper bound, so a receiver
|
|
446
|
+
# declared `T` resolves to that bound (narrowing the fan-out, or
|
|
447
|
+
# pinning it to one concrete class) instead of dropping to no
|
|
448
|
+
# edge at all (no project class is named `T`).
|
|
449
|
+
type_param_bounds = self._collect_type_param_bounds(func_cst, source)
|
|
450
|
+
|
|
451
|
+
edges_added = self._resolve_caller_body(
|
|
452
|
+
body, source, module_name, enclosing_class_id, import_map,
|
|
453
|
+
wildcard_packages, static_wildcard_classes, field_types_own,
|
|
454
|
+
own_param_types, type_param_bounds, caller_node, caller_id,
|
|
455
|
+
fp_str, edges_added,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
edges_added = self._resolve_initializer_call_sites(
|
|
459
|
+
parse, source, module_name, fp_str, func_id_by_position,
|
|
460
|
+
import_map, wildcard_packages, static_wildcard_classes, edges_added,
|
|
461
|
+
)
|
|
462
|
+
return edges_added
|
|
463
|
+
|
|
464
|
+
def _resolve_caller_body(
|
|
465
|
+
self, body, source: bytes, module_name: str, enclosing_class_id,
|
|
466
|
+
import_map, wildcard_packages, static_wildcard_classes, field_types_own,
|
|
467
|
+
own_param_types, type_param_bounds, caller_node, caller_id: str,
|
|
468
|
+
fp_str: str, edges_added: int,
|
|
469
|
+
) -> int:
|
|
470
|
+
"""Shared per-caller resolution tail: scope inference over `body`
|
|
471
|
+
(a method/constructor's `block`, or — for a synthetic `<init>`/
|
|
472
|
+
`<clinit>` caller, see `_resolve_initializer_call_sites` — a bare
|
|
473
|
+
field-initializer expression or a `static`/instance-initializer
|
|
474
|
+
`block`) followed by method-reference and call-site resolution.
|
|
475
|
+
Factored out of `_resolve_project_call_sites` so both real methods
|
|
476
|
+
and synthetic initializer callers run through exactly one call
|
|
477
|
+
resolution path (PROBLEMS.md bug group 4)."""
|
|
478
|
+
local_types = self._collect_local_scopes(
|
|
479
|
+
body, source, field_types=field_types_own,
|
|
480
|
+
param_types=own_param_types,
|
|
481
|
+
)
|
|
482
|
+
self._collect_var_method_call_scopes(
|
|
483
|
+
body, source, module_name, enclosing_class_id, import_map,
|
|
484
|
+
wildcard_packages, local_types, own_param_types,
|
|
485
|
+
type_param_bounds,
|
|
486
|
+
)
|
|
487
|
+
self._collect_lambda_parameter_scopes(
|
|
488
|
+
body, source, module_name, enclosing_class_id, import_map,
|
|
489
|
+
wildcard_packages, local_types, own_param_types,
|
|
490
|
+
type_param_bounds, static_wildcard_classes,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# P4.13 target typing augments the existing receiver scopes:
|
|
494
|
+
# preserve the current branch's var/lambda inference, then add
|
|
495
|
+
# SAM-derived parameter scopes used by functional references.
|
|
496
|
+
self._add_lambda_param_scopes(
|
|
497
|
+
body, source, module_name, import_map, wildcard_packages,
|
|
498
|
+
local_types, own_param_types, field_types_own, caller_node,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
resolved_refs, edges_added = self._resolve_method_reference_pass(
|
|
502
|
+
body, source, module_name, enclosing_class_id, import_map,
|
|
503
|
+
wildcard_packages, local_types, own_param_types, field_types_own,
|
|
504
|
+
caller_node, caller_id, fp_str, edges_added,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
local_callable_targets = self._local_callable_targets(
|
|
508
|
+
body, source, resolved_refs,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
return self._resolve_call_site_pass(
|
|
512
|
+
body, source, module_name, enclosing_class_id, import_map,
|
|
513
|
+
wildcard_packages, static_wildcard_classes, local_types,
|
|
514
|
+
own_param_types, type_param_bounds, local_callable_targets,
|
|
515
|
+
caller_node, caller_id, fp_str, edges_added,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# ------------------------------------------------------------ bug group 4:
|
|
519
|
+
# field initializers / static+instance initializer blocks
|
|
520
|
+
|
|
521
|
+
def _iter_initializer_sites(
|
|
522
|
+
self, root, file_path_str: str, source: bytes, extractor,
|
|
523
|
+
):
|
|
524
|
+
"""Yield `(owner_node, body_node, kind)` for every field initializer
|
|
525
|
+
expression, `static { }` block, and bare instance-initializer `{ }`
|
|
526
|
+
block anywhere in the file — the three CST shapes JLS 8.6/8.7 fold
|
|
527
|
+
into a class's implicit instance-initializer (`kind="init"`) or
|
|
528
|
+
class-initializer (`kind="clinit"`). `owner_node` is the enclosing
|
|
529
|
+
declaration (`field_declaration`/`static_initializer`/the block
|
|
530
|
+
itself) used to find the nearest enclosing class and to anchor
|
|
531
|
+
`_captured_param_types`; `body_node` is what actually gets walked
|
|
532
|
+
for call sites (a field's initializer *expression*, not the whole
|
|
533
|
+
`field_declaration`, so a multi-declarator `int a = f(), b = g();`
|
|
534
|
+
yields one entry per initializer).
|
|
535
|
+
|
|
536
|
+
Deliberately not gated by `_NESTED_SCOPE_BOUNDARY`: unlike
|
|
537
|
+
`_collect_call_sites` (which walks *within* an already-found
|
|
538
|
+
caller's body and must stop at a nested method/class), this is a
|
|
539
|
+
fresh top-level scan for these three node types anywhere in the
|
|
540
|
+
file, including inside local/anonymous classes — exactly how
|
|
541
|
+
`JavaSymbolExtractor._build`'s own top-level `_walk` finds nested
|
|
542
|
+
`field_declaration`s today.
|
|
543
|
+
"""
|
|
544
|
+
for node in self._initializer_declarations(root, file_path_str):
|
|
545
|
+
if node.type == "field_declaration":
|
|
546
|
+
is_static = "static" in extractor._extract_modifiers(node, source)
|
|
547
|
+
kind = "clinit" if is_static else "init"
|
|
548
|
+
for child in node.children:
|
|
549
|
+
if child.type != "variable_declarator":
|
|
550
|
+
continue
|
|
551
|
+
value = child.child_by_field_name("value")
|
|
552
|
+
if value is not None:
|
|
553
|
+
yield node, value, kind
|
|
554
|
+
elif node.type == "static_initializer":
|
|
555
|
+
block = next((c for c in node.children if c.type == "block"), None)
|
|
556
|
+
if block is not None:
|
|
557
|
+
yield node, block, "clinit"
|
|
558
|
+
elif node.type == "block" and node.parent is not None \
|
|
559
|
+
and node.parent.type in _INIT_BLOCK_PARENTS:
|
|
560
|
+
yield node, node, "init"
|
|
561
|
+
|
|
562
|
+
@staticmethod
|
|
563
|
+
def _nearest_enclosing_class_cst(node):
|
|
564
|
+
"""Nearest enclosing class-like CST node (real declaration, or an
|
|
565
|
+
anonymous class's own `class_body`) — the node-object counterpart of
|
|
566
|
+
`JavaSymbolExtractor._find_parent_class`, which returns only the
|
|
567
|
+
*name*. Needed here to key into `class_id_by_position` (itself keyed
|
|
568
|
+
by the exact CST node `JavaSymbolExtractor` positioned the CLASS
|
|
569
|
+
Symbol on) rather than a second, driftable name-based lookup."""
|
|
570
|
+
p = node.parent
|
|
571
|
+
while p is not None:
|
|
572
|
+
if p.type in _ENCLOSING_TYPE_CST_TYPES:
|
|
573
|
+
return p
|
|
574
|
+
if p.type == "class_body" and p.parent is not None \
|
|
575
|
+
and p.parent.type in ("object_creation_expression", "enum_constant"):
|
|
576
|
+
return p
|
|
577
|
+
p = p.parent
|
|
578
|
+
return None
|
|
579
|
+
|
|
580
|
+
def _ensure_initializer_member_stub(self, class_node, kind: str) -> str:
|
|
581
|
+
"""Materialize the synthetic `<init>`/`<clinit>` FUNCTION `class_node`
|
|
582
|
+
needs, mirroring `GeneratedMembersMixin._add_generated_member_stub`'s
|
|
583
|
+
direct-`GraphNode` pattern (no CST node backs either, so there is no
|
|
584
|
+
`Symbol` to route through `JavaSymbolExtractor`). `<`/`>` are illegal
|
|
585
|
+
in a Java identifier, so `f"{class_node.id}.<init>"` can never collide
|
|
586
|
+
with a real member's id (a no-arg constructor's own id is
|
|
587
|
+
`f"{class_node.id}.()"`, never `.{name}"` for any declared name)."""
|
|
588
|
+
name = "<clinit>" if kind == "clinit" else "<init>"
|
|
589
|
+
member_id = f"{class_node.id}.{name}"
|
|
590
|
+
if member_id not in self.builder._nodes:
|
|
591
|
+
self.builder._add_node(GraphNode(
|
|
592
|
+
id=member_id,
|
|
593
|
+
name=name,
|
|
594
|
+
node_type=NodeType.FUNCTION,
|
|
595
|
+
module=class_node.module,
|
|
596
|
+
file_path=class_node.file_path,
|
|
597
|
+
line=class_node.line,
|
|
598
|
+
column=class_node.column,
|
|
599
|
+
contained_in=class_node.name,
|
|
600
|
+
language="java",
|
|
601
|
+
metadata={
|
|
602
|
+
"java_is_static": kind == "clinit",
|
|
603
|
+
"java_is_abstract": False,
|
|
604
|
+
"synthetic": "class_initializer" if kind == "clinit" else "instance_initializer",
|
|
605
|
+
},
|
|
606
|
+
))
|
|
607
|
+
self.builder._add_edge(GraphEdge(
|
|
608
|
+
source=class_node.id,
|
|
609
|
+
target=member_id,
|
|
610
|
+
edge_type=EdgeType.CONTAINS,
|
|
611
|
+
file_path=class_node.file_path,
|
|
612
|
+
line=class_node.line,
|
|
613
|
+
column=class_node.column,
|
|
614
|
+
strategy="java_resolver",
|
|
615
|
+
))
|
|
616
|
+
return member_id
|
|
617
|
+
|
|
618
|
+
def _synthesize_initializer_members(
|
|
619
|
+
self, parses: Dict[Path, ParseResult],
|
|
620
|
+
class_id_by_position: Dict[Tuple[str, int, int], str],
|
|
621
|
+
) -> None:
|
|
622
|
+
extractor = self._method_extractor()
|
|
623
|
+
for file_path, parse in parses.items():
|
|
624
|
+
source = parse.source_bytes
|
|
625
|
+
fp_str = str(file_path)
|
|
626
|
+
for owner_node, _body, kind in self._iter_initializer_sites(
|
|
627
|
+
parse.tree.root_node, fp_str, source, extractor,
|
|
628
|
+
):
|
|
629
|
+
class_cst = self._nearest_enclosing_class_cst(owner_node)
|
|
630
|
+
if class_cst is None:
|
|
631
|
+
continue
|
|
632
|
+
class_id = class_id_by_position.get(
|
|
633
|
+
(fp_str, class_cst.start_point[0] + 1, class_cst.start_point[1]),
|
|
634
|
+
)
|
|
635
|
+
if class_id is None or class_id not in self.builder._nodes:
|
|
636
|
+
continue
|
|
637
|
+
self._ensure_initializer_member_stub(
|
|
638
|
+
self.builder._nodes[class_id], kind,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
def _resolve_initializer_call_sites(
|
|
642
|
+
self, parse: ParseResult, source: bytes, module_name: str, fp_str: str,
|
|
643
|
+
func_id_by_position: Dict[Tuple[str, int, int], str],
|
|
644
|
+
import_map, wildcard_packages, static_wildcard_classes,
|
|
645
|
+
edges_added: int,
|
|
646
|
+
) -> int:
|
|
647
|
+
"""Route call sites found inside field initializers / static+instance
|
|
648
|
+
initializer blocks to the synthetic `<init>`/`<clinit>` FUNCTION
|
|
649
|
+
`_synthesize_initializer_members` already created for their class —
|
|
650
|
+
the fix for PROBLEMS.md bug group 4 (these call sites previously had
|
|
651
|
+
no caller CST node `_iter_method_cst` could ever yield, so the edge
|
|
652
|
+
was silently dropped)."""
|
|
653
|
+
extractor = self._method_extractor()
|
|
654
|
+
for owner_node, body, kind in self._iter_initializer_sites(
|
|
655
|
+
parse.tree.root_node, fp_str, source, extractor,
|
|
656
|
+
):
|
|
657
|
+
class_cst = self._nearest_enclosing_class_cst(owner_node)
|
|
658
|
+
if class_cst is None:
|
|
659
|
+
continue
|
|
660
|
+
class_id = self._class_id_by_position.get(
|
|
661
|
+
(fp_str, class_cst.start_point[0] + 1, class_cst.start_point[1]),
|
|
662
|
+
)
|
|
663
|
+
if class_id is None:
|
|
664
|
+
continue
|
|
665
|
+
name = "<clinit>" if kind == "clinit" else "<init>"
|
|
666
|
+
caller_id = f"{class_id}.{name}"
|
|
667
|
+
if caller_id not in self.builder._nodes:
|
|
668
|
+
continue
|
|
669
|
+
caller_node = self.builder._nodes[caller_id]
|
|
670
|
+
field_types_own = self._visible_bare_field_types(class_id)
|
|
671
|
+
# Neither a field initializer nor a static/instance-initializer
|
|
672
|
+
# block declares parameters of its own; an anonymous/local
|
|
673
|
+
# class's initializer may still read an effectively-final local
|
|
674
|
+
# of its lexically enclosing method, same as a real method there
|
|
675
|
+
# would (`_captured_param_types` walks up from `owner_node`
|
|
676
|
+
# exactly as it would from that method's own CST node).
|
|
677
|
+
own_param_types = self._captured_param_types(
|
|
678
|
+
owner_node, source, fp_str, func_id_by_position,
|
|
679
|
+
)
|
|
680
|
+
edges_added = self._resolve_caller_body(
|
|
681
|
+
body, source, module_name, class_id, import_map,
|
|
682
|
+
wildcard_packages, static_wildcard_classes, field_types_own,
|
|
683
|
+
own_param_types, {}, caller_node, caller_id, fp_str, edges_added,
|
|
684
|
+
)
|
|
685
|
+
return edges_added
|
|
686
|
+
|
|
687
|
+
def _captured_param_types(
|
|
688
|
+
self, func_cst, source: bytes, file_path: str,
|
|
689
|
+
func_id_by_position: Dict[Tuple[str, int, int], str],
|
|
690
|
+
) -> Dict[str, str]:
|
|
691
|
+
"""Declared parameters captured from the nearest lexical method.
|
|
692
|
+
|
|
693
|
+
Java permits an anonymous or local class to use effectively-final
|
|
694
|
+
parameters from the method that contains its declaration. A member
|
|
695
|
+
class has no enclosing method in its CST ancestry, so this contributes
|
|
696
|
+
no facts there. The current method's parameters are overlaid by the
|
|
697
|
+
caller and therefore correctly shadow captured names.
|
|
698
|
+
"""
|
|
699
|
+
parent = func_cst.parent
|
|
700
|
+
while parent is not None:
|
|
701
|
+
if parent.type in (
|
|
702
|
+
"method_declaration", "constructor_declaration",
|
|
703
|
+
"compact_constructor_declaration",
|
|
704
|
+
):
|
|
705
|
+
outer_id = func_id_by_position.get(
|
|
706
|
+
(file_path, parent.start_point[0] + 1, parent.start_point[1]),
|
|
707
|
+
)
|
|
708
|
+
if outer_id is None:
|
|
709
|
+
return {}
|
|
710
|
+
return dict(zip(
|
|
711
|
+
self._param_names(parent, source),
|
|
712
|
+
self._param_type_lists.get(outer_id, []),
|
|
713
|
+
))
|
|
714
|
+
parent = parent.parent
|
|
715
|
+
return {}
|
|
716
|
+
|
|
717
|
+
def _resolve_method_reference_pass(
|
|
718
|
+
self, body, source: bytes, module_name: str, enclosing_class_id,
|
|
719
|
+
import_map, wildcard_packages, local_types, own_param_types,
|
|
720
|
+
field_types_own, caller_node, caller_id: str, fp_str: str, edges_added: int,
|
|
721
|
+
):
|
|
722
|
+
resolved_refs: Dict[int, Set[str]] = {}
|
|
723
|
+
for ref_node in self._collect_method_references(body):
|
|
724
|
+
target_id, confidence, ref_etype, ref_meta = self._resolve_method_reference(
|
|
725
|
+
ref_node, source, module_name, enclosing_class_id,
|
|
726
|
+
import_map, wildcard_packages, local_types,
|
|
727
|
+
own_param_types, field_types_own, caller_node,
|
|
728
|
+
)
|
|
729
|
+
if not target_id:
|
|
730
|
+
caller_node.metadata.setdefault(
|
|
731
|
+
"unresolved_method_references", []
|
|
732
|
+
).append({
|
|
733
|
+
"text": self._text(ref_node, source),
|
|
734
|
+
"line": ref_node.start_point[0] + 1,
|
|
735
|
+
"column": ref_node.start_point[1],
|
|
736
|
+
})
|
|
737
|
+
continue
|
|
738
|
+
targets = target_id if isinstance(target_id, list) else [target_id]
|
|
739
|
+
resolved_refs[ref_node.start_byte] = set(targets)
|
|
740
|
+
for tid in targets:
|
|
741
|
+
# A recursive reference (`this::walk` inside `walk`,
|
|
742
|
+
# `Node::contains` inside `contains`) still gets its
|
|
743
|
+
# CALLABLE_REF/REFERENCES edge, exactly as a recursive bare
|
|
744
|
+
# call already does.
|
|
745
|
+
if tid not in self.builder._nodes:
|
|
746
|
+
continue
|
|
747
|
+
edge = GraphEdge(
|
|
748
|
+
source=caller_id, target=tid, edge_type=ref_etype,
|
|
749
|
+
file_path=fp_str, line=ref_node.start_point[0] + 1,
|
|
750
|
+
column=ref_node.start_point[1],
|
|
751
|
+
start_byte=ref_node.start_byte, end_byte=ref_node.end_byte,
|
|
752
|
+
confidence=float(confidence), metadata=dict(ref_meta),
|
|
753
|
+
strategy="java_resolver",
|
|
754
|
+
)
|
|
755
|
+
self.builder._record_edge_occurrence(edge)
|
|
756
|
+
if (caller_id, tid, ref_etype) in self.builder._edge_keys:
|
|
757
|
+
continue
|
|
758
|
+
self.builder._add_edge(edge)
|
|
759
|
+
# Backward-compatible semantic view: pre-P4.13
|
|
760
|
+
# consumers query method references as REFERENCES,
|
|
761
|
+
# while the richer CALLABLE_REF edge carries SAM
|
|
762
|
+
# target-typing metadata for newer consumers.
|
|
763
|
+
if ref_etype == EdgeType.CALLABLE_REF:
|
|
764
|
+
compat_edge = GraphEdge(
|
|
765
|
+
source=caller_id, target=tid,
|
|
766
|
+
edge_type=EdgeType.REFERENCES,
|
|
767
|
+
file_path=fp_str,
|
|
768
|
+
line=ref_node.start_point[0] + 1,
|
|
769
|
+
column=ref_node.start_point[1],
|
|
770
|
+
start_byte=ref_node.start_byte,
|
|
771
|
+
end_byte=ref_node.end_byte,
|
|
772
|
+
confidence=float(confidence),
|
|
773
|
+
metadata=dict(ref_meta),
|
|
774
|
+
strategy="java_resolver",
|
|
775
|
+
)
|
|
776
|
+
self.builder._record_edge_occurrence(compat_edge)
|
|
777
|
+
compat_key = (caller_id, tid, EdgeType.REFERENCES)
|
|
778
|
+
if compat_key not in self.builder._edge_keys:
|
|
779
|
+
self.builder._add_edge(compat_edge)
|
|
780
|
+
# `.calls`/`.called_by` model actual invocation (mirrored by
|
|
781
|
+
# memory.py's persistence reload, which only repopulates
|
|
782
|
+
# them from EdgeType.CALLS rows). A method reference
|
|
783
|
+
# (`Type::method`) never invokes anything by itself, so it
|
|
784
|
+
# must not inflate caller/callee bookkeeping here — the
|
|
785
|
+
# CALLABLE_REF/REFERENCES edges above already make it
|
|
786
|
+
# visible to blast-radius/impact analysis via
|
|
787
|
+
# IMPACT_EDGE_TYPES.
|
|
788
|
+
edges_added += 1
|
|
789
|
+
|
|
790
|
+
return resolved_refs, edges_added
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def _resolve_call_site_pass(
|
|
794
|
+
self, body, source: bytes, module_name: str, enclosing_class_id,
|
|
795
|
+
import_map, wildcard_packages, static_wildcard_classes, local_types,
|
|
796
|
+
own_param_types, type_param_bounds, local_callable_targets, caller_node,
|
|
797
|
+
caller_id: str, fp_str: str, edges_added: int,
|
|
798
|
+
) -> int:
|
|
799
|
+
for call_node, call_kind in self._collect_call_sites(body):
|
|
800
|
+
if call_kind != "method":
|
|
801
|
+
resolver = (
|
|
802
|
+
self._resolve_explicit_constructor
|
|
803
|
+
if call_kind == "explicit_constructor"
|
|
804
|
+
else self._resolve_constructor
|
|
805
|
+
)
|
|
806
|
+
target_id, confidence, constructor_signature = resolver(
|
|
807
|
+
call_node, source, module_name, import_map, wildcard_packages,
|
|
808
|
+
local_types, own_param_types, enclosing_class_id,
|
|
809
|
+
)
|
|
810
|
+
etype = EdgeType.INSTANTIATES
|
|
811
|
+
edge_meta: Dict[str, object] = {
|
|
812
|
+
"constructor_signature": constructor_signature,
|
|
813
|
+
}
|
|
814
|
+
else:
|
|
815
|
+
indirect = self._resolve_callable_container_dispatch(
|
|
816
|
+
call_node, source, enclosing_class_id,
|
|
817
|
+
local_callable_targets, local_types, own_param_types,
|
|
818
|
+
)
|
|
819
|
+
if indirect:
|
|
820
|
+
target_id, confidence, edge_meta = indirect
|
|
821
|
+
else:
|
|
822
|
+
target_id, confidence, edge_meta = self._resolve_call(
|
|
823
|
+
call_node, source, module_name, enclosing_class_id,
|
|
824
|
+
import_map, wildcard_packages, local_types, own_param_types,
|
|
825
|
+
static_wildcard_classes, type_param_bounds,
|
|
826
|
+
)
|
|
827
|
+
etype = EdgeType.CALLS
|
|
828
|
+
if not target_id:
|
|
829
|
+
continue
|
|
830
|
+
targets = target_id if isinstance(target_id, list) else [target_id]
|
|
831
|
+
target_overrides = edge_meta.get("_target_overrides", {})
|
|
832
|
+
shared_edge_meta = {
|
|
833
|
+
key: value for key, value in edge_meta.items()
|
|
834
|
+
if key != "_target_overrides"
|
|
835
|
+
}
|
|
836
|
+
for tid in targets:
|
|
837
|
+
if tid not in self.builder._nodes:
|
|
838
|
+
continue
|
|
839
|
+
override = (
|
|
840
|
+
target_overrides.get(tid, {})
|
|
841
|
+
if isinstance(target_overrides, dict) else {}
|
|
842
|
+
)
|
|
843
|
+
target_confidence = override.get("confidence", confidence)
|
|
844
|
+
target_metadata = override.get("metadata", shared_edge_meta)
|
|
845
|
+
edge = GraphEdge(
|
|
846
|
+
source=caller_id,
|
|
847
|
+
target=tid,
|
|
848
|
+
edge_type=etype,
|
|
849
|
+
file_path=fp_str,
|
|
850
|
+
line=call_node.start_point[0] + 1,
|
|
851
|
+
column=call_node.start_point[1],
|
|
852
|
+
start_byte=call_node.start_byte,
|
|
853
|
+
end_byte=call_node.end_byte,
|
|
854
|
+
confidence=float(target_confidence),
|
|
855
|
+
metadata=dict(target_metadata) if target_metadata else {},
|
|
856
|
+
strategy="java_resolver",
|
|
857
|
+
)
|
|
858
|
+
self.builder._record_edge_occurrence(edge)
|
|
859
|
+
key = (caller_id, tid, etype)
|
|
860
|
+
if key in self.builder._edge_keys:
|
|
861
|
+
continue
|
|
862
|
+
self.builder._add_edge(edge)
|
|
863
|
+
caller_node.calls.add(tid)
|
|
864
|
+
self.builder._nodes[tid].called_by.add(caller_id)
|
|
865
|
+
edges_added += 1
|
|
866
|
+
return edges_added
|