Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions codeanalyzer/semantic_analysis/call_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,19 @@ def filter_external_edges(
walking every callable in the symbol table recursively (including nested
functions and closures via ``callables``) plus every class, so
PyCG-discovered closure nodes are correctly recognised as app symbols.

Module names count as app symbols too (#131). PyCG attributes a call made in
module scope to the MODULE -- ``app -> functools.reduce`` for a module-level
``functools.reduce(...)``, or for a decorator applied to a top-level
definition, since a decorator executes in its enclosing scope. Without the
module names here, both endpoints looked third-party and every module-scope
call to a library target was discarded as lib→lib.
"""
app_symbols: set = {c.signature for c in iter_callables_in_symbol_table(symbol_table)}
app_symbols.update(cls.signature for cls in iter_classes_in_symbol_table(symbol_table))
app_symbols.update(
mod.module_name for mod in symbol_table.values() if mod.module_name
)

return [
e for e in edges
Expand Down
68 changes: 68 additions & 0 deletions test/test_call_graph_module_scope_edges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Module-scope calls to library targets are not dropped as lib->lib (#131).

`filter_external_edges` keeps an edge when either endpoint is an app symbol, but
it built `app_symbols` from callables and classes only. PyCG attributes a call in
module scope to the MODULE (`app -> functools.reduce`), and a module name was in
neither set — so every module-level call to a library target was discarded as if
both ends were third-party.

Surfaced as "decorators with arguments are dropped", but decorators are only how
it was noticed: a plain `TOTAL = functools.reduce(...)` at module scope was lost
the same way.
"""
from codeanalyzer.schema.py_schema import PyCallable, PyCallEdge, PyModule
from codeanalyzer.semantic_analysis.call_graph import filter_external_edges


def _symbol_table() -> dict:
fn = PyCallable(name="in_a_function", path="app.py", signature="app.in_a_function")
return {
"app.py": PyModule(
file_path="app.py",
module_name="app",
functions={"in_a_function": fn},
)
}


def test_module_scope_call_to_a_library_target_is_kept():
st = _symbol_table()
edges = [PyCallEdge(src="app", dst="functools.reduce", weight=1, prov=["pycg"])]
assert filter_external_edges(edges, st) == edges


def test_module_scope_decorator_application_is_kept():
"""The symptom in the issue title: a decorator applied at module scope."""
st = _symbol_table()
edges = [
PyCallEdge(src="app", dst="functools.lru_cache", weight=1, prov=["pycg"]),
PyCallEdge(src="app", dst="functools.cache", weight=1, prov=["pycg"]),
]
assert filter_external_edges(edges, st) == edges


def test_a_genuine_library_to_library_edge_is_still_dropped():
"""The filter's actual purpose must survive the fix."""
st = _symbol_table()
edges = [PyCallEdge(src="os.path.join", dst="os.sep", weight=1, prov=["pycg"])]
assert filter_external_edges(edges, st) == []


def test_callable_endpoints_are_unaffected():
st = _symbol_table()
edges = [
PyCallEdge(src="app.in_a_function", dst="functools.reduce", weight=1, prov=["jedi"]),
PyCallEdge(src="app", dst="app.in_a_function", weight=1, prov=["pycg"]),
]
assert filter_external_edges(edges, st) == edges


def test_a_module_named_like_a_library_is_still_an_app_symbol():
"""Module names come from the analyzed symbol table, so shadowing is fine."""
st = {
"functools.py": PyModule(
file_path="functools.py", module_name="functools", functions={}
)
}
edges = [PyCallEdge(src="functools", dst="os.getcwd", weight=1, prov=["pycg"])]
assert filter_external_edges(edges, st) == edges