Skip to content

Commit 7b78484

Browse files
committed
Restore Jedi call graph at level 1; filter lib→lib edges
- Fix regression: Jedi call-graph edges are now always built at analysis_level >= 1 (level 1 = Jedi only, level 2 = Jedi + PyCG). - Add filter_external_edges() in call_graph.py: drops edges where both source and target are outside the app namespace, using the full recursive callable walk (inner_callables, inner_classes) so nested functions and closures are correctly treated as app symbols. - Apply filter unconditionally after call graph construction in core.py. Signed-off-by: Saurabh Sinha <sinha108@gmail.com>
1 parent 9eea71f commit 7b78484

3 files changed

Lines changed: 30 additions & 12 deletions

File tree

codeanalyzer/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def main(
3232
typer.Option(
3333
"-a",
3434
"--analysis-level",
35-
help="Analysis depth: 1=symbol table only, 2=+call graph (PyCG+Jedi).",
35+
help="Analysis depth: 1=symbol table+Jedi call graph, 2=+PyCG call graph.",
3636
min=1,
3737
max=2,
3838
),

codeanalyzer/core.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from codeanalyzer.schema import PyApplication, PyModule, model_dump_json, model_validate_json
1212
from codeanalyzer.schema.py_schema import PyCallEdge
1313
from codeanalyzer.semantic_analysis.call_graph import (
14+
filter_external_edges,
1415
jedi_call_graph_edges,
1516
merge_edges,
1617
resolve_unresolved_constructors,
@@ -329,23 +330,19 @@ def analyze(self) -> PyApplication:
329330
# Build symbol table from cached application if available (if no available, the build a new one)
330331
symbol_table = self._build_symbol_table(cached_pyapplication.symbol_table if cached_pyapplication else {})
331332

332-
# Level 1: symbol table only — constructor heuristic still runs to
333-
# enrich PyCallsite.callee_signature inside the symbol table itself,
334-
# but no call_graph edge list is produced.
335333
resolve_unresolved_constructors(symbol_table)
336334

337-
call_graph = []
335+
# Level 1: Jedi call graph (always built when analysis_level >= 1).
336+
jedi_edges = jedi_call_graph_edges(symbol_table)
337+
call_graph = list(jedi_edges)
338+
338339
if self.analysis_level >= 2:
339-
# Level 2: build call graph.
340-
# 1. Derive Jedi edges from the augmented symbol table.
341-
# 2. Run PyCG (iterative name-pointer analysis) for additional
342-
# edges — particularly locally-scoped function calls and
343-
# higher-order patterns that Jedi misses.
344-
# 3. Merge; provenance unions for edges seen by both backends.
345-
jedi_edges = jedi_call_graph_edges(symbol_table)
340+
# Level 2: add PyCG edges and merge with Jedi.
346341
pycg_edges = self._get_pycg_call_graph(symbol_table)
347342
call_graph = merge_edges(jedi_edges, pycg_edges)
348343

344+
call_graph = filter_external_edges(call_graph, symbol_table)
345+
349346
# Recreate pyapplication
350347
app = PyApplication.builder().symbol_table(symbol_table).call_graph(call_graph).build()
351348

codeanalyzer/semantic_analysis/call_graph.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,27 @@ def scope_score(c: PyClass, _caller_sig: str = caller.signature) -> int:
246246
return resolved
247247

248248

249+
def filter_external_edges(
250+
edges: List[PyCallEdge],
251+
symbol_table: Dict[str, PyModule],
252+
) -> List[PyCallEdge]:
253+
"""Remove edges where both source and target are outside the app namespace.
254+
255+
Edges where an app callable calls a library function (or vice-versa) are
256+
retained; only lib→lib edges are dropped. The app symbol set is built by
257+
walking every callable in the symbol table recursively (including nested
258+
functions and closures via ``inner_callables``) plus every class, so
259+
PyCG-discovered closure nodes are correctly recognised as app symbols.
260+
"""
261+
app_symbols: set = {c.signature for c in iter_callables_in_symbol_table(symbol_table)}
262+
app_symbols.update(cls.signature for cls in iter_classes_in_symbol_table(symbol_table))
263+
264+
return [
265+
e for e in edges
266+
if e.source in app_symbols or e.target in app_symbols
267+
]
268+
269+
249270
def merge_edges(*edge_lists: list) -> list:
250271
"""Merge multiple ``List[PyCallEdge]`` into one.
251272

0 commit comments

Comments
 (0)