Summary
Applying a decorator (@my_decorator) creates no edge between the decorated function and the decorator. The decorated function looks independent of the thing that wraps it, so reverse-impact queries miss it.
Reproduction
pkg/deco.py
pkg/consumer.py
# pkg/deco.py
def my_decorator(func):
def wrapper(*a, **k):
return func(*a, **k)
return wrapper
# pkg/consumer.py
from pkg.deco import my_decorator
@my_decorator
def business_logic(x):
return x * 2
graphify extract . --code-only --force on 0.9.25 — full edge list:
consumer.py --contains-----> business_logic()
consumer.py --imports_from--> deco.py
consumer.py --imports-------> my_decorator()
deco.py --contains-----> my_decorator()
There is no business_logic() -> my_decorator() edge. The only link is the file-level import.
$ graphify affected "my_decorator" --depth 2
- consumer.py [imports] pkg/consumer.py:L1
business_logic() — the thing whose behaviour actually changes if the decorator changes — is not reported.
Why it matters
This is the standard "framework entrypoint" pattern. In our fleet, a shared package exposes its whole public API as two decorators:
# md_dataset/process.py
def md_py(func): ... # -> get_file_manager() -> load_data(...)
def md_upload(func): ... # -> get_file_manager().save_tables(...)
Six downstream repos consume the package only through @md_py / @md_upload. Because decoration creates no edge, reverse traversal from the decorator finds none of the functions it wraps, and:
$ graphify affected md_py --depth 2
No affected nodes found.
That is a false negative on a "what breaks?" question — cheap, fast, confident, and wrong. It is more dangerous than no answer, because an empty result reads as "nothing depends on this."
Suggested fix
When a decorated_definition / decorator list is present, emit an edge from the decorated symbol to the decorator (e.g. decorated_by, or reuse uses / calls). Python is the clearest case; the same applies to TS/JS decorators.
Possibly related: #1566 (extending indirect_call to remaining dispatch patterns) — decoration is arguably another dispatch pattern.
Environment
graphifyy 0.9.25 · macOS 15 (darwin 25.5.0) · Python 3.9 · --code-only
Summary
Applying a decorator (
@my_decorator) creates no edge between the decorated function and the decorator. The decorated function looks independent of the thing that wraps it, so reverse-impact queries miss it.Reproduction
graphify extract . --code-only --forceon 0.9.25 — full edge list:There is no
business_logic() -> my_decorator()edge. The only link is the file-level import.business_logic()— the thing whose behaviour actually changes if the decorator changes — is not reported.Why it matters
This is the standard "framework entrypoint" pattern. In our fleet, a shared package exposes its whole public API as two decorators:
Six downstream repos consume the package only through
@md_py/@md_upload. Because decoration creates no edge, reverse traversal from the decorator finds none of the functions it wraps, and:That is a false negative on a "what breaks?" question — cheap, fast, confident, and wrong. It is more dangerous than no answer, because an empty result reads as "nothing depends on this."
Suggested fix
When a
decorated_definition/ decorator list is present, emit an edge from the decorated symbol to the decorator (e.g.decorated_by, or reuseuses/calls). Python is the clearest case; the same applies to TS/JS decorators.Possibly related: #1566 (extending
indirect_callto remaining dispatch patterns) — decoration is arguably another dispatch pattern.Environment
graphifyy 0.9.25 · macOS 15 (darwin 25.5.0) · Python 3.9 ·
--code-only