Documentation · Static metrics

km deps

Dependency graph analysis

Analyzes internal module dependencies by parsing import/use/require statements. Builds a directed graph of file-level coupling and detects cycles using Tarjan’s SCC algorithm.

km deps [path]

Supports Elixir (every module referred to in the code, with alias undone; see the notes below), Rust (every path that names a module of the workspace: use, crate::, super::, a child module; see the notes below), Python (import and from … import, absolute and relative; see the notes below), JavaScript/TypeScript (relative import/require), Go (imports matching the module path from go.mod), and Kaikai (import a.b.c, including the as and .{…} forms). External dependencies (crates, npm packages, the Kaikai stdlib) are ignored.

Files in any other language are left out of the graph instead of being listed with zero dependencies. The table footer, the unsupported array of the JSON output and the unsupported:N field of the short format say how many files were skipped, so “not measured” is never shown as “no dependencies”.

Elixir notes:

  • Dependencies are between modules, and most need no import, so every module name in the code counts as a reference: calls, structs, use, import, require, @behaviour, defimpl. Comments, strings, heredocs and sigils are set aside first, so a doctest is not a dependency.
  • alias is undone, including as:, grouped and multi-line forms, and __MODULE__. The statement itself is not a use.
  • Nested defmodules are named after the module that contains them, told by indentation as mix format leaves it.
  • A module defined in several files (projects made from one template) resolves to the file nearest the one referring to it.
  • Comments and literals hold no references, but the code a string interpolates does: "Total: #{Orders.total(order)}" uses Orders.
  • A Phoenix controller uses the views named after it (PageController and PageJSON, PageHTML, PageView), and a module uses the components its ~H templates render.
  • Not seen: modules named at run time (apply/3, configuration), those generated by macros, and those a Phoenix router names under the alias of a scope.

Rust notes:

  • A dependency is a path: use crate::git::GitRepo, a qualified super::analyzer::run(x), a call on a child module (report::print(x)). Groups, as, self, globs and multi-line use are read. mod x; only says where a module lives and adds no edge.
  • Each crate is a tree of modules grown from its root (src/lib.rs, src/main.rs, a file of src/bin, tests, examples or benches, build.rs) by its mod declarations, #[path] included. A path leads to the file of the deepest module it names: the item may be defined there or only re-exported.
  • Other targets of a package reach its library by name (my_app::orders from tests/ or main.rs), and so do the other crates of the workspace. The name comes from [package] name, with - read as _.
  • A type uses the files that hold its impl blocks, since what they define is reached through the type.
  • For km impact, a file that holds #[test] functions has a test of its own; a file of tests/ that holds them, or a tests.rs module, is a test. What a package runs (src/main.rs, src/bin, examples, benches, build.rs) is an entry point.
  • Comments, strings and $crate in a macro name nothing.
  • Not seen: what a macro generates or names, include!, a library with a [lib] path or name of its own, a dependency renamed in Cargo.toml, and modules behind cfg, which all count. A use only from an inline test module still makes the file a dependent. A test that runs the binary (assert_cmd, CARGO_BIN_EXE_*) names no file, so it protects none. Two packages of the same name in one repository are not told apart.

Python notes:

  • Every import a.b and from a.b import c is read, wherever it is written: in a function, under if TYPE_CHECKING:, over several lines in parentheses or with a backslash. One written in a docstring or a comment is not an import.
  • A dotted path names a/b.py or the package a/b/__init__.py; a stub (.pyi) stands for a module with no source. Only the deepest module is used: import a.b.c adds no edge to a/__init__.py.
  • In from a.b import c, c is the submodule a/b/c.py when that file exists, and a name a.b defines otherwise.
  • A relative import starts at the package of the importing file, one level up per extra dot.
  • An absolute import is looked up from each directory above the importing file that is not itself a package (it holds no __init__.py), nearest first, and from the src directory under each. That covers a project run from its root, a src layout with tests/ beside it, and several projects in one repository. Then come the roots the nearest pyproject.toml above declares: where the build backend finds the packages (where and package-dir of setuptools, packages of Poetry, sources and packages of Hatch, python-source of maturin, package-dir of PDM, module-root of uv) and what pytest adds (pythonpath). What is found under none of them is external: the standard library, installed packages.
  • For km impact, test_*.py, *_test.py and tests.py are tests, and conftest.py is test support, which protects nothing; __main__.py, setup.py and manage.py are entry points, and so is what sits under management/commands.
  • Not seen: modules named at run time (importlib, __import__, Django settings and INSTALLED_APPS), roots added at run time (sys.path, PYTHONPATH, a .pth file) or declared in setup.py, setup.cfg or pytest.ini, and a namespace package spread over several roots. A name a package re-exports leads to its __init__.py, and from there to the module that defines it.

Kaikai notes:

  • import a.b.c names a/b/c.kai relative to a package root, not to the importing file. Each ancestor directory of the importing file is tried, nearest first, so run km deps on a directory that contains the package root.
  • When no such file exists, import a can name a package directory a/ that carries a kai.toml; the import then depends on every .kai file directly inside it.
  • An import that resolves to no analysed file is external and adds no edge.
  • The files of one Kaikai package merge, so a module can use a name declared in another file of its package without importing it. The import graph is therefore a lower bound on the real dependencies.
FlagDescription
--format {table,json,short,terse}Output format (default: table)
--cycles-onlyShow only files that participate in a dependency cycle
--sort-by METRICSort by fan-out (default) or fan-in
--top NShow only top N files (default: 20)

Example output:

Dependency Graph
────────────────────────────────────────────────────────────────────────
 File                 Language Fan-In Fan-Out Cycle
────────────────────────────────────────────────────────────────────────
 report_helpers.rs        Rust     26       1    no
 util.rs                  Rust     25       2   yes
 walk.rs                  Rust     24       1    no
────────────────────────────────────────────────────────────────────────

Dependency cycles: 14
  Cycle 1 (3 files):
    cogcom/analyzer.rs
    cogcom/detection.rs
    cogcom/report.rs