codegraph-engine 2.1.2__py3-none-any.whl → 2.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +1 -1
- codegraph/agent_brain.py +1961 -0
- codegraph/agent_capabilities.py +3500 -0
- codegraph/agent_rules.py +530 -0
- codegraph/architecture.py +48 -5
- codegraph/binding_resolver.py +616 -0
- codegraph/cli.py +345 -5
- codegraph/config.py +2 -0
- codegraph/context.py +845 -93
- codegraph/database/__init__.py +58 -0
- codegraph/database/extractor.py +2956 -0
- codegraph/database/interrogation.py +1085 -0
- codegraph/database/models.py +310 -0
- codegraph/epistemic.py +95 -0
- codegraph/errors.py +7 -0
- codegraph/evidence/citations.py +22 -1
- codegraph/evidence_contract.py +455 -0
- codegraph/frameworks.py +372 -11
- codegraph/git.py +92 -10
- codegraph/graph/models.py +15 -0
- codegraph/graph/traversal.py +680 -121
- codegraph/indexing/__init__.py +15 -1
- codegraph/indexing/classifier.py +343 -58
- codegraph/indexing/indexer.py +1515 -141
- codegraph/indexing/models.py +75 -0
- codegraph/indexing/parser.py +1843 -70
- codegraph/indexing/scanner.py +4 -1
- codegraph/indexing/telemetry.py +326 -0
- codegraph/installer.py +1652 -0
- codegraph/interrogation.py +708 -122
- codegraph/mcp/server.py +525 -107
- codegraph/mcp_diagnostics.py +879 -0
- codegraph/models.py +14 -0
- codegraph/monorepo.py +922 -0
- codegraph/observability.py +113 -2
- codegraph/optimizer.py +647 -42
- codegraph/ranking.py +61 -16
- codegraph/resolver.py +2006 -77
- codegraph/resources/monitor.py +1 -1
- codegraph/retrieval_policy.py +96 -17
- codegraph/route_composer.py +363 -0
- codegraph/runtime/__init__.py +32 -0
- codegraph/runtime/ingestor.py +672 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/search/__init__.py +2 -0
- codegraph/search/hybrid.py +572 -53
- codegraph/security/__init__.py +50 -2
- codegraph/security/paths.py +162 -8
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_decorators.py +341 -0
- codegraph/target_resolver.py +6 -6
- codegraph/tool_selection_eval.py +1158 -0
- codegraph_engine-2.1.7.dist-info/METADATA +617 -0
- codegraph_engine-2.1.7.dist-info/RECORD +84 -0
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/licenses/LICENSE +1 -1
- codegraph_engine-2.1.2.dist-info/METADATA +0 -336
- codegraph_engine-2.1.2.dist-info/RECORD +0 -63
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"""Conservative Deterministic Local Binding and Lightweight Data-Flow Engine.
|
|
2
|
+
|
|
3
|
+
Resolves:
|
|
4
|
+
- name -> symbol
|
|
5
|
+
- name -> imported symbol
|
|
6
|
+
- name -> module.attribute
|
|
7
|
+
- name -> function reference
|
|
8
|
+
- name -> class reference
|
|
9
|
+
- alias -> alias -> target (bounded chain)
|
|
10
|
+
- dictionary key -> symbol
|
|
11
|
+
- list element -> symbol
|
|
12
|
+
- simple assignment chains
|
|
13
|
+
|
|
14
|
+
Adheres to:
|
|
15
|
+
1. Lexical scope priority before module/global fallback (inner scopes shadow outer scopes)
|
|
16
|
+
2. Source-order awareness for sequential reassignments
|
|
17
|
+
3. Conditional/branching reassignments remain POSSIBLE or UNKNOWN
|
|
18
|
+
4. RESOLVES_TO is an identity/binding edge, never a CALLS edge
|
|
19
|
+
5. Attribute resolution only occurs when the base object is statically grounded
|
|
20
|
+
6. Conservative JS/TS handling
|
|
21
|
+
7. Cycle-safe with visited set and max_depth = 10
|
|
22
|
+
8. Bounded, deterministic, no runtime execution
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
from codegraph.epistemic import RelationshipEvidenceClass
|
|
30
|
+
from codegraph.indexing.models import BindingRef, ImportRef, Symbol
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LocalBindingResolver:
|
|
37
|
+
"""Pure deterministic local variable binding and alias chain resolver."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
bindings: list[BindingRef],
|
|
42
|
+
symbols: list[Symbol],
|
|
43
|
+
imports: list[ImportRef],
|
|
44
|
+
known_files: set[str],
|
|
45
|
+
resolve_symbol_in_module: Callable[[str, str], Symbol | None],
|
|
46
|
+
resolve_import_target: Callable[[ImportRef], tuple[str | None, str | None]],
|
|
47
|
+
) -> None:
|
|
48
|
+
self.bindings = bindings
|
|
49
|
+
self.symbols = symbols
|
|
50
|
+
self.imports = imports
|
|
51
|
+
self.known_files = known_files
|
|
52
|
+
self.resolve_symbol_in_module = resolve_symbol_in_module
|
|
53
|
+
self.resolve_import_target = resolve_import_target
|
|
54
|
+
|
|
55
|
+
# Index bindings: (file_path, target_name) -> list[BindingRef] and target_name -> list[BindingRef]
|
|
56
|
+
self.bindings_by_file_target: dict[tuple[str, str], list[BindingRef]] = {}
|
|
57
|
+
self.bindings_by_target: dict[str, list[BindingRef]] = {}
|
|
58
|
+
for b in bindings:
|
|
59
|
+
self.bindings_by_file_target.setdefault((b.file_path, b.target_name), []).append(b)
|
|
60
|
+
self.bindings_by_target.setdefault(b.target_name, []).append(b)
|
|
61
|
+
|
|
62
|
+
# Ensure all binding lists are deterministically sorted by line
|
|
63
|
+
for b_list in self.bindings_by_file_target.values():
|
|
64
|
+
b_list.sort(key=lambda b: (b.line, b.column or 0))
|
|
65
|
+
|
|
66
|
+
# Index symbols by (file_path, name) and canonical_id
|
|
67
|
+
self.file_symbols: dict[tuple[str, str], list[Symbol]] = {}
|
|
68
|
+
self.canonical_map: dict[str, Symbol] = {}
|
|
69
|
+
for s in symbols:
|
|
70
|
+
self.file_symbols.setdefault((s.path, s.name), []).append(s)
|
|
71
|
+
self.canonical_map[s.canonical_id] = s
|
|
72
|
+
|
|
73
|
+
# Index imports by (file_path, local_name)
|
|
74
|
+
self.file_imports: dict[tuple[str, str], list[ImportRef]] = {}
|
|
75
|
+
for imp in imports:
|
|
76
|
+
loc = imp.local_name or imp.alias or imp.name or ""
|
|
77
|
+
if loc:
|
|
78
|
+
self.file_imports.setdefault((imp.source_file, loc), []).append(imp)
|
|
79
|
+
|
|
80
|
+
self._resolve_cache: dict[
|
|
81
|
+
tuple[str, str, str, int | None],
|
|
82
|
+
tuple[str | None, RelationshipEvidenceClass, str],
|
|
83
|
+
] = {}
|
|
84
|
+
|
|
85
|
+
def resolve(
|
|
86
|
+
self,
|
|
87
|
+
file_path: str,
|
|
88
|
+
name: str,
|
|
89
|
+
scope: str = "",
|
|
90
|
+
use_line: int | None = None,
|
|
91
|
+
visited: set[tuple[str, str, str]] | None = None,
|
|
92
|
+
depth: int = 0,
|
|
93
|
+
) -> tuple[str | None, RelationshipEvidenceClass, str]:
|
|
94
|
+
"""Resolve a variable or alias identifier to its target canonical ID.
|
|
95
|
+
|
|
96
|
+
Returns (canonical_id | None, evidence_class, reason).
|
|
97
|
+
"""
|
|
98
|
+
is_top_level = visited is None and depth == 0
|
|
99
|
+
cache_key = (file_path, name, scope, use_line)
|
|
100
|
+
if is_top_level and cache_key in self._resolve_cache:
|
|
101
|
+
return self._resolve_cache[cache_key]
|
|
102
|
+
|
|
103
|
+
res = self._resolve_impl(file_path, name, scope, use_line, visited, depth)
|
|
104
|
+
if is_top_level:
|
|
105
|
+
self._resolve_cache[cache_key] = res
|
|
106
|
+
return res
|
|
107
|
+
|
|
108
|
+
def _resolve_impl(
|
|
109
|
+
self,
|
|
110
|
+
file_path: str,
|
|
111
|
+
name: str,
|
|
112
|
+
scope: str = "",
|
|
113
|
+
use_line: int | None = None,
|
|
114
|
+
visited: set[tuple[str, str, str]] | None = None,
|
|
115
|
+
depth: int = 0,
|
|
116
|
+
) -> tuple[str | None, RelationshipEvidenceClass, str]:
|
|
117
|
+
visited = visited or set()
|
|
118
|
+
key = (file_path, scope, name)
|
|
119
|
+
|
|
120
|
+
# Cycle protection
|
|
121
|
+
if key in visited:
|
|
122
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "cyclic_alias"
|
|
123
|
+
|
|
124
|
+
# Bounded depth
|
|
125
|
+
if depth >= 10:
|
|
126
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "max_depth_exceeded"
|
|
127
|
+
|
|
128
|
+
visited.add(key)
|
|
129
|
+
|
|
130
|
+
# 1. Candidate scope search order (inner scopes shadow outer scopes)
|
|
131
|
+
if scope:
|
|
132
|
+
parts = scope.split(".")
|
|
133
|
+
candidate_scopes = [".".join(parts[:i]) for i in range(len(parts), 0, -1)] + [""]
|
|
134
|
+
else:
|
|
135
|
+
candidate_scopes = [""]
|
|
136
|
+
|
|
137
|
+
all_file_bindings = self.bindings_by_file_target.get((file_path, name), [])
|
|
138
|
+
scope_bindings: list[BindingRef] = []
|
|
139
|
+
matching_scope: str | None = None
|
|
140
|
+
|
|
141
|
+
for scp in candidate_scopes:
|
|
142
|
+
matched = [b for b in all_file_bindings if b.scope == scp]
|
|
143
|
+
if matched:
|
|
144
|
+
matching_scope = scp
|
|
145
|
+
scope_bindings = matched
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
# If no local binding exists in any lexical scope:
|
|
149
|
+
if not scope_bindings or matching_scope is None:
|
|
150
|
+
ev_cls = (
|
|
151
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED
|
|
152
|
+
if depth > 0
|
|
153
|
+
else RelationshipEvidenceClass.AST_VERIFIED
|
|
154
|
+
)
|
|
155
|
+
# Check if name is an import in this file
|
|
156
|
+
for imp in self.file_imports.get((file_path, name), []):
|
|
157
|
+
_tpath, target_mod = self.resolve_import_target(imp)
|
|
158
|
+
if target_mod:
|
|
159
|
+
orig_name = imp.imported_name or name
|
|
160
|
+
if orig_name != "*":
|
|
161
|
+
sym = self.resolve_symbol_in_module(target_mod, orig_name)
|
|
162
|
+
if sym:
|
|
163
|
+
return (
|
|
164
|
+
sym.canonical_id,
|
|
165
|
+
ev_cls,
|
|
166
|
+
f"imported_symbol -> {sym.canonical_id}",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Check if name is a direct symbol definition in this file
|
|
170
|
+
for s in self.file_symbols.get((file_path, name), []):
|
|
171
|
+
if not s.scope or s.scope in candidate_scopes:
|
|
172
|
+
return (
|
|
173
|
+
s.canonical_id,
|
|
174
|
+
ev_cls,
|
|
175
|
+
f"direct_symbol -> {s.canonical_id}",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"unresolved_identifier: {name}"
|
|
179
|
+
|
|
180
|
+
# 2. Source-order awareness and sequential reassignment (all_file_bindings is pre-sorted)
|
|
181
|
+
|
|
182
|
+
if use_line is not None:
|
|
183
|
+
before = [b for b in scope_bindings if b.line <= use_line]
|
|
184
|
+
if not before:
|
|
185
|
+
# Forward reference (used before assignment)
|
|
186
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"used_before_assignment: {name}"
|
|
187
|
+
|
|
188
|
+
selected = before[-1]
|
|
189
|
+
|
|
190
|
+
# 3. Conditional / branching assignments must remain POSSIBLE
|
|
191
|
+
if selected.is_conditional:
|
|
192
|
+
return None, RelationshipEvidenceClass.POSSIBLE, "conditional_binding"
|
|
193
|
+
|
|
194
|
+
# Check for conditional ambiguity up to use_line
|
|
195
|
+
if any(b.is_conditional for b in before):
|
|
196
|
+
return None, RelationshipEvidenceClass.POSSIBLE, "conditional_ambiguity"
|
|
197
|
+
else:
|
|
198
|
+
if any(b.is_conditional for b in scope_bindings):
|
|
199
|
+
return None, RelationshipEvidenceClass.POSSIBLE, "conditional_binding"
|
|
200
|
+
if len(scope_bindings) > 1:
|
|
201
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "ambiguous_reassignment"
|
|
202
|
+
selected = scope_bindings[0]
|
|
203
|
+
|
|
204
|
+
# 4. Resolve RHS based on expression kind
|
|
205
|
+
if selected.expr_kind == "DYNAMIC":
|
|
206
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "runtime_dynamic_attribute"
|
|
207
|
+
|
|
208
|
+
if selected.expr_kind == "IDENTIFIER":
|
|
209
|
+
rhs_name = selected.base_expr or selected.source_expr
|
|
210
|
+
|
|
211
|
+
# Prevent immediate self-assignment
|
|
212
|
+
if rhs_name == name and selected.scope == matching_scope:
|
|
213
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "cyclic_alias"
|
|
214
|
+
|
|
215
|
+
# Check direct symbol in the same file
|
|
216
|
+
for s in self.file_symbols.get((file_path, rhs_name), []):
|
|
217
|
+
if not s.scope or s.scope in candidate_scopes:
|
|
218
|
+
return (
|
|
219
|
+
s.canonical_id,
|
|
220
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
221
|
+
f"local_alias -> {s.canonical_id}",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Check direct import in file
|
|
225
|
+
for imp in self.file_imports.get((file_path, rhs_name), []):
|
|
226
|
+
_tpath, target_mod = self.resolve_import_target(imp)
|
|
227
|
+
if target_mod:
|
|
228
|
+
orig_name = imp.imported_name or rhs_name
|
|
229
|
+
if orig_name != "*":
|
|
230
|
+
sym = self.resolve_symbol_in_module(target_mod, orig_name)
|
|
231
|
+
if sym:
|
|
232
|
+
return (
|
|
233
|
+
sym.canonical_id,
|
|
234
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
235
|
+
f"imported_alias -> {sym.canonical_id}",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Otherwise recursively follow alias chain in same scope
|
|
239
|
+
return self.resolve(
|
|
240
|
+
file_path=file_path,
|
|
241
|
+
name=rhs_name,
|
|
242
|
+
scope=selected.scope,
|
|
243
|
+
use_line=selected.line,
|
|
244
|
+
visited=set(visited),
|
|
245
|
+
depth=depth + 1,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if selected.expr_kind == "ATTRIBUTE":
|
|
249
|
+
# Requirement 5: Base object must be statically grounded
|
|
250
|
+
base = selected.base_expr
|
|
251
|
+
attr = selected.attr_name
|
|
252
|
+
|
|
253
|
+
grounded_mod: str | None = None
|
|
254
|
+
grounded_sym: Symbol | None = None
|
|
255
|
+
|
|
256
|
+
# Check if base is an imported module/symbol
|
|
257
|
+
for imp in self.file_imports.get((file_path, base), []):
|
|
258
|
+
_tpath, target_mod = self.resolve_import_target(imp)
|
|
259
|
+
if target_mod:
|
|
260
|
+
if imp.import_type in ("namespace", "module") or imp.imported_name in (None, "*"):
|
|
261
|
+
grounded_mod = target_mod
|
|
262
|
+
else:
|
|
263
|
+
orig_name = imp.imported_name or base
|
|
264
|
+
grounded_sym = self.resolve_symbol_in_module(target_mod, orig_name)
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
# Check if base is a class or top-level symbol in this file
|
|
268
|
+
if not grounded_mod and not grounded_sym:
|
|
269
|
+
if base in ("celery", "click", "app", "django", "typer"):
|
|
270
|
+
grounded_mod = base
|
|
271
|
+
else:
|
|
272
|
+
for s in self.file_symbols.get((file_path, base), []):
|
|
273
|
+
if s.kind in ("class", "function", "method"):
|
|
274
|
+
grounded_sym = s
|
|
275
|
+
break
|
|
276
|
+
|
|
277
|
+
# Check if base is a local alias that resolves to a grounded symbol
|
|
278
|
+
if not grounded_mod and not grounded_sym:
|
|
279
|
+
base_id, base_ev, _base_reason = self.resolve(
|
|
280
|
+
file_path=file_path,
|
|
281
|
+
name=base,
|
|
282
|
+
scope=selected.scope,
|
|
283
|
+
use_line=selected.line,
|
|
284
|
+
visited=set(visited),
|
|
285
|
+
depth=depth + 1,
|
|
286
|
+
)
|
|
287
|
+
if base_id and base_ev == RelationshipEvidenceClass.DATAFLOW_VERIFIED:
|
|
288
|
+
if base_id in self.canonical_map:
|
|
289
|
+
grounded_sym = self.canonical_map[base_id]
|
|
290
|
+
|
|
291
|
+
if not grounded_mod and not grounded_sym:
|
|
292
|
+
# Base is UNGROUNDED! Must return UNKNOWN
|
|
293
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"unknown_attribute_base: {base}"
|
|
294
|
+
|
|
295
|
+
# Base is grounded: resolve attribute
|
|
296
|
+
if grounded_mod:
|
|
297
|
+
target_sym = self.resolve_symbol_in_module(grounded_mod, attr)
|
|
298
|
+
if target_sym:
|
|
299
|
+
return (
|
|
300
|
+
target_sym.canonical_id,
|
|
301
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
302
|
+
f"attribute_alias -> {target_sym.canonical_id}",
|
|
303
|
+
)
|
|
304
|
+
# Framework / external library module attribute
|
|
305
|
+
if grounded_mod in ("celery", "click", "app", "django", "typer") or "." in grounded_mod:
|
|
306
|
+
return (
|
|
307
|
+
f"{grounded_mod}.{attr}",
|
|
308
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
309
|
+
f"external_attribute_alias -> {grounded_mod}.{attr}",
|
|
310
|
+
)
|
|
311
|
+
elif grounded_sym:
|
|
312
|
+
cand_canon = f"{grounded_sym.canonical_id}.{attr}"
|
|
313
|
+
if cand_canon in self.canonical_map:
|
|
314
|
+
return (
|
|
315
|
+
cand_canon,
|
|
316
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
317
|
+
f"method_alias -> {cand_canon}",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"attribute_not_found_on_base: {base}.{attr}"
|
|
321
|
+
|
|
322
|
+
if selected.expr_kind == "SUBSCRIPT":
|
|
323
|
+
target_dict = selected.subscript_target or ""
|
|
324
|
+
sub_key = selected.subscript_key
|
|
325
|
+
idx = selected.subscript_index
|
|
326
|
+
return self.resolve_subscript(
|
|
327
|
+
file_path=file_path,
|
|
328
|
+
target_dict=target_dict,
|
|
329
|
+
key=sub_key,
|
|
330
|
+
index=idx,
|
|
331
|
+
scope=selected.scope,
|
|
332
|
+
use_line=selected.line,
|
|
333
|
+
visited=set(visited),
|
|
334
|
+
depth=depth,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "unsupported_binding_kind"
|
|
338
|
+
|
|
339
|
+
def resolve_subscript(
|
|
340
|
+
self,
|
|
341
|
+
file_path: str,
|
|
342
|
+
target_dict: str,
|
|
343
|
+
key: str | None = None,
|
|
344
|
+
index: int | None = None,
|
|
345
|
+
scope: str = "",
|
|
346
|
+
use_line: int | None = None,
|
|
347
|
+
visited: set[tuple[str, str, str]] | None = None,
|
|
348
|
+
depth: int = 0,
|
|
349
|
+
) -> tuple[str | None, RelationshipEvidenceClass, str]:
|
|
350
|
+
"""Resolve a subscript access (e.g. HANDLERS['create'] or PIPELINE[0]) deterministically."""
|
|
351
|
+
visited = visited or set()
|
|
352
|
+
if depth > 10:
|
|
353
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "max_depth_exceeded"
|
|
354
|
+
|
|
355
|
+
if key == "<DYNAMIC>":
|
|
356
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "dynamic_dispatch"
|
|
357
|
+
|
|
358
|
+
# 1. Look for bindings of target_dict in the current file
|
|
359
|
+
dict_bindings = self.bindings_by_file_target.get((file_path, target_dict), [])
|
|
360
|
+
dict_candidates = [
|
|
361
|
+
b
|
|
362
|
+
for b in dict_bindings
|
|
363
|
+
if (b.scope == scope or not b.scope)
|
|
364
|
+
and (use_line is None or b.line <= use_line)
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
# Check for subscript assignment in dict_candidates (e.g. HANDLERS["create"] = create_user)
|
|
368
|
+
if key is not None and dict_candidates:
|
|
369
|
+
# Walk backwards from use_line to find latest assignment for this key
|
|
370
|
+
for b in reversed(dict_candidates):
|
|
371
|
+
if (
|
|
372
|
+
b.expr_kind in ("DICT_ASSIGN", "EVENT_ON", "SUBSCRIPT_ASSIGN", "CALL_REGISTER")
|
|
373
|
+
and b.subscript_key == key
|
|
374
|
+
):
|
|
375
|
+
if b.is_conditional:
|
|
376
|
+
return None, RelationshipEvidenceClass.POSSIBLE, "conditional_ambiguity"
|
|
377
|
+
if b.source_expr == "<DYNAMIC>":
|
|
378
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "dynamic_dispatch"
|
|
379
|
+
if "." in b.source_expr:
|
|
380
|
+
base, attr = b.source_expr.split(".", 1)
|
|
381
|
+
return self._resolve_attribute_expr(
|
|
382
|
+
file_path, base, attr, scope, b.line, set(visited), depth + 1
|
|
383
|
+
)
|
|
384
|
+
target_id, _ev, _rsn = self.resolve(
|
|
385
|
+
file_path=file_path,
|
|
386
|
+
name=b.source_expr,
|
|
387
|
+
scope=scope,
|
|
388
|
+
use_line=b.line,
|
|
389
|
+
visited=set(visited),
|
|
390
|
+
depth=depth + 1,
|
|
391
|
+
)
|
|
392
|
+
if target_id:
|
|
393
|
+
return (
|
|
394
|
+
target_id,
|
|
395
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
396
|
+
f"dict_assignment -> {target_id}",
|
|
397
|
+
)
|
|
398
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"unresolved_subscript_val: {b.source_expr}"
|
|
399
|
+
|
|
400
|
+
# Check for literal definition (DICT_LITERAL or LIST_LITERAL)
|
|
401
|
+
if dict_candidates:
|
|
402
|
+
dict_b = dict_candidates[-1]
|
|
403
|
+
if dict_b.expr_kind == "DICT_LITERAL" and key is not None:
|
|
404
|
+
for k, val_name in dict_b.dict_entries:
|
|
405
|
+
if k == key:
|
|
406
|
+
if "." in val_name:
|
|
407
|
+
base, attr = val_name.split(".", 1)
|
|
408
|
+
return self._resolve_attribute_expr(
|
|
409
|
+
file_path, base, attr, scope, dict_b.line, set(visited), depth + 1
|
|
410
|
+
)
|
|
411
|
+
target_id, _ev, _rsn = self.resolve(
|
|
412
|
+
file_path=file_path,
|
|
413
|
+
name=val_name,
|
|
414
|
+
scope=scope,
|
|
415
|
+
use_line=dict_b.line,
|
|
416
|
+
visited=set(visited),
|
|
417
|
+
depth=depth + 1,
|
|
418
|
+
)
|
|
419
|
+
if target_id:
|
|
420
|
+
return (
|
|
421
|
+
target_id,
|
|
422
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
423
|
+
f"dict_subscript -> {target_id}",
|
|
424
|
+
)
|
|
425
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"unresolved_subscript_val: {val_name}"
|
|
426
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"key_not_in_dict: {key}"
|
|
427
|
+
|
|
428
|
+
if dict_b.expr_kind == "LIST_LITERAL" and index is not None:
|
|
429
|
+
if 0 <= index < len(dict_b.list_entries):
|
|
430
|
+
elem_name = dict_b.list_entries[index]
|
|
431
|
+
if "." in elem_name:
|
|
432
|
+
base, attr = elem_name.split(".", 1)
|
|
433
|
+
return self._resolve_attribute_expr(
|
|
434
|
+
file_path, base, attr, scope, dict_b.line, set(visited), depth + 1
|
|
435
|
+
)
|
|
436
|
+
target_id, _ev, _rsn = self.resolve(
|
|
437
|
+
file_path=file_path,
|
|
438
|
+
name=elem_name,
|
|
439
|
+
scope=scope,
|
|
440
|
+
use_line=dict_b.line,
|
|
441
|
+
visited=set(visited),
|
|
442
|
+
depth=depth + 1,
|
|
443
|
+
)
|
|
444
|
+
if target_id:
|
|
445
|
+
return (
|
|
446
|
+
target_id,
|
|
447
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
448
|
+
f"list_subscript -> {target_id}",
|
|
449
|
+
)
|
|
450
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"unresolved_subscript_elem: {elem_name}"
|
|
451
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"index_out_of_bounds: {index}"
|
|
452
|
+
|
|
453
|
+
# 2. Check cross-file imports: e.g. from registry import HANDLERS
|
|
454
|
+
for imp in self.file_imports.get((file_path, target_dict), []):
|
|
455
|
+
_tpath, target_mod = self.resolve_import_target(imp)
|
|
456
|
+
if target_mod:
|
|
457
|
+
orig_name = imp.imported_name or target_dict
|
|
458
|
+
return self.resolve_cross_file_registry(
|
|
459
|
+
target_mod, orig_name, key, visited=set(visited), depth=depth + 1
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"unknown_subscript_target: {target_dict}"
|
|
463
|
+
|
|
464
|
+
def resolve_cross_file_registry(
|
|
465
|
+
self,
|
|
466
|
+
target_mod: str,
|
|
467
|
+
target_name: str,
|
|
468
|
+
key: str | None,
|
|
469
|
+
visited: set[tuple[str, str, str]] | None = None,
|
|
470
|
+
depth: int = 0,
|
|
471
|
+
) -> tuple[str | None, RelationshipEvidenceClass, str]:
|
|
472
|
+
"""Resolve a subscript key in a cross-file shared registry."""
|
|
473
|
+
if key is None or key == "<DYNAMIC>":
|
|
474
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "dynamic_dispatch"
|
|
475
|
+
|
|
476
|
+
visited = visited or set()
|
|
477
|
+
v_key = (target_mod, target_name, key)
|
|
478
|
+
if v_key in visited or depth > 10:
|
|
479
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "cycle_or_depth_exceeded"
|
|
480
|
+
visited.add(v_key)
|
|
481
|
+
|
|
482
|
+
candidates: list[BindingRef] = []
|
|
483
|
+
for b in self.bindings_by_target.get(target_name, ()):
|
|
484
|
+
if (
|
|
485
|
+
b.expr_kind in ("DICT_ASSIGN", "EVENT_ON", "SUBSCRIPT_ASSIGN", "CALL_REGISTER")
|
|
486
|
+
and b.subscript_key == key
|
|
487
|
+
):
|
|
488
|
+
candidates.append(b)
|
|
489
|
+
elif b.expr_kind == "DICT_LITERAL":
|
|
490
|
+
for k, _v in b.dict_entries:
|
|
491
|
+
if k == key:
|
|
492
|
+
candidates.append(b)
|
|
493
|
+
break
|
|
494
|
+
|
|
495
|
+
if not candidates:
|
|
496
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"key_not_in_cross_file_registry: {key}"
|
|
497
|
+
|
|
498
|
+
# If any candidate is conditional, order/presence is ambiguous -> POSSIBLE
|
|
499
|
+
if any(c.is_conditional for c in candidates):
|
|
500
|
+
return None, RelationshipEvidenceClass.POSSIBLE, "conditional_ambiguity"
|
|
501
|
+
|
|
502
|
+
resolved_targets: set[str] = set()
|
|
503
|
+
for cand in candidates:
|
|
504
|
+
if cand.expr_kind == "DICT_LITERAL":
|
|
505
|
+
for k, v in cand.dict_entries:
|
|
506
|
+
if k == key:
|
|
507
|
+
tid, _ev, _rsn = self.resolve(
|
|
508
|
+
cand.file_path,
|
|
509
|
+
v,
|
|
510
|
+
scope=cand.scope,
|
|
511
|
+
use_line=cand.line,
|
|
512
|
+
visited=set(visited),
|
|
513
|
+
depth=depth + 1,
|
|
514
|
+
)
|
|
515
|
+
if tid:
|
|
516
|
+
resolved_targets.add(tid)
|
|
517
|
+
else:
|
|
518
|
+
if cand.source_expr == "<DYNAMIC>":
|
|
519
|
+
return None, RelationshipEvidenceClass.UNKNOWN, "dynamic_dispatch"
|
|
520
|
+
if "." in cand.source_expr:
|
|
521
|
+
base, attr = cand.source_expr.split(".", 1)
|
|
522
|
+
tid, _ev, _rsn = self._resolve_attribute_expr(
|
|
523
|
+
cand.file_path, base, attr, cand.scope, cand.line, set(visited), depth + 1
|
|
524
|
+
)
|
|
525
|
+
if tid:
|
|
526
|
+
resolved_targets.add(tid)
|
|
527
|
+
else:
|
|
528
|
+
tid, _ev, _rsn = self.resolve(
|
|
529
|
+
cand.file_path,
|
|
530
|
+
cand.source_expr,
|
|
531
|
+
scope=cand.scope,
|
|
532
|
+
use_line=cand.line,
|
|
533
|
+
visited=set(visited),
|
|
534
|
+
depth=depth + 1,
|
|
535
|
+
)
|
|
536
|
+
if tid:
|
|
537
|
+
resolved_targets.add(tid)
|
|
538
|
+
|
|
539
|
+
if not resolved_targets:
|
|
540
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"unresolved_cross_file_registry_key: {key}"
|
|
541
|
+
|
|
542
|
+
if len(resolved_targets) > 1:
|
|
543
|
+
# Conflicting mutations across files -> POSSIBLE
|
|
544
|
+
return None, RelationshipEvidenceClass.POSSIBLE, "cross_file_mutation_ambiguity"
|
|
545
|
+
|
|
546
|
+
final_id = next(iter(resolved_targets))
|
|
547
|
+
return (
|
|
548
|
+
final_id,
|
|
549
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
550
|
+
f"cross_file_registry -> {final_id}",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
def _resolve_attribute_expr(
|
|
554
|
+
self,
|
|
555
|
+
file_path: str,
|
|
556
|
+
base: str,
|
|
557
|
+
attr: str,
|
|
558
|
+
scope: str,
|
|
559
|
+
line: int,
|
|
560
|
+
visited: set[tuple[str, str, str]],
|
|
561
|
+
depth: int,
|
|
562
|
+
) -> tuple[str | None, RelationshipEvidenceClass, str]:
|
|
563
|
+
"""Resolve attribute expression when base is statically grounded."""
|
|
564
|
+
grounded_mod: str | None = None
|
|
565
|
+
grounded_sym: Symbol | None = None
|
|
566
|
+
|
|
567
|
+
for imp in self.file_imports.get((file_path, base), []):
|
|
568
|
+
_tpath, target_mod = self.resolve_import_target(imp)
|
|
569
|
+
if target_mod:
|
|
570
|
+
if imp.import_type in ("namespace", "module") or imp.imported_name in (None, "*"):
|
|
571
|
+
grounded_mod = target_mod
|
|
572
|
+
else:
|
|
573
|
+
orig_name = imp.imported_name or base
|
|
574
|
+
grounded_sym = self.resolve_symbol_in_module(target_mod, orig_name)
|
|
575
|
+
break
|
|
576
|
+
|
|
577
|
+
if not grounded_mod and not grounded_sym:
|
|
578
|
+
for s in self.file_symbols.get((file_path, base), []):
|
|
579
|
+
if s.kind in ("class", "function", "method"):
|
|
580
|
+
grounded_sym = s
|
|
581
|
+
break
|
|
582
|
+
|
|
583
|
+
if not grounded_mod and not grounded_sym:
|
|
584
|
+
base_id, base_ev, _base_reason = self.resolve(
|
|
585
|
+
file_path=file_path,
|
|
586
|
+
name=base,
|
|
587
|
+
scope=scope,
|
|
588
|
+
use_line=line,
|
|
589
|
+
visited=set(visited),
|
|
590
|
+
depth=depth + 1,
|
|
591
|
+
)
|
|
592
|
+
if base_id and base_ev == RelationshipEvidenceClass.DATAFLOW_VERIFIED:
|
|
593
|
+
if base_id in self.canonical_map:
|
|
594
|
+
grounded_sym = self.canonical_map[base_id]
|
|
595
|
+
|
|
596
|
+
if not grounded_mod and not grounded_sym:
|
|
597
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"unknown_attribute_base: {base}"
|
|
598
|
+
|
|
599
|
+
if grounded_mod:
|
|
600
|
+
target_sym = self.resolve_symbol_in_module(grounded_mod, attr)
|
|
601
|
+
if target_sym:
|
|
602
|
+
return (
|
|
603
|
+
target_sym.canonical_id,
|
|
604
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
605
|
+
f"attribute_alias -> {target_sym.canonical_id}",
|
|
606
|
+
)
|
|
607
|
+
elif grounded_sym:
|
|
608
|
+
cand_canon = f"{grounded_sym.canonical_id}.{attr}"
|
|
609
|
+
if cand_canon in self.canonical_map:
|
|
610
|
+
return (
|
|
611
|
+
cand_canon,
|
|
612
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED,
|
|
613
|
+
f"method_alias -> {cand_canon}",
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
return None, RelationshipEvidenceClass.UNKNOWN, f"attribute_not_found_on_base: {base}.{attr}"
|