Skip to content
Arxo Arxo

Function-Level Cycles

Arxo can detect cycles not only between modules (imports) but also between functions (calls). Function-level cycles show where runtime call chains form loops — useful when module-level analysis is clean but you still have tight coupling inside a file or across a few files.

For an overview of circular dependencies, see Circular Dependencies.

LevelWhat it detectsUse when
ModuleImport cycles (A imports B, B imports A)You want to fix dependency structure, tree-shaking, builds
FunctionCall cycles (f1 calls f2, f2 calls f1)You want to find circular call chains, refactor within a module

Function-level analysis requires a call graph. Arxo builds the call graph when the language and config support it (TypeScript/JavaScript, Rust, Python, Java, etc.). If the call graph is not available, only module-level SCC metrics are reported.

MetricWhat it tells youIdeal
scc.function.component_countNumber of independent call-graph componentsHigh (= function count)
scc.function.max_cycle_sizeSize of the largest function-level cycle0 (no call cycles)
scc.function.total_nodes_in_cyclesFunctions stuck in call cycles0
scc.function.call_massTotal call volume (call_count) inside cycles0
scc.function.cycle_cut_candidates_countNumber of suggested edges to break call cycles

Call mass is the sum of call_count on edges inside cycles. Higher call mass means more runtime traffic flows through circular calls — a stronger signal to refactor.

When the call graph is available, Arxo suggests which call edge to remove to break each function-level cycle. Each candidate has:

  • from / to — the caller and callee (function or node IDs)
  • call_count — how often that call appears; lower = cheaper to break

Start with the edge that has the lowest call_count — it’s the least-used dependency and usually the easiest to remove or invert.

{
"cycle_cut_candidates": [
{
"from": "src/document/builders/join.js::buildJoin",
"to": "src/document/utilities/assert-doc.js::assertDoc",
"call_count": 2,
"note": "Cheapest edge to cut in this cycle"
}
]
}
  • Module-level is clean — No import cycles, but code still feels tangled. Function-level cycles can explain it.
  • Refactoring inside a module — You want to break circular calls between functions or classes.
  • Runtime coupling — High call_mass means a lot of execution flows through cycles; fixing them can simplify control flow and testing.

Function-level metrics appear in the SCC result when the engine has built a call graph. No extra config is required beyond enabling the scc metric; if the language supports call analysis, scc.function.* and cycle_cut_candidates will be populated.

To inspect candidates:

Terminal window
arxo analyze --format json | jq '.ui_schemas.scc.issues.categories.cycle_cut_candidates.critical'