codegraph-engine 3.0.0__1-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 +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
codegraph/resolver.py
ADDED
|
@@ -0,0 +1,3968 @@
|
|
|
1
|
+
"""Dedicated Reference & Symbol Resolution Engine.
|
|
2
|
+
|
|
3
|
+
Pipeline:
|
|
4
|
+
Parser IR (Symbols, Imports, Calls, Inheritance, Routes)
|
|
5
|
+
↓
|
|
6
|
+
Module Resolver (deterministic file/package resolution)
|
|
7
|
+
↓
|
|
8
|
+
Re-export Chain Resolver
|
|
9
|
+
↓
|
|
10
|
+
Symbol & Relationship Resolver (lexical -> canonical -> alias -> qualified -> module -> framework)
|
|
11
|
+
↓
|
|
12
|
+
Confidence & Source-Hash Evidence Assignment
|
|
13
|
+
↓
|
|
14
|
+
Deduplicated References & Graph Edges
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import posixpath
|
|
20
|
+
import re
|
|
21
|
+
import sqlite3
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from codegraph.binding_resolver import LocalBindingResolver
|
|
26
|
+
from codegraph.epistemic import RelationshipEvidenceClass
|
|
27
|
+
from codegraph.frameworks import RouteDetection
|
|
28
|
+
from codegraph.graph.models import GraphEdge
|
|
29
|
+
from codegraph.indexing.models import (
|
|
30
|
+
BindingRef,
|
|
31
|
+
CallRef,
|
|
32
|
+
ImportRef,
|
|
33
|
+
InheritanceRef,
|
|
34
|
+
Reference,
|
|
35
|
+
Symbol,
|
|
36
|
+
normalize_module,
|
|
37
|
+
)
|
|
38
|
+
from codegraph.monorepo import WorkspaceInfo
|
|
39
|
+
from codegraph.route_composer import ComposedMountEdge
|
|
40
|
+
from codegraph.route_topology import match_route_topology, normalize_route
|
|
41
|
+
from codegraph.tsconfig import TsConfigResolver
|
|
42
|
+
|
|
43
|
+
_TEST_FILE_RE = re.compile(
|
|
44
|
+
r"(^|[_/])test[_s]?[_/]|[_/]spec[_/]|test[_s]?\.(py|js|ts|jsx|tsx)$|spec\.(py|js|ts|jsx|tsx)$",
|
|
45
|
+
re.IGNORECASE,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
DEFAULT_MAX_REEXPORT_DEPTH = 16
|
|
49
|
+
DEFAULT_MAX_WILDCARD_EXPANSIONS = 64
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_module_to_path(
|
|
53
|
+
imported_module: str,
|
|
54
|
+
source_file: str,
|
|
55
|
+
known_files: set[str],
|
|
56
|
+
workspace: WorkspaceInfo | None = None,
|
|
57
|
+
tsconfig_resolver: TsConfigResolver | None = None,
|
|
58
|
+
) -> str | None:
|
|
59
|
+
"""Deterministically resolve an import specifier to a repository-relative file path.
|
|
60
|
+
|
|
61
|
+
Never guesses files outside `known_files`.
|
|
62
|
+
"""
|
|
63
|
+
if not imported_module:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
# 0. Check workspace package mappings for cross-package imports
|
|
67
|
+
if workspace is not None:
|
|
68
|
+
target_pkg, entrypoint = workspace.resolve_package_import(imported_module, source_file)
|
|
69
|
+
if entrypoint:
|
|
70
|
+
clean_entry = entrypoint.replace("\\", "/").lstrip("./")
|
|
71
|
+
if clean_entry in known_files:
|
|
72
|
+
return clean_entry
|
|
73
|
+
for ext in (".ts", ".tsx", ".js", ".jsx", ".py"):
|
|
74
|
+
if f"{clean_entry}{ext}" in known_files:
|
|
75
|
+
return f"{clean_entry}{ext}"
|
|
76
|
+
if f"{clean_entry}/index{ext}" in known_files:
|
|
77
|
+
return f"{clean_entry}/index{ext}"
|
|
78
|
+
if f"{clean_entry}/__init__{ext}" in known_files:
|
|
79
|
+
return f"{clean_entry}/__init__{ext}"
|
|
80
|
+
|
|
81
|
+
# 0b. Check tsconfig.json / jsconfig.json path aliases
|
|
82
|
+
if tsconfig_resolver is not None:
|
|
83
|
+
alias_target = tsconfig_resolver.resolve_alias(imported_module, source_file, known_files)
|
|
84
|
+
if alias_target:
|
|
85
|
+
return alias_target
|
|
86
|
+
|
|
87
|
+
src_clean = source_file.replace("\\", "/").lstrip("./")
|
|
88
|
+
src_dir = posixpath.dirname(src_clean)
|
|
89
|
+
|
|
90
|
+
candidates: list[str] = []
|
|
91
|
+
|
|
92
|
+
if imported_module.startswith(".") or "/" in imported_module:
|
|
93
|
+
# Relative or path-based import (JS/TS or relative path)
|
|
94
|
+
if imported_module.startswith("."):
|
|
95
|
+
joined = posixpath.normpath(posixpath.join(src_dir, imported_module)) if src_dir else posixpath.normpath(imported_module)
|
|
96
|
+
else:
|
|
97
|
+
joined = posixpath.normpath(imported_module.lstrip("/"))
|
|
98
|
+
|
|
99
|
+
if joined.startswith(".."):
|
|
100
|
+
return None
|
|
101
|
+
if joined == ".":
|
|
102
|
+
joined = src_dir
|
|
103
|
+
|
|
104
|
+
candidates.append(joined)
|
|
105
|
+
for ext in (".ts", ".tsx", ".js", ".jsx", ".py", ".mjs", ".cjs"):
|
|
106
|
+
candidates.append(f"{joined}{ext}")
|
|
107
|
+
for idx_file in ("index.ts", "index.tsx", "index.js", "index.jsx", "__init__.py"):
|
|
108
|
+
candidates.append(posixpath.join(joined, idx_file) if joined else idx_file)
|
|
109
|
+
else:
|
|
110
|
+
# Dotted module specifier (Python or package root path)
|
|
111
|
+
mod_path = imported_module.replace(".", "/")
|
|
112
|
+
prefixes = [""]
|
|
113
|
+
if src_dir:
|
|
114
|
+
prefixes.append(f"{src_dir}/")
|
|
115
|
+
# Also try top-level package root (e.g. 'src/')
|
|
116
|
+
first_seg = src_dir.split("/")[0]
|
|
117
|
+
if f"{first_seg}/" not in prefixes:
|
|
118
|
+
prefixes.append(f"{first_seg}/")
|
|
119
|
+
if "src/" not in prefixes:
|
|
120
|
+
prefixes.append("src/")
|
|
121
|
+
|
|
122
|
+
for prefix in prefixes:
|
|
123
|
+
base = f"{prefix}{mod_path}"
|
|
124
|
+
candidates.append(f"{base}.py")
|
|
125
|
+
candidates.append(f"{base}/__init__.py")
|
|
126
|
+
for ext in (".ts", ".tsx", ".js", ".jsx"):
|
|
127
|
+
candidates.append(f"{base}{ext}")
|
|
128
|
+
candidates.append(f"{base}/index{ext}")
|
|
129
|
+
|
|
130
|
+
for cand in candidates:
|
|
131
|
+
norm = cand.lstrip("./")
|
|
132
|
+
if norm in known_files:
|
|
133
|
+
return norm
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class ResolutionOutput:
|
|
139
|
+
references: list[Reference]
|
|
140
|
+
edges: list[GraphEdge]
|
|
141
|
+
resolved_imports: dict[tuple[str, int, str], tuple[str | None, str | None]]
|
|
142
|
+
resolved_calls: dict[tuple[str, int, str], tuple[str | None, str]]
|
|
143
|
+
findings: list[dict[str, object]] = field(default_factory=list)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ReferenceResolver:
|
|
148
|
+
"""Resolves imports, aliases, re-exports, inheritance, calls, and routes across a repository."""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
symbols: list[Symbol],
|
|
153
|
+
imports: list[ImportRef],
|
|
154
|
+
calls: list[CallRef],
|
|
155
|
+
inheritance: list[InheritanceRef],
|
|
156
|
+
routes: list[RouteDetection],
|
|
157
|
+
known_files: set[str],
|
|
158
|
+
file_hashes: dict[str, str],
|
|
159
|
+
indexed_commit: str | None = None,
|
|
160
|
+
mount_edges: list[ComposedMountEdge] | None = None,
|
|
161
|
+
bindings: list[BindingRef] | None = None,
|
|
162
|
+
workspace: WorkspaceInfo | None = None,
|
|
163
|
+
tsconfig_resolver: TsConfigResolver | None = None,
|
|
164
|
+
repository: Path | None = None,
|
|
165
|
+
max_reexport_depth: int = 16,
|
|
166
|
+
max_wildcard_expansions: int = 64,
|
|
167
|
+
) -> None:
|
|
168
|
+
self.symbols = symbols
|
|
169
|
+
self.imports = imports
|
|
170
|
+
self.calls = calls
|
|
171
|
+
self.inheritance = inheritance
|
|
172
|
+
self.routes = routes
|
|
173
|
+
self.mount_edges = mount_edges or []
|
|
174
|
+
self.bindings = bindings or []
|
|
175
|
+
self.known_files = known_files
|
|
176
|
+
self.file_hashes = file_hashes
|
|
177
|
+
self.indexed_commit = indexed_commit
|
|
178
|
+
self.workspace = workspace
|
|
179
|
+
self.tsconfig_resolver = tsconfig_resolver
|
|
180
|
+
self.repository = repository
|
|
181
|
+
self.max_reexport_depth = max_reexport_depth
|
|
182
|
+
self.max_wildcard_expansions = max_wildcard_expansions
|
|
183
|
+
|
|
184
|
+
# Memoization caches and budget tracking
|
|
185
|
+
self._import_target_cache: dict[tuple[str, str], tuple[str | None, str | None]] = {}
|
|
186
|
+
self._symbol_in_module_cache: dict[tuple[str, str], Symbol | None] = {}
|
|
187
|
+
self.budget_findings: list[dict[str, object]] = []
|
|
188
|
+
self._seen_budget_keys: set[tuple[str, str]] = set()
|
|
189
|
+
|
|
190
|
+
# Indexed lookup maps (O(1) lookup)
|
|
191
|
+
self.canonical_to_symbol: dict[str, Symbol] = {}
|
|
192
|
+
self.qualified_to_symbols: dict[str, list[Symbol]] = {}
|
|
193
|
+
self.module_to_symbols: dict[str, dict[str, Symbol]] = {}
|
|
194
|
+
self.short_to_symbols: dict[str, list[Symbol]] = {}
|
|
195
|
+
self.path_to_symbols: dict[str, list[Symbol]] = {}
|
|
196
|
+
self.file_to_module: dict[str, str] = {
|
|
197
|
+
f: normalize_module(f) for f in known_files
|
|
198
|
+
}
|
|
199
|
+
self.module_to_file: dict[str, str] = {
|
|
200
|
+
normalize_module(f): f for f in sorted(known_files)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for sym in symbols:
|
|
204
|
+
self.canonical_to_symbol[sym.canonical_id] = sym
|
|
205
|
+
self.qualified_to_symbols.setdefault(sym.qualified_name, []).append(sym)
|
|
206
|
+
mod_map = self.module_to_symbols.setdefault(sym.module, {})
|
|
207
|
+
mod_map[sym.qualified_name] = sym
|
|
208
|
+
mod_map.setdefault(sym.name, sym)
|
|
209
|
+
self.short_to_symbols.setdefault(sym.name, []).append(sym)
|
|
210
|
+
self.path_to_symbols.setdefault(sym.path, []).append(sym)
|
|
211
|
+
|
|
212
|
+
self.file_imports: dict[str, list[ImportRef]] = {}
|
|
213
|
+
for imp in imports:
|
|
214
|
+
self.file_imports.setdefault(imp.source_file, []).append(imp)
|
|
215
|
+
|
|
216
|
+
# Map (source_module, exported_name) -> (target_module, target_name)
|
|
217
|
+
self.reexport_map: dict[tuple[str, str], tuple[str, str]] = {}
|
|
218
|
+
self.wildcard_reexports: dict[str, list[str]] = {}
|
|
219
|
+
self._build_reexport_maps()
|
|
220
|
+
|
|
221
|
+
# Local deterministic binding and data-flow engine
|
|
222
|
+
self.binding_resolver = LocalBindingResolver(
|
|
223
|
+
bindings=self.bindings,
|
|
224
|
+
symbols=self.symbols,
|
|
225
|
+
imports=self.imports,
|
|
226
|
+
known_files=self.known_files,
|
|
227
|
+
resolve_symbol_in_module=self.resolve_symbol_in_module,
|
|
228
|
+
resolve_import_target=self._resolve_import_target_module,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def _record_budget_exceeded(
|
|
232
|
+
self,
|
|
233
|
+
module: str,
|
|
234
|
+
symbol_name: str,
|
|
235
|
+
file_path: str | None = None,
|
|
236
|
+
line: int = 1,
|
|
237
|
+
) -> None:
|
|
238
|
+
b_key = (module, symbol_name)
|
|
239
|
+
if b_key in self._seen_budget_keys:
|
|
240
|
+
return
|
|
241
|
+
self._seen_budget_keys.add(b_key)
|
|
242
|
+
self.budget_findings.append(
|
|
243
|
+
{
|
|
244
|
+
"pattern": "WILDCARD_OR_REEXPORT_RESOLUTION",
|
|
245
|
+
"status": "UNKNOWN",
|
|
246
|
+
"reason": "resolution_budget_exceeded",
|
|
247
|
+
"module": module,
|
|
248
|
+
"symbol": symbol_name,
|
|
249
|
+
"file": file_path or self.module_to_file.get(module, ""),
|
|
250
|
+
"line": line,
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def resolve_symbol_diagnostic(con: sqlite3.Connection, query: str) -> dict[str, object]:
|
|
256
|
+
return resolve_symbol_diagnostic(con, query)
|
|
257
|
+
|
|
258
|
+
def _resolve_import_target_module(self, imp: ImportRef) -> tuple[str | None, str | None]:
|
|
259
|
+
"""Return (resolved_file_path, resolved_module) for an ImportRef."""
|
|
260
|
+
if self.workspace is not None:
|
|
261
|
+
cache_ctx = imp.source_file
|
|
262
|
+
else:
|
|
263
|
+
cache_ctx = posixpath.dirname(imp.source_file.replace("\\", "/").lstrip("./"))
|
|
264
|
+
cache_key = (imp.imported_module, cache_ctx)
|
|
265
|
+
if cache_key in self._import_target_cache:
|
|
266
|
+
return self._import_target_cache[cache_key]
|
|
267
|
+
|
|
268
|
+
res = self._resolve_import_target_module_uncached(imp)
|
|
269
|
+
self._import_target_cache[cache_key] = res
|
|
270
|
+
return res
|
|
271
|
+
|
|
272
|
+
def _resolve_import_target_module_uncached(self, imp: ImportRef) -> tuple[str | None, str | None]:
|
|
273
|
+
target_path = resolve_module_to_path(
|
|
274
|
+
imp.imported_module,
|
|
275
|
+
imp.source_file,
|
|
276
|
+
self.known_files,
|
|
277
|
+
workspace=self.workspace,
|
|
278
|
+
tsconfig_resolver=self.tsconfig_resolver,
|
|
279
|
+
)
|
|
280
|
+
if target_path:
|
|
281
|
+
return target_path, normalize_module(target_path)
|
|
282
|
+
|
|
283
|
+
# Maybe imported_module is `pkg.mod.Symbol` where `pkg.mod` is a file
|
|
284
|
+
if "." in imp.imported_module and not imp.imported_module.startswith("."):
|
|
285
|
+
parent_mod, last_seg = imp.imported_module.rsplit(".", 1)
|
|
286
|
+
parent_path = resolve_module_to_path(
|
|
287
|
+
parent_mod,
|
|
288
|
+
imp.source_file,
|
|
289
|
+
self.known_files,
|
|
290
|
+
workspace=self.workspace,
|
|
291
|
+
tsconfig_resolver=self.tsconfig_resolver,
|
|
292
|
+
)
|
|
293
|
+
if parent_path:
|
|
294
|
+
return parent_path, normalize_module(parent_path)
|
|
295
|
+
# Check if normalized module exists directly
|
|
296
|
+
if imp.imported_module in self.module_to_file:
|
|
297
|
+
p = self.module_to_file[imp.imported_module]
|
|
298
|
+
return p, imp.imported_module
|
|
299
|
+
if f"src.{imp.imported_module}" in self.module_to_file:
|
|
300
|
+
mod = f"src.{imp.imported_module}"
|
|
301
|
+
return self.module_to_file[mod], mod
|
|
302
|
+
del last_seg
|
|
303
|
+
|
|
304
|
+
if imp.imported_module in self.module_to_file:
|
|
305
|
+
p = self.module_to_file[imp.imported_module]
|
|
306
|
+
return p, imp.imported_module
|
|
307
|
+
if f"src.{imp.imported_module}" in self.module_to_file:
|
|
308
|
+
mod = f"src.{imp.imported_module}"
|
|
309
|
+
return self.module_to_file[mod], mod
|
|
310
|
+
|
|
311
|
+
return None, None
|
|
312
|
+
|
|
313
|
+
def _build_reexport_maps(self) -> None:
|
|
314
|
+
for imp in self.imports:
|
|
315
|
+
if not imp.is_reexport:
|
|
316
|
+
continue
|
|
317
|
+
_target_path, target_mod = self._resolve_import_target_module(imp)
|
|
318
|
+
if not target_mod:
|
|
319
|
+
continue
|
|
320
|
+
src_mod = imp.source_module or normalize_module(imp.source_file)
|
|
321
|
+
if imp.imported_name == "*" and not imp.alias:
|
|
322
|
+
targets = self.wildcard_reexports.setdefault(src_mod, [])
|
|
323
|
+
if target_mod not in targets:
|
|
324
|
+
targets.append(target_mod)
|
|
325
|
+
else:
|
|
326
|
+
exported_nm = imp.exported_name or imp.alias or imp.imported_name or imp.local_name
|
|
327
|
+
orig_nm = imp.imported_name or exported_nm
|
|
328
|
+
if exported_nm and orig_nm:
|
|
329
|
+
self.reexport_map[(src_mod, exported_nm)] = (target_mod, orig_nm)
|
|
330
|
+
|
|
331
|
+
def resolve_symbol_in_module(
|
|
332
|
+
self,
|
|
333
|
+
module: str,
|
|
334
|
+
symbol_name: str,
|
|
335
|
+
visited: set[tuple[str, str]] | None = None,
|
|
336
|
+
depth: int = 0,
|
|
337
|
+
budget: list[int] | None = None,
|
|
338
|
+
) -> Symbol | None:
|
|
339
|
+
"""Resolve `symbol_name` in `module`, following re-export chains deterministically with bounded budgets."""
|
|
340
|
+
is_top_level = visited is None and depth == 0
|
|
341
|
+
cache_key = (module, symbol_name)
|
|
342
|
+
if is_top_level and cache_key in self._symbol_in_module_cache:
|
|
343
|
+
return self._symbol_in_module_cache[cache_key]
|
|
344
|
+
|
|
345
|
+
if budget is None:
|
|
346
|
+
budget = [0]
|
|
347
|
+
|
|
348
|
+
res = self._resolve_symbol_in_module_impl(
|
|
349
|
+
module, symbol_name, visited, depth, budget, root_module=module, root_symbol=symbol_name
|
|
350
|
+
)
|
|
351
|
+
if is_top_level:
|
|
352
|
+
self._symbol_in_module_cache[cache_key] = res
|
|
353
|
+
return res
|
|
354
|
+
|
|
355
|
+
def _resolve_symbol_in_module_impl(
|
|
356
|
+
self,
|
|
357
|
+
module: str,
|
|
358
|
+
symbol_name: str,
|
|
359
|
+
visited: set[tuple[str, str]] | None,
|
|
360
|
+
depth: int,
|
|
361
|
+
budget: list[int],
|
|
362
|
+
root_module: str,
|
|
363
|
+
root_symbol: str,
|
|
364
|
+
) -> Symbol | None:
|
|
365
|
+
if depth > self.max_reexport_depth or budget[0] > self.max_wildcard_expansions:
|
|
366
|
+
self._record_budget_exceeded(root_module, root_symbol)
|
|
367
|
+
self._record_budget_exceeded(module, symbol_name)
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
visited = visited or set()
|
|
371
|
+
key = (module, symbol_name)
|
|
372
|
+
if key in visited:
|
|
373
|
+
return None
|
|
374
|
+
visited.add(key)
|
|
375
|
+
|
|
376
|
+
# 1. Direct symbol in module
|
|
377
|
+
mod_syms = self.module_to_symbols.get(module)
|
|
378
|
+
if mod_syms and symbol_name in mod_syms:
|
|
379
|
+
return mod_syms[symbol_name]
|
|
380
|
+
|
|
381
|
+
# Check canonical ID directly
|
|
382
|
+
canon_cand = f"{module}.{symbol_name}"
|
|
383
|
+
if canon_cand in self.canonical_to_symbol:
|
|
384
|
+
return self.canonical_to_symbol[canon_cand]
|
|
385
|
+
|
|
386
|
+
# If symbol_name is dotted (e.g. AS.login or AuthService.login), resolve prefix first
|
|
387
|
+
if "." in symbol_name:
|
|
388
|
+
prefix, rest = symbol_name.split(".", 1)
|
|
389
|
+
base_sym = self._resolve_symbol_in_module_impl(
|
|
390
|
+
module, prefix, set(visited), depth + 1, budget, root_module, root_symbol
|
|
391
|
+
)
|
|
392
|
+
if base_sym:
|
|
393
|
+
method_canon = f"{base_sym.canonical_id}.{rest}"
|
|
394
|
+
if method_canon in self.canonical_to_symbol:
|
|
395
|
+
return self.canonical_to_symbol[method_canon]
|
|
396
|
+
|
|
397
|
+
# 2. Named re-export in this module
|
|
398
|
+
if key in self.reexport_map:
|
|
399
|
+
target_mod, orig_name = self.reexport_map[key]
|
|
400
|
+
return self._resolve_symbol_in_module_impl(
|
|
401
|
+
target_mod, orig_name, visited, depth + 1, budget, root_module, root_symbol
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# 3. Wildcard re-exports in this module (bounded by max_wildcard_expansions)
|
|
405
|
+
for target_mod in self.wildcard_reexports.get(module, ()):
|
|
406
|
+
budget[0] += 1
|
|
407
|
+
if budget[0] > self.max_wildcard_expansions:
|
|
408
|
+
self._record_budget_exceeded(root_module, root_symbol)
|
|
409
|
+
self._record_budget_exceeded(module, symbol_name)
|
|
410
|
+
return None
|
|
411
|
+
found = self._resolve_symbol_in_module_impl(
|
|
412
|
+
target_mod, symbol_name, visited, depth + 1, budget, root_module, root_symbol
|
|
413
|
+
)
|
|
414
|
+
if found:
|
|
415
|
+
return found
|
|
416
|
+
|
|
417
|
+
# 4. Default export fallback: if symbol_name == "default" and module has a single class/function
|
|
418
|
+
if symbol_name == "default" and mod_syms:
|
|
419
|
+
top_level = [s for s in mod_syms.values() if not s.scope]
|
|
420
|
+
if len(top_level) == 1:
|
|
421
|
+
return top_level[0]
|
|
422
|
+
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
def resolve_identifier_in_file(
|
|
426
|
+
self,
|
|
427
|
+
identifier: str,
|
|
428
|
+
source_file: str,
|
|
429
|
+
caller_canonical_id: str | None = None,
|
|
430
|
+
use_line: int | None = None,
|
|
431
|
+
) -> tuple[Symbol | None, str, str]:
|
|
432
|
+
"""Resolve an identifier or dotted expression inside `source_file`.
|
|
433
|
+
|
|
434
|
+
Returns (Symbol | None, confidence, resolution_reason).
|
|
435
|
+
"""
|
|
436
|
+
src_mod = self.file_to_module.get(source_file) or normalize_module(source_file)
|
|
437
|
+
|
|
438
|
+
# Level 1: Lexical / local scope
|
|
439
|
+
if caller_canonical_id and caller_canonical_id in self.canonical_to_symbol:
|
|
440
|
+
caller_sym = self.canonical_to_symbol[caller_canonical_id]
|
|
441
|
+
# Check nested symbol inside caller or sibling in same scope
|
|
442
|
+
nested_canon = f"{caller_sym.canonical_id}.{identifier}"
|
|
443
|
+
if nested_canon in self.canonical_to_symbol:
|
|
444
|
+
return (
|
|
445
|
+
self.canonical_to_symbol[nested_canon],
|
|
446
|
+
"HIGH",
|
|
447
|
+
"Local nested symbol in lexical scope",
|
|
448
|
+
)
|
|
449
|
+
if caller_sym.scope:
|
|
450
|
+
sibling_canon = f"{src_mod}.{caller_sym.scope}.{identifier}"
|
|
451
|
+
if sibling_canon in self.canonical_to_symbol:
|
|
452
|
+
return (
|
|
453
|
+
self.canonical_to_symbol[sibling_canon],
|
|
454
|
+
"HIGH",
|
|
455
|
+
f"Class/scope member in {caller_sym.scope}",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Same-module symbol
|
|
459
|
+
local_sym = self.resolve_symbol_in_module(src_mod, identifier)
|
|
460
|
+
if local_sym:
|
|
461
|
+
return local_sym, "HIGH", f"Defined in same module '{src_mod}'"
|
|
462
|
+
if (src_mod, identifier) in self._seen_budget_keys:
|
|
463
|
+
return None, "UNKNOWN", "resolution_budget_exceeded"
|
|
464
|
+
|
|
465
|
+
# Level 2: Exact canonical symbol match
|
|
466
|
+
if identifier in self.canonical_to_symbol:
|
|
467
|
+
return (
|
|
468
|
+
self.canonical_to_symbol[identifier],
|
|
469
|
+
"HIGH",
|
|
470
|
+
"Exact canonical symbol match",
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Level 3 & 4: Imported alias, named import, default import, or namespace/qualified import
|
|
474
|
+
imps = self.file_imports.get(source_file, [])
|
|
475
|
+
if "." in identifier:
|
|
476
|
+
prefix, rest = identifier.split(".", 1)
|
|
477
|
+
# Check if prefix is a local class in same module
|
|
478
|
+
local_cls = self.resolve_symbol_in_module(src_mod, prefix)
|
|
479
|
+
if local_cls:
|
|
480
|
+
member_canon = f"{local_cls.canonical_id}.{rest}"
|
|
481
|
+
if member_canon in self.canonical_to_symbol:
|
|
482
|
+
return (
|
|
483
|
+
self.canonical_to_symbol[member_canon],
|
|
484
|
+
"HIGH",
|
|
485
|
+
f"Resolved method '{rest}' on local class '{local_cls.canonical_id}'",
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# Check if prefix is an imported symbol/alias or namespace
|
|
489
|
+
for imp in imps:
|
|
490
|
+
if imp.local_name == prefix or imp.alias == prefix or imp.name == prefix:
|
|
491
|
+
_tpath, target_mod = self._resolve_import_target_module(imp)
|
|
492
|
+
if target_mod:
|
|
493
|
+
if imp.import_type in ("namespace", "module") or imp.imported_name in (None, "*"):
|
|
494
|
+
sym = self.resolve_symbol_in_module(target_mod, rest)
|
|
495
|
+
if sym:
|
|
496
|
+
return (
|
|
497
|
+
sym,
|
|
498
|
+
"HIGH",
|
|
499
|
+
f"Resolved via namespace import '{prefix}' -> '{sym.canonical_id}'",
|
|
500
|
+
)
|
|
501
|
+
if (target_mod, rest) in self._seen_budget_keys:
|
|
502
|
+
return None, "UNKNOWN", "resolution_budget_exceeded"
|
|
503
|
+
else:
|
|
504
|
+
orig_cls = imp.imported_name or prefix
|
|
505
|
+
cls_sym = self.resolve_symbol_in_module(
|
|
506
|
+
target_mod, orig_cls
|
|
507
|
+
) or self.resolve_symbol_in_module(target_mod, prefix)
|
|
508
|
+
if cls_sym:
|
|
509
|
+
method_canon = f"{cls_sym.canonical_id}.{rest}"
|
|
510
|
+
if method_canon in self.canonical_to_symbol:
|
|
511
|
+
return (
|
|
512
|
+
self.canonical_to_symbol[method_canon],
|
|
513
|
+
"HIGH",
|
|
514
|
+
f"Resolved via imported symbol '{prefix}' -> '{method_canon}'",
|
|
515
|
+
)
|
|
516
|
+
# Or target_mod is actually a package and orig_cls is a submodule
|
|
517
|
+
sub_sym = self.resolve_symbol_in_module(f"{target_mod}.{orig_cls}", rest)
|
|
518
|
+
if sub_sym:
|
|
519
|
+
return (
|
|
520
|
+
sub_sym,
|
|
521
|
+
"HIGH",
|
|
522
|
+
f"Resolved via submodule import '{prefix}.{rest}' -> '{sub_sym.canonical_id}'",
|
|
523
|
+
)
|
|
524
|
+
else:
|
|
525
|
+
wildcard_imps: list[ImportRef] = []
|
|
526
|
+
for imp in imps:
|
|
527
|
+
if imp.local_name == identifier or imp.alias == identifier:
|
|
528
|
+
_tpath, target_mod = self._resolve_import_target_module(imp)
|
|
529
|
+
if target_mod:
|
|
530
|
+
if imp.import_type == "default":
|
|
531
|
+
sym = self.resolve_symbol_in_module(
|
|
532
|
+
target_mod, imp.local_name
|
|
533
|
+
) or self.resolve_symbol_in_module(target_mod, "default")
|
|
534
|
+
else:
|
|
535
|
+
target_name = imp.imported_name or identifier
|
|
536
|
+
sym = self.resolve_symbol_in_module(target_mod, target_name)
|
|
537
|
+
if sym:
|
|
538
|
+
return (
|
|
539
|
+
sym,
|
|
540
|
+
"HIGH",
|
|
541
|
+
f"Resolved via import '{imp.local_name}' from '{target_mod}'",
|
|
542
|
+
)
|
|
543
|
+
if (target_mod, imp.imported_name or identifier) in self._seen_budget_keys:
|
|
544
|
+
return None, "UNKNOWN", "resolution_budget_exceeded"
|
|
545
|
+
elif imp.imported_name == "*" and not imp.alias:
|
|
546
|
+
wildcard_imps.append(imp)
|
|
547
|
+
|
|
548
|
+
if wildcard_imps and not identifier.startswith("_"):
|
|
549
|
+
if len(wildcard_imps) > self.max_wildcard_expansions:
|
|
550
|
+
self._record_budget_exceeded(src_mod, identifier, file_path=source_file, line=use_line or 1)
|
|
551
|
+
return None, "UNKNOWN", "resolution_budget_exceeded"
|
|
552
|
+
wildcard_matches: dict[str, tuple[Symbol, str]] = {}
|
|
553
|
+
wc_budget = [0]
|
|
554
|
+
for w_imp in wildcard_imps:
|
|
555
|
+
_tpath, target_mod = self._resolve_import_target_module(w_imp)
|
|
556
|
+
if not target_mod:
|
|
557
|
+
continue
|
|
558
|
+
wc_budget[0] += 1
|
|
559
|
+
if wc_budget[0] > self.max_wildcard_expansions:
|
|
560
|
+
self._record_budget_exceeded(src_mod, identifier, file_path=source_file, line=use_line or 1)
|
|
561
|
+
return None, "UNKNOWN", "resolution_budget_exceeded"
|
|
562
|
+
w_sym = self.resolve_symbol_in_module(target_mod, identifier, budget=wc_budget)
|
|
563
|
+
if (target_mod, identifier) in self._seen_budget_keys or wc_budget[0] > self.max_wildcard_expansions:
|
|
564
|
+
self._record_budget_exceeded(src_mod, identifier, file_path=source_file, line=use_line or 1)
|
|
565
|
+
return None, "UNKNOWN", "resolution_budget_exceeded"
|
|
566
|
+
if w_sym:
|
|
567
|
+
wildcard_matches[w_sym.canonical_id] = (w_sym, target_mod)
|
|
568
|
+
if len(wildcard_matches) == 1:
|
|
569
|
+
only_sym, only_mod = next(iter(wildcard_matches.values()))
|
|
570
|
+
return (
|
|
571
|
+
only_sym,
|
|
572
|
+
"HIGH",
|
|
573
|
+
f"Resolved via wildcard import from '{only_mod}'",
|
|
574
|
+
)
|
|
575
|
+
if len(wildcard_matches) > 1:
|
|
576
|
+
return None, "UNKNOWN", "ambiguous_wildcard_import"
|
|
577
|
+
|
|
578
|
+
# Level 5: Exact qualified name match if unique across repository
|
|
579
|
+
if "." in identifier and identifier in self.qualified_to_symbols:
|
|
580
|
+
cands = self.qualified_to_symbols[identifier]
|
|
581
|
+
if len(cands) == 1:
|
|
582
|
+
return (
|
|
583
|
+
cands[0],
|
|
584
|
+
"MEDIUM",
|
|
585
|
+
f"Unique qualified symbol match '{cands[0].canonical_id}'",
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Level 6: Local Binding and Data-Flow resolution
|
|
589
|
+
caller_scope = ""
|
|
590
|
+
if caller_canonical_id and caller_canonical_id in self.canonical_to_symbol:
|
|
591
|
+
caller_scope = self.canonical_to_symbol[caller_canonical_id].scope
|
|
592
|
+
|
|
593
|
+
canon_id, ev_class, reason = self.binding_resolver.resolve(
|
|
594
|
+
file_path=source_file,
|
|
595
|
+
name=identifier,
|
|
596
|
+
scope=caller_scope,
|
|
597
|
+
use_line=use_line,
|
|
598
|
+
)
|
|
599
|
+
if canon_id:
|
|
600
|
+
if canon_id in self.canonical_to_symbol:
|
|
601
|
+
return self.canonical_to_symbol[canon_id], "HIGH", reason
|
|
602
|
+
# Dotted or submodule symbol
|
|
603
|
+
target_sym = self.resolve_symbol_in_module(src_mod, canon_id)
|
|
604
|
+
if target_sym:
|
|
605
|
+
return target_sym, "HIGH", reason
|
|
606
|
+
|
|
607
|
+
return None, "UNKNOWN", f"No matching local or imported symbol found ({reason if canon_id is None else ''})."
|
|
608
|
+
|
|
609
|
+
def resolve_all(self) -> ResolutionOutput:
|
|
610
|
+
"""Run the full resolution pipeline and emit deduplicated References and GraphEdges."""
|
|
611
|
+
references: list[Reference] = []
|
|
612
|
+
edges: list[GraphEdge] = []
|
|
613
|
+
seen_refs: set[tuple[str, str | None, str, str, int]] = set()
|
|
614
|
+
seen_edges: set[tuple[str, str, str, str, int]] = set()
|
|
615
|
+
resolved_imports: dict[tuple[str, int, str], tuple[str | None, str | None]] = {}
|
|
616
|
+
resolved_calls: dict[tuple[str, int, str], tuple[str | None, str]] = {}
|
|
617
|
+
|
|
618
|
+
def add_ref(ref: Reference) -> None:
|
|
619
|
+
key = (
|
|
620
|
+
ref.source_symbol_id,
|
|
621
|
+
ref.target_symbol_id,
|
|
622
|
+
ref.relationship,
|
|
623
|
+
ref.path,
|
|
624
|
+
ref.start_line,
|
|
625
|
+
)
|
|
626
|
+
if key not in seen_refs:
|
|
627
|
+
seen_refs.add(key)
|
|
628
|
+
references.append(ref)
|
|
629
|
+
|
|
630
|
+
_NON_CODE_EXTS = (
|
|
631
|
+
".html",
|
|
632
|
+
".htm",
|
|
633
|
+
".jinja",
|
|
634
|
+
".jinja2",
|
|
635
|
+
".css",
|
|
636
|
+
".yaml",
|
|
637
|
+
".yml",
|
|
638
|
+
".json",
|
|
639
|
+
".md",
|
|
640
|
+
".markdown",
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
def add_edge(edge: GraphEdge) -> None:
|
|
644
|
+
fpath = str(edge.file or "")
|
|
645
|
+
if fpath.endswith(_NON_CODE_EXTS):
|
|
646
|
+
return
|
|
647
|
+
key = (
|
|
648
|
+
edge.source,
|
|
649
|
+
edge.target,
|
|
650
|
+
edge.relationship,
|
|
651
|
+
edge.file,
|
|
652
|
+
edge.start_line,
|
|
653
|
+
)
|
|
654
|
+
if key not in seen_edges:
|
|
655
|
+
seen_edges.add(key)
|
|
656
|
+
edges.append(edge)
|
|
657
|
+
|
|
658
|
+
def _find_caller_id_for_binding(b: BindingRef) -> str:
|
|
659
|
+
for s in self.path_to_symbols.get(b.file_path, ()):
|
|
660
|
+
if s.start_line <= b.line <= s.end_line:
|
|
661
|
+
return s.canonical_id
|
|
662
|
+
mod = self.file_to_module.get(b.file_path) or normalize_module(b.file_path)
|
|
663
|
+
return f"{mod}.{b.scope}" if b.scope else mod
|
|
664
|
+
|
|
665
|
+
# 0. Definition & Containment edges
|
|
666
|
+
for sym in self.symbols:
|
|
667
|
+
if sym.path.endswith(_NON_CODE_EXTS):
|
|
668
|
+
continue
|
|
669
|
+
f_hash = self.file_hashes.get(sym.path, "")
|
|
670
|
+
if sym.parent_symbol_id:
|
|
671
|
+
add_edge(
|
|
672
|
+
GraphEdge(
|
|
673
|
+
source=sym.parent_symbol_id,
|
|
674
|
+
target=sym.canonical_id,
|
|
675
|
+
relationship="CONTAINS",
|
|
676
|
+
confidence="HIGH",
|
|
677
|
+
file=sym.path,
|
|
678
|
+
start_line=sym.start_line,
|
|
679
|
+
end_line=sym.end_line,
|
|
680
|
+
evidence=f"Scope containment {sym.parent_symbol_id} -> {sym.canonical_id}",
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
else:
|
|
684
|
+
add_edge(
|
|
685
|
+
GraphEdge(
|
|
686
|
+
source=sym.path,
|
|
687
|
+
target=sym.canonical_id,
|
|
688
|
+
relationship="DEFINES",
|
|
689
|
+
confidence="HIGH",
|
|
690
|
+
file=sym.path,
|
|
691
|
+
start_line=sym.start_line,
|
|
692
|
+
end_line=sym.end_line,
|
|
693
|
+
evidence=f"File {sym.path} defines {sym.canonical_id}",
|
|
694
|
+
)
|
|
695
|
+
)
|
|
696
|
+
del f_hash
|
|
697
|
+
|
|
698
|
+
# 1. Resolve Imports & Re-exports
|
|
699
|
+
for imp in self.imports:
|
|
700
|
+
src_mod = imp.source_module or normalize_module(imp.source_file)
|
|
701
|
+
f_hash = self.file_hashes.get(imp.source_file, "")
|
|
702
|
+
target_path, target_mod = self._resolve_import_target_module(imp)
|
|
703
|
+
resolved_imports[(imp.source_file, imp.line, imp.local_name)] = (
|
|
704
|
+
target_path,
|
|
705
|
+
target_mod,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
if imp.is_reexport:
|
|
709
|
+
exp_name = imp.exported_name or imp.alias or imp.imported_name or "*"
|
|
710
|
+
orig_name = imp.imported_name or exp_name
|
|
711
|
+
target_sym = (
|
|
712
|
+
self.resolve_symbol_in_module(target_mod, orig_name)
|
|
713
|
+
if (target_mod and orig_name != "*")
|
|
714
|
+
else None
|
|
715
|
+
)
|
|
716
|
+
target_id = (
|
|
717
|
+
target_sym.canonical_id
|
|
718
|
+
if target_sym
|
|
719
|
+
else (f"{target_mod}.{orig_name}" if target_mod else f"{imp.imported_module}.{orig_name}")
|
|
720
|
+
)
|
|
721
|
+
conf = "HIGH" if (target_sym or target_mod) else "LOW"
|
|
722
|
+
src_id = f"{src_mod}.{exp_name}" if exp_name != "*" else src_mod
|
|
723
|
+
add_ref(
|
|
724
|
+
Reference(
|
|
725
|
+
source_symbol_id=src_id,
|
|
726
|
+
target_symbol_id=target_id,
|
|
727
|
+
relationship="REEXPORTS",
|
|
728
|
+
confidence=conf,
|
|
729
|
+
path=imp.source_file,
|
|
730
|
+
start_line=imp.line,
|
|
731
|
+
end_line=imp.line,
|
|
732
|
+
evidence=f"Re-export '{exp_name}' from '{imp.imported_module}' -> '{target_id}'",
|
|
733
|
+
source_hash=f_hash,
|
|
734
|
+
indexed_commit=self.indexed_commit,
|
|
735
|
+
)
|
|
736
|
+
)
|
|
737
|
+
add_edge(
|
|
738
|
+
GraphEdge(
|
|
739
|
+
source=src_id,
|
|
740
|
+
target=target_id,
|
|
741
|
+
relationship="REEXPORTS",
|
|
742
|
+
confidence=conf,
|
|
743
|
+
file=imp.source_file,
|
|
744
|
+
start_line=imp.line,
|
|
745
|
+
end_line=imp.line,
|
|
746
|
+
evidence=f"Re-export '{exp_name}' from '{imp.imported_module}'",
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
else:
|
|
750
|
+
target_sym = None
|
|
751
|
+
if target_mod and imp.imported_name and imp.imported_name != "*":
|
|
752
|
+
target_sym = self.resolve_symbol_in_module(target_mod, imp.imported_name)
|
|
753
|
+
target_id = (
|
|
754
|
+
target_sym.canonical_id
|
|
755
|
+
if target_sym
|
|
756
|
+
else (target_mod or imp.imported_module)
|
|
757
|
+
)
|
|
758
|
+
conf = "HIGH"
|
|
759
|
+
add_ref(
|
|
760
|
+
Reference(
|
|
761
|
+
source_symbol_id=src_mod,
|
|
762
|
+
target_symbol_id=target_id,
|
|
763
|
+
relationship="IMPORTS",
|
|
764
|
+
confidence=conf,
|
|
765
|
+
path=imp.source_file,
|
|
766
|
+
start_line=imp.line,
|
|
767
|
+
end_line=imp.line,
|
|
768
|
+
evidence=f"Import '{imp.local_name}' from '{imp.imported_module}'",
|
|
769
|
+
source_hash=f_hash,
|
|
770
|
+
indexed_commit=self.indexed_commit,
|
|
771
|
+
)
|
|
772
|
+
)
|
|
773
|
+
add_edge(
|
|
774
|
+
GraphEdge(
|
|
775
|
+
source=imp.source_file,
|
|
776
|
+
target=target_path or imp.imported_module,
|
|
777
|
+
relationship="IMPORTS",
|
|
778
|
+
confidence="HIGH",
|
|
779
|
+
file=imp.source_file,
|
|
780
|
+
start_line=imp.line,
|
|
781
|
+
end_line=imp.line,
|
|
782
|
+
evidence=f"Import '{imp.imported_module}' at {imp.source_file}:{imp.line}",
|
|
783
|
+
)
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
# 2. Resolve Inheritance (EXTENDS / IMPLEMENTS) inside a map so inherited methods can also resolve
|
|
787
|
+
class_bases_map: dict[str, list[str]] = {}
|
|
788
|
+
for inh in self.inheritance:
|
|
789
|
+
f_hash = self.file_hashes.get(inh.source_file, "")
|
|
790
|
+
target_sym, conf, reason = self.resolve_identifier_in_file(
|
|
791
|
+
inh.base_name, inh.source_file, inh.source_canonical_id
|
|
792
|
+
)
|
|
793
|
+
target_id = target_sym.canonical_id if target_sym else inh.base_name
|
|
794
|
+
if target_sym:
|
|
795
|
+
class_bases_map.setdefault(inh.source_canonical_id, []).append(
|
|
796
|
+
target_sym.canonical_id
|
|
797
|
+
)
|
|
798
|
+
add_ref(
|
|
799
|
+
Reference(
|
|
800
|
+
source_symbol_id=inh.source_canonical_id,
|
|
801
|
+
target_symbol_id=target_id,
|
|
802
|
+
relationship=inh.relationship,
|
|
803
|
+
confidence=conf,
|
|
804
|
+
path=inh.source_file,
|
|
805
|
+
start_line=inh.line,
|
|
806
|
+
end_line=inh.line,
|
|
807
|
+
evidence=f"{inh.source_canonical_id} {inh.relationship} {target_id} ({reason})",
|
|
808
|
+
source_hash=f_hash,
|
|
809
|
+
indexed_commit=self.indexed_commit,
|
|
810
|
+
)
|
|
811
|
+
)
|
|
812
|
+
add_edge(
|
|
813
|
+
GraphEdge(
|
|
814
|
+
source=inh.source_canonical_id,
|
|
815
|
+
target=target_id,
|
|
816
|
+
relationship=inh.relationship,
|
|
817
|
+
confidence=conf,
|
|
818
|
+
file=inh.source_file,
|
|
819
|
+
start_line=inh.line,
|
|
820
|
+
end_line=inh.line,
|
|
821
|
+
evidence=f"{inh.source_canonical_id} {inh.relationship} {target_id}",
|
|
822
|
+
)
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
# 3. Resolve Call Sites (with shared receiver, local binding, and factory return resolution)
|
|
826
|
+
file_bindings_map: dict[str, list[BindingRef]] = {}
|
|
827
|
+
receiver_bindings_by_fts: dict[tuple[str, str, str], list[BindingRef]] = {}
|
|
828
|
+
call_return_bindings_by_file: dict[str, list[BindingRef]] = {}
|
|
829
|
+
_RECEIVER_EXPR_KINDS = frozenset(
|
|
830
|
+
("CALL_RETURN", "TYPE_ANNOTATION", "IDENTIFIER", "ATTRIBUTE", "DYNAMIC", "DYNAMIC_CALL_RETURN")
|
|
831
|
+
)
|
|
832
|
+
for b_item in self.bindings:
|
|
833
|
+
file_bindings_map.setdefault(b_item.file_path, []).append(b_item)
|
|
834
|
+
if b_item.expr_kind in _RECEIVER_EXPR_KINDS:
|
|
835
|
+
receiver_bindings_by_fts.setdefault(
|
|
836
|
+
(b_item.file_path, b_item.target_name, b_item.scope), []
|
|
837
|
+
).append(b_item)
|
|
838
|
+
if b_item.expr_kind == "CALL_RETURN" and not b_item.is_conditional:
|
|
839
|
+
call_return_bindings_by_file.setdefault(b_item.file_path, []).append(b_item)
|
|
840
|
+
|
|
841
|
+
def _lookup_method_on_class(class_canon: str, method_name: str) -> tuple[Symbol | None, str]:
|
|
842
|
+
direct_cand = f"{class_canon}.{method_name}"
|
|
843
|
+
if direct_cand in self.canonical_to_symbol:
|
|
844
|
+
return self.canonical_to_symbol[direct_cand], class_canon
|
|
845
|
+
visited_bases: set[str] = {class_canon}
|
|
846
|
+
base_queue: list[str] = list(class_bases_map.get(class_canon, []))
|
|
847
|
+
while base_queue and len(visited_bases) <= 15:
|
|
848
|
+
base_canon = base_queue.pop(0)
|
|
849
|
+
if base_canon in visited_bases:
|
|
850
|
+
continue
|
|
851
|
+
visited_bases.add(base_canon)
|
|
852
|
+
cand = f"{base_canon}.{method_name}"
|
|
853
|
+
if cand in self.canonical_to_symbol:
|
|
854
|
+
return self.canonical_to_symbol[cand], base_canon
|
|
855
|
+
base_queue.extend(class_bases_map.get(base_canon, []))
|
|
856
|
+
return None, class_canon
|
|
857
|
+
|
|
858
|
+
factory_return_cache: dict[str, tuple[Symbol | None, str]] = {}
|
|
859
|
+
|
|
860
|
+
def _resolve_factory_return_class(f_sym: Symbol, depth: int = 0) -> tuple[Symbol | None, str]:
|
|
861
|
+
if depth == 0 and f_sym.canonical_id in factory_return_cache:
|
|
862
|
+
return factory_return_cache[f_sym.canonical_id]
|
|
863
|
+
if depth > 5 or not f_sym.return_type:
|
|
864
|
+
return None, ""
|
|
865
|
+
raw_ret = f_sym.return_type.strip()
|
|
866
|
+
if raw_ret in ("<AMBIGUOUS>", "<DYNAMIC>") or "|" in raw_ret:
|
|
867
|
+
return None, ""
|
|
868
|
+
clean_ret = raw_ret.split("[")[0].strip().strip("'\"")
|
|
869
|
+
if not clean_ret:
|
|
870
|
+
return None, ""
|
|
871
|
+
ret_sym, _rc, _rr = self.resolve_identifier_in_file(
|
|
872
|
+
clean_ret, f_sym.path, f_sym.canonical_id
|
|
873
|
+
)
|
|
874
|
+
if not ret_sym:
|
|
875
|
+
cand_c = f"{f_sym.module}.{clean_ret}"
|
|
876
|
+
ret_sym = self.canonical_to_symbol.get(cand_c)
|
|
877
|
+
if not ret_sym:
|
|
878
|
+
if depth == 0:
|
|
879
|
+
factory_return_cache[f_sym.canonical_id] = (None, "")
|
|
880
|
+
return None, ""
|
|
881
|
+
if ret_sym.kind == "class":
|
|
882
|
+
res = (ret_sym, f"factory '{f_sym.canonical_id}()' -> '{ret_sym.canonical_id}'")
|
|
883
|
+
if depth == 0:
|
|
884
|
+
factory_return_cache[f_sym.canonical_id] = res
|
|
885
|
+
return res
|
|
886
|
+
if ret_sym.kind in ("function", "method"):
|
|
887
|
+
sub_cls, sub_rsn = _resolve_factory_return_class(ret_sym, depth + 1)
|
|
888
|
+
if sub_cls:
|
|
889
|
+
res = (sub_cls, f"factory '{f_sym.canonical_id}()' -> {sub_rsn}")
|
|
890
|
+
if depth == 0:
|
|
891
|
+
factory_return_cache[f_sym.canonical_id] = res
|
|
892
|
+
return res
|
|
893
|
+
if depth == 0:
|
|
894
|
+
factory_return_cache[f_sym.canonical_id] = (None, "")
|
|
895
|
+
return None, ""
|
|
896
|
+
|
|
897
|
+
def _resolve_receiver_class(
|
|
898
|
+
recv_part: str,
|
|
899
|
+
source_file: str,
|
|
900
|
+
caller_id: str,
|
|
901
|
+
caller_qname: str,
|
|
902
|
+
use_line: int,
|
|
903
|
+
depth: int = 0,
|
|
904
|
+
) -> tuple[Symbol | None, Symbol | None, str]:
|
|
905
|
+
if depth > 5 or not recv_part:
|
|
906
|
+
return None, None, ""
|
|
907
|
+
|
|
908
|
+
f_bindings = file_bindings_map.get(source_file)
|
|
909
|
+
if f_bindings:
|
|
910
|
+
caller_sym_obj = self.canonical_to_symbol.get(caller_id)
|
|
911
|
+
eff_qname = caller_qname or (caller_sym_obj.qualified_name if caller_sym_obj else "")
|
|
912
|
+
enclosing_cls = (
|
|
913
|
+
caller_sym_obj.scope
|
|
914
|
+
if (caller_sym_obj and caller_sym_obj.scope)
|
|
915
|
+
else (eff_qname.rsplit(".", 1)[0] if "." in eff_qname else "")
|
|
916
|
+
)
|
|
917
|
+
candidate_scopes: list[str] = []
|
|
918
|
+
for sc_cand in (
|
|
919
|
+
eff_qname,
|
|
920
|
+
caller_id,
|
|
921
|
+
enclosing_cls,
|
|
922
|
+
f"{caller_sym_obj.module}.{enclosing_cls}" if (caller_sym_obj and enclosing_cls) else "",
|
|
923
|
+
"",
|
|
924
|
+
):
|
|
925
|
+
if sc_cand not in candidate_scopes:
|
|
926
|
+
candidate_scopes.append(sc_cand)
|
|
927
|
+
|
|
928
|
+
matched_scope_bindings: list[BindingRef] = []
|
|
929
|
+
for sc in candidate_scopes:
|
|
930
|
+
sc_matches = receiver_bindings_by_fts.get((source_file, recv_part, sc))
|
|
931
|
+
if sc_matches:
|
|
932
|
+
prior = [b for b in sc_matches if b.line <= use_line]
|
|
933
|
+
matched_scope_bindings = prior if prior else (sc_matches if recv_part.startswith("self.") else [])
|
|
934
|
+
if matched_scope_bindings:
|
|
935
|
+
break
|
|
936
|
+
|
|
937
|
+
if matched_scope_bindings:
|
|
938
|
+
if any(b.expr_kind in ("DYNAMIC", "DYNAMIC_CALL_RETURN") or b.source_expr == "<DYNAMIC>" for b in matched_scope_bindings):
|
|
939
|
+
return None, None, "Dynamic receiver assignment"
|
|
940
|
+
if any(b.is_conditional for b in matched_scope_bindings):
|
|
941
|
+
return None, None, "Conditional receiver assignment"
|
|
942
|
+
call_or_id_exprs = {
|
|
943
|
+
b.source_expr for b in matched_scope_bindings
|
|
944
|
+
if b.expr_kind in ("CALL_RETURN", "IDENTIFIER", "ATTRIBUTE")
|
|
945
|
+
}
|
|
946
|
+
if len(call_or_id_exprs) > 1:
|
|
947
|
+
return None, None, "Conflicting receiver assignments in scope"
|
|
948
|
+
|
|
949
|
+
# Prefer CALL_RETURN / IDENTIFIER / ATTRIBUTE first (to preserve factory provenance), then TYPE_ANNOTATION
|
|
950
|
+
chosen_b = next(
|
|
951
|
+
(b for b in reversed(matched_scope_bindings) if b.expr_kind in ("CALL_RETURN", "IDENTIFIER", "ATTRIBUTE")),
|
|
952
|
+
matched_scope_bindings[-1],
|
|
953
|
+
)
|
|
954
|
+
if chosen_b.expr_kind == "TYPE_ANNOTATION":
|
|
955
|
+
ann_sym, _, _ = self.resolve_identifier_in_file(
|
|
956
|
+
chosen_b.source_expr, chosen_b.file_path, caller_id, use_line=chosen_b.line
|
|
957
|
+
)
|
|
958
|
+
if ann_sym and ann_sym.kind == "class":
|
|
959
|
+
return ann_sym, None, f"Type annotation '{chosen_b.source_expr}' -> '{ann_sym.canonical_id}'"
|
|
960
|
+
else:
|
|
961
|
+
src_expr = chosen_b.source_expr[:-2].strip() if chosen_b.source_expr.endswith("()") else chosen_b.source_expr.strip()
|
|
962
|
+
cal_sym, _, _ = self.resolve_identifier_in_file(
|
|
963
|
+
src_expr, chosen_b.file_path, caller_id, use_line=chosen_b.line
|
|
964
|
+
)
|
|
965
|
+
if not cal_sym and "." in src_expr:
|
|
966
|
+
f_recv, f_meth = src_expr.rsplit(".", 1)
|
|
967
|
+
f_cls_sym, _, _ = _resolve_receiver_class(
|
|
968
|
+
f_recv, chosen_b.file_path, caller_id, caller_qname, chosen_b.line, depth + 1
|
|
969
|
+
)
|
|
970
|
+
if f_cls_sym:
|
|
971
|
+
cal_sym, _ = _lookup_method_on_class(f_cls_sym.canonical_id, f_meth)
|
|
972
|
+
if cal_sym:
|
|
973
|
+
if cal_sym.kind == "class":
|
|
974
|
+
return cal_sym, None, f"Constructed instance of '{cal_sym.canonical_id}'"
|
|
975
|
+
if cal_sym.kind in ("function", "method"):
|
|
976
|
+
ret_cls, ret_rsn = _resolve_factory_return_class(cal_sym, depth + 1)
|
|
977
|
+
if ret_cls:
|
|
978
|
+
return ret_cls, cal_sym, f"Resolved via {ret_rsn}"
|
|
979
|
+
|
|
980
|
+
# Direct symbol lookup for receiver (class name or factory function name in chained call)
|
|
981
|
+
recv_sym, _rconf, _rreason = self.resolve_identifier_in_file(
|
|
982
|
+
recv_part, source_file, caller_id, use_line=use_line
|
|
983
|
+
)
|
|
984
|
+
if recv_sym:
|
|
985
|
+
if recv_sym.kind == "class":
|
|
986
|
+
return recv_sym, None, f"Direct receiver class '{recv_sym.canonical_id}'"
|
|
987
|
+
if recv_sym.kind in ("function", "method"):
|
|
988
|
+
ret_cls, ret_rsn = _resolve_factory_return_class(recv_sym, depth + 1)
|
|
989
|
+
if ret_cls:
|
|
990
|
+
return ret_cls, recv_sym, f"Resolved via {ret_rsn}"
|
|
991
|
+
elif "." in recv_part:
|
|
992
|
+
sub_recv, sub_meth = recv_part.rsplit(".", 1)
|
|
993
|
+
sub_cls, _, _ = _resolve_receiver_class(
|
|
994
|
+
sub_recv, source_file, caller_id, caller_qname, use_line, depth + 1
|
|
995
|
+
)
|
|
996
|
+
if sub_cls:
|
|
997
|
+
meth_sym, _ = _lookup_method_on_class(sub_cls.canonical_id, sub_meth)
|
|
998
|
+
if meth_sym:
|
|
999
|
+
ret_cls, ret_rsn = _resolve_factory_return_class(meth_sym, depth + 1)
|
|
1000
|
+
if ret_cls:
|
|
1001
|
+
return ret_cls, meth_sym, f"Resolved via {ret_rsn}"
|
|
1002
|
+
|
|
1003
|
+
# Fallback if parser substituted local variable with same-file factory's return type string
|
|
1004
|
+
cr_bindings = call_return_bindings_by_file.get(source_file)
|
|
1005
|
+
if cr_bindings:
|
|
1006
|
+
for b in reversed(cr_bindings):
|
|
1007
|
+
if b.line <= use_line:
|
|
1008
|
+
f_sym, _, _ = self.resolve_identifier_in_file(
|
|
1009
|
+
b.source_expr, b.file_path, caller_id, use_line=b.line
|
|
1010
|
+
)
|
|
1011
|
+
if f_sym and f_sym.kind in ("function", "method"):
|
|
1012
|
+
ret_cls, ret_rsn = _resolve_factory_return_class(f_sym, depth + 1)
|
|
1013
|
+
if ret_cls and (ret_cls.name == recv_part or ret_cls.qualified_name == recv_part):
|
|
1014
|
+
return ret_cls, f_sym, f"Resolved via {ret_rsn}"
|
|
1015
|
+
|
|
1016
|
+
return None, None, ""
|
|
1017
|
+
|
|
1018
|
+
for call in self.calls:
|
|
1019
|
+
f_hash = self.file_hashes.get(call.source_file, "")
|
|
1020
|
+
caller_id = call.caller_canonical_id or (self.file_to_module.get(call.source_file) or normalize_module(call.source_file))
|
|
1021
|
+
caller_sym_for_qname = self.canonical_to_symbol.get(caller_id)
|
|
1022
|
+
caller_qname = call.caller_symbol or (caller_sym_for_qname.qualified_name if caller_sym_for_qname else "")
|
|
1023
|
+
expr = call.qualified_callee or call.callee
|
|
1024
|
+
via_factory_sym: Symbol | None = None
|
|
1025
|
+
resolved_via_dataflow = False
|
|
1026
|
+
is_pytorch_forward = False
|
|
1027
|
+
is_concrete_module = False
|
|
1028
|
+
|
|
1029
|
+
target_sym, conf, reason = self.resolve_identifier_in_file(
|
|
1030
|
+
expr, call.source_file, caller_id, use_line=call.line
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
# Check receiver resolution (local binding, self.<attr>, inherited method, or factory return type)
|
|
1034
|
+
if "." in expr:
|
|
1035
|
+
recv_part, method_part = expr.rsplit(".", 1)
|
|
1036
|
+
if not target_sym:
|
|
1037
|
+
recv_cls_sym, via_factory_sym, recv_reason = _resolve_receiver_class(
|
|
1038
|
+
recv_part, call.source_file, caller_id, caller_qname, call.line
|
|
1039
|
+
)
|
|
1040
|
+
if recv_cls_sym:
|
|
1041
|
+
meth_sym, owner_canon = _lookup_method_on_class(recv_cls_sym.canonical_id, method_part)
|
|
1042
|
+
if meth_sym:
|
|
1043
|
+
target_sym = meth_sym
|
|
1044
|
+
conf = "HIGH"
|
|
1045
|
+
resolved_via_dataflow = True
|
|
1046
|
+
if owner_canon != recv_cls_sym.canonical_id:
|
|
1047
|
+
reason = f"{recv_reason}; inherited method on '{owner_canon}'"
|
|
1048
|
+
else:
|
|
1049
|
+
reason = recv_reason
|
|
1050
|
+
# If not a method, check if expr or self.<attr> is an nn.Module submodule invocation (e.g. self.encoder(x))
|
|
1051
|
+
if not target_sym:
|
|
1052
|
+
submod_cls_sym, _, submod_reason = _resolve_receiver_class(
|
|
1053
|
+
expr, call.source_file, caller_id, caller_qname, call.line
|
|
1054
|
+
)
|
|
1055
|
+
if not submod_cls_sym:
|
|
1056
|
+
for b in self.bindings:
|
|
1057
|
+
if b.file_path == call.source_file and b.target_name == f"self.{method_part}":
|
|
1058
|
+
if b.expr_kind == "CALL_RETURN" and b.source_expr:
|
|
1059
|
+
s_cls, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, caller_id, use_line=b.line)
|
|
1060
|
+
if s_cls and s_cls.kind == "class":
|
|
1061
|
+
submod_cls_sym = s_cls
|
|
1062
|
+
submod_reason = f"submodule assignment 'self.{method_part} = {b.source_expr}()'"
|
|
1063
|
+
break
|
|
1064
|
+
elif b.expr_kind == "TYPE_ANNOTATION" and b.source_expr:
|
|
1065
|
+
s_cls, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, caller_id, use_line=b.line)
|
|
1066
|
+
if s_cls and s_cls.kind == "class":
|
|
1067
|
+
submod_cls_sym = s_cls
|
|
1068
|
+
submod_reason = f"submodule annotation 'self.{method_part}: {b.source_expr}'"
|
|
1069
|
+
break
|
|
1070
|
+
if submod_cls_sym:
|
|
1071
|
+
fwd_sym, _ = _lookup_method_on_class(submod_cls_sym.canonical_id, "forward")
|
|
1072
|
+
if fwd_sym:
|
|
1073
|
+
target_sym = fwd_sym
|
|
1074
|
+
resolved_via_dataflow = True
|
|
1075
|
+
is_pytorch_forward = True
|
|
1076
|
+
is_concrete_module = "Constructed instance" in submod_reason or "factory" in submod_reason or "submodule assignment" in submod_reason
|
|
1077
|
+
conf = "HIGH" if is_concrete_module else "MEDIUM"
|
|
1078
|
+
reason = f"PyTorch submodule forward dispatch via {submod_reason}"
|
|
1079
|
+
else:
|
|
1080
|
+
# Even if parser substituted receiver -> ClassName, check if it came from a local factory binding
|
|
1081
|
+
for b in call_return_bindings_by_file.get(call.source_file, ()):
|
|
1082
|
+
if b.scope in (caller_qname, caller_id) and b.line <= call.line:
|
|
1083
|
+
f_sym, _, _ = self.resolve_identifier_in_file(
|
|
1084
|
+
b.source_expr, b.file_path, caller_id, use_line=b.line
|
|
1085
|
+
)
|
|
1086
|
+
if f_sym and f_sym.kind in ("function", "method"):
|
|
1087
|
+
ret_cls, ret_rsn = _resolve_factory_return_class(f_sym)
|
|
1088
|
+
if ret_cls and target_sym.canonical_id.startswith(ret_cls.canonical_id + "."):
|
|
1089
|
+
via_factory_sym = f_sym
|
|
1090
|
+
resolved_via_dataflow = True
|
|
1091
|
+
reason = f"Resolved via {ret_rsn}"
|
|
1092
|
+
break
|
|
1093
|
+
|
|
1094
|
+
if target_sym and via_factory_sym:
|
|
1095
|
+
add_ref(
|
|
1096
|
+
Reference(
|
|
1097
|
+
source_symbol_id=caller_id,
|
|
1098
|
+
target_symbol_id=via_factory_sym.canonical_id,
|
|
1099
|
+
relationship="CALLS",
|
|
1100
|
+
confidence="HIGH",
|
|
1101
|
+
path=call.source_file,
|
|
1102
|
+
start_line=call.line,
|
|
1103
|
+
end_line=call.end_line,
|
|
1104
|
+
evidence=f"Call factory '{via_factory_sym.canonical_id}()' at {call.source_file}:{call.line}",
|
|
1105
|
+
source_hash=f_hash,
|
|
1106
|
+
indexed_commit=self.indexed_commit,
|
|
1107
|
+
)
|
|
1108
|
+
)
|
|
1109
|
+
add_edge(
|
|
1110
|
+
GraphEdge(
|
|
1111
|
+
source=caller_id,
|
|
1112
|
+
target=via_factory_sym.canonical_id,
|
|
1113
|
+
relationship="CALLS",
|
|
1114
|
+
confidence="HIGH",
|
|
1115
|
+
file=call.source_file,
|
|
1116
|
+
start_line=call.line,
|
|
1117
|
+
end_line=call.end_line,
|
|
1118
|
+
evidence=f"Resolved factory call {caller_id} -> {via_factory_sym.canonical_id} at {call.source_file}:{call.line}",
|
|
1119
|
+
)
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
else:
|
|
1123
|
+
# expr has no dot (e.g. model(x))
|
|
1124
|
+
# Only dispatch to forward if target_sym is NOT already a class (constructor call Classifier() is __init__)
|
|
1125
|
+
if not target_sym or target_sym.kind not in ("function", "method", "class"):
|
|
1126
|
+
mod_cls_sym, _, mod_reason = _resolve_receiver_class(
|
|
1127
|
+
expr, call.source_file, caller_id, caller_qname, call.line
|
|
1128
|
+
)
|
|
1129
|
+
if mod_cls_sym and mod_cls_sym.kind == "class":
|
|
1130
|
+
fwd_sym, _ = _lookup_method_on_class(mod_cls_sym.canonical_id, "forward")
|
|
1131
|
+
if fwd_sym:
|
|
1132
|
+
target_sym = fwd_sym
|
|
1133
|
+
resolved_via_dataflow = True
|
|
1134
|
+
is_pytorch_forward = True
|
|
1135
|
+
is_concrete_module = "Constructed instance" in mod_reason or "factory" in mod_reason
|
|
1136
|
+
conf = "HIGH" if is_concrete_module else "MEDIUM"
|
|
1137
|
+
reason = f"PyTorch module forward dispatch via {mod_reason}"
|
|
1138
|
+
|
|
1139
|
+
if target_sym:
|
|
1140
|
+
resolved_calls[(call.source_file, call.line, call.callee)] = (
|
|
1141
|
+
target_sym.canonical_id,
|
|
1142
|
+
conf,
|
|
1143
|
+
)
|
|
1144
|
+
call_ev_cls = (
|
|
1145
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED.value
|
|
1146
|
+
if resolved_via_dataflow
|
|
1147
|
+
else RelationshipEvidenceClass.AST_VERIFIED.value
|
|
1148
|
+
)
|
|
1149
|
+
add_ref(
|
|
1150
|
+
Reference(
|
|
1151
|
+
source_symbol_id=caller_id,
|
|
1152
|
+
target_symbol_id=target_sym.canonical_id,
|
|
1153
|
+
relationship="CALLS",
|
|
1154
|
+
confidence=conf,
|
|
1155
|
+
path=call.source_file,
|
|
1156
|
+
start_line=call.line,
|
|
1157
|
+
end_line=call.end_line,
|
|
1158
|
+
evidence=f"Call '{expr}()' at {call.source_file}:{call.line} -> {target_sym.canonical_id} ({reason})",
|
|
1159
|
+
source_hash=f_hash,
|
|
1160
|
+
indexed_commit=self.indexed_commit,
|
|
1161
|
+
)
|
|
1162
|
+
)
|
|
1163
|
+
add_edge(
|
|
1164
|
+
GraphEdge(
|
|
1165
|
+
source=caller_id,
|
|
1166
|
+
target=target_sym.canonical_id,
|
|
1167
|
+
relationship="CALLS",
|
|
1168
|
+
confidence=conf,
|
|
1169
|
+
file=call.source_file,
|
|
1170
|
+
start_line=call.line,
|
|
1171
|
+
end_line=call.end_line,
|
|
1172
|
+
evidence=f"Resolved call {caller_id} -> {target_sym.canonical_id} at {call.source_file}:{call.line} ({reason})",
|
|
1173
|
+
evidence_class=call_ev_cls,
|
|
1174
|
+
)
|
|
1175
|
+
)
|
|
1176
|
+
if is_pytorch_forward:
|
|
1177
|
+
fwd_ev_cls = (
|
|
1178
|
+
RelationshipEvidenceClass.AST_VERIFIED.value
|
|
1179
|
+
if is_concrete_module
|
|
1180
|
+
else RelationshipEvidenceClass.POSSIBLE.value
|
|
1181
|
+
)
|
|
1182
|
+
add_ref(
|
|
1183
|
+
Reference(
|
|
1184
|
+
source_symbol_id=caller_id,
|
|
1185
|
+
target_symbol_id=target_sym.canonical_id,
|
|
1186
|
+
relationship="DISPATCHES_FORWARD",
|
|
1187
|
+
confidence=conf,
|
|
1188
|
+
path=call.source_file,
|
|
1189
|
+
start_line=call.line,
|
|
1190
|
+
end_line=call.end_line,
|
|
1191
|
+
evidence=f"Module invocation '{expr}()' at {call.source_file}:{call.line} dispatches to {target_sym.canonical_id} ({reason})",
|
|
1192
|
+
source_hash=f_hash,
|
|
1193
|
+
indexed_commit=self.indexed_commit,
|
|
1194
|
+
)
|
|
1195
|
+
)
|
|
1196
|
+
add_edge(
|
|
1197
|
+
GraphEdge(
|
|
1198
|
+
source=caller_id,
|
|
1199
|
+
target=target_sym.canonical_id,
|
|
1200
|
+
relationship="DISPATCHES_FORWARD",
|
|
1201
|
+
confidence=conf,
|
|
1202
|
+
file=call.source_file,
|
|
1203
|
+
start_line=call.line,
|
|
1204
|
+
end_line=call.end_line,
|
|
1205
|
+
evidence=f"Resolved PyTorch forward dispatch {caller_id} -> {target_sym.canonical_id} at {call.source_file}:{call.line} ({reason})",
|
|
1206
|
+
evidence_class=fwd_ev_cls,
|
|
1207
|
+
reason="pytorch_forward_dispatch",
|
|
1208
|
+
)
|
|
1209
|
+
)
|
|
1210
|
+
elif "[" in expr and "]" in expr:
|
|
1211
|
+
dict_part, raw_k = expr.split("[", 1)
|
|
1212
|
+
raw_k = raw_k.rstrip("]")
|
|
1213
|
+
clean_k = raw_k.strip("'\"")
|
|
1214
|
+
idx_val = int(clean_k) if clean_k.isdigit() else None
|
|
1215
|
+
key_val = None if idx_val is not None else clean_k
|
|
1216
|
+
disp_target, disp_ev, disp_rsn = self.binding_resolver.resolve_subscript(
|
|
1217
|
+
call.source_file, dict_part, key=key_val, index=idx_val, use_line=call.line
|
|
1218
|
+
)
|
|
1219
|
+
if disp_target:
|
|
1220
|
+
add_ref(
|
|
1221
|
+
Reference(
|
|
1222
|
+
source_symbol_id=caller_id,
|
|
1223
|
+
target_symbol_id=disp_target,
|
|
1224
|
+
relationship="DISPATCHES_TO",
|
|
1225
|
+
confidence="HIGH",
|
|
1226
|
+
path=call.source_file,
|
|
1227
|
+
start_line=call.line,
|
|
1228
|
+
end_line=call.end_line,
|
|
1229
|
+
evidence=f"Dispatch call '{expr}()' -> {disp_target} ({disp_rsn})",
|
|
1230
|
+
source_hash=f_hash,
|
|
1231
|
+
indexed_commit=self.indexed_commit,
|
|
1232
|
+
)
|
|
1233
|
+
)
|
|
1234
|
+
add_edge(
|
|
1235
|
+
GraphEdge(
|
|
1236
|
+
source=caller_id,
|
|
1237
|
+
target=disp_target,
|
|
1238
|
+
relationship="DISPATCHES_TO",
|
|
1239
|
+
confidence="HIGH",
|
|
1240
|
+
file=call.source_file,
|
|
1241
|
+
start_line=call.line,
|
|
1242
|
+
end_line=call.end_line,
|
|
1243
|
+
evidence=f"Dispatch call '{expr}()' -> {disp_target} at {call.source_file}:{call.line}",
|
|
1244
|
+
evidence_class=disp_ev.value,
|
|
1245
|
+
reason=disp_rsn,
|
|
1246
|
+
)
|
|
1247
|
+
)
|
|
1248
|
+
else:
|
|
1249
|
+
resolved_calls[(call.source_file, call.line, call.callee)] = (
|
|
1250
|
+
None,
|
|
1251
|
+
"UNKNOWN",
|
|
1252
|
+
)
|
|
1253
|
+
add_ref(
|
|
1254
|
+
Reference(
|
|
1255
|
+
source_symbol_id=caller_id,
|
|
1256
|
+
target_symbol_id=None,
|
|
1257
|
+
relationship="UNRESOLVED_REFERENCE",
|
|
1258
|
+
confidence="UNKNOWN",
|
|
1259
|
+
path=call.source_file,
|
|
1260
|
+
start_line=call.line,
|
|
1261
|
+
end_line=call.end_line,
|
|
1262
|
+
evidence=f"Unresolved dispatch call '{expr}()' at {call.source_file}:{call.line} ({disp_rsn})",
|
|
1263
|
+
source_hash=f_hash,
|
|
1264
|
+
indexed_commit=self.indexed_commit,
|
|
1265
|
+
)
|
|
1266
|
+
)
|
|
1267
|
+
else:
|
|
1268
|
+
# Target cannot be statically resolved via lexical/import/module rules
|
|
1269
|
+
resolved_calls[(call.source_file, call.line, call.callee)] = (
|
|
1270
|
+
None,
|
|
1271
|
+
"UNKNOWN",
|
|
1272
|
+
)
|
|
1273
|
+
add_ref(
|
|
1274
|
+
Reference(
|
|
1275
|
+
source_symbol_id=caller_id,
|
|
1276
|
+
target_symbol_id=None,
|
|
1277
|
+
relationship="UNRESOLVED_REFERENCE",
|
|
1278
|
+
confidence="UNKNOWN",
|
|
1279
|
+
path=call.source_file,
|
|
1280
|
+
start_line=call.line,
|
|
1281
|
+
end_line=call.end_line,
|
|
1282
|
+
evidence=f"Unresolved call '{expr}()' at {call.source_file}:{call.line} ({reason})",
|
|
1283
|
+
source_hash=f_hash,
|
|
1284
|
+
indexed_commit=self.indexed_commit,
|
|
1285
|
+
)
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
# 4. Framework Route -> Handler edges
|
|
1289
|
+
for rt in self.routes:
|
|
1290
|
+
f_hash = self.file_hashes.get(rt.file_path, "")
|
|
1291
|
+
handler_sym, conf, _reason = self.resolve_identifier_in_file(
|
|
1292
|
+
rt.handler_name, rt.file_path, use_line=rt.line
|
|
1293
|
+
)
|
|
1294
|
+
target_id = handler_sym.canonical_id if handler_sym else rt.handler_canonical_id
|
|
1295
|
+
edge_conf = "HIGH" if handler_sym else conf
|
|
1296
|
+
add_ref(
|
|
1297
|
+
Reference(
|
|
1298
|
+
source_symbol_id=rt.endpoint_id,
|
|
1299
|
+
target_symbol_id=target_id,
|
|
1300
|
+
relationship="ROUTES_TO",
|
|
1301
|
+
confidence=edge_conf,
|
|
1302
|
+
path=rt.file_path,
|
|
1303
|
+
start_line=rt.line,
|
|
1304
|
+
end_line=rt.line,
|
|
1305
|
+
evidence=rt.evidence,
|
|
1306
|
+
source_hash=f_hash,
|
|
1307
|
+
indexed_commit=self.indexed_commit,
|
|
1308
|
+
)
|
|
1309
|
+
)
|
|
1310
|
+
add_edge(
|
|
1311
|
+
GraphEdge(
|
|
1312
|
+
source=rt.endpoint_id,
|
|
1313
|
+
target=target_id,
|
|
1314
|
+
relationship="ROUTES_TO",
|
|
1315
|
+
confidence=edge_conf,
|
|
1316
|
+
file=rt.file_path,
|
|
1317
|
+
start_line=rt.line,
|
|
1318
|
+
end_line=rt.line,
|
|
1319
|
+
evidence=rt.evidence,
|
|
1320
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
1321
|
+
)
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
# 5. Verified Router Mount edges (MOUNTS)
|
|
1325
|
+
for me in self.mount_edges:
|
|
1326
|
+
f_hash = self.file_hashes.get(me.file_path, "")
|
|
1327
|
+
add_ref(
|
|
1328
|
+
Reference(
|
|
1329
|
+
source_symbol_id=me.parent_canonical_id,
|
|
1330
|
+
target_symbol_id=me.child_canonical_id,
|
|
1331
|
+
relationship="MOUNTS",
|
|
1332
|
+
confidence="HIGH",
|
|
1333
|
+
path=me.file_path,
|
|
1334
|
+
start_line=me.line,
|
|
1335
|
+
end_line=me.line,
|
|
1336
|
+
evidence=me.evidence,
|
|
1337
|
+
source_hash=f_hash,
|
|
1338
|
+
indexed_commit=self.indexed_commit,
|
|
1339
|
+
)
|
|
1340
|
+
)
|
|
1341
|
+
add_edge(
|
|
1342
|
+
GraphEdge(
|
|
1343
|
+
source=me.parent_canonical_id,
|
|
1344
|
+
target=me.child_canonical_id,
|
|
1345
|
+
relationship="MOUNTS",
|
|
1346
|
+
confidence="HIGH",
|
|
1347
|
+
file=me.file_path,
|
|
1348
|
+
start_line=me.line,
|
|
1349
|
+
end_line=me.line,
|
|
1350
|
+
evidence=me.evidence,
|
|
1351
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
1352
|
+
reason="router_mount",
|
|
1353
|
+
)
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
# Pre-group bindings by expr_kind and pre-index inheritance/routes for O(1) lookups
|
|
1357
|
+
bindings_by_kind: dict[str, list[BindingRef]] = {}
|
|
1358
|
+
event_listeners_by_key: dict[str, list[BindingRef]] = {}
|
|
1359
|
+
_EVENT_LISTENER_KINDS = frozenset(
|
|
1360
|
+
("EVENT_ON", "SUBSCRIBE", "CALL_REGISTER", "REGISTRY_ADD", "REGISTRY_CALL", "DECORATOR_REGISTER")
|
|
1361
|
+
)
|
|
1362
|
+
for b in self.bindings:
|
|
1363
|
+
bindings_by_kind.setdefault(b.expr_kind, []).append(b)
|
|
1364
|
+
if b.expr_kind in _EVENT_LISTENER_KINDS and b.subscript_key:
|
|
1365
|
+
event_listeners_by_key.setdefault(b.subscript_key, []).append(b)
|
|
1366
|
+
|
|
1367
|
+
inheritance_by_source: dict[str, list[InheritanceRef]] = {}
|
|
1368
|
+
for inh in self.inheritance:
|
|
1369
|
+
inheritance_by_source.setdefault(inh.source_canonical_id, []).append(inh)
|
|
1370
|
+
|
|
1371
|
+
# 6. Local Bindings and Alias Chains (RESOLVES_TO)
|
|
1372
|
+
for b in (
|
|
1373
|
+
bindings_by_kind.get("IDENTIFIER", [])
|
|
1374
|
+
+ bindings_by_kind.get("ATTRIBUTE", [])
|
|
1375
|
+
+ bindings_by_kind.get("SUBSCRIPT", [])
|
|
1376
|
+
):
|
|
1377
|
+
b_target_id, ev_class, reason = self.binding_resolver.resolve(
|
|
1378
|
+
file_path=b.file_path,
|
|
1379
|
+
name=b.target_name,
|
|
1380
|
+
scope=b.scope,
|
|
1381
|
+
use_line=b.line,
|
|
1382
|
+
)
|
|
1383
|
+
if b_target_id and ev_class == RelationshipEvidenceClass.DATAFLOW_VERIFIED:
|
|
1384
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
1385
|
+
src_mod = self.file_to_module.get(b.file_path) or normalize_module(b.file_path)
|
|
1386
|
+
source_id = f"{src_mod}.{b.scope}.{b.target_name}" if b.scope else f"{src_mod}.{b.target_name}"
|
|
1387
|
+
add_ref(
|
|
1388
|
+
Reference(
|
|
1389
|
+
source_symbol_id=source_id,
|
|
1390
|
+
target_symbol_id=b_target_id,
|
|
1391
|
+
relationship="RESOLVES_TO",
|
|
1392
|
+
confidence="HIGH",
|
|
1393
|
+
path=b.file_path,
|
|
1394
|
+
start_line=b.line,
|
|
1395
|
+
end_line=b.line,
|
|
1396
|
+
evidence=f"Local binding alias '{b.target_name}' -> '{b_target_id}' ({b.source_expr})",
|
|
1397
|
+
source_hash=f_hash,
|
|
1398
|
+
indexed_commit=self.indexed_commit,
|
|
1399
|
+
)
|
|
1400
|
+
)
|
|
1401
|
+
add_edge(
|
|
1402
|
+
GraphEdge(
|
|
1403
|
+
source=source_id,
|
|
1404
|
+
target=b_target_id,
|
|
1405
|
+
relationship="RESOLVES_TO",
|
|
1406
|
+
confidence="HIGH",
|
|
1407
|
+
file=b.file_path,
|
|
1408
|
+
start_line=b.line,
|
|
1409
|
+
end_line=b.line,
|
|
1410
|
+
evidence=f"Local binding alias '{b.target_name}' -> '{b_target_id}' ({b.source_expr})",
|
|
1411
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
1412
|
+
reason=reason,
|
|
1413
|
+
)
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
# 7. Registries, Event Listeners, and Dispatch Registrations
|
|
1417
|
+
findings: list[dict[str, object]] = list(self.budget_findings)
|
|
1418
|
+
|
|
1419
|
+
# Pre-compute static overwrite state for DICT_ASSIGN bindings in the same (file_path, scope, target_name, subscript_key)
|
|
1420
|
+
dict_assign_groups: dict[tuple[str, str, str, str], list[BindingRef]] = {}
|
|
1421
|
+
for b in bindings_by_kind.get("DICT_ASSIGN", ()):
|
|
1422
|
+
if b.subscript_key and b.subscript_key != "<DYNAMIC>":
|
|
1423
|
+
g_key = (b.file_path, b.scope, b.target_name, b.subscript_key)
|
|
1424
|
+
dict_assign_groups.setdefault(g_key, []).append(b)
|
|
1425
|
+
|
|
1426
|
+
overwritten_dict_assign_lines: set[tuple[str, str, str, str, int]] = set()
|
|
1427
|
+
ambiguous_dict_assign_keys: set[tuple[str, str, str, str]] = set()
|
|
1428
|
+
for g_key, g_list in dict_assign_groups.items():
|
|
1429
|
+
if len(g_list) <= 1:
|
|
1430
|
+
continue
|
|
1431
|
+
g_sorted = sorted(g_list, key=lambda x: (x.line, x.column or 0))
|
|
1432
|
+
last_uncond_idx = -1
|
|
1433
|
+
for idx, item in enumerate(g_sorted):
|
|
1434
|
+
if not item.is_conditional:
|
|
1435
|
+
last_uncond_idx = idx
|
|
1436
|
+
if last_uncond_idx >= 0:
|
|
1437
|
+
for idx in range(last_uncond_idx):
|
|
1438
|
+
overwritten_dict_assign_lines.add((*g_key, g_sorted[idx].line))
|
|
1439
|
+
if last_uncond_idx < len(g_sorted) - 1:
|
|
1440
|
+
ambiguous_dict_assign_keys.add(g_key)
|
|
1441
|
+
else:
|
|
1442
|
+
ambiguous_dict_assign_keys.add(g_key)
|
|
1443
|
+
|
|
1444
|
+
for b in self.bindings:
|
|
1445
|
+
if b.expr_kind == "LIST_LITERAL" and b.list_entries:
|
|
1446
|
+
t_lower = b.target_name.lower()
|
|
1447
|
+
if t_lower in ("handlers", "registry", "callbacks", "plugins", "listeners", "handler_list", "plugin_list"):
|
|
1448
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
1449
|
+
source_id = f"{self.file_to_module.get(b.file_path) or normalize_module(b.file_path)}.{b.target_name}"
|
|
1450
|
+
for idx_elt, elt_expr in enumerate(b.list_entries):
|
|
1451
|
+
target_sym, _conf, _rsn = self.resolve_identifier_in_file(
|
|
1452
|
+
elt_expr, b.file_path, use_line=b.line
|
|
1453
|
+
)
|
|
1454
|
+
if not target_sym:
|
|
1455
|
+
continue
|
|
1456
|
+
target_canon = target_sym.canonical_id
|
|
1457
|
+
ev_class = RelationshipEvidenceClass.POSSIBLE if b.is_conditional else RelationshipEvidenceClass.DATAFLOW_VERIFIED
|
|
1458
|
+
edge_conf = "LOW" if b.is_conditional else "HIGH"
|
|
1459
|
+
edge_reason = "ambiguous_dispatch_assignment" if b.is_conditional else f"Statically verified list handler registration [{idx_elt}] -> '{target_canon}'"
|
|
1460
|
+
evidence_str = f"{b.target_name} DISPATCHES_TO {target_canon} (index={idx_elt})"
|
|
1461
|
+
add_ref(
|
|
1462
|
+
Reference(
|
|
1463
|
+
source_symbol_id=source_id,
|
|
1464
|
+
target_symbol_id=target_canon,
|
|
1465
|
+
relationship="DISPATCHES_TO",
|
|
1466
|
+
confidence=edge_conf,
|
|
1467
|
+
path=b.file_path,
|
|
1468
|
+
start_line=b.line,
|
|
1469
|
+
end_line=b.line,
|
|
1470
|
+
evidence=evidence_str,
|
|
1471
|
+
source_hash=f_hash,
|
|
1472
|
+
indexed_commit=self.indexed_commit,
|
|
1473
|
+
)
|
|
1474
|
+
)
|
|
1475
|
+
add_edge(
|
|
1476
|
+
GraphEdge(
|
|
1477
|
+
source=source_id,
|
|
1478
|
+
target=target_canon,
|
|
1479
|
+
relationship="DISPATCHES_TO",
|
|
1480
|
+
confidence=edge_conf,
|
|
1481
|
+
file=b.file_path,
|
|
1482
|
+
start_line=b.line,
|
|
1483
|
+
end_line=b.line,
|
|
1484
|
+
evidence=evidence_str,
|
|
1485
|
+
evidence_class=ev_class.value,
|
|
1486
|
+
reason=edge_reason,
|
|
1487
|
+
)
|
|
1488
|
+
)
|
|
1489
|
+
continue
|
|
1490
|
+
|
|
1491
|
+
if b.expr_kind not in ("CALL_REGISTER", "DICT_ASSIGN", "EVENT_ON", "SUBSCRIBE"):
|
|
1492
|
+
continue
|
|
1493
|
+
|
|
1494
|
+
if b.expr_kind == "DICT_ASSIGN" and b.subscript_key and b.subscript_key != "<DYNAMIC>":
|
|
1495
|
+
g_key = (b.file_path, b.scope, b.target_name, b.subscript_key)
|
|
1496
|
+
if (*g_key, b.line) in overwritten_dict_assign_lines:
|
|
1497
|
+
findings.append({
|
|
1498
|
+
"pattern": "DICT_ASSIGN",
|
|
1499
|
+
"status": "OVERWRITTEN",
|
|
1500
|
+
"reason": "overwritten_by_later_static_assignment",
|
|
1501
|
+
"file": b.file_path,
|
|
1502
|
+
"line": b.line,
|
|
1503
|
+
"target_name": b.target_name,
|
|
1504
|
+
"key": b.subscript_key,
|
|
1505
|
+
"source_expr": b.source_expr,
|
|
1506
|
+
})
|
|
1507
|
+
continue
|
|
1508
|
+
|
|
1509
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
1510
|
+
is_dynamic = (b.subscript_key == "<DYNAMIC>" or b.source_expr == "<DYNAMIC>")
|
|
1511
|
+
|
|
1512
|
+
if is_dynamic:
|
|
1513
|
+
reason = (
|
|
1514
|
+
"dynamic_dispatch"
|
|
1515
|
+
if b.expr_kind == "DICT_ASSIGN"
|
|
1516
|
+
else (
|
|
1517
|
+
"dynamic_event_name"
|
|
1518
|
+
if b.subscript_key == "<DYNAMIC>"
|
|
1519
|
+
else "dynamic_registration"
|
|
1520
|
+
)
|
|
1521
|
+
)
|
|
1522
|
+
findings.append({
|
|
1523
|
+
"pattern": b.expr_kind,
|
|
1524
|
+
"status": "UNKNOWN",
|
|
1525
|
+
"reason": reason,
|
|
1526
|
+
"file": b.file_path,
|
|
1527
|
+
"line": b.line,
|
|
1528
|
+
"target_name": b.target_name,
|
|
1529
|
+
})
|
|
1530
|
+
# Invariant: NEVER emit an edge with fake/unknown target canonical ID (Correction 2)
|
|
1531
|
+
continue
|
|
1532
|
+
|
|
1533
|
+
target_sym, _conf, _reason = self.resolve_identifier_in_file(
|
|
1534
|
+
b.source_expr, b.file_path, use_line=b.line
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1537
|
+
if not target_sym:
|
|
1538
|
+
findings.append({
|
|
1539
|
+
"pattern": b.expr_kind,
|
|
1540
|
+
"status": "UNKNOWN",
|
|
1541
|
+
"reason": "unknown_registry_target",
|
|
1542
|
+
"file": b.file_path,
|
|
1543
|
+
"line": b.line,
|
|
1544
|
+
"target_name": b.target_name,
|
|
1545
|
+
"source_expr": b.source_expr,
|
|
1546
|
+
})
|
|
1547
|
+
# Invariant: NEVER emit an edge with fake/unknown target canonical ID (Correction 2)
|
|
1548
|
+
continue
|
|
1549
|
+
|
|
1550
|
+
# Target is statically grounded!
|
|
1551
|
+
target_canon = target_sym.canonical_id
|
|
1552
|
+
if b.expr_kind == "CALL_REGISTER":
|
|
1553
|
+
rel = "REGISTERS"
|
|
1554
|
+
elif b.expr_kind in ("EVENT_ON", "SUBSCRIBE"):
|
|
1555
|
+
rel = "EVENT_LISTENER"
|
|
1556
|
+
else:
|
|
1557
|
+
rel = "DISPATCHES_TO"
|
|
1558
|
+
|
|
1559
|
+
is_ambig_key = (
|
|
1560
|
+
b.expr_kind == "DICT_ASSIGN"
|
|
1561
|
+
and b.subscript_key is not None
|
|
1562
|
+
and (b.file_path, b.scope, b.target_name, b.subscript_key) in ambiguous_dict_assign_keys
|
|
1563
|
+
)
|
|
1564
|
+
if b.is_conditional or is_ambig_key:
|
|
1565
|
+
ev_class = RelationshipEvidenceClass.POSSIBLE
|
|
1566
|
+
edge_conf = "LOW"
|
|
1567
|
+
edge_reason = "ambiguous_dispatch_assignment"
|
|
1568
|
+
else:
|
|
1569
|
+
ev_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
|
|
1570
|
+
edge_conf = "HIGH"
|
|
1571
|
+
edge_reason = f"Statically verified registration of '{b.subscript_key}' -> '{target_canon}'"
|
|
1572
|
+
|
|
1573
|
+
source_id = f"{self.file_to_module.get(b.file_path) or normalize_module(b.file_path)}.{b.target_name}"
|
|
1574
|
+
evidence_str = f"{b.target_name} {rel} {target_canon} (key='{b.subscript_key}')"
|
|
1575
|
+
|
|
1576
|
+
add_ref(
|
|
1577
|
+
Reference(
|
|
1578
|
+
source_symbol_id=source_id,
|
|
1579
|
+
target_symbol_id=target_canon,
|
|
1580
|
+
relationship=rel,
|
|
1581
|
+
confidence=edge_conf,
|
|
1582
|
+
path=b.file_path,
|
|
1583
|
+
start_line=b.line,
|
|
1584
|
+
end_line=b.line,
|
|
1585
|
+
evidence=evidence_str,
|
|
1586
|
+
source_hash=f_hash,
|
|
1587
|
+
indexed_commit=self.indexed_commit,
|
|
1588
|
+
)
|
|
1589
|
+
)
|
|
1590
|
+
add_edge(
|
|
1591
|
+
GraphEdge(
|
|
1592
|
+
source=source_id,
|
|
1593
|
+
target=target_canon,
|
|
1594
|
+
relationship=rel,
|
|
1595
|
+
confidence=edge_conf,
|
|
1596
|
+
file=b.file_path,
|
|
1597
|
+
start_line=b.line,
|
|
1598
|
+
end_line=b.line,
|
|
1599
|
+
evidence=evidence_str,
|
|
1600
|
+
evidence_class=ev_class.value,
|
|
1601
|
+
reason=edge_reason,
|
|
1602
|
+
)
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
# 8. Semantic Decorators (TASK_HANDLER, COMMAND_HANDLER, EVENT_LISTENER, REGISTERS)
|
|
1606
|
+
for b in bindings_by_kind.get("SEMANTIC_DECORATOR", ()):
|
|
1607
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
1608
|
+
is_dynamic = b.subscript_target is not None
|
|
1609
|
+
metadata: dict[str, object] = {}
|
|
1610
|
+
if b.subscript_key:
|
|
1611
|
+
try:
|
|
1612
|
+
metadata = json.loads(b.subscript_key)
|
|
1613
|
+
except Exception:
|
|
1614
|
+
metadata = {}
|
|
1615
|
+
|
|
1616
|
+
if is_dynamic:
|
|
1617
|
+
findings.append({
|
|
1618
|
+
"pattern": "SEMANTIC_DECORATOR",
|
|
1619
|
+
"role": b.attr_name,
|
|
1620
|
+
"status": "UNKNOWN",
|
|
1621
|
+
"reason": b.subscript_target or "dynamic_argument",
|
|
1622
|
+
"file": b.file_path,
|
|
1623
|
+
"line": b.line,
|
|
1624
|
+
"target_name": b.target_name,
|
|
1625
|
+
"decorator": b.source_expr,
|
|
1626
|
+
})
|
|
1627
|
+
# Invariant: Never emit edge for dynamic/unknown targets
|
|
1628
|
+
continue
|
|
1629
|
+
|
|
1630
|
+
rel = b.attr_name
|
|
1631
|
+
assert rel != "CALLS", f"Invariant violated: Semantic decorator {rel} cannot be CALLS"
|
|
1632
|
+
|
|
1633
|
+
target_canon = b.target_name
|
|
1634
|
+
meta_ev = str(metadata.get("evidence_class", ""))
|
|
1635
|
+
ev_class = (
|
|
1636
|
+
RelationshipEvidenceClass.POSSIBLE
|
|
1637
|
+
if b.is_conditional
|
|
1638
|
+
else (
|
|
1639
|
+
RelationshipEvidenceClass(meta_ev)
|
|
1640
|
+
if meta_ev in RelationshipEvidenceClass._value2member_map_
|
|
1641
|
+
else (
|
|
1642
|
+
RelationshipEvidenceClass.DATAFLOW_VERIFIED
|
|
1643
|
+
if b.base_expr == "registry"
|
|
1644
|
+
else RelationshipEvidenceClass.FRAMEWORK_VERIFIED
|
|
1645
|
+
)
|
|
1646
|
+
)
|
|
1647
|
+
)
|
|
1648
|
+
edge_conf = "LOW" if b.is_conditional else "HIGH"
|
|
1649
|
+
edge_reason = (
|
|
1650
|
+
f"Conditional registration via '@{b.source_expr}'"
|
|
1651
|
+
if b.is_conditional
|
|
1652
|
+
else f"Statically verified registration via '@{b.source_expr}'"
|
|
1653
|
+
)
|
|
1654
|
+
|
|
1655
|
+
if rel == "EVENT_LISTENER":
|
|
1656
|
+
events = metadata.get("events")
|
|
1657
|
+
if isinstance(events, list) and events:
|
|
1658
|
+
for ev in events:
|
|
1659
|
+
ev_str = str(ev)
|
|
1660
|
+
ev_sym, _conf, _ = self.resolve_identifier_in_file(
|
|
1661
|
+
ev_str, b.file_path, use_line=b.line
|
|
1662
|
+
)
|
|
1663
|
+
src_id = ev_sym.canonical_id if ev_sym else ev_str
|
|
1664
|
+
ev_evidence = f"Event listener '{target_canon}' registered for event '{ev_str}' via '@{b.source_expr}'"
|
|
1665
|
+
|
|
1666
|
+
add_ref(
|
|
1667
|
+
Reference(
|
|
1668
|
+
source_symbol_id=src_id,
|
|
1669
|
+
target_symbol_id=target_canon,
|
|
1670
|
+
relationship=rel,
|
|
1671
|
+
confidence=edge_conf,
|
|
1672
|
+
path=b.file_path,
|
|
1673
|
+
start_line=b.line,
|
|
1674
|
+
end_line=b.line,
|
|
1675
|
+
evidence=ev_evidence,
|
|
1676
|
+
source_hash=f_hash,
|
|
1677
|
+
indexed_commit=self.indexed_commit,
|
|
1678
|
+
)
|
|
1679
|
+
)
|
|
1680
|
+
add_edge(
|
|
1681
|
+
GraphEdge(
|
|
1682
|
+
source=src_id,
|
|
1683
|
+
target=target_canon,
|
|
1684
|
+
relationship=rel,
|
|
1685
|
+
confidence=edge_conf,
|
|
1686
|
+
file=b.file_path,
|
|
1687
|
+
start_line=b.line,
|
|
1688
|
+
end_line=b.line,
|
|
1689
|
+
evidence=ev_evidence,
|
|
1690
|
+
evidence_class=ev_class.value,
|
|
1691
|
+
reason=edge_reason,
|
|
1692
|
+
)
|
|
1693
|
+
)
|
|
1694
|
+
# Django signal sender linking: @receiver(signal, sender=Model)
|
|
1695
|
+
sender_val = metadata.get("sender")
|
|
1696
|
+
if sender_val:
|
|
1697
|
+
sender_str = str(sender_val)
|
|
1698
|
+
sender_sym, _, _ = self.resolve_identifier_in_file(
|
|
1699
|
+
sender_str, b.file_path, use_line=b.line
|
|
1700
|
+
)
|
|
1701
|
+
if not sender_sym and "." in sender_str:
|
|
1702
|
+
sender_sym, _, _ = self.resolve_identifier_in_file(
|
|
1703
|
+
sender_str.split(".")[-1], b.file_path, use_line=b.line
|
|
1704
|
+
)
|
|
1705
|
+
if not sender_sym:
|
|
1706
|
+
short_target = sender_str.split(".")[-1]
|
|
1707
|
+
if short_target in self.short_to_symbols:
|
|
1708
|
+
for s in self.short_to_symbols[short_target]:
|
|
1709
|
+
if s.kind == "class":
|
|
1710
|
+
sender_sym = s
|
|
1711
|
+
break
|
|
1712
|
+
if not sender_sym:
|
|
1713
|
+
sender_sym = self.short_to_symbols[short_target][0]
|
|
1714
|
+
|
|
1715
|
+
sender_canon = sender_sym.canonical_id if sender_sym else sender_str
|
|
1716
|
+
sig_evidence = f"Django signal '{ev_str}' from sender '{sender_str}' triggers listener '{target_canon}' via '@{b.source_expr}'"
|
|
1717
|
+
add_ref(
|
|
1718
|
+
Reference(
|
|
1719
|
+
source_symbol_id=sender_canon,
|
|
1720
|
+
target_symbol_id=target_canon,
|
|
1721
|
+
relationship="TRIGGERS_SIGNAL",
|
|
1722
|
+
confidence=edge_conf,
|
|
1723
|
+
path=b.file_path,
|
|
1724
|
+
start_line=b.line,
|
|
1725
|
+
end_line=b.line,
|
|
1726
|
+
evidence=sig_evidence,
|
|
1727
|
+
source_hash=f_hash,
|
|
1728
|
+
indexed_commit=self.indexed_commit,
|
|
1729
|
+
)
|
|
1730
|
+
)
|
|
1731
|
+
add_edge(
|
|
1732
|
+
GraphEdge(
|
|
1733
|
+
source=sender_canon,
|
|
1734
|
+
target=target_canon,
|
|
1735
|
+
relationship="TRIGGERS_SIGNAL",
|
|
1736
|
+
confidence=edge_conf,
|
|
1737
|
+
file=b.file_path,
|
|
1738
|
+
start_line=b.line,
|
|
1739
|
+
end_line=b.line,
|
|
1740
|
+
evidence=sig_evidence,
|
|
1741
|
+
evidence_class=ev_class.value,
|
|
1742
|
+
reason=f"django_signal:{ev_str}",
|
|
1743
|
+
)
|
|
1744
|
+
)
|
|
1745
|
+
lifecycle_method = "delete" if "delete" in ev_str.lower() else "save"
|
|
1746
|
+
save_source = f"{sender_canon}.{lifecycle_method}"
|
|
1747
|
+
add_edge(
|
|
1748
|
+
GraphEdge(
|
|
1749
|
+
source=save_source,
|
|
1750
|
+
target=target_canon,
|
|
1751
|
+
relationship="TRIGGERS_SIGNAL",
|
|
1752
|
+
confidence=edge_conf,
|
|
1753
|
+
file=b.file_path,
|
|
1754
|
+
start_line=b.line,
|
|
1755
|
+
end_line=b.line,
|
|
1756
|
+
evidence=sig_evidence,
|
|
1757
|
+
evidence_class=ev_class.value,
|
|
1758
|
+
reason=f"django_signal:{ev_str}",
|
|
1759
|
+
)
|
|
1760
|
+
)
|
|
1761
|
+
add_edge(
|
|
1762
|
+
GraphEdge(
|
|
1763
|
+
source=save_source,
|
|
1764
|
+
target=target_canon,
|
|
1765
|
+
relationship="CALLS",
|
|
1766
|
+
confidence=edge_conf,
|
|
1767
|
+
file=b.file_path,
|
|
1768
|
+
start_line=b.line,
|
|
1769
|
+
end_line=b.line,
|
|
1770
|
+
evidence=sig_evidence,
|
|
1771
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
1772
|
+
reason=f"django_signal:{ev_str}",
|
|
1773
|
+
)
|
|
1774
|
+
)
|
|
1775
|
+
add_edge(
|
|
1776
|
+
GraphEdge(
|
|
1777
|
+
source=target_canon,
|
|
1778
|
+
target=sender_canon,
|
|
1779
|
+
relationship="HANDLES_SIGNAL",
|
|
1780
|
+
confidence=edge_conf,
|
|
1781
|
+
file=b.file_path,
|
|
1782
|
+
start_line=b.line,
|
|
1783
|
+
end_line=b.line,
|
|
1784
|
+
evidence=f"Listener '{target_canon}' handles '{ev_str}' from '{sender_canon}'",
|
|
1785
|
+
evidence_class=ev_class.value,
|
|
1786
|
+
reason=f"django_signal:{ev_str}",
|
|
1787
|
+
)
|
|
1788
|
+
)
|
|
1789
|
+
else:
|
|
1790
|
+
src_id = b.source_expr
|
|
1791
|
+
ev_evidence = f"Event listener '{target_canon}' registered via '@{b.source_expr}'"
|
|
1792
|
+
add_ref(
|
|
1793
|
+
Reference(
|
|
1794
|
+
source_symbol_id=src_id,
|
|
1795
|
+
target_symbol_id=target_canon,
|
|
1796
|
+
relationship=rel,
|
|
1797
|
+
confidence=edge_conf,
|
|
1798
|
+
path=b.file_path,
|
|
1799
|
+
start_line=b.line,
|
|
1800
|
+
end_line=b.line,
|
|
1801
|
+
evidence=ev_evidence,
|
|
1802
|
+
source_hash=f_hash,
|
|
1803
|
+
indexed_commit=self.indexed_commit,
|
|
1804
|
+
)
|
|
1805
|
+
)
|
|
1806
|
+
add_edge(
|
|
1807
|
+
GraphEdge(
|
|
1808
|
+
source=src_id,
|
|
1809
|
+
target=target_canon,
|
|
1810
|
+
relationship=rel,
|
|
1811
|
+
confidence=edge_conf,
|
|
1812
|
+
file=b.file_path,
|
|
1813
|
+
start_line=b.line,
|
|
1814
|
+
end_line=b.line,
|
|
1815
|
+
evidence=ev_evidence,
|
|
1816
|
+
evidence_class=ev_class.value,
|
|
1817
|
+
reason=edge_reason,
|
|
1818
|
+
)
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
elif rel == "COMMAND_HANDLER":
|
|
1822
|
+
cmd_name = str(metadata.get("command") or target_canon.split(".")[-1])
|
|
1823
|
+
source_id = f"{b.base_expr}:{cmd_name}" if b.base_expr else cmd_name
|
|
1824
|
+
ev_evidence = f"Command handler '{target_canon}' registered for command '{cmd_name}' via '@{b.source_expr}'"
|
|
1825
|
+
add_ref(
|
|
1826
|
+
Reference(
|
|
1827
|
+
source_symbol_id=source_id,
|
|
1828
|
+
target_symbol_id=target_canon,
|
|
1829
|
+
relationship=rel,
|
|
1830
|
+
confidence=edge_conf,
|
|
1831
|
+
path=b.file_path,
|
|
1832
|
+
start_line=b.line,
|
|
1833
|
+
end_line=b.line,
|
|
1834
|
+
evidence=ev_evidence,
|
|
1835
|
+
source_hash=f_hash,
|
|
1836
|
+
indexed_commit=self.indexed_commit,
|
|
1837
|
+
)
|
|
1838
|
+
)
|
|
1839
|
+
add_edge(
|
|
1840
|
+
GraphEdge(
|
|
1841
|
+
source=source_id,
|
|
1842
|
+
target=target_canon,
|
|
1843
|
+
relationship=rel,
|
|
1844
|
+
confidence=edge_conf,
|
|
1845
|
+
file=b.file_path,
|
|
1846
|
+
start_line=b.line,
|
|
1847
|
+
end_line=b.line,
|
|
1848
|
+
evidence=ev_evidence,
|
|
1849
|
+
evidence_class=ev_class.value,
|
|
1850
|
+
reason=edge_reason,
|
|
1851
|
+
)
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
elif rel == "TASK_HANDLER":
|
|
1855
|
+
task_name = str(metadata.get("task_name") or target_canon)
|
|
1856
|
+
source_id = task_name if task_name != target_canon else b.source_expr
|
|
1857
|
+
ev_evidence = f"Task handler '{target_canon}' registered via '@{b.source_expr}'"
|
|
1858
|
+
add_ref(
|
|
1859
|
+
Reference(
|
|
1860
|
+
source_symbol_id=source_id,
|
|
1861
|
+
target_symbol_id=target_canon,
|
|
1862
|
+
relationship=rel,
|
|
1863
|
+
confidence=edge_conf,
|
|
1864
|
+
path=b.file_path,
|
|
1865
|
+
start_line=b.line,
|
|
1866
|
+
end_line=b.line,
|
|
1867
|
+
evidence=ev_evidence,
|
|
1868
|
+
source_hash=f_hash,
|
|
1869
|
+
indexed_commit=self.indexed_commit,
|
|
1870
|
+
)
|
|
1871
|
+
)
|
|
1872
|
+
add_edge(
|
|
1873
|
+
GraphEdge(
|
|
1874
|
+
source=source_id,
|
|
1875
|
+
target=target_canon,
|
|
1876
|
+
relationship=rel,
|
|
1877
|
+
confidence=edge_conf,
|
|
1878
|
+
file=b.file_path,
|
|
1879
|
+
start_line=b.line,
|
|
1880
|
+
end_line=b.line,
|
|
1881
|
+
evidence=ev_evidence,
|
|
1882
|
+
evidence_class=ev_class.value,
|
|
1883
|
+
reason=edge_reason,
|
|
1884
|
+
)
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
elif rel == "REGISTERS":
|
|
1888
|
+
source_id = f"{self.file_to_module.get(b.file_path) or normalize_module(b.file_path)}.{b.source_expr.split('.')[0]}"
|
|
1889
|
+
ev_evidence = f"Registry handler '{target_canon}' registered via '@{b.source_expr}'"
|
|
1890
|
+
add_ref(
|
|
1891
|
+
Reference(
|
|
1892
|
+
source_symbol_id=source_id,
|
|
1893
|
+
target_symbol_id=target_canon,
|
|
1894
|
+
relationship=rel,
|
|
1895
|
+
confidence=edge_conf,
|
|
1896
|
+
path=b.file_path,
|
|
1897
|
+
start_line=b.line,
|
|
1898
|
+
end_line=b.line,
|
|
1899
|
+
evidence=ev_evidence,
|
|
1900
|
+
source_hash=f_hash,
|
|
1901
|
+
indexed_commit=self.indexed_commit,
|
|
1902
|
+
)
|
|
1903
|
+
)
|
|
1904
|
+
add_edge(
|
|
1905
|
+
GraphEdge(
|
|
1906
|
+
source=source_id,
|
|
1907
|
+
target=target_canon,
|
|
1908
|
+
relationship=rel,
|
|
1909
|
+
confidence=edge_conf,
|
|
1910
|
+
file=b.file_path,
|
|
1911
|
+
start_line=b.line,
|
|
1912
|
+
end_line=b.line,
|
|
1913
|
+
evidence=ev_evidence,
|
|
1914
|
+
evidence_class=ev_class.value,
|
|
1915
|
+
reason=edge_reason,
|
|
1916
|
+
)
|
|
1917
|
+
)
|
|
1918
|
+
|
|
1919
|
+
elif rel == "TOOL_HANDLER":
|
|
1920
|
+
source_id = f"@{b.source_expr}"
|
|
1921
|
+
ev_evidence = f"AI agent tool '{target_canon}' registered via '@{b.source_expr}'"
|
|
1922
|
+
add_ref(
|
|
1923
|
+
Reference(
|
|
1924
|
+
source_symbol_id=target_canon,
|
|
1925
|
+
target_symbol_id=source_id,
|
|
1926
|
+
relationship=rel,
|
|
1927
|
+
confidence=edge_conf,
|
|
1928
|
+
path=b.file_path,
|
|
1929
|
+
start_line=b.line,
|
|
1930
|
+
end_line=b.line,
|
|
1931
|
+
evidence=ev_evidence,
|
|
1932
|
+
source_hash=f_hash,
|
|
1933
|
+
indexed_commit=self.indexed_commit,
|
|
1934
|
+
)
|
|
1935
|
+
)
|
|
1936
|
+
add_edge(
|
|
1937
|
+
GraphEdge(
|
|
1938
|
+
source=target_canon,
|
|
1939
|
+
target=source_id,
|
|
1940
|
+
relationship=rel,
|
|
1941
|
+
confidence=edge_conf,
|
|
1942
|
+
file=b.file_path,
|
|
1943
|
+
start_line=b.line,
|
|
1944
|
+
end_line=b.line,
|
|
1945
|
+
evidence=ev_evidence,
|
|
1946
|
+
evidence_class=ev_class.value,
|
|
1947
|
+
reason=edge_reason,
|
|
1948
|
+
)
|
|
1949
|
+
)
|
|
1950
|
+
|
|
1951
|
+
# 8b. Django Signal Receivers via connect()
|
|
1952
|
+
for b in bindings_by_kind.get("DJANGO_SIGNAL_RECEIVER", ()):
|
|
1953
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
1954
|
+
receiver_sym, _, _ = self.resolve_identifier_in_file(
|
|
1955
|
+
b.target_name, b.file_path, use_line=b.line
|
|
1956
|
+
)
|
|
1957
|
+
if not receiver_sym and b.target_name in self.short_to_symbols:
|
|
1958
|
+
receiver_sym = self.short_to_symbols[b.target_name][0]
|
|
1959
|
+
target_canon = receiver_sym.canonical_id if receiver_sym else b.target_name
|
|
1960
|
+
|
|
1961
|
+
sig_name = b.base_expr
|
|
1962
|
+
sender_str = b.source_expr
|
|
1963
|
+
sender_sym = None
|
|
1964
|
+
if sender_str:
|
|
1965
|
+
sender_sym, _, _ = self.resolve_identifier_in_file(
|
|
1966
|
+
sender_str, b.file_path, use_line=b.line
|
|
1967
|
+
)
|
|
1968
|
+
if not sender_sym and "." in sender_str:
|
|
1969
|
+
sender_sym, _, _ = self.resolve_identifier_in_file(
|
|
1970
|
+
sender_str.split(".")[-1], b.file_path, use_line=b.line
|
|
1971
|
+
)
|
|
1972
|
+
if not sender_sym:
|
|
1973
|
+
short_s = sender_str.split(".")[-1]
|
|
1974
|
+
if short_s in self.short_to_symbols:
|
|
1975
|
+
for s in self.short_to_symbols[short_s]:
|
|
1976
|
+
if s.kind == "class":
|
|
1977
|
+
sender_sym = s
|
|
1978
|
+
break
|
|
1979
|
+
if not sender_sym:
|
|
1980
|
+
sender_sym = self.short_to_symbols[short_s][0]
|
|
1981
|
+
|
|
1982
|
+
sender_canon = sender_sym.canonical_id if sender_sym else (sender_str or sig_name)
|
|
1983
|
+
sig_ev = f"Signal '{sig_name}' connects sender '{sender_str or '*'}' to '{target_canon}'"
|
|
1984
|
+
add_ref(
|
|
1985
|
+
Reference(
|
|
1986
|
+
source_symbol_id=sender_canon,
|
|
1987
|
+
target_symbol_id=target_canon,
|
|
1988
|
+
relationship="TRIGGERS_SIGNAL",
|
|
1989
|
+
confidence="HIGH" if sender_sym else "MEDIUM",
|
|
1990
|
+
path=b.file_path,
|
|
1991
|
+
start_line=b.line,
|
|
1992
|
+
end_line=b.line,
|
|
1993
|
+
evidence=sig_ev,
|
|
1994
|
+
source_hash=f_hash,
|
|
1995
|
+
indexed_commit=self.indexed_commit,
|
|
1996
|
+
)
|
|
1997
|
+
)
|
|
1998
|
+
add_edge(
|
|
1999
|
+
GraphEdge(
|
|
2000
|
+
source=sender_canon,
|
|
2001
|
+
target=target_canon,
|
|
2002
|
+
relationship="TRIGGERS_SIGNAL",
|
|
2003
|
+
confidence="HIGH" if sender_sym else "MEDIUM",
|
|
2004
|
+
file=b.file_path,
|
|
2005
|
+
start_line=b.line,
|
|
2006
|
+
end_line=b.line,
|
|
2007
|
+
evidence=sig_ev,
|
|
2008
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2009
|
+
reason=f"django_signal:{sig_name}",
|
|
2010
|
+
)
|
|
2011
|
+
)
|
|
2012
|
+
lifecycle_method = "delete" if "delete" in sig_name.lower() else "save"
|
|
2013
|
+
add_edge(
|
|
2014
|
+
GraphEdge(
|
|
2015
|
+
source=f"{sender_canon}.{lifecycle_method}",
|
|
2016
|
+
target=target_canon,
|
|
2017
|
+
relationship="CALLS",
|
|
2018
|
+
confidence="HIGH" if sender_sym else "MEDIUM",
|
|
2019
|
+
file=b.file_path,
|
|
2020
|
+
start_line=b.line,
|
|
2021
|
+
end_line=b.line,
|
|
2022
|
+
evidence=sig_ev,
|
|
2023
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2024
|
+
reason=f"django_signal:{sig_name}",
|
|
2025
|
+
)
|
|
2026
|
+
)
|
|
2027
|
+
add_edge(
|
|
2028
|
+
GraphEdge(
|
|
2029
|
+
source=target_canon,
|
|
2030
|
+
target=sender_canon,
|
|
2031
|
+
relationship="HANDLES_SIGNAL",
|
|
2032
|
+
confidence="HIGH" if sender_sym else "MEDIUM",
|
|
2033
|
+
file=b.file_path,
|
|
2034
|
+
start_line=b.line,
|
|
2035
|
+
end_line=b.line,
|
|
2036
|
+
evidence=f"Listener '{target_canon}' handles '{sig_name}' from '{sender_canon}'",
|
|
2037
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2038
|
+
reason=f"django_signal:{sig_name}",
|
|
2039
|
+
)
|
|
2040
|
+
)
|
|
2041
|
+
|
|
2042
|
+
# 8c. Django Signal Dispatches via send() / send_robust()
|
|
2043
|
+
for b in bindings_by_kind.get("DJANGO_SIGNAL_SEND", ()):
|
|
2044
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2045
|
+
sig_name = b.target_name
|
|
2046
|
+
for b_rcv in bindings_by_kind.get("DJANGO_SIGNAL_RECEIVER", ()):
|
|
2047
|
+
if b_rcv.base_expr == sig_name or sig_name.endswith(b_rcv.base_expr):
|
|
2048
|
+
rcv_sym, _, _ = self.resolve_identifier_in_file(b_rcv.target_name, b_rcv.file_path, use_line=b_rcv.line)
|
|
2049
|
+
if not rcv_sym and b_rcv.target_name in self.short_to_symbols:
|
|
2050
|
+
rcv_sym = self.short_to_symbols[b_rcv.target_name][0]
|
|
2051
|
+
target_id = rcv_sym.canonical_id if rcv_sym else b_rcv.target_name
|
|
2052
|
+
ev_str = f"Caller dispatches signal '{sig_name}' triggering '{target_id}'"
|
|
2053
|
+
add_edge(
|
|
2054
|
+
GraphEdge(
|
|
2055
|
+
source=caller_id,
|
|
2056
|
+
target=target_id,
|
|
2057
|
+
relationship="TRIGGERS_SIGNAL",
|
|
2058
|
+
confidence="HIGH",
|
|
2059
|
+
file=b.file_path,
|
|
2060
|
+
start_line=b.line,
|
|
2061
|
+
end_line=b.line,
|
|
2062
|
+
evidence=ev_str,
|
|
2063
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2064
|
+
reason=f"signal_send:{sig_name}",
|
|
2065
|
+
)
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
# 8d. Celery Task Dispatches via .delay() / .apply_async() / send_task()
|
|
2069
|
+
for b in bindings_by_kind.get("CELERY_DISPATCH", ()):
|
|
2070
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2071
|
+
task_name = b.target_name
|
|
2072
|
+
task_sym, _, _ = self.resolve_identifier_in_file(task_name, b.file_path, use_line=b.line)
|
|
2073
|
+
if not task_sym and task_name in self.short_to_symbols:
|
|
2074
|
+
task_sym = self.short_to_symbols[task_name][0]
|
|
2075
|
+
target_id = task_sym.canonical_id if task_sym else task_name
|
|
2076
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2077
|
+
ev_str = f"Caller dispatches background Celery task '{task_name}' via .{b.base_expr}() at {b.file_path}:{b.line}"
|
|
2078
|
+
add_ref(
|
|
2079
|
+
Reference(
|
|
2080
|
+
source_symbol_id=caller_id,
|
|
2081
|
+
target_symbol_id=target_id,
|
|
2082
|
+
relationship="DISPATCHES_TASK",
|
|
2083
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2084
|
+
path=b.file_path,
|
|
2085
|
+
start_line=b.line,
|
|
2086
|
+
end_line=b.line,
|
|
2087
|
+
evidence=ev_str,
|
|
2088
|
+
source_hash=f_hash,
|
|
2089
|
+
indexed_commit=self.indexed_commit,
|
|
2090
|
+
)
|
|
2091
|
+
)
|
|
2092
|
+
add_edge(
|
|
2093
|
+
GraphEdge(
|
|
2094
|
+
source=caller_id,
|
|
2095
|
+
target=target_id,
|
|
2096
|
+
relationship="DISPATCHES_TASK",
|
|
2097
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2098
|
+
file=b.file_path,
|
|
2099
|
+
start_line=b.line,
|
|
2100
|
+
end_line=b.line,
|
|
2101
|
+
evidence=ev_str,
|
|
2102
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2103
|
+
reason=f"celery_dispatch:{b.base_expr}",
|
|
2104
|
+
)
|
|
2105
|
+
)
|
|
2106
|
+
add_edge(
|
|
2107
|
+
GraphEdge(
|
|
2108
|
+
source=caller_id,
|
|
2109
|
+
target=target_id,
|
|
2110
|
+
relationship="CALLS",
|
|
2111
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2112
|
+
file=b.file_path,
|
|
2113
|
+
start_line=b.line,
|
|
2114
|
+
end_line=b.line,
|
|
2115
|
+
evidence=ev_str,
|
|
2116
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2117
|
+
reason=f"celery_dispatch:{b.base_expr}",
|
|
2118
|
+
)
|
|
2119
|
+
)
|
|
2120
|
+
|
|
2121
|
+
# 8e. Ray Remote Task Dispatches via .remote()
|
|
2122
|
+
for b in bindings_by_kind.get("RAY_REMOTE", ()):
|
|
2123
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2124
|
+
task_name = b.target_name
|
|
2125
|
+
task_sym, _, _ = self.resolve_identifier_in_file(task_name, b.file_path, use_line=b.line)
|
|
2126
|
+
if not task_sym and task_name in self.short_to_symbols:
|
|
2127
|
+
task_sym = self.short_to_symbols[task_name][0]
|
|
2128
|
+
target_id = task_sym.canonical_id if task_sym else task_name
|
|
2129
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2130
|
+
ev_str = f"Caller dispatches remote Ray task '{task_name}' via .{b.base_expr}() at {b.file_path}:{b.line}"
|
|
2131
|
+
add_ref(
|
|
2132
|
+
Reference(
|
|
2133
|
+
source_symbol_id=caller_id,
|
|
2134
|
+
target_symbol_id=target_id,
|
|
2135
|
+
relationship="DISPATCHES_TASK",
|
|
2136
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2137
|
+
path=b.file_path,
|
|
2138
|
+
start_line=b.line,
|
|
2139
|
+
end_line=b.line,
|
|
2140
|
+
evidence=ev_str,
|
|
2141
|
+
source_hash=f_hash,
|
|
2142
|
+
indexed_commit=self.indexed_commit,
|
|
2143
|
+
)
|
|
2144
|
+
)
|
|
2145
|
+
add_edge(
|
|
2146
|
+
GraphEdge(
|
|
2147
|
+
source=caller_id,
|
|
2148
|
+
target=target_id,
|
|
2149
|
+
relationship="DISPATCHES_TASK",
|
|
2150
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2151
|
+
file=b.file_path,
|
|
2152
|
+
start_line=b.line,
|
|
2153
|
+
end_line=b.line,
|
|
2154
|
+
evidence=ev_str,
|
|
2155
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2156
|
+
reason=f"ray_remote:{b.base_expr}",
|
|
2157
|
+
)
|
|
2158
|
+
)
|
|
2159
|
+
add_edge(
|
|
2160
|
+
GraphEdge(
|
|
2161
|
+
source=caller_id,
|
|
2162
|
+
target=target_id,
|
|
2163
|
+
relationship="CALLS",
|
|
2164
|
+
confidence="HIGH" if task_sym else "MEDIUM",
|
|
2165
|
+
file=b.file_path,
|
|
2166
|
+
start_line=b.line,
|
|
2167
|
+
end_line=b.line,
|
|
2168
|
+
evidence=ev_str,
|
|
2169
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2170
|
+
reason=f"ray_remote:{b.base_expr}",
|
|
2171
|
+
)
|
|
2172
|
+
)
|
|
2173
|
+
|
|
2174
|
+
# 8f. Agent Tool Registrations (e.g. AgentExecutor(tools=[...]))
|
|
2175
|
+
for b in bindings_by_kind.get("AGENT_TOOL_REGISTRATION", ()):
|
|
2176
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2177
|
+
tool_name = b.target_name
|
|
2178
|
+
tool_sym, _, _ = self.resolve_identifier_in_file(tool_name, b.file_path, use_line=b.line)
|
|
2179
|
+
if not tool_sym and tool_name in self.short_to_symbols:
|
|
2180
|
+
tool_sym = self.short_to_symbols[tool_name][0]
|
|
2181
|
+
target_id = tool_sym.canonical_id if tool_sym else tool_name
|
|
2182
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2183
|
+
ev_str = f"Agent '{b.base_expr}' registers tool '{tool_name}' at {b.file_path}:{b.line}"
|
|
2184
|
+
add_ref(
|
|
2185
|
+
Reference(
|
|
2186
|
+
source_symbol_id=caller_id,
|
|
2187
|
+
target_symbol_id=target_id,
|
|
2188
|
+
relationship="REGISTERS",
|
|
2189
|
+
confidence="HIGH" if tool_sym else "MEDIUM",
|
|
2190
|
+
path=b.file_path,
|
|
2191
|
+
start_line=b.line,
|
|
2192
|
+
end_line=b.line,
|
|
2193
|
+
evidence=ev_str,
|
|
2194
|
+
source_hash=f_hash,
|
|
2195
|
+
indexed_commit=self.indexed_commit,
|
|
2196
|
+
)
|
|
2197
|
+
)
|
|
2198
|
+
add_edge(
|
|
2199
|
+
GraphEdge(
|
|
2200
|
+
source=caller_id,
|
|
2201
|
+
target=target_id,
|
|
2202
|
+
relationship="REGISTERS",
|
|
2203
|
+
confidence="HIGH" if tool_sym else "MEDIUM",
|
|
2204
|
+
file=b.file_path,
|
|
2205
|
+
start_line=b.line,
|
|
2206
|
+
end_line=b.line,
|
|
2207
|
+
evidence=ev_str,
|
|
2208
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2209
|
+
reason=f"agent_tool_registration:{b.base_expr}",
|
|
2210
|
+
)
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
# 8g. LCEL Pipeline Chains (prompt | llm | output_parser)
|
|
2214
|
+
for b in bindings_by_kind.get("LCEL_PIPELINE", ()):
|
|
2215
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2216
|
+
try:
|
|
2217
|
+
steps: list[str] = json.loads(b.source_expr)
|
|
2218
|
+
except Exception:
|
|
2219
|
+
steps = []
|
|
2220
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2221
|
+
step_syms: list[Symbol | None] = []
|
|
2222
|
+
for s_name in steps:
|
|
2223
|
+
s_sym, _, _ = self.resolve_identifier_in_file(
|
|
2224
|
+
s_name, b.file_path, caller_canonical_id=caller_id, use_line=b.line
|
|
2225
|
+
)
|
|
2226
|
+
if not s_sym and self.binding_resolver:
|
|
2227
|
+
alias_t, _, _ = self.binding_resolver.resolve(
|
|
2228
|
+
b.file_path, s_name, scope=b.scope, use_line=b.line
|
|
2229
|
+
)
|
|
2230
|
+
if alias_t and alias_t in self.canonical_to_symbol:
|
|
2231
|
+
s_sym = self.canonical_to_symbol[alias_t]
|
|
2232
|
+
elif alias_t:
|
|
2233
|
+
s_sym, _, _ = self.resolve_identifier_in_file(alias_t, b.file_path, use_line=b.line)
|
|
2234
|
+
if not s_sym and alias_t in self.short_to_symbols:
|
|
2235
|
+
s_sym = self.short_to_symbols[alias_t][0]
|
|
2236
|
+
if not s_sym and s_name in self.short_to_symbols:
|
|
2237
|
+
s_sym = self.short_to_symbols[s_name][0]
|
|
2238
|
+
step_syms.append(s_sym)
|
|
2239
|
+
|
|
2240
|
+
# Link consecutive pipeline steps: step[i] -> PIPELINE_STEP -> step[i+1]
|
|
2241
|
+
for i in range(len(steps) - 1):
|
|
2242
|
+
sym_from = step_syms[i]
|
|
2243
|
+
sym_to = step_syms[i + 1]
|
|
2244
|
+
from_id = sym_from.canonical_id if sym_from is not None else steps[i]
|
|
2245
|
+
to_id = sym_to.canonical_id if sym_to is not None else steps[i + 1]
|
|
2246
|
+
ev_str = f"LCEL pipeline step '{steps[i]}' -> '{steps[i+1]}' at {b.file_path}:{b.line}"
|
|
2247
|
+
add_ref(
|
|
2248
|
+
Reference(
|
|
2249
|
+
source_symbol_id=from_id,
|
|
2250
|
+
target_symbol_id=to_id,
|
|
2251
|
+
relationship="PIPELINE_STEP",
|
|
2252
|
+
confidence="HIGH" if (sym_from is not None and sym_to is not None) else "MEDIUM",
|
|
2253
|
+
path=b.file_path,
|
|
2254
|
+
start_line=b.line,
|
|
2255
|
+
end_line=b.line,
|
|
2256
|
+
evidence=ev_str,
|
|
2257
|
+
source_hash=f_hash,
|
|
2258
|
+
indexed_commit=self.indexed_commit,
|
|
2259
|
+
)
|
|
2260
|
+
)
|
|
2261
|
+
add_edge(
|
|
2262
|
+
GraphEdge(
|
|
2263
|
+
source=from_id,
|
|
2264
|
+
target=to_id,
|
|
2265
|
+
relationship="PIPELINE_STEP",
|
|
2266
|
+
confidence="HIGH" if (step_syms[i] and step_syms[i+1]) else "MEDIUM",
|
|
2267
|
+
file=b.file_path,
|
|
2268
|
+
start_line=b.line,
|
|
2269
|
+
end_line=b.line,
|
|
2270
|
+
evidence=ev_str,
|
|
2271
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2272
|
+
reason="lcel_pipe",
|
|
2273
|
+
)
|
|
2274
|
+
)
|
|
2275
|
+
|
|
2276
|
+
# 8h. Python View -> Template Rendering (Flask, Django, FastAPI)
|
|
2277
|
+
for b in bindings_by_kind.get("RENDERS_TEMPLATE", ()):
|
|
2278
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
2279
|
+
raw_tmpl = b.target_name
|
|
2280
|
+
resolved_tmpl = raw_tmpl
|
|
2281
|
+
if raw_tmpl not in self.known_files:
|
|
2282
|
+
for kf in self.known_files:
|
|
2283
|
+
if kf.endswith(f"/{raw_tmpl}") or kf == raw_tmpl:
|
|
2284
|
+
resolved_tmpl = kf
|
|
2285
|
+
break
|
|
2286
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2287
|
+
ev_str = f"View renders HTML template '{raw_tmpl}' at {b.file_path}:{b.line}"
|
|
2288
|
+
add_ref(
|
|
2289
|
+
Reference(
|
|
2290
|
+
source_symbol_id=caller_id,
|
|
2291
|
+
target_symbol_id=resolved_tmpl,
|
|
2292
|
+
relationship="RENDERS_TEMPLATE",
|
|
2293
|
+
confidence="HIGH",
|
|
2294
|
+
path=b.file_path,
|
|
2295
|
+
start_line=b.line,
|
|
2296
|
+
end_line=b.line,
|
|
2297
|
+
evidence=ev_str,
|
|
2298
|
+
source_hash=f_hash,
|
|
2299
|
+
indexed_commit=self.indexed_commit,
|
|
2300
|
+
)
|
|
2301
|
+
)
|
|
2302
|
+
add_edge(
|
|
2303
|
+
GraphEdge(
|
|
2304
|
+
source=caller_id,
|
|
2305
|
+
target=resolved_tmpl,
|
|
2306
|
+
relationship="RENDERS_TEMPLATE",
|
|
2307
|
+
confidence="HIGH",
|
|
2308
|
+
file=b.file_path,
|
|
2309
|
+
start_line=b.line,
|
|
2310
|
+
end_line=b.line,
|
|
2311
|
+
evidence=ev_str,
|
|
2312
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2313
|
+
reason=f"template_render:{raw_tmpl}",
|
|
2314
|
+
)
|
|
2315
|
+
)
|
|
2316
|
+
|
|
2317
|
+
# 9. Dependency Injection & Configuration Intelligence
|
|
2318
|
+
# 9a. Statically grounded provider registration (PROVIDES)
|
|
2319
|
+
provider_map: dict[str, list[tuple[str, BindingRef, str]]] = {}
|
|
2320
|
+
for b in self.bindings:
|
|
2321
|
+
if b.expr_kind == "SEMANTIC_DECORATOR" and b.attr_name == "PROVIDES":
|
|
2322
|
+
if b.subscript_target is not None:
|
|
2323
|
+
findings.append({
|
|
2324
|
+
"pattern": "DI_PROVIDES",
|
|
2325
|
+
"status": "UNKNOWN",
|
|
2326
|
+
"reason": b.subscript_target or "dynamic_provider",
|
|
2327
|
+
"file": b.file_path,
|
|
2328
|
+
"line": b.line,
|
|
2329
|
+
"target_name": b.target_name,
|
|
2330
|
+
})
|
|
2331
|
+
continue
|
|
2332
|
+
dec_meta: dict[str, object] = {}
|
|
2333
|
+
if b.subscript_key:
|
|
2334
|
+
try:
|
|
2335
|
+
dec_meta = json.loads(b.subscript_key)
|
|
2336
|
+
except Exception:
|
|
2337
|
+
dec_meta = {}
|
|
2338
|
+
iface_raw = str(dec_meta.get("interface") or "")
|
|
2339
|
+
impl_canon = b.target_name
|
|
2340
|
+
iface_canon = impl_canon
|
|
2341
|
+
if iface_raw and iface_raw != impl_canon.split(".")[-1]:
|
|
2342
|
+
if_sym, _, _ = self.resolve_identifier_in_file(iface_raw, b.file_path, use_line=b.line)
|
|
2343
|
+
if not if_sym and len(self.short_to_symbols.get(iface_raw, [])) == 1:
|
|
2344
|
+
if_sym = self.short_to_symbols[iface_raw][0]
|
|
2345
|
+
if if_sym:
|
|
2346
|
+
iface_canon = if_sym.canonical_id
|
|
2347
|
+
provider_map.setdefault(iface_canon, []).append((impl_canon, b, "decorator_provider"))
|
|
2348
|
+
continue
|
|
2349
|
+
|
|
2350
|
+
if b.expr_kind != "DI_PROVIDES":
|
|
2351
|
+
continue
|
|
2352
|
+
|
|
2353
|
+
if (
|
|
2354
|
+
b.subscript_target == "dynamic_provider"
|
|
2355
|
+
or b.target_name == "<DYNAMIC>"
|
|
2356
|
+
or b.source_expr == "<DYNAMIC>"
|
|
2357
|
+
):
|
|
2358
|
+
findings.append({
|
|
2359
|
+
"pattern": "DI_PROVIDES",
|
|
2360
|
+
"status": "UNKNOWN",
|
|
2361
|
+
"reason": "dynamic_provider",
|
|
2362
|
+
"file": b.file_path,
|
|
2363
|
+
"line": b.line,
|
|
2364
|
+
"target_name": b.target_name,
|
|
2365
|
+
})
|
|
2366
|
+
continue
|
|
2367
|
+
|
|
2368
|
+
iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
|
|
2369
|
+
if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
|
|
2370
|
+
iface_sym = self.short_to_symbols[b.target_name][0]
|
|
2371
|
+
|
|
2372
|
+
impl_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2373
|
+
if not impl_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2374
|
+
impl_sym = self.short_to_symbols[b.source_expr][0]
|
|
2375
|
+
|
|
2376
|
+
if not iface_sym and b.base_expr == "nestjs_provider" and impl_sym:
|
|
2377
|
+
iface_canon = b.target_name
|
|
2378
|
+
impl_canon = impl_sym.canonical_id
|
|
2379
|
+
provider_map.setdefault(iface_canon, []).append((impl_canon, b, "nestjs_provider"))
|
|
2380
|
+
continue
|
|
2381
|
+
|
|
2382
|
+
if not iface_sym or not impl_sym:
|
|
2383
|
+
findings.append({
|
|
2384
|
+
"pattern": "DI_PROVIDES",
|
|
2385
|
+
"status": "UNKNOWN",
|
|
2386
|
+
"reason": "dynamic_provider",
|
|
2387
|
+
"file": b.file_path,
|
|
2388
|
+
"line": b.line,
|
|
2389
|
+
"target_name": b.target_name,
|
|
2390
|
+
"source_expr": b.source_expr,
|
|
2391
|
+
})
|
|
2392
|
+
continue
|
|
2393
|
+
|
|
2394
|
+
iface_canon = iface_sym.canonical_id
|
|
2395
|
+
impl_canon = impl_sym.canonical_id
|
|
2396
|
+
# For 1-arg container.provide(Impl) / container.singleton(Impl), check if Impl extends a base interface
|
|
2397
|
+
if iface_canon == impl_canon and b.target_name == b.source_expr:
|
|
2398
|
+
for inh in inheritance_by_source.get(impl_canon, ()):
|
|
2399
|
+
base_sym, _, _ = self.resolve_identifier_in_file(inh.base_name, inh.source_file, use_line=inh.line)
|
|
2400
|
+
if not base_sym and len(self.short_to_symbols.get(inh.base_name, [])) == 1:
|
|
2401
|
+
base_sym = self.short_to_symbols[inh.base_name][0]
|
|
2402
|
+
if base_sym:
|
|
2403
|
+
iface_canon = base_sym.canonical_id
|
|
2404
|
+
break
|
|
2405
|
+
provider_map.setdefault(iface_canon, []).append((impl_canon, b, "provider_mapping"))
|
|
2406
|
+
|
|
2407
|
+
# Emit PROVIDES edges (Ambiguity: multiple distinct providers -> POSSIBLE)
|
|
2408
|
+
for iface_canon, reg_list in provider_map.items():
|
|
2409
|
+
distinct_impls = set(impl_c for impl_c, _, _ in reg_list)
|
|
2410
|
+
is_ambiguous = len(distinct_impls) > 1
|
|
2411
|
+
|
|
2412
|
+
for impl_canon, b, _ in reg_list:
|
|
2413
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2414
|
+
if is_ambiguous:
|
|
2415
|
+
ev_class = RelationshipEvidenceClass.POSSIBLE
|
|
2416
|
+
edge_conf = "LOW"
|
|
2417
|
+
edge_reason = "multiple_providers"
|
|
2418
|
+
elif b.is_conditional:
|
|
2419
|
+
ev_class = RelationshipEvidenceClass.POSSIBLE
|
|
2420
|
+
edge_conf = "LOW"
|
|
2421
|
+
edge_reason = "conditional_provider"
|
|
2422
|
+
else:
|
|
2423
|
+
ev_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
|
|
2424
|
+
edge_conf = "HIGH"
|
|
2425
|
+
edge_reason = f"Statically verified provider mapping '{b.target_name}' -> '{impl_canon}'"
|
|
2426
|
+
|
|
2427
|
+
ev_str = f"Provider mapping: {iface_canon} PROVIDES {impl_canon} at {b.file_path}:{b.line}"
|
|
2428
|
+
add_ref(
|
|
2429
|
+
Reference(
|
|
2430
|
+
source_symbol_id=iface_canon,
|
|
2431
|
+
target_symbol_id=impl_canon,
|
|
2432
|
+
relationship="PROVIDES",
|
|
2433
|
+
confidence=edge_conf,
|
|
2434
|
+
path=b.file_path,
|
|
2435
|
+
start_line=b.line,
|
|
2436
|
+
end_line=b.line,
|
|
2437
|
+
evidence=ev_str,
|
|
2438
|
+
source_hash=f_hash,
|
|
2439
|
+
indexed_commit=self.indexed_commit,
|
|
2440
|
+
)
|
|
2441
|
+
)
|
|
2442
|
+
add_edge(
|
|
2443
|
+
GraphEdge(
|
|
2444
|
+
source=iface_canon,
|
|
2445
|
+
target=impl_canon,
|
|
2446
|
+
relationship="PROVIDES",
|
|
2447
|
+
confidence=edge_conf,
|
|
2448
|
+
file=b.file_path,
|
|
2449
|
+
start_line=b.line,
|
|
2450
|
+
end_line=b.line,
|
|
2451
|
+
evidence=ev_str,
|
|
2452
|
+
evidence_class=ev_class.value,
|
|
2453
|
+
reason=edge_reason,
|
|
2454
|
+
)
|
|
2455
|
+
)
|
|
2456
|
+
|
|
2457
|
+
# 9b. Dependency Resolution (RESOLVES_DEPENDENCY)
|
|
2458
|
+
for b in bindings_by_kind.get("DI_RESOLVES", ()):
|
|
2459
|
+
if b.subscript_target == "dynamic_provider" or b.target_name == "<DYNAMIC>":
|
|
2460
|
+
findings.append({
|
|
2461
|
+
"pattern": "DI_RESOLVES",
|
|
2462
|
+
"status": "UNKNOWN",
|
|
2463
|
+
"reason": "dynamic_provider",
|
|
2464
|
+
"file": b.file_path,
|
|
2465
|
+
"line": b.line,
|
|
2466
|
+
})
|
|
2467
|
+
continue
|
|
2468
|
+
|
|
2469
|
+
iface_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
|
|
2470
|
+
if not iface_sym and len(self.short_to_symbols.get(b.target_name, [])) == 1:
|
|
2471
|
+
iface_sym = self.short_to_symbols[b.target_name][0]
|
|
2472
|
+
|
|
2473
|
+
if not iface_sym:
|
|
2474
|
+
findings.append({
|
|
2475
|
+
"pattern": "DI_RESOLVES",
|
|
2476
|
+
"status": "UNKNOWN",
|
|
2477
|
+
"reason": "unknown_dependency_interface",
|
|
2478
|
+
"file": b.file_path,
|
|
2479
|
+
"line": b.line,
|
|
2480
|
+
"target_name": b.target_name,
|
|
2481
|
+
})
|
|
2482
|
+
continue
|
|
2483
|
+
|
|
2484
|
+
iface_canon = iface_sym.canonical_id
|
|
2485
|
+
if iface_canon not in provider_map:
|
|
2486
|
+
findings.append({
|
|
2487
|
+
"pattern": "DI_RESOLVES",
|
|
2488
|
+
"status": "UNKNOWN",
|
|
2489
|
+
"reason": "no_provider_registered",
|
|
2490
|
+
"file": b.file_path,
|
|
2491
|
+
"line": b.line,
|
|
2492
|
+
"target_name": b.target_name,
|
|
2493
|
+
})
|
|
2494
|
+
continue
|
|
2495
|
+
|
|
2496
|
+
reg_list = provider_map[iface_canon]
|
|
2497
|
+
distinct_impls = set(impl_c for impl_c, _, _ in reg_list)
|
|
2498
|
+
is_ambiguous = len(distinct_impls) > 1
|
|
2499
|
+
|
|
2500
|
+
if is_ambiguous:
|
|
2501
|
+
findings.append({
|
|
2502
|
+
"pattern": "DI_RESOLVES",
|
|
2503
|
+
"status": "POSSIBLE",
|
|
2504
|
+
"reason": "multiple_providers",
|
|
2505
|
+
"file": b.file_path,
|
|
2506
|
+
"line": b.line,
|
|
2507
|
+
"target_name": b.target_name,
|
|
2508
|
+
})
|
|
2509
|
+
|
|
2510
|
+
for impl_canon, _orig_b, _ in reg_list:
|
|
2511
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2512
|
+
if is_ambiguous:
|
|
2513
|
+
ev_class = RelationshipEvidenceClass.POSSIBLE
|
|
2514
|
+
edge_conf = "LOW"
|
|
2515
|
+
edge_reason = "multiple_providers"
|
|
2516
|
+
else:
|
|
2517
|
+
ev_class = RelationshipEvidenceClass.DATAFLOW_VERIFIED
|
|
2518
|
+
edge_conf = "HIGH"
|
|
2519
|
+
edge_reason = f"Deterministic dependency resolution '{iface_canon}' -> '{impl_canon}'"
|
|
2520
|
+
|
|
2521
|
+
ev_str = f"Dependency resolution: {iface_canon} RESOLVES_DEPENDENCY {impl_canon} at {b.file_path}:{b.line}"
|
|
2522
|
+
add_ref(
|
|
2523
|
+
Reference(
|
|
2524
|
+
source_symbol_id=iface_canon,
|
|
2525
|
+
target_symbol_id=impl_canon,
|
|
2526
|
+
relationship="RESOLVES_DEPENDENCY",
|
|
2527
|
+
confidence=edge_conf,
|
|
2528
|
+
path=b.file_path,
|
|
2529
|
+
start_line=b.line,
|
|
2530
|
+
end_line=b.line,
|
|
2531
|
+
evidence=ev_str,
|
|
2532
|
+
source_hash=f_hash,
|
|
2533
|
+
indexed_commit=self.indexed_commit,
|
|
2534
|
+
)
|
|
2535
|
+
)
|
|
2536
|
+
add_edge(
|
|
2537
|
+
GraphEdge(
|
|
2538
|
+
source=iface_canon,
|
|
2539
|
+
target=impl_canon,
|
|
2540
|
+
relationship="RESOLVES_DEPENDENCY",
|
|
2541
|
+
confidence=edge_conf,
|
|
2542
|
+
file=b.file_path,
|
|
2543
|
+
start_line=b.line,
|
|
2544
|
+
end_line=b.line,
|
|
2545
|
+
evidence=ev_str,
|
|
2546
|
+
evidence_class=ev_class.value,
|
|
2547
|
+
reason=edge_reason,
|
|
2548
|
+
)
|
|
2549
|
+
)
|
|
2550
|
+
|
|
2551
|
+
# 9c. FastAPI Depends(...) Injection (INJECTS)
|
|
2552
|
+
# 1. Collect modern FastAPI Annotated aliases (e.g. SessionDep = Annotated[Session, Depends(get_db)])
|
|
2553
|
+
alias_to_dep: dict[str, str] = {}
|
|
2554
|
+
alias_name_to_dep: dict[str, str] = {}
|
|
2555
|
+
for b in bindings_by_kind.get("DI_ALIAS_INJECTS", []):
|
|
2556
|
+
dep_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2557
|
+
if not dep_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2558
|
+
dep_sym = self.short_to_symbols[b.source_expr][0]
|
|
2559
|
+
if dep_sym:
|
|
2560
|
+
dep_canon = dep_sym.canonical_id
|
|
2561
|
+
alias_sym, _, _ = self.resolve_identifier_in_file(b.target_name, b.file_path, use_line=b.line)
|
|
2562
|
+
alias_canon = alias_sym.canonical_id if alias_sym else f"{b.file_path}:{b.target_name}"
|
|
2563
|
+
alias_to_dep[alias_canon] = dep_canon
|
|
2564
|
+
alias_name_to_dep[b.target_name] = dep_canon
|
|
2565
|
+
ev_str = f"FastAPI Annotated Alias: '{alias_canon}' resolves to Depends({dep_canon}) at {b.file_path}:{b.line}"
|
|
2566
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2567
|
+
add_ref(
|
|
2568
|
+
Reference(
|
|
2569
|
+
source_symbol_id=alias_canon,
|
|
2570
|
+
target_symbol_id=dep_canon,
|
|
2571
|
+
relationship="DEPENDS_ON",
|
|
2572
|
+
confidence="HIGH",
|
|
2573
|
+
path=b.file_path,
|
|
2574
|
+
start_line=b.line,
|
|
2575
|
+
end_line=b.line,
|
|
2576
|
+
evidence=ev_str,
|
|
2577
|
+
source_hash=f_hash,
|
|
2578
|
+
indexed_commit=self.indexed_commit,
|
|
2579
|
+
)
|
|
2580
|
+
)
|
|
2581
|
+
add_edge(
|
|
2582
|
+
GraphEdge(
|
|
2583
|
+
source=alias_canon,
|
|
2584
|
+
target=dep_canon,
|
|
2585
|
+
relationship="DEPENDS_ON",
|
|
2586
|
+
confidence="HIGH",
|
|
2587
|
+
file=b.file_path,
|
|
2588
|
+
start_line=b.line,
|
|
2589
|
+
end_line=b.line,
|
|
2590
|
+
evidence=ev_str,
|
|
2591
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2592
|
+
reason="fastapi_annotated_alias",
|
|
2593
|
+
)
|
|
2594
|
+
)
|
|
2595
|
+
|
|
2596
|
+
# 2. Resolve function parameters typed with Annotated aliases (e.g. session: SessionDep, current_user: CurrentUser)
|
|
2597
|
+
for b in bindings_by_kind.get("PARAM_TYPE_DI", []):
|
|
2598
|
+
param_type = b.source_expr.split("[")[0].strip().split(".")[-1]
|
|
2599
|
+
dep_injected_canon: str | None = alias_name_to_dep.get(param_type)
|
|
2600
|
+
if not dep_injected_canon:
|
|
2601
|
+
type_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2602
|
+
if type_sym and type_sym.canonical_id in alias_to_dep:
|
|
2603
|
+
dep_injected_canon = alias_to_dep[type_sym.canonical_id]
|
|
2604
|
+
elif type_sym and type_sym.name in alias_name_to_dep:
|
|
2605
|
+
dep_injected_canon = alias_name_to_dep[type_sym.name]
|
|
2606
|
+
if dep_injected_canon:
|
|
2607
|
+
handler_canon = b.target_name
|
|
2608
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2609
|
+
ev_str = f"FastAPI Depends (via {param_type}): '{handler_canon}' INJECTS '{dep_injected_canon}' at {b.file_path}:{b.line}"
|
|
2610
|
+
add_ref(
|
|
2611
|
+
Reference(
|
|
2612
|
+
source_symbol_id=handler_canon,
|
|
2613
|
+
target_symbol_id=dep_injected_canon,
|
|
2614
|
+
relationship="INJECTS",
|
|
2615
|
+
confidence="HIGH",
|
|
2616
|
+
path=b.file_path,
|
|
2617
|
+
start_line=b.line,
|
|
2618
|
+
end_line=b.line,
|
|
2619
|
+
evidence=ev_str,
|
|
2620
|
+
source_hash=f_hash,
|
|
2621
|
+
indexed_commit=self.indexed_commit,
|
|
2622
|
+
)
|
|
2623
|
+
)
|
|
2624
|
+
add_edge(
|
|
2625
|
+
GraphEdge(
|
|
2626
|
+
source=handler_canon,
|
|
2627
|
+
target=dep_injected_canon,
|
|
2628
|
+
relationship="INJECTS",
|
|
2629
|
+
confidence="HIGH",
|
|
2630
|
+
file=b.file_path,
|
|
2631
|
+
start_line=b.line,
|
|
2632
|
+
end_line=b.line,
|
|
2633
|
+
evidence=ev_str,
|
|
2634
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2635
|
+
reason="fastapi_annotated_alias_injects",
|
|
2636
|
+
)
|
|
2637
|
+
)
|
|
2638
|
+
handler_short = handler_canon.split(".")[-1]
|
|
2639
|
+
for rt in self.routes:
|
|
2640
|
+
matches_handler = (
|
|
2641
|
+
rt.handler_canonical_id == handler_canon
|
|
2642
|
+
or (rt.handler_name == handler_short and rt.file_path == b.file_path)
|
|
2643
|
+
)
|
|
2644
|
+
if matches_handler:
|
|
2645
|
+
rt_ev = f"Route endpoint '{rt.endpoint_id}' INJECTS '{dep_injected_canon}' (via {param_type}) at {rt.file_path}:{rt.line}"
|
|
2646
|
+
add_ref(
|
|
2647
|
+
Reference(
|
|
2648
|
+
source_symbol_id=rt.endpoint_id,
|
|
2649
|
+
target_symbol_id=dep_injected_canon,
|
|
2650
|
+
relationship="INJECTS",
|
|
2651
|
+
confidence="HIGH",
|
|
2652
|
+
path=rt.file_path,
|
|
2653
|
+
start_line=rt.line,
|
|
2654
|
+
end_line=rt.line,
|
|
2655
|
+
evidence=rt_ev,
|
|
2656
|
+
source_hash=f_hash,
|
|
2657
|
+
indexed_commit=self.indexed_commit,
|
|
2658
|
+
)
|
|
2659
|
+
)
|
|
2660
|
+
add_edge(
|
|
2661
|
+
GraphEdge(
|
|
2662
|
+
source=rt.endpoint_id,
|
|
2663
|
+
target=dep_injected_canon,
|
|
2664
|
+
relationship="INJECTS",
|
|
2665
|
+
confidence="HIGH",
|
|
2666
|
+
file=rt.file_path,
|
|
2667
|
+
start_line=rt.line,
|
|
2668
|
+
end_line=rt.line,
|
|
2669
|
+
evidence=rt_ev,
|
|
2670
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2671
|
+
reason="fastapi_annotated_alias_injects",
|
|
2672
|
+
)
|
|
2673
|
+
)
|
|
2674
|
+
|
|
2675
|
+
# 3. Standard router and direct function Depends(...) bindings
|
|
2676
|
+
for b in (
|
|
2677
|
+
bindings_by_kind.get("ROUTER_DI_INJECTS", [])
|
|
2678
|
+
+ bindings_by_kind.get("DI_INJECTS", [])
|
|
2679
|
+
):
|
|
2680
|
+
if b.expr_kind == "ROUTER_DI_INJECTS":
|
|
2681
|
+
if b.subscript_target == "dynamic_provider" or b.source_expr == "<DYNAMIC>":
|
|
2682
|
+
findings.append({
|
|
2683
|
+
"pattern": "DI_INJECTS",
|
|
2684
|
+
"status": "UNKNOWN",
|
|
2685
|
+
"reason": "dynamic_provider",
|
|
2686
|
+
"file": b.file_path,
|
|
2687
|
+
"line": b.line,
|
|
2688
|
+
"target_name": b.target_name,
|
|
2689
|
+
})
|
|
2690
|
+
continue
|
|
2691
|
+
|
|
2692
|
+
dep_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2693
|
+
if not dep_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2694
|
+
dep_sym = self.short_to_symbols[b.source_expr][0]
|
|
2695
|
+
if not dep_sym:
|
|
2696
|
+
findings.append({
|
|
2697
|
+
"pattern": "DI_INJECTS",
|
|
2698
|
+
"status": "UNKNOWN",
|
|
2699
|
+
"reason": "unknown_dependency_target",
|
|
2700
|
+
"file": b.file_path,
|
|
2701
|
+
"line": b.line,
|
|
2702
|
+
"source_expr": b.source_expr,
|
|
2703
|
+
})
|
|
2704
|
+
continue
|
|
2705
|
+
|
|
2706
|
+
dep_canon = dep_sym.canonical_id
|
|
2707
|
+
router_var = b.target_name
|
|
2708
|
+
# Also resolve if router_var was imported into b.file_path (e.g. app.include_router(users_router, dependencies=[...]))
|
|
2709
|
+
router_sym, _, _ = self.resolve_identifier_in_file(router_var, b.file_path, use_line=b.line)
|
|
2710
|
+
target_router_names = {router_var}
|
|
2711
|
+
if router_sym:
|
|
2712
|
+
target_router_names.add(router_sym.name)
|
|
2713
|
+
for imp in self.file_imports.get(b.file_path, ()):
|
|
2714
|
+
if imp.local_name == router_var or imp.alias == router_var or imp.name == router_var:
|
|
2715
|
+
if imp.imported_name:
|
|
2716
|
+
target_router_names.add(imp.imported_name)
|
|
2717
|
+
|
|
2718
|
+
for rt in self.routes:
|
|
2719
|
+
matches_router = (
|
|
2720
|
+
rt.router_name in target_router_names
|
|
2721
|
+
or (router_var == "app" and rt.file_path == b.file_path)
|
|
2722
|
+
)
|
|
2723
|
+
if not matches_router:
|
|
2724
|
+
continue
|
|
2725
|
+
rt_f_hash = self.file_hashes.get(rt.file_path, "")
|
|
2726
|
+
if rt.handler_canonical_id:
|
|
2727
|
+
h_ev = f"Router/App Depends: '{rt.handler_canonical_id}' INJECTS '{dep_canon}' via '{router_var}' at {b.file_path}:{b.line}"
|
|
2728
|
+
add_ref(
|
|
2729
|
+
Reference(
|
|
2730
|
+
source_symbol_id=rt.handler_canonical_id,
|
|
2731
|
+
target_symbol_id=dep_canon,
|
|
2732
|
+
relationship="INJECTS",
|
|
2733
|
+
confidence="HIGH",
|
|
2734
|
+
path=b.file_path,
|
|
2735
|
+
start_line=b.line,
|
|
2736
|
+
end_line=b.line,
|
|
2737
|
+
evidence=h_ev,
|
|
2738
|
+
source_hash=rt_f_hash,
|
|
2739
|
+
indexed_commit=self.indexed_commit,
|
|
2740
|
+
)
|
|
2741
|
+
)
|
|
2742
|
+
add_edge(
|
|
2743
|
+
GraphEdge(
|
|
2744
|
+
source=rt.handler_canonical_id,
|
|
2745
|
+
target=dep_canon,
|
|
2746
|
+
relationship="INJECTS",
|
|
2747
|
+
confidence="HIGH",
|
|
2748
|
+
file=b.file_path,
|
|
2749
|
+
start_line=b.line,
|
|
2750
|
+
end_line=b.line,
|
|
2751
|
+
evidence=h_ev,
|
|
2752
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2753
|
+
reason="fastapi_router_depends",
|
|
2754
|
+
)
|
|
2755
|
+
)
|
|
2756
|
+
rt_ev = f"Route endpoint '{rt.endpoint_id}' INJECTS '{dep_canon}' via '{router_var}' at {b.file_path}:{b.line}"
|
|
2757
|
+
add_ref(
|
|
2758
|
+
Reference(
|
|
2759
|
+
source_symbol_id=rt.endpoint_id,
|
|
2760
|
+
target_symbol_id=dep_canon,
|
|
2761
|
+
relationship="INJECTS",
|
|
2762
|
+
confidence="HIGH",
|
|
2763
|
+
path=rt.file_path,
|
|
2764
|
+
start_line=rt.line,
|
|
2765
|
+
end_line=rt.line,
|
|
2766
|
+
evidence=rt_ev,
|
|
2767
|
+
source_hash=rt_f_hash,
|
|
2768
|
+
indexed_commit=self.indexed_commit,
|
|
2769
|
+
)
|
|
2770
|
+
)
|
|
2771
|
+
add_edge(
|
|
2772
|
+
GraphEdge(
|
|
2773
|
+
source=rt.endpoint_id,
|
|
2774
|
+
target=dep_canon,
|
|
2775
|
+
relationship="INJECTS",
|
|
2776
|
+
confidence="HIGH",
|
|
2777
|
+
file=rt.file_path,
|
|
2778
|
+
start_line=rt.line,
|
|
2779
|
+
end_line=rt.line,
|
|
2780
|
+
evidence=rt_ev,
|
|
2781
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2782
|
+
reason="fastapi_router_depends",
|
|
2783
|
+
)
|
|
2784
|
+
)
|
|
2785
|
+
continue
|
|
2786
|
+
|
|
2787
|
+
if b.subscript_target == "dynamic_provider" or b.source_expr == "<DYNAMIC>":
|
|
2788
|
+
findings.append({
|
|
2789
|
+
"pattern": "DI_INJECTS",
|
|
2790
|
+
"status": "UNKNOWN",
|
|
2791
|
+
"reason": "dynamic_provider",
|
|
2792
|
+
"file": b.file_path,
|
|
2793
|
+
"line": b.line,
|
|
2794
|
+
"target_name": b.target_name,
|
|
2795
|
+
})
|
|
2796
|
+
continue
|
|
2797
|
+
|
|
2798
|
+
dep_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2799
|
+
if not dep_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2800
|
+
dep_sym = self.short_to_symbols[b.source_expr][0]
|
|
2801
|
+
|
|
2802
|
+
if not dep_sym:
|
|
2803
|
+
findings.append({
|
|
2804
|
+
"pattern": "DI_INJECTS",
|
|
2805
|
+
"status": "UNKNOWN",
|
|
2806
|
+
"reason": "unknown_dependency_target",
|
|
2807
|
+
"file": b.file_path,
|
|
2808
|
+
"line": b.line,
|
|
2809
|
+
"source_expr": b.source_expr,
|
|
2810
|
+
})
|
|
2811
|
+
continue
|
|
2812
|
+
|
|
2813
|
+
dep_canon = dep_sym.canonical_id
|
|
2814
|
+
handler_canon = b.target_name
|
|
2815
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2816
|
+
ev_str = f"FastAPI Depends: '{handler_canon}' INJECTS '{dep_canon}' at {b.file_path}:{b.line}"
|
|
2817
|
+
|
|
2818
|
+
add_ref(
|
|
2819
|
+
Reference(
|
|
2820
|
+
source_symbol_id=handler_canon,
|
|
2821
|
+
target_symbol_id=dep_canon,
|
|
2822
|
+
relationship="INJECTS",
|
|
2823
|
+
confidence="HIGH",
|
|
2824
|
+
path=b.file_path,
|
|
2825
|
+
start_line=b.line,
|
|
2826
|
+
end_line=b.line,
|
|
2827
|
+
evidence=ev_str,
|
|
2828
|
+
source_hash=f_hash,
|
|
2829
|
+
indexed_commit=self.indexed_commit,
|
|
2830
|
+
)
|
|
2831
|
+
)
|
|
2832
|
+
add_edge(
|
|
2833
|
+
GraphEdge(
|
|
2834
|
+
source=handler_canon,
|
|
2835
|
+
target=dep_canon,
|
|
2836
|
+
relationship="INJECTS",
|
|
2837
|
+
confidence="HIGH",
|
|
2838
|
+
file=b.file_path,
|
|
2839
|
+
start_line=b.line,
|
|
2840
|
+
end_line=b.line,
|
|
2841
|
+
evidence=ev_str,
|
|
2842
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2843
|
+
reason="fastapi_depends",
|
|
2844
|
+
)
|
|
2845
|
+
)
|
|
2846
|
+
|
|
2847
|
+
# Also link matching route endpoint_id -> dep_canon (scoped to this file or exact canonical ID)
|
|
2848
|
+
handler_short = handler_canon.split(".")[-1]
|
|
2849
|
+
for rt in self.routes:
|
|
2850
|
+
matches_handler = (
|
|
2851
|
+
rt.handler_canonical_id == handler_canon
|
|
2852
|
+
or (rt.handler_name == handler_short and rt.file_path == b.file_path)
|
|
2853
|
+
)
|
|
2854
|
+
if matches_handler:
|
|
2855
|
+
rt_ev = f"Route endpoint '{rt.endpoint_id}' INJECTS '{dep_canon}' at {rt.file_path}:{rt.line}"
|
|
2856
|
+
add_ref(
|
|
2857
|
+
Reference(
|
|
2858
|
+
source_symbol_id=rt.endpoint_id,
|
|
2859
|
+
target_symbol_id=dep_canon,
|
|
2860
|
+
relationship="INJECTS",
|
|
2861
|
+
confidence="HIGH",
|
|
2862
|
+
path=rt.file_path,
|
|
2863
|
+
start_line=rt.line,
|
|
2864
|
+
end_line=rt.line,
|
|
2865
|
+
evidence=rt_ev,
|
|
2866
|
+
source_hash=f_hash,
|
|
2867
|
+
indexed_commit=self.indexed_commit,
|
|
2868
|
+
)
|
|
2869
|
+
)
|
|
2870
|
+
add_edge(
|
|
2871
|
+
GraphEdge(
|
|
2872
|
+
source=rt.endpoint_id,
|
|
2873
|
+
target=dep_canon,
|
|
2874
|
+
relationship="INJECTS",
|
|
2875
|
+
confidence="HIGH",
|
|
2876
|
+
file=rt.file_path,
|
|
2877
|
+
start_line=rt.line,
|
|
2878
|
+
end_line=rt.line,
|
|
2879
|
+
evidence=rt_ev,
|
|
2880
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2881
|
+
reason="fastapi_depends",
|
|
2882
|
+
)
|
|
2883
|
+
)
|
|
2884
|
+
|
|
2885
|
+
# 9d. Constructor & @inject Type-Based Injection (INJECTS)
|
|
2886
|
+
for b in bindings_by_kind.get("CONSTRUCTOR_PARAM", ()):
|
|
2887
|
+
ann_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2888
|
+
if not ann_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2889
|
+
ann_sym = self.short_to_symbols[b.source_expr][0]
|
|
2890
|
+
|
|
2891
|
+
if not ann_sym:
|
|
2892
|
+
continue
|
|
2893
|
+
|
|
2894
|
+
ann_canon = ann_sym.canonical_id
|
|
2895
|
+
# Rule: Only create injection relationship when supported DI semantics exist!
|
|
2896
|
+
# i.e., provider mapping exists for this interface OR @inject decorator was present
|
|
2897
|
+
has_provider = ann_canon in provider_map
|
|
2898
|
+
is_inject_dec = (b.base_expr == "inject_decorator")
|
|
2899
|
+
|
|
2900
|
+
if not (has_provider or is_inject_dec):
|
|
2901
|
+
continue
|
|
2902
|
+
|
|
2903
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2904
|
+
caller_canon = b.target_name
|
|
2905
|
+
ev_str = f"Constructor injection: '{caller_canon}' INJECTS '{ann_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
|
|
2906
|
+
|
|
2907
|
+
add_ref(
|
|
2908
|
+
Reference(
|
|
2909
|
+
source_symbol_id=caller_canon,
|
|
2910
|
+
target_symbol_id=ann_canon,
|
|
2911
|
+
relationship="INJECTS",
|
|
2912
|
+
confidence="HIGH",
|
|
2913
|
+
path=b.file_path,
|
|
2914
|
+
start_line=b.line,
|
|
2915
|
+
end_line=b.line,
|
|
2916
|
+
evidence=ev_str,
|
|
2917
|
+
source_hash=f_hash,
|
|
2918
|
+
indexed_commit=self.indexed_commit,
|
|
2919
|
+
)
|
|
2920
|
+
)
|
|
2921
|
+
add_edge(
|
|
2922
|
+
GraphEdge(
|
|
2923
|
+
source=caller_canon,
|
|
2924
|
+
target=ann_canon,
|
|
2925
|
+
relationship="INJECTS",
|
|
2926
|
+
confidence="HIGH",
|
|
2927
|
+
file=b.file_path,
|
|
2928
|
+
start_line=b.line,
|
|
2929
|
+
end_line=b.line,
|
|
2930
|
+
evidence=ev_str,
|
|
2931
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
2932
|
+
reason="constructor_injection",
|
|
2933
|
+
)
|
|
2934
|
+
)
|
|
2935
|
+
|
|
2936
|
+
# 9d-nest. NestJS Constructor & @Inject Parameter Injection (INJECTS)
|
|
2937
|
+
for b in bindings_by_kind.get("NESTJS_INJECTS", ()):
|
|
2938
|
+
ann_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
2939
|
+
if not ann_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
2940
|
+
ann_sym = self.short_to_symbols[b.source_expr][0]
|
|
2941
|
+
|
|
2942
|
+
injected_canon: str | None = None
|
|
2943
|
+
if ann_sym:
|
|
2944
|
+
injected_canon = ann_sym.canonical_id
|
|
2945
|
+
elif b.source_expr in provider_map:
|
|
2946
|
+
injected_canon = provider_map[b.source_expr][0][0]
|
|
2947
|
+
elif b.source_expr:
|
|
2948
|
+
injected_canon = f"token.{b.source_expr}"
|
|
2949
|
+
|
|
2950
|
+
if not injected_canon:
|
|
2951
|
+
continue
|
|
2952
|
+
|
|
2953
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
2954
|
+
caller_canon = b.target_name
|
|
2955
|
+
ev_str = f"NestJS injection: '{caller_canon}' INJECTS '{injected_canon}' (param='{b.attr_name}') at {b.file_path}:{b.line}"
|
|
2956
|
+
|
|
2957
|
+
add_ref(
|
|
2958
|
+
Reference(
|
|
2959
|
+
source_symbol_id=caller_canon,
|
|
2960
|
+
target_symbol_id=injected_canon,
|
|
2961
|
+
relationship="INJECTS",
|
|
2962
|
+
confidence="HIGH",
|
|
2963
|
+
path=b.file_path,
|
|
2964
|
+
start_line=b.line,
|
|
2965
|
+
end_line=b.line,
|
|
2966
|
+
evidence=ev_str,
|
|
2967
|
+
source_hash=f_hash,
|
|
2968
|
+
indexed_commit=self.indexed_commit,
|
|
2969
|
+
)
|
|
2970
|
+
)
|
|
2971
|
+
add_edge(
|
|
2972
|
+
GraphEdge(
|
|
2973
|
+
source=caller_canon,
|
|
2974
|
+
target=injected_canon,
|
|
2975
|
+
relationship="INJECTS",
|
|
2976
|
+
confidence="HIGH",
|
|
2977
|
+
file=b.file_path,
|
|
2978
|
+
start_line=b.line,
|
|
2979
|
+
end_line=b.line,
|
|
2980
|
+
evidence=ev_str,
|
|
2981
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
2982
|
+
reason="nestjs_injection",
|
|
2983
|
+
)
|
|
2984
|
+
)
|
|
2985
|
+
|
|
2986
|
+
# Link routes of this controller to the injected dependency
|
|
2987
|
+
for rt in self.routes:
|
|
2988
|
+
if rt.handler_canonical_id.startswith(f"{caller_canon}."):
|
|
2989
|
+
rt_ev = f"NestJS Route endpoint '{rt.endpoint_id}' INJECTS '{injected_canon}' via {caller_canon} at {rt.file_path}:{rt.line}"
|
|
2990
|
+
add_edge(
|
|
2991
|
+
GraphEdge(
|
|
2992
|
+
source=rt.endpoint_id,
|
|
2993
|
+
target=injected_canon,
|
|
2994
|
+
relationship="INJECTS",
|
|
2995
|
+
confidence="HIGH",
|
|
2996
|
+
file=rt.file_path,
|
|
2997
|
+
start_line=rt.line,
|
|
2998
|
+
end_line=rt.line,
|
|
2999
|
+
evidence=rt_ev,
|
|
3000
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3001
|
+
reason="nestjs_route_injection",
|
|
3002
|
+
)
|
|
3003
|
+
)
|
|
3004
|
+
|
|
3005
|
+
# 9d-ctrl. NestJS Controller Mounting (MOUNTS)
|
|
3006
|
+
for b in bindings_by_kind.get("NESTJS_CONTROLLER", ()):
|
|
3007
|
+
ctrl_sym, _, _ = self.resolve_identifier_in_file(b.source_expr, b.file_path, use_line=b.line)
|
|
3008
|
+
if not ctrl_sym and len(self.short_to_symbols.get(b.source_expr, [])) == 1:
|
|
3009
|
+
ctrl_sym = self.short_to_symbols[b.source_expr][0]
|
|
3010
|
+
if ctrl_sym:
|
|
3011
|
+
ctrl_canon = ctrl_sym.canonical_id
|
|
3012
|
+
add_edge(
|
|
3013
|
+
GraphEdge(
|
|
3014
|
+
source=b.target_name,
|
|
3015
|
+
target=ctrl_canon,
|
|
3016
|
+
relationship="MOUNTS",
|
|
3017
|
+
confidence="HIGH",
|
|
3018
|
+
file=b.file_path,
|
|
3019
|
+
start_line=b.line,
|
|
3020
|
+
end_line=b.line,
|
|
3021
|
+
evidence=f"NestJS Module '{b.target_name}' mounts controller '{ctrl_canon}'",
|
|
3022
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3023
|
+
reason="nestjs_module_mount",
|
|
3024
|
+
)
|
|
3025
|
+
)
|
|
3026
|
+
|
|
3027
|
+
# 9d-cycle. Static DI Cycle Detection (INJECTS / PROVIDES / RESOLVES_DEPENDENCY)
|
|
3028
|
+
di_adj: dict[str, list[tuple[str, str, int]]] = {}
|
|
3029
|
+
for ed in list(edges):
|
|
3030
|
+
if ed.relationship == "INJECTS" or (
|
|
3031
|
+
ed.relationship in ("PROVIDES", "RESOLVES_DEPENDENCY") and ed.source != ed.target
|
|
3032
|
+
):
|
|
3033
|
+
if not ed.source.startswith("API_ENDPOINT:"):
|
|
3034
|
+
di_adj.setdefault(ed.source, []).append((ed.target, ed.file or "", ed.start_line or 1))
|
|
3035
|
+
|
|
3036
|
+
di_visit_state: dict[str, int] = {} # 0=unvisited, 1=visiting, 2=visited
|
|
3037
|
+
di_path_stack: list[str] = []
|
|
3038
|
+
reported_cycles: set[tuple[str, ...]] = set()
|
|
3039
|
+
|
|
3040
|
+
def _detect_di_cycles(u_node: str) -> None:
|
|
3041
|
+
di_visit_state[u_node] = 1
|
|
3042
|
+
di_path_stack.append(u_node)
|
|
3043
|
+
for v_node, ed_file, ed_line in di_adj.get(u_node, []):
|
|
3044
|
+
st = di_visit_state.get(v_node, 0)
|
|
3045
|
+
if st == 1:
|
|
3046
|
+
# Found cycle!
|
|
3047
|
+
if v_node in di_path_stack:
|
|
3048
|
+
c_idx = di_path_stack.index(v_node)
|
|
3049
|
+
cycle_seq = tuple(di_path_stack[c_idx:] + [v_node])
|
|
3050
|
+
else:
|
|
3051
|
+
cycle_seq = (u_node, v_node, u_node)
|
|
3052
|
+
if cycle_seq not in reported_cycles:
|
|
3053
|
+
reported_cycles.add(cycle_seq)
|
|
3054
|
+
cycle_str = " -> ".join(cycle_seq)
|
|
3055
|
+
findings.append({
|
|
3056
|
+
"pattern": "DI_CYCLE",
|
|
3057
|
+
"status": "CONFLICT",
|
|
3058
|
+
"reason": "di_dependency_cycle",
|
|
3059
|
+
"file": ed_file,
|
|
3060
|
+
"line": ed_line,
|
|
3061
|
+
"source": u_node,
|
|
3062
|
+
"target": v_node,
|
|
3063
|
+
"cycle": list(cycle_seq),
|
|
3064
|
+
})
|
|
3065
|
+
add_edge(
|
|
3066
|
+
GraphEdge(
|
|
3067
|
+
source=u_node,
|
|
3068
|
+
target=v_node,
|
|
3069
|
+
relationship="DI_CYCLE",
|
|
3070
|
+
confidence="HIGH",
|
|
3071
|
+
file=ed_file,
|
|
3072
|
+
start_line=ed_line,
|
|
3073
|
+
end_line=ed_line,
|
|
3074
|
+
evidence=f"Circular dependency detected: {cycle_str}",
|
|
3075
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
3076
|
+
reason="di_dependency_cycle",
|
|
3077
|
+
)
|
|
3078
|
+
)
|
|
3079
|
+
elif st == 0:
|
|
3080
|
+
_detect_di_cycles(v_node)
|
|
3081
|
+
di_path_stack.pop()
|
|
3082
|
+
di_visit_state[u_node] = 2
|
|
3083
|
+
|
|
3084
|
+
for start_n in sorted(di_adj.keys()):
|
|
3085
|
+
if di_visit_state.get(start_n, 0) == 0:
|
|
3086
|
+
_detect_di_cycles(start_n)
|
|
3087
|
+
|
|
3088
|
+
# 9e. Environment & Configuration References (CONFIGURES)
|
|
3089
|
+
for b in (
|
|
3090
|
+
bindings_by_kind.get("ENV_CONFIG", [])
|
|
3091
|
+
+ bindings_by_kind.get("DYNAMIC_CONFIG", [])
|
|
3092
|
+
):
|
|
3093
|
+
if (
|
|
3094
|
+
b.subscript_target in ("runtime_configuration", "dynamic_provider")
|
|
3095
|
+
or b.target_name == "<DYNAMIC>"
|
|
3096
|
+
):
|
|
3097
|
+
findings.append({
|
|
3098
|
+
"pattern": b.expr_kind,
|
|
3099
|
+
"status": "UNKNOWN",
|
|
3100
|
+
"reason": b.subscript_target or "runtime_configuration",
|
|
3101
|
+
"file": b.file_path,
|
|
3102
|
+
"line": b.line,
|
|
3103
|
+
})
|
|
3104
|
+
continue
|
|
3105
|
+
|
|
3106
|
+
var_name = b.target_name
|
|
3107
|
+
target_scope = b.scope or (self.file_to_module.get(b.file_path) or normalize_module(b.file_path))
|
|
3108
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3109
|
+
ev_str = f"Environment reference '{var_name}' CONFIGURES '{target_scope}' at {b.file_path}:{b.line}"
|
|
3110
|
+
|
|
3111
|
+
add_ref(
|
|
3112
|
+
Reference(
|
|
3113
|
+
source_symbol_id=var_name,
|
|
3114
|
+
target_symbol_id=target_scope,
|
|
3115
|
+
relationship="CONFIGURES",
|
|
3116
|
+
confidence="HIGH",
|
|
3117
|
+
path=b.file_path,
|
|
3118
|
+
start_line=b.line,
|
|
3119
|
+
end_line=b.line,
|
|
3120
|
+
evidence=ev_str,
|
|
3121
|
+
source_hash=f_hash,
|
|
3122
|
+
indexed_commit=self.indexed_commit,
|
|
3123
|
+
)
|
|
3124
|
+
)
|
|
3125
|
+
add_edge(
|
|
3126
|
+
GraphEdge(
|
|
3127
|
+
source=var_name,
|
|
3128
|
+
target=target_scope,
|
|
3129
|
+
relationship="CONFIGURES",
|
|
3130
|
+
confidence="HIGH",
|
|
3131
|
+
file=b.file_path,
|
|
3132
|
+
start_line=b.line,
|
|
3133
|
+
end_line=b.line,
|
|
3134
|
+
evidence=ev_str,
|
|
3135
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
3136
|
+
reason="environment_reference",
|
|
3137
|
+
)
|
|
3138
|
+
)
|
|
3139
|
+
|
|
3140
|
+
# 10. Test Intelligence & Deterministic Test Discovery
|
|
3141
|
+
|
|
3142
|
+
# 10a. Direct Test Calls (TESTS)
|
|
3143
|
+
# Any call originating from a test file to a production symbol
|
|
3144
|
+
for call in self.calls:
|
|
3145
|
+
if not _TEST_FILE_RE.search(call.source_file):
|
|
3146
|
+
continue
|
|
3147
|
+
t_caller_id: str = call.caller_canonical_id or (self.file_to_module.get(call.source_file) or normalize_module(call.source_file))
|
|
3148
|
+
callee_key = (call.source_file, call.line, call.callee)
|
|
3149
|
+
if callee_key not in resolved_calls:
|
|
3150
|
+
callee_key = (call.source_file, call.line, call.qualified_callee or call.callee)
|
|
3151
|
+
if callee_key in resolved_calls:
|
|
3152
|
+
res_canon, conf = resolved_calls[callee_key]
|
|
3153
|
+
if res_canon and res_canon != t_caller_id:
|
|
3154
|
+
t_target_canon: str = res_canon
|
|
3155
|
+
f_hash = self.file_hashes.get(call.source_file, "")
|
|
3156
|
+
ev_str = f"Test directly exercises symbol '{t_target_canon}' at {call.source_file}:{call.line}"
|
|
3157
|
+
add_ref(
|
|
3158
|
+
Reference(
|
|
3159
|
+
source_symbol_id=t_caller_id,
|
|
3160
|
+
target_symbol_id=t_target_canon,
|
|
3161
|
+
relationship="TESTS",
|
|
3162
|
+
confidence=conf,
|
|
3163
|
+
path=call.source_file,
|
|
3164
|
+
start_line=call.line,
|
|
3165
|
+
end_line=call.end_line,
|
|
3166
|
+
evidence=ev_str,
|
|
3167
|
+
source_hash=f_hash,
|
|
3168
|
+
indexed_commit=self.indexed_commit,
|
|
3169
|
+
)
|
|
3170
|
+
)
|
|
3171
|
+
add_edge(
|
|
3172
|
+
GraphEdge(
|
|
3173
|
+
source=t_caller_id,
|
|
3174
|
+
target=t_target_canon,
|
|
3175
|
+
relationship="TESTS",
|
|
3176
|
+
confidence=conf,
|
|
3177
|
+
file=call.source_file,
|
|
3178
|
+
start_line=call.line,
|
|
3179
|
+
end_line=call.end_line,
|
|
3180
|
+
evidence=ev_str,
|
|
3181
|
+
evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value,
|
|
3182
|
+
reason="direct_test_call",
|
|
3183
|
+
)
|
|
3184
|
+
)
|
|
3185
|
+
|
|
3186
|
+
# 10b. Test Route Intelligence (TESTS_ROUTE & handler coverage)
|
|
3187
|
+
for b in (
|
|
3188
|
+
bindings_by_kind.get("TEST_ROUTE", [])
|
|
3189
|
+
+ bindings_by_kind.get("TEST_PAGE_GOTO", [])
|
|
3190
|
+
):
|
|
3191
|
+
if b.expr_kind == "TEST_ROUTE":
|
|
3192
|
+
target_route = b.subscript_key
|
|
3193
|
+
http_method = b.source_expr.upper() # e.g. "POST", "GET"
|
|
3194
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3195
|
+
|
|
3196
|
+
if not target_route or target_route == "<DYNAMIC>":
|
|
3197
|
+
findings.append({
|
|
3198
|
+
"pattern": "TEST_ROUTE",
|
|
3199
|
+
"status": "UNKNOWN",
|
|
3200
|
+
"reason": "dynamic_test_route",
|
|
3201
|
+
"file": b.file_path,
|
|
3202
|
+
"line": b.line,
|
|
3203
|
+
})
|
|
3204
|
+
continue
|
|
3205
|
+
|
|
3206
|
+
clean_target = posixpath.normpath(target_route.split("?")[0])
|
|
3207
|
+
matched = [
|
|
3208
|
+
r for r in self.routes
|
|
3209
|
+
if (posixpath.normpath(r.route_path) == clean_target or posixpath.normpath(r.normalized_route) == clean_target)
|
|
3210
|
+
and (r.http_method == http_method or http_method in ("REQUEST", ""))
|
|
3211
|
+
]
|
|
3212
|
+
|
|
3213
|
+
if not matched:
|
|
3214
|
+
matched = [
|
|
3215
|
+
r for r in self.routes
|
|
3216
|
+
if (r.route_path.rstrip("/") == clean_target.rstrip("/") or r.normalized_route.rstrip("/") == clean_target.rstrip("/"))
|
|
3217
|
+
and (r.http_method == http_method or http_method in ("REQUEST", ""))
|
|
3218
|
+
]
|
|
3219
|
+
|
|
3220
|
+
if matched:
|
|
3221
|
+
for r in matched:
|
|
3222
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3223
|
+
ev_str = f"Test client exercises route '{r.route_path}' [{r.http_method}] at {b.file_path}:{b.line}"
|
|
3224
|
+
add_ref(
|
|
3225
|
+
Reference(
|
|
3226
|
+
source_symbol_id=caller_id,
|
|
3227
|
+
target_symbol_id=r.endpoint_id,
|
|
3228
|
+
relationship="TESTS_ROUTE",
|
|
3229
|
+
confidence="HIGH",
|
|
3230
|
+
path=b.file_path,
|
|
3231
|
+
start_line=b.line,
|
|
3232
|
+
end_line=b.line,
|
|
3233
|
+
evidence=ev_str,
|
|
3234
|
+
source_hash=f_hash,
|
|
3235
|
+
indexed_commit=self.indexed_commit,
|
|
3236
|
+
)
|
|
3237
|
+
)
|
|
3238
|
+
add_edge(
|
|
3239
|
+
GraphEdge(
|
|
3240
|
+
source=caller_id,
|
|
3241
|
+
target=r.endpoint_id,
|
|
3242
|
+
relationship="TESTS_ROUTE",
|
|
3243
|
+
confidence="HIGH",
|
|
3244
|
+
file=b.file_path,
|
|
3245
|
+
start_line=b.line,
|
|
3246
|
+
end_line=b.line,
|
|
3247
|
+
evidence=ev_str,
|
|
3248
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3249
|
+
reason=f"http_client_test:{r.http_method} {r.route_path}",
|
|
3250
|
+
)
|
|
3251
|
+
)
|
|
3252
|
+
if r.handler_canonical_id:
|
|
3253
|
+
h_ev = f"Route test exercises handler '{r.handler_name}' via '{r.route_path}' at {b.file_path}:{b.line}"
|
|
3254
|
+
add_ref(
|
|
3255
|
+
Reference(
|
|
3256
|
+
source_symbol_id=caller_id,
|
|
3257
|
+
target_symbol_id=r.handler_canonical_id,
|
|
3258
|
+
relationship="TESTS",
|
|
3259
|
+
confidence="MEDIUM",
|
|
3260
|
+
path=b.file_path,
|
|
3261
|
+
start_line=b.line,
|
|
3262
|
+
end_line=b.line,
|
|
3263
|
+
evidence=h_ev,
|
|
3264
|
+
source_hash=f_hash,
|
|
3265
|
+
indexed_commit=self.indexed_commit,
|
|
3266
|
+
)
|
|
3267
|
+
)
|
|
3268
|
+
add_edge(
|
|
3269
|
+
GraphEdge(
|
|
3270
|
+
source=caller_id,
|
|
3271
|
+
target=r.handler_canonical_id,
|
|
3272
|
+
relationship="TESTS",
|
|
3273
|
+
confidence="MEDIUM",
|
|
3274
|
+
file=b.file_path,
|
|
3275
|
+
start_line=b.line,
|
|
3276
|
+
end_line=b.line,
|
|
3277
|
+
evidence=h_ev,
|
|
3278
|
+
evidence_class=RelationshipEvidenceClass.POSSIBLE.value,
|
|
3279
|
+
reason=f"route_handler_test:{r.route_path}",
|
|
3280
|
+
)
|
|
3281
|
+
)
|
|
3282
|
+
else:
|
|
3283
|
+
findings.append({
|
|
3284
|
+
"pattern": "TEST_ROUTE",
|
|
3285
|
+
"status": "UNKNOWN",
|
|
3286
|
+
"reason": f"unmatched_test_route:{clean_target}",
|
|
3287
|
+
"file": b.file_path,
|
|
3288
|
+
"line": b.line,
|
|
3289
|
+
})
|
|
3290
|
+
|
|
3291
|
+
elif b.expr_kind == "TEST_PAGE_GOTO":
|
|
3292
|
+
target_url = b.subscript_key
|
|
3293
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3294
|
+
|
|
3295
|
+
if not target_url or target_url == "<DYNAMIC>":
|
|
3296
|
+
findings.append({
|
|
3297
|
+
"pattern": "PLAYWRIGHT_ROUTE",
|
|
3298
|
+
"status": "UNKNOWN",
|
|
3299
|
+
"reason": "runtime_navigation",
|
|
3300
|
+
"file": b.file_path,
|
|
3301
|
+
"line": b.line,
|
|
3302
|
+
})
|
|
3303
|
+
continue
|
|
3304
|
+
|
|
3305
|
+
clean_url = target_url
|
|
3306
|
+
if "://" in clean_url:
|
|
3307
|
+
clean_url = "/" + clean_url.split("://", 1)[1].split("/", 1)[-1] if "/" in clean_url.split("://", 1)[1] else "/"
|
|
3308
|
+
clean_url = posixpath.normpath(clean_url.split("?")[0].split("#")[0])
|
|
3309
|
+
|
|
3310
|
+
matched = [
|
|
3311
|
+
r for r in self.routes
|
|
3312
|
+
if posixpath.normpath(r.route_path) == clean_url or posixpath.normpath(r.normalized_route) == clean_url
|
|
3313
|
+
or r.route_path.rstrip("/") == clean_url.rstrip("/")
|
|
3314
|
+
]
|
|
3315
|
+
if matched:
|
|
3316
|
+
for r in matched:
|
|
3317
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3318
|
+
ev_str = f"Playwright page.goto exercises route '{r.route_path}' at {b.file_path}:{b.line}"
|
|
3319
|
+
add_ref(
|
|
3320
|
+
Reference(
|
|
3321
|
+
source_symbol_id=caller_id,
|
|
3322
|
+
target_symbol_id=r.endpoint_id,
|
|
3323
|
+
relationship="TESTS_ROUTE",
|
|
3324
|
+
confidence="HIGH",
|
|
3325
|
+
path=b.file_path,
|
|
3326
|
+
start_line=b.line,
|
|
3327
|
+
end_line=b.line,
|
|
3328
|
+
evidence=ev_str,
|
|
3329
|
+
source_hash=f_hash,
|
|
3330
|
+
indexed_commit=self.indexed_commit,
|
|
3331
|
+
)
|
|
3332
|
+
)
|
|
3333
|
+
add_edge(
|
|
3334
|
+
GraphEdge(
|
|
3335
|
+
source=caller_id,
|
|
3336
|
+
target=r.endpoint_id,
|
|
3337
|
+
relationship="TESTS_ROUTE",
|
|
3338
|
+
confidence="HIGH",
|
|
3339
|
+
file=b.file_path,
|
|
3340
|
+
start_line=b.line,
|
|
3341
|
+
end_line=b.line,
|
|
3342
|
+
evidence=ev_str,
|
|
3343
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3344
|
+
reason=f"playwright_goto:{r.route_path}",
|
|
3345
|
+
)
|
|
3346
|
+
)
|
|
3347
|
+
if r.handler_canonical_id:
|
|
3348
|
+
add_edge(
|
|
3349
|
+
GraphEdge(
|
|
3350
|
+
source=caller_id,
|
|
3351
|
+
target=r.handler_canonical_id,
|
|
3352
|
+
relationship="TESTS",
|
|
3353
|
+
confidence="MEDIUM",
|
|
3354
|
+
file=b.file_path,
|
|
3355
|
+
start_line=b.line,
|
|
3356
|
+
end_line=b.line,
|
|
3357
|
+
evidence=ev_str,
|
|
3358
|
+
evidence_class=RelationshipEvidenceClass.POSSIBLE.value,
|
|
3359
|
+
reason=f"playwright_handler_test:{r.route_path}",
|
|
3360
|
+
)
|
|
3361
|
+
)
|
|
3362
|
+
else:
|
|
3363
|
+
findings.append({
|
|
3364
|
+
"pattern": "PLAYWRIGHT_ROUTE",
|
|
3365
|
+
"status": "POSSIBLE",
|
|
3366
|
+
"reason": f"unmatched_browser_url:{clean_url}",
|
|
3367
|
+
"file": b.file_path,
|
|
3368
|
+
"line": b.line,
|
|
3369
|
+
})
|
|
3370
|
+
|
|
3371
|
+
# 10c. DI / Provider Test Intelligence (TESTS_PROVIDER)
|
|
3372
|
+
fixture_defs: dict[str, str] = {}
|
|
3373
|
+
for b in bindings_by_kind.get("PYTEST_FIXTURE_DEF", ()):
|
|
3374
|
+
fixture_defs[b.target_name] = b.source_expr
|
|
3375
|
+
|
|
3376
|
+
for b in bindings_by_kind.get("TEST_FIXTURE_USE", ()):
|
|
3377
|
+
fix_name = b.target_name
|
|
3378
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3379
|
+
|
|
3380
|
+
target_prov_sym = None
|
|
3381
|
+
if fix_name in fixture_defs:
|
|
3382
|
+
ret_nm = fixture_defs[fix_name]
|
|
3383
|
+
target_prov_sym, _, _ = self.resolve_identifier_in_file(ret_nm, b.file_path, use_line=b.line)
|
|
3384
|
+
if not target_prov_sym and ret_nm in self.short_to_symbols:
|
|
3385
|
+
target_prov_sym = self.short_to_symbols[ret_nm][0]
|
|
3386
|
+
if not target_prov_sym:
|
|
3387
|
+
target_prov_sym, _, _ = self.resolve_identifier_in_file(fix_name, b.file_path, use_line=b.line)
|
|
3388
|
+
if not target_prov_sym and fix_name in self.short_to_symbols:
|
|
3389
|
+
target_prov_sym = self.short_to_symbols[fix_name][0]
|
|
3390
|
+
|
|
3391
|
+
if target_prov_sym:
|
|
3392
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3393
|
+
ev_str = f"Test fixture '{fix_name}' provides '{target_prov_sym.canonical_id}' at {b.file_path}:{b.line}"
|
|
3394
|
+
add_ref(
|
|
3395
|
+
Reference(
|
|
3396
|
+
source_symbol_id=caller_id,
|
|
3397
|
+
target_symbol_id=target_prov_sym.canonical_id,
|
|
3398
|
+
relationship="TESTS_PROVIDER",
|
|
3399
|
+
confidence="HIGH",
|
|
3400
|
+
path=b.file_path,
|
|
3401
|
+
start_line=b.line,
|
|
3402
|
+
end_line=b.line,
|
|
3403
|
+
evidence=ev_str,
|
|
3404
|
+
source_hash=f_hash,
|
|
3405
|
+
indexed_commit=self.indexed_commit,
|
|
3406
|
+
)
|
|
3407
|
+
)
|
|
3408
|
+
add_edge(
|
|
3409
|
+
GraphEdge(
|
|
3410
|
+
source=caller_id,
|
|
3411
|
+
target=target_prov_sym.canonical_id,
|
|
3412
|
+
relationship="TESTS_PROVIDER",
|
|
3413
|
+
confidence="HIGH",
|
|
3414
|
+
file=b.file_path,
|
|
3415
|
+
start_line=b.line,
|
|
3416
|
+
end_line=b.line,
|
|
3417
|
+
evidence=ev_str,
|
|
3418
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
3419
|
+
reason=f"fixture_dependency_provider:{fix_name}",
|
|
3420
|
+
)
|
|
3421
|
+
)
|
|
3422
|
+
|
|
3423
|
+
# 10d. Event Dispatch & Registry Invocations in Tests
|
|
3424
|
+
for b in bindings_by_kind.get("EVENT_DISPATCH", ()):
|
|
3425
|
+
event_name = b.subscript_key
|
|
3426
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3427
|
+
|
|
3428
|
+
if not event_name or event_name == "<DYNAMIC>":
|
|
3429
|
+
findings.append({
|
|
3430
|
+
"pattern": "EVENT_DISPATCH",
|
|
3431
|
+
"status": "UNKNOWN",
|
|
3432
|
+
"reason": "dynamic_event_name",
|
|
3433
|
+
"file": b.file_path,
|
|
3434
|
+
"line": b.line,
|
|
3435
|
+
})
|
|
3436
|
+
continue
|
|
3437
|
+
|
|
3438
|
+
matched_listeners = event_listeners_by_key.get(event_name, ())
|
|
3439
|
+
for b2 in matched_listeners:
|
|
3440
|
+
target_handler, _, _ = self.resolve_identifier_in_file(b2.source_expr, b2.file_path, use_line=b2.line)
|
|
3441
|
+
if not target_handler and b2.source_expr in self.short_to_symbols:
|
|
3442
|
+
target_handler = self.short_to_symbols[b2.source_expr][0]
|
|
3443
|
+
if target_handler:
|
|
3444
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3445
|
+
ev_str = f"Test dispatches event '{event_name}' exercising listener '{target_handler.canonical_id}' at {b.file_path}:{b.line}"
|
|
3446
|
+
add_ref(
|
|
3447
|
+
Reference(
|
|
3448
|
+
source_symbol_id=caller_id,
|
|
3449
|
+
target_symbol_id=target_handler.canonical_id,
|
|
3450
|
+
relationship="TESTS_EVENT_HANDLER",
|
|
3451
|
+
confidence="MEDIUM",
|
|
3452
|
+
path=b.file_path,
|
|
3453
|
+
start_line=b.line,
|
|
3454
|
+
end_line=b.line,
|
|
3455
|
+
evidence=ev_str,
|
|
3456
|
+
source_hash=f_hash,
|
|
3457
|
+
indexed_commit=self.indexed_commit,
|
|
3458
|
+
)
|
|
3459
|
+
)
|
|
3460
|
+
add_edge(
|
|
3461
|
+
GraphEdge(
|
|
3462
|
+
source=caller_id,
|
|
3463
|
+
target=target_handler.canonical_id,
|
|
3464
|
+
relationship="TESTS_EVENT_HANDLER",
|
|
3465
|
+
confidence="MEDIUM",
|
|
3466
|
+
file=b.file_path,
|
|
3467
|
+
start_line=b.line,
|
|
3468
|
+
end_line=b.line,
|
|
3469
|
+
evidence=ev_str,
|
|
3470
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3471
|
+
reason=f"event_dispatch_test:{event_name}",
|
|
3472
|
+
)
|
|
3473
|
+
)
|
|
3474
|
+
|
|
3475
|
+
# 12. Monorepo / Workspace package dependencies
|
|
3476
|
+
if self.workspace and self.workspace.dependencies:
|
|
3477
|
+
for dep in self.workspace.dependencies:
|
|
3478
|
+
src_pkg = self.workspace.packages.get(dep.source_package_id)
|
|
3479
|
+
edge_file = ""
|
|
3480
|
+
if src_pkg:
|
|
3481
|
+
for ep in src_pkg.entrypoints:
|
|
3482
|
+
if ep in self.known_files:
|
|
3483
|
+
edge_file = ep
|
|
3484
|
+
break
|
|
3485
|
+
if not edge_file and src_pkg.root_path:
|
|
3486
|
+
for kf in self.known_files:
|
|
3487
|
+
if kf.startswith(f"{src_pkg.root_path}/"):
|
|
3488
|
+
edge_file = kf
|
|
3489
|
+
break
|
|
3490
|
+
if not edge_file and src_pkg.manifest_path in self.known_files:
|
|
3491
|
+
edge_file = src_pkg.manifest_path
|
|
3492
|
+
|
|
3493
|
+
if edge_file:
|
|
3494
|
+
add_edge(
|
|
3495
|
+
GraphEdge(
|
|
3496
|
+
source=dep.source_package_id,
|
|
3497
|
+
target=dep.target_package_id,
|
|
3498
|
+
relationship="DEPENDS_ON_PACKAGE",
|
|
3499
|
+
confidence="HIGH",
|
|
3500
|
+
file=edge_file,
|
|
3501
|
+
start_line=1,
|
|
3502
|
+
end_line=1,
|
|
3503
|
+
evidence=dep.evidence or f"{dep.source_name} depends on {dep.target_name}",
|
|
3504
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3505
|
+
reason="workspace_package_manifest_dependency",
|
|
3506
|
+
)
|
|
3507
|
+
)
|
|
3508
|
+
|
|
3509
|
+
# 12. Frontend Web Graph (React, JSX, Hooks, Styles, HTML - Pillar 4)
|
|
3510
|
+
for b in bindings_by_kind.get("REACT_RENDERS", ()):
|
|
3511
|
+
child_name = b.target_name
|
|
3512
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3513
|
+
target_sym, _, _ = self.resolve_identifier_in_file(child_name, b.file_path, use_line=b.line)
|
|
3514
|
+
if not target_sym and child_name in self.short_to_symbols:
|
|
3515
|
+
candidates = [s for s in self.short_to_symbols[child_name] if s.kind in ("component", "function")]
|
|
3516
|
+
if candidates:
|
|
3517
|
+
target_sym = candidates[0]
|
|
3518
|
+
target_id = target_sym.canonical_id if target_sym else child_name
|
|
3519
|
+
ev_str = f"React component renders '<{child_name} />' at {b.file_path}:{b.line}"
|
|
3520
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3521
|
+
add_ref(
|
|
3522
|
+
Reference(
|
|
3523
|
+
source_symbol_id=caller_id,
|
|
3524
|
+
target_symbol_id=target_id,
|
|
3525
|
+
relationship="RENDERS",
|
|
3526
|
+
confidence="HIGH" if target_sym else "MEDIUM",
|
|
3527
|
+
path=b.file_path,
|
|
3528
|
+
start_line=b.line,
|
|
3529
|
+
end_line=b.line,
|
|
3530
|
+
evidence=ev_str,
|
|
3531
|
+
source_hash=f_hash,
|
|
3532
|
+
indexed_commit=self.indexed_commit,
|
|
3533
|
+
)
|
|
3534
|
+
)
|
|
3535
|
+
add_edge(
|
|
3536
|
+
GraphEdge(
|
|
3537
|
+
source=caller_id,
|
|
3538
|
+
target=target_id,
|
|
3539
|
+
relationship="RENDERS",
|
|
3540
|
+
confidence="HIGH" if target_sym else "MEDIUM",
|
|
3541
|
+
file=b.file_path,
|
|
3542
|
+
start_line=b.line,
|
|
3543
|
+
end_line=b.line,
|
|
3544
|
+
evidence=ev_str,
|
|
3545
|
+
evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value if target_sym else RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3546
|
+
reason=f"react_jsx_render:{child_name}",
|
|
3547
|
+
)
|
|
3548
|
+
)
|
|
3549
|
+
|
|
3550
|
+
for b in bindings_by_kind.get("REACT_USES_HOOK", ()):
|
|
3551
|
+
hook_name = b.target_name
|
|
3552
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3553
|
+
target_sym, _, _ = self.resolve_identifier_in_file(hook_name, b.file_path, use_line=b.line)
|
|
3554
|
+
if not target_sym and hook_name in self.short_to_symbols:
|
|
3555
|
+
candidates = [s for s in self.short_to_symbols[hook_name] if s.kind in ("hook", "function")]
|
|
3556
|
+
if candidates:
|
|
3557
|
+
target_sym = candidates[0]
|
|
3558
|
+
target_id = target_sym.canonical_id if target_sym else hook_name
|
|
3559
|
+
ev_str = f"React component invokes hook '{hook_name}()' at {b.file_path}:{b.line}"
|
|
3560
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3561
|
+
add_ref(
|
|
3562
|
+
Reference(
|
|
3563
|
+
source_symbol_id=caller_id,
|
|
3564
|
+
target_symbol_id=target_id,
|
|
3565
|
+
relationship="USES_HOOK",
|
|
3566
|
+
confidence="HIGH" if target_sym else "MEDIUM",
|
|
3567
|
+
path=b.file_path,
|
|
3568
|
+
start_line=b.line,
|
|
3569
|
+
end_line=b.line,
|
|
3570
|
+
evidence=ev_str,
|
|
3571
|
+
source_hash=f_hash,
|
|
3572
|
+
indexed_commit=self.indexed_commit,
|
|
3573
|
+
)
|
|
3574
|
+
)
|
|
3575
|
+
add_edge(
|
|
3576
|
+
GraphEdge(
|
|
3577
|
+
source=caller_id,
|
|
3578
|
+
target=target_id,
|
|
3579
|
+
relationship="USES_HOOK",
|
|
3580
|
+
confidence="HIGH" if target_sym else "MEDIUM",
|
|
3581
|
+
file=b.file_path,
|
|
3582
|
+
start_line=b.line,
|
|
3583
|
+
end_line=b.line,
|
|
3584
|
+
evidence=ev_str,
|
|
3585
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3586
|
+
reason=f"react_hook_call:{hook_name}",
|
|
3587
|
+
)
|
|
3588
|
+
)
|
|
3589
|
+
|
|
3590
|
+
for b in bindings_by_kind.get("REACT_IMPORTS_STYLE", ()):
|
|
3591
|
+
style_rel = b.target_name
|
|
3592
|
+
src_dir = posixpath.dirname(b.file_path.replace("\\", "/").lstrip("./"))
|
|
3593
|
+
resolved_style = posixpath.normpath(posixpath.join(src_dir, style_rel)) if style_rel.startswith(".") else style_rel
|
|
3594
|
+
if resolved_style not in self.known_files:
|
|
3595
|
+
for kf in self.known_files:
|
|
3596
|
+
if kf.endswith(f"/{style_rel}") or kf == style_rel:
|
|
3597
|
+
resolved_style = kf
|
|
3598
|
+
break
|
|
3599
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3600
|
+
ev_str = f"Component imports stylesheet '{style_rel}' at {b.file_path}:{b.line}"
|
|
3601
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3602
|
+
add_ref(
|
|
3603
|
+
Reference(
|
|
3604
|
+
source_symbol_id=caller_id,
|
|
3605
|
+
target_symbol_id=resolved_style,
|
|
3606
|
+
relationship="IMPORTS_STYLE",
|
|
3607
|
+
confidence="HIGH",
|
|
3608
|
+
path=b.file_path,
|
|
3609
|
+
start_line=b.line,
|
|
3610
|
+
end_line=b.line,
|
|
3611
|
+
evidence=ev_str,
|
|
3612
|
+
source_hash=f_hash,
|
|
3613
|
+
indexed_commit=self.indexed_commit,
|
|
3614
|
+
)
|
|
3615
|
+
)
|
|
3616
|
+
add_edge(
|
|
3617
|
+
GraphEdge(
|
|
3618
|
+
source=caller_id,
|
|
3619
|
+
target=resolved_style,
|
|
3620
|
+
relationship="IMPORTS_STYLE",
|
|
3621
|
+
confidence="HIGH",
|
|
3622
|
+
file=b.file_path,
|
|
3623
|
+
start_line=b.line,
|
|
3624
|
+
end_line=b.line,
|
|
3625
|
+
evidence=ev_str,
|
|
3626
|
+
evidence_class=RelationshipEvidenceClass.AST_VERIFIED.value,
|
|
3627
|
+
reason=f"react_style_import:{style_rel}",
|
|
3628
|
+
)
|
|
3629
|
+
)
|
|
3630
|
+
|
|
3631
|
+
for b in bindings_by_kind.get("REACT_USES_STYLE_CLASS", ()):
|
|
3632
|
+
cls_name = b.target_name
|
|
3633
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3634
|
+
ev_str = f"Component applies CSS class '.{cls_name}' at {b.file_path}:{b.line}"
|
|
3635
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3636
|
+
add_ref(
|
|
3637
|
+
Reference(
|
|
3638
|
+
source_symbol_id=caller_id,
|
|
3639
|
+
target_symbol_id=f".{cls_name}",
|
|
3640
|
+
relationship="USES_STYLE_CLASS",
|
|
3641
|
+
confidence="HIGH",
|
|
3642
|
+
path=b.file_path,
|
|
3643
|
+
start_line=b.line,
|
|
3644
|
+
end_line=b.line,
|
|
3645
|
+
evidence=ev_str,
|
|
3646
|
+
source_hash=f_hash,
|
|
3647
|
+
indexed_commit=self.indexed_commit,
|
|
3648
|
+
)
|
|
3649
|
+
)
|
|
3650
|
+
add_edge(
|
|
3651
|
+
GraphEdge(
|
|
3652
|
+
source=caller_id,
|
|
3653
|
+
target=f".{cls_name}",
|
|
3654
|
+
relationship="USES_STYLE_CLASS",
|
|
3655
|
+
confidence="HIGH",
|
|
3656
|
+
file=b.file_path,
|
|
3657
|
+
start_line=b.line,
|
|
3658
|
+
end_line=b.line,
|
|
3659
|
+
evidence=ev_str,
|
|
3660
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3661
|
+
reason=f"css_class_use:.{cls_name}",
|
|
3662
|
+
)
|
|
3663
|
+
)
|
|
3664
|
+
|
|
3665
|
+
for b in bindings_by_kind.get("REACT_FETCH_ROUTE", ()):
|
|
3666
|
+
raw_url = b.target_name
|
|
3667
|
+
caller_id = _find_caller_id_for_binding(b)
|
|
3668
|
+
client_method = b.base_expr or "GET"
|
|
3669
|
+
norm_client = normalize_route(client_method, raw_url)
|
|
3670
|
+
|
|
3671
|
+
matched_routes: list[tuple[RouteDetection, str]] = []
|
|
3672
|
+
for r in self.routes:
|
|
3673
|
+
if (
|
|
3674
|
+
r.route_path == raw_url
|
|
3675
|
+
or r.normalized_route == raw_url
|
|
3676
|
+
or r.route_path.rstrip("/") == raw_url.rstrip("/")
|
|
3677
|
+
):
|
|
3678
|
+
matched_routes.append((r, "EXACT"))
|
|
3679
|
+
continue
|
|
3680
|
+
norm_backend = normalize_route(r.http_method, r.route_path)
|
|
3681
|
+
is_matched, match_type = match_route_topology(
|
|
3682
|
+
norm_client, norm_backend, allow_proxy_prefix=True, match_methods=True
|
|
3683
|
+
)
|
|
3684
|
+
if is_matched:
|
|
3685
|
+
matched_routes.append((r, match_type))
|
|
3686
|
+
|
|
3687
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3688
|
+
if matched_routes:
|
|
3689
|
+
for r, match_type in matched_routes:
|
|
3690
|
+
ev_str = f"Client fetches route '{r.route_path}' ({match_type}) at {b.file_path}:{b.line}"
|
|
3691
|
+
add_ref(
|
|
3692
|
+
Reference(
|
|
3693
|
+
source_symbol_id=caller_id,
|
|
3694
|
+
target_symbol_id=r.endpoint_id,
|
|
3695
|
+
relationship="FETCHES_ROUTE",
|
|
3696
|
+
confidence="HIGH",
|
|
3697
|
+
path=b.file_path,
|
|
3698
|
+
start_line=b.line,
|
|
3699
|
+
end_line=b.line,
|
|
3700
|
+
evidence=ev_str,
|
|
3701
|
+
source_hash=f_hash,
|
|
3702
|
+
indexed_commit=self.indexed_commit,
|
|
3703
|
+
)
|
|
3704
|
+
)
|
|
3705
|
+
add_edge(
|
|
3706
|
+
GraphEdge(
|
|
3707
|
+
source=caller_id,
|
|
3708
|
+
target=r.endpoint_id,
|
|
3709
|
+
relationship="FETCHES_ROUTE",
|
|
3710
|
+
confidence="HIGH",
|
|
3711
|
+
file=b.file_path,
|
|
3712
|
+
start_line=b.line,
|
|
3713
|
+
end_line=b.line,
|
|
3714
|
+
evidence=ev_str,
|
|
3715
|
+
evidence_class=RelationshipEvidenceClass.FRAMEWORK_VERIFIED.value,
|
|
3716
|
+
reason=f"client_fetch:{r.route_path}",
|
|
3717
|
+
)
|
|
3718
|
+
)
|
|
3719
|
+
if r.handler_canonical_id:
|
|
3720
|
+
h_ev = f"Client invokes backend handler '{r.handler_canonical_id}' via {r.route_path} ({match_type}) at {b.file_path}:{b.line}"
|
|
3721
|
+
add_ref(
|
|
3722
|
+
Reference(
|
|
3723
|
+
source_symbol_id=caller_id,
|
|
3724
|
+
target_symbol_id=r.handler_canonical_id,
|
|
3725
|
+
relationship="CALLS",
|
|
3726
|
+
confidence="HIGH",
|
|
3727
|
+
path=b.file_path,
|
|
3728
|
+
start_line=b.line,
|
|
3729
|
+
end_line=b.line,
|
|
3730
|
+
evidence=h_ev,
|
|
3731
|
+
source_hash=f_hash,
|
|
3732
|
+
indexed_commit=self.indexed_commit,
|
|
3733
|
+
)
|
|
3734
|
+
)
|
|
3735
|
+
add_edge(
|
|
3736
|
+
GraphEdge(
|
|
3737
|
+
source=caller_id,
|
|
3738
|
+
target=r.handler_canonical_id,
|
|
3739
|
+
relationship="CALLS",
|
|
3740
|
+
confidence="HIGH",
|
|
3741
|
+
file=b.file_path,
|
|
3742
|
+
start_line=b.line,
|
|
3743
|
+
end_line=b.line,
|
|
3744
|
+
evidence=h_ev,
|
|
3745
|
+
evidence_class=RelationshipEvidenceClass.DATAFLOW_VERIFIED.value,
|
|
3746
|
+
reason=f"client_handler_call:{r.route_path}",
|
|
3747
|
+
)
|
|
3748
|
+
)
|
|
3749
|
+
else:
|
|
3750
|
+
ev_str = f"Client fetches external/custom route '{raw_url}' at {b.file_path}:{b.line}"
|
|
3751
|
+
add_edge(
|
|
3752
|
+
GraphEdge(
|
|
3753
|
+
source=caller_id,
|
|
3754
|
+
target=raw_url,
|
|
3755
|
+
relationship="FETCHES_ROUTE",
|
|
3756
|
+
confidence="LOW",
|
|
3757
|
+
file=b.file_path,
|
|
3758
|
+
start_line=b.line,
|
|
3759
|
+
end_line=b.line,
|
|
3760
|
+
evidence=ev_str,
|
|
3761
|
+
evidence_class=RelationshipEvidenceClass.POSSIBLE.value,
|
|
3762
|
+
reason=f"client_fetch:{raw_url}",
|
|
3763
|
+
)
|
|
3764
|
+
)
|
|
3765
|
+
|
|
3766
|
+
for b in bindings_by_kind.get("HTML_LOADS_SCRIPT", ()):
|
|
3767
|
+
src_val = b.target_name
|
|
3768
|
+
target_file = src_val
|
|
3769
|
+
for kf in self.known_files:
|
|
3770
|
+
if kf.endswith(f"/{src_val}") or kf == src_val:
|
|
3771
|
+
target_file = kf
|
|
3772
|
+
break
|
|
3773
|
+
ev_str = f"HTML loads script '{src_val}' at {b.file_path}:{b.line}"
|
|
3774
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3775
|
+
add_ref(
|
|
3776
|
+
Reference(
|
|
3777
|
+
source_symbol_id=b.file_path,
|
|
3778
|
+
target_symbol_id=target_file,
|
|
3779
|
+
relationship="LOADS_SCRIPT",
|
|
3780
|
+
confidence="HIGH",
|
|
3781
|
+
path=b.file_path,
|
|
3782
|
+
start_line=b.line,
|
|
3783
|
+
end_line=b.line,
|
|
3784
|
+
evidence=ev_str,
|
|
3785
|
+
source_hash=f_hash,
|
|
3786
|
+
indexed_commit=self.indexed_commit,
|
|
3787
|
+
)
|
|
3788
|
+
)
|
|
3789
|
+
add_edge(
|
|
3790
|
+
GraphEdge(
|
|
3791
|
+
source=b.file_path,
|
|
3792
|
+
target=target_file,
|
|
3793
|
+
relationship="LOADS_SCRIPT",
|
|
3794
|
+
confidence="HIGH",
|
|
3795
|
+
file=b.file_path,
|
|
3796
|
+
start_line=b.line,
|
|
3797
|
+
end_line=b.line,
|
|
3798
|
+
evidence=ev_str,
|
|
3799
|
+
evidence_class=RelationshipEvidenceClass.STATIC_VERIFIED.value,
|
|
3800
|
+
reason=f"html_script:{src_val}",
|
|
3801
|
+
)
|
|
3802
|
+
)
|
|
3803
|
+
|
|
3804
|
+
for b in bindings_by_kind.get("HTML_LOADS_STYLESHEET", ()):
|
|
3805
|
+
href_val = b.target_name
|
|
3806
|
+
target_file = href_val
|
|
3807
|
+
for kf in self.known_files:
|
|
3808
|
+
if kf.endswith(f"/{href_val}") or kf == href_val:
|
|
3809
|
+
target_file = kf
|
|
3810
|
+
break
|
|
3811
|
+
ev_str = f"HTML loads stylesheet '{href_val}' at {b.file_path}:{b.line}"
|
|
3812
|
+
f_hash = self.file_hashes.get(b.file_path, "")
|
|
3813
|
+
add_ref(
|
|
3814
|
+
Reference(
|
|
3815
|
+
source_symbol_id=b.file_path,
|
|
3816
|
+
target_symbol_id=target_file,
|
|
3817
|
+
relationship="LOADS_STYLESHEET",
|
|
3818
|
+
confidence="HIGH",
|
|
3819
|
+
path=b.file_path,
|
|
3820
|
+
start_line=b.line,
|
|
3821
|
+
end_line=b.line,
|
|
3822
|
+
evidence=ev_str,
|
|
3823
|
+
source_hash=f_hash,
|
|
3824
|
+
indexed_commit=self.indexed_commit,
|
|
3825
|
+
)
|
|
3826
|
+
)
|
|
3827
|
+
add_edge(
|
|
3828
|
+
GraphEdge(
|
|
3829
|
+
source=b.file_path,
|
|
3830
|
+
target=target_file,
|
|
3831
|
+
relationship="LOADS_STYLESHEET",
|
|
3832
|
+
confidence="HIGH",
|
|
3833
|
+
file=b.file_path,
|
|
3834
|
+
start_line=b.line,
|
|
3835
|
+
end_line=b.line,
|
|
3836
|
+
evidence=ev_str,
|
|
3837
|
+
evidence_class=RelationshipEvidenceClass.STATIC_VERIFIED.value,
|
|
3838
|
+
reason=f"html_stylesheet:{href_val}",
|
|
3839
|
+
)
|
|
3840
|
+
)
|
|
3841
|
+
|
|
3842
|
+
# Roblox & Luau Intelligence (Phases R2-R8)
|
|
3843
|
+
has_luau_or_roblox = any(
|
|
3844
|
+
f.endswith((".lua", ".luau")) for f in self.known_files
|
|
3845
|
+
) or any(
|
|
3846
|
+
b.expr_kind.startswith("ROBLOX_") for b in self.bindings
|
|
3847
|
+
)
|
|
3848
|
+
if has_luau_or_roblox:
|
|
3849
|
+
from codegraph.roblox import resolve_roblox_intelligence
|
|
3850
|
+
|
|
3851
|
+
repo_path = Path(self.repository) if self.repository is not None else None
|
|
3852
|
+
resolve_roblox_intelligence(
|
|
3853
|
+
repository=repo_path,
|
|
3854
|
+
symbols=self.symbols,
|
|
3855
|
+
bindings=self.bindings,
|
|
3856
|
+
known_files=self.known_files,
|
|
3857
|
+
file_hashes=self.file_hashes,
|
|
3858
|
+
indexed_commit=self.indexed_commit,
|
|
3859
|
+
add_ref=add_ref,
|
|
3860
|
+
add_edge=add_edge,
|
|
3861
|
+
resolved_imports=resolved_imports,
|
|
3862
|
+
)
|
|
3863
|
+
|
|
3864
|
+
return ResolutionOutput(
|
|
3865
|
+
references=references,
|
|
3866
|
+
edges=edges,
|
|
3867
|
+
resolved_imports=resolved_imports,
|
|
3868
|
+
resolved_calls=resolved_calls,
|
|
3869
|
+
findings=findings,
|
|
3870
|
+
)
|
|
3871
|
+
|
|
3872
|
+
|
|
3873
|
+
def resolve_symbol_diagnostic(
|
|
3874
|
+
con: sqlite3.Connection,
|
|
3875
|
+
expression: str,
|
|
3876
|
+
context_file: str | None = None,
|
|
3877
|
+
) -> dict[str, object]:
|
|
3878
|
+
"""Provide structured diagnostic output for `codegraph resolve <expression>`."""
|
|
3879
|
+
expr = expression.strip()
|
|
3880
|
+
if not expr:
|
|
3881
|
+
return {
|
|
3882
|
+
"input": expr,
|
|
3883
|
+
"confidence": "UNKNOWN",
|
|
3884
|
+
"reason": "Empty symbol expression.",
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
# 1. Direct canonical or qualified match in symbols table
|
|
3888
|
+
direct = con.execute(
|
|
3889
|
+
"SELECT canonical_id, qualified_name, module, path, start_line "
|
|
3890
|
+
"FROM symbols WHERE canonical_id=? OR qualified_name=? ORDER BY path, start_line LIMIT 1",
|
|
3891
|
+
(expr, expr),
|
|
3892
|
+
).fetchone()
|
|
3893
|
+
|
|
3894
|
+
# Check if expr starts with an imported alias first (e.g., AS.login or authenticate)
|
|
3895
|
+
prefix = expr.split(".", 1)[0]
|
|
3896
|
+
rest = expr.split(".", 1)[1] if "." in expr else ""
|
|
3897
|
+
|
|
3898
|
+
imp_query = (
|
|
3899
|
+
"SELECT source_path, module, imported_module, name, imported_name, alias, local_name, resolved_module "
|
|
3900
|
+
"FROM imports WHERE (alias=? OR local_name=?)"
|
|
3901
|
+
)
|
|
3902
|
+
params: list[object] = [prefix, prefix]
|
|
3903
|
+
if context_file:
|
|
3904
|
+
imp_query += " AND source_path=?"
|
|
3905
|
+
params.append(context_file)
|
|
3906
|
+
imp_rows = con.execute(imp_query, params).fetchall()
|
|
3907
|
+
|
|
3908
|
+
for imp in imp_rows:
|
|
3909
|
+
target_mod = imp["resolved_module"] or imp["imported_module"] or imp["module"]
|
|
3910
|
+
orig_name = imp["imported_name"] or imp["name"] or prefix
|
|
3911
|
+
candidate_qname = f"{orig_name}.{rest}" if rest else orig_name
|
|
3912
|
+
sym_row = con.execute(
|
|
3913
|
+
"SELECT canonical_id, qualified_name, module, path, start_line "
|
|
3914
|
+
"FROM symbols WHERE (module=? OR module LIKE ?) AND (qualified_name=? OR name=?) "
|
|
3915
|
+
"ORDER BY path, start_line LIMIT 1",
|
|
3916
|
+
(target_mod, f"%.{target_mod}", candidate_qname, candidate_qname),
|
|
3917
|
+
).fetchone()
|
|
3918
|
+
if sym_row:
|
|
3919
|
+
return {
|
|
3920
|
+
"input": expr,
|
|
3921
|
+
"alias": imp["alias"] or imp["local_name"],
|
|
3922
|
+
"resolved_module": target_mod,
|
|
3923
|
+
"resolved_symbol": sym_row["qualified_name"],
|
|
3924
|
+
"canonical_id": sym_row["canonical_id"],
|
|
3925
|
+
"confidence": "HIGH",
|
|
3926
|
+
"evidence": f"{sym_row['path']}:{sym_row['start_line']}",
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
if direct:
|
|
3930
|
+
return {
|
|
3931
|
+
"input": expr,
|
|
3932
|
+
"alias": None,
|
|
3933
|
+
"resolved_module": direct["module"],
|
|
3934
|
+
"resolved_symbol": direct["qualified_name"],
|
|
3935
|
+
"canonical_id": direct["canonical_id"],
|
|
3936
|
+
"confidence": "HIGH",
|
|
3937
|
+
"evidence": f"{direct['path']}:{direct['start_line']}",
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
# Check short name match
|
|
3941
|
+
short_rows = con.execute(
|
|
3942
|
+
"SELECT canonical_id, qualified_name, module, path, start_line FROM symbols WHERE name=?",
|
|
3943
|
+
(expr,),
|
|
3944
|
+
).fetchall()
|
|
3945
|
+
if len(short_rows) == 1:
|
|
3946
|
+
r = short_rows[0]
|
|
3947
|
+
return {
|
|
3948
|
+
"input": expr,
|
|
3949
|
+
"alias": None,
|
|
3950
|
+
"resolved_module": r["module"],
|
|
3951
|
+
"resolved_symbol": r["qualified_name"],
|
|
3952
|
+
"canonical_id": r["canonical_id"],
|
|
3953
|
+
"confidence": "MEDIUM",
|
|
3954
|
+
"evidence": f"{r['path']}:{r['start_line']}",
|
|
3955
|
+
}
|
|
3956
|
+
if len(short_rows) > 1:
|
|
3957
|
+
return {
|
|
3958
|
+
"input": expr,
|
|
3959
|
+
"confidence": "LOW",
|
|
3960
|
+
"reason": f"Ambiguous symbol '{expr}' found in {len(short_rows)} modules: "
|
|
3961
|
+
+ ", ".join(r["canonical_id"] for r in short_rows[:5]),
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
return {
|
|
3965
|
+
"input": expr,
|
|
3966
|
+
"confidence": "UNKNOWN",
|
|
3967
|
+
"reason": "No matching imported symbol found.",
|
|
3968
|
+
}
|