Also called Function call graph, Call tree, Caller-callee graph
How a call graph is measured
A call graph is built, not measured, but its quality can be judged by precision (how many edges are real) and recall (how many real calls are present). The build has three steps:
- Parse each file and find every call expression.
- Resolve each call to the function it names.
- Add an edge from the calling function to the target.
Step 2 is where graphs differ. A call to a plain function in the same file resolves with certainty. A method call on a variable depends on the variable's type, and with inheritance one call site can reach several targets.
| Call | Resolution |
|---|---|
helper() defined in the same file | Certain |
from billing import charge; charge(x) | Follows the import |
obj.save() where obj has a declared type | Needs type information |
getattr(obj, name)() or handlers[name]() | Cannot be resolved statically |
A static graph over-approximates (it includes calls that never happen) or under-approximates (it misses dynamic ones), depending on how conservative the analyzer is. A dynamic graph, recorded by a profiler or tracer, is exact for that run and silent about every path the run did not take.
Why call graphs matter
The call graph answers questions that imports cannot. Before changing a function you want its callers, not only the files that import its module. Tests that reach a change are found by walking call edges to test functions. Code with no incoming call edges is a dead code candidate. Walking callers outward from a change gives its blast radius. Because many edges are inferences, the useful graphs also say how sure they are.
How to measure a call graph yourself
- Python:
pyan3generates static call graphs, andpython -m cProfileplus a viewer such assnakevizorgprof2dotgives a dynamic one. - Go:
go-callvisrenders a program's call graph interactively, anddeadcodeuses the same kind of analysis (Rapid Type Analysis) frommain. - C and C++: Doxygen with
CALL_GRAPH = YESandCALLER_GRAPH = YESdraws call and caller graphs through Graphviz (HAVE_DOT = YES). - Java, C#, Kotlin: the IDE's call hierarchy view (IntelliJ, Visual Studio) shows callers and callees for one symbol on demand.
- Any language, dynamic: a sampling profiler or tracer records the calls that actually ran.
An IDE call hierarchy is the right tool for one function in one language. A standalone graph tool fits a diagram or a one-off audit. Neither feeds the graph into risk scoring, test selection and cleanup queries.
How repowise measures call graphs
repowise builds a resolved call graph at index time from tree-sitter parses, with no LLM calls. It covers the Full and Good language tiers (26 languages: see the language support reference). Symbols and call edges live in the same graph as file imports, so you can walk from a file to its symbols to their callers.
Every calls edge carries one of 39 resolution origins from a fixed vocabulary, and each origin has one fixed confidence:
| Confidence | Example origins | Meaning |
|---|---|---|
| 0.95 | same_file, self_scope | Defined in the calling file, or a method on the caller's own class |
| 0.90 | import_scoped, same_package | Resolved through an import or a sibling file |
| 0.75 | receiver_global | The class and method pair exists somewhere in the repo |
| 0.50 | global_unique | The name is unique repo-wide: a guess, labelled as one |
- MCP:
get_contextwithinclude=["callers"]or["callees"]returns call edges, and an empty callers list comes with a*_basisexplaining why.get_symbolreturns one symbol with its edges.get_dead_codeandget_riskread the same graph. - CLI:
repowise impacted-tests main...HEADlists the tests that reach a diff.