View Source mix xref (Mix v1.17.3)

Prints cross reference information between modules.

The xref task expects a mode as first argument:

$ mix xref MODE

All available modes are discussed below, after a brief introduction to xref.

This task is automatically re-enabled, so you can print information multiple times in the same Mix invocation.

A brief introduction to xref

The goal of xref is to analyze the dependencies between modules and files. It is most commonly used to find problematic areas where touching one file in a project causes a large subset of the project to recompile. The most common cause of these problems are the so-called "compile-connected" files. Those are files you depend on at compile-time (for example, by invoking its macro or using it in the body of amodule) which also have their own dependencies.

Therefore, if your goal is to reduce recompilations, the first step is to run:

$ mix xref graph --format stats --label compile-connected

This command will show general information about the project, but focus on compile-connected dependencies. In the stats, you will see the following report:

Top 10 files with most incoming dependencies:
  * lib/livebook_web.ex (97)
  * lib/livebook/config.ex (3)
  * proto/lib/livebook_proto/deployment_group.pb.ex (2)
  * lib/livebook_web/plugs/memory_provider.ex (2)
  * proto/lib/livebook_proto/user_connected.pb.ex (1)

You can see the first file, "lib/livebook_web.ex", is depended on by 97 other files and, because we are using compile-connected, it also means that "lib/livebook_web.ex" itself has its own dependencies. We can find which files depend on "lib/livebook_web.ex" at compile time like this:

$ mix xref graph --sink lib/livebook_web.ex --label compile --only-nodes

And you can find the files lib/livebook_web.ex depends on like this:

$ mix xref graph --source lib/livebook_web.ex --only-nodes

The trouble here is precisely that, if any of the files in the latter command changes, all of the files in the first command will be recompiled, because compile time dependencies are transitive.

Having compile time dependencies is a common feature in Elixir projects. However, the modules you depend on at compile-time must avoid runtime dependencies within the same project. You can understand all of the dependencies of a given file by running:

$ mix xref trace lib/livebook_web.ex

The command above will output three types of dependencies, which we detail next.

Dependency types

Elixir tracks three types of dependencies between modules: compile, exports, and runtime. If a module has a compile time dependency on another module, the caller module has to be recompiled whenever the callee changes (or any runtime dependency of the callee changes). Let's see an example:

# lib/a.ex
defmodule A do
  @hello B.hello()
  def hello, do: @hello
end

# lib/b.ex
defmodule B do
  def hello, do: "hello"
  def world, do: C.world()
end

# lib/c.ex
defmodule C do
  def world, do: "world"
end

If C.world/0 changes, B is marked as stale. B does not need to be recompiled, because it depends on C at runtime, but anything that depends on B at compile-time has to recompile, and that includes A.

Compile-time dependencies are typically added when using macros or when invoking functions in the module body (outside of functions). This type of transitive compile-time dependencies, such as A depending on C at compile-time through B, are called compile-connected.

Export dependencies are compile time dependencies on the module API, namely structs and its public definitions. For example, if you import a module but only use its functions, it is an export dependency. If you use a struct, it is an export dependency too. Export dependencies are only recompiled if the module API changes. Note, however, that compile time dependencies have higher precedence than exports. Therefore if you import a module and use its macros, it is a compile time dependency.

Runtime dependencies are added whenever you invoke another module inside a function. Modules with runtime dependencies do not have to be compiled when the callee changes, unless there is a transitive compile or an outdated export time dependency between them.

Over the next sections, we will explain what which mix xref command does in detail.

mix xref trace FILE

Compiles the given file listing all dependencies within the same app. It includes the type and line for each one. Example:

$ mix xref trace lib/my_app/router.ex

The --label option may be given to keep only certain traces (compile, runtime or export):

$ mix xref trace lib/my_app/router.ex --label compile

If you have an umbrella application, we also recommend using the --include-siblings flag to see the dependencies from sibling applications. The trace command is not currently supported at the umbrella root.

Example

Imagine the given file lib/b.ex:

defmodule B do
  import A
  A.macro()
  macro()
  A.fun()
  fun()
  def calls_macro, do: A.macro()
  def calls_fun, do: A.fun()
  def calls_struct, do: %A{}
end

mix xref trace will print:

lib/b.ex:2: require A (export)
lib/b.ex:3: call A.macro/0 (compile)
lib/b.ex:4: import A.macro/0 (compile)
lib/b.ex:5: call A.fun/0 (compile)
lib/b.ex:6: call A.fun/0 (compile)
lib/b.ex:6: import A.fun/0 (compile)
lib/b.ex:7: call A.macro/0 (compile)
lib/b.ex:8: call A.fun/0 (runtime)
lib/b.ex:9: struct A (export)

mix xref graph

Emits a file dependency graph where an edge from A to B indicates that A (source) depends on B (sink).

$ mix xref graph --format stats

For any non-small project, the output of mix xref graph itself, without any additional flags, is not useful: once your project grows, it is hard to gather actionable feedback by looking at the graph as a whole. Instead, mix xref graph is better used as a "database", which can help you answer queries about your project.

The following options are accepted:

  • --exclude - path to exclude. Can be repeated to exclude multiple paths.

  • --label - only shows relationships with the given label. The labels are "compile", "export" and "runtime". By default, the --label option does not change how the graph is computed, it simply filters the printed graph to show only relationships with the given label. However, you can pass --only-direct to trim the graph to only the nodes that have the direct relationship given by label. There is also a special label called "compile-connected" that keeps only compile-time files with at least one transitive dependency. See "Dependency types" section below.

  • --group - provide comma-separated paths to consider as a group. Dependencies from and into multiple files of the group are considered a single dependency. Dependencies between the group elements are ignored. This is useful when you are computing compile and compile-connected dependencies and you want a series of files to be treated as one. The group is printed using the first path, with a + suffix. Can be repeated to create multiple groups.

  • --only-direct - keeps only files with the direct relationship given by --label

  • --only-nodes - only shows the node names (no edges). Generally useful with the --sink flag

  • --source - displays all files that the given source file references (directly or indirectly). Can be repeated to display references from multiple sources.

  • --sink - displays all files that reference the given file (directly or indirectly). Can be repeated.

  • --min-cycle-size - controls the minimum cycle size on formats like stats and cycles

  • --format - can be set to one of:

    • pretty - prints the graph to the terminal using Unicode characters. Each prints each file followed by the files it depends on. This is the default except on Windows;

    • plain - the same as pretty except ASCII characters are used instead of Unicode characters. This is the default on Windows;

    • stats - prints general statistics about the graph;

    • cycles - prints all cycles in the graph;

    • dot - produces a DOT graph description in xref_graph.dot in the current directory. Warning: this will override any previously generated file

  • --output (since v1.15.0) - can be set to one of

    • - - prints the output to standard output;

    • a path - writes the output graph to the given path

    Defaults to xref_graph.dot in the current directory.

The --source and --sink options are particularly useful when trying to understand how the modules in a particular file interact with the whole system. You can combine those options with --label and --only-nodes to get all files that exhibit a certain property, for example:

# To show all compile-connected relationships
$ mix xref graph --label compile-connected

# To get the tree that depend on lib/foo.ex at compile time
$ mix xref graph --label compile --sink lib/foo.ex

# To get all files that depend on lib/foo.ex at compile time
$ mix xref graph --label compile --sink lib/foo.ex --only-nodes

# To get all paths between two files
$ mix xref graph --source lib/foo.ex --sink lib/bar.ex

# To show general statistics about the graph
$ mix xref graph --format stats

# To show all cycles with at least one compile-time dependency
$ mix xref graph --format cycles --label compile-connected

Understanding the printed graph

When mix xref graph runs, it will print a tree of the following format. Imagine the following code:

# lib/a.ex
defmodule A do
  IO.puts B.hello()
end

# lib/b.ex
defmodule B do
  def hello, do: C.world()
end

# lib/c.ex
defmodule C do
  def world, do: "hello world"
end

It will print:

$ mix xref graph
lib/a.ex
└── lib/b.ex (compile)
lib/b.ex
└── lib/c.ex
lib/c.ex

This tree means that lib/a.ex depends on lib/b.ex at compile time. And lib/b.ex depends on lib/c.ex at runtime. This is often problematic because if lib/c.ex changes, lib/a.ex also has to recompile due to this indirect compile time dependency. When you pass --label compile, the graph shows only the compile-time dependencies:

$ mix xref graph --label compile
lib/a.ex
└── lib/b.ex (compile)

The --label compile flag removes all non-compile dependencies. However, this can be misleading because having direct compile time dependencies is not necessarily an issue. The biggest concern are the transitive compile time dependencies. You can get all compile time dependencies that cause transitive compile time dependencies by using --label compile-connected:

$ mix xref graph --label compile-connected
lib/a.ex
└── lib/b.ex (compile)

The above says lib/a.ex depends on lib/b.ex and that causes transitive compile time dependencies - as we know, lib/a.ex also depends on lib/c.ex. We can retrieve those transitive dependencies by passing lib/b.ex as --source to mix xref graph:

$ mix xref graph --source lib/b.ex
lib/b.ex
└── lib/c.ex

Similarly, you can use the --label compile and the --sink flag to find all compile time dependencies that will recompile once the sink changes:

$ mix xref graph --label compile --sink lib/c.ex
lib/a.ex
└── lib/b.ex (compile)

If you have an umbrella application, we also recommend using the --include-siblings flag to see the dependencies from sibling applications. When invoked at the umbrella root, the graph command will list all files from all umbrella children, without any namespacing.

Understanding the printed cycle

If you run mix xref graph --format cycle, Elixir will print cycles of shape:

Cycle of length 3:

    lib/c.ex
    lib/b.ex
    lib/a.ex

The cycles are given in order: c.ex depends on b.ex which depends on a.ex which depends on c.ex. In particular, you want to avoid cycles with compile dependencies in there. You can find those cycles with:

$ mix xref graph --format cycles --label compile-connected

Which may look like this:

Cycle of length 3:

    lib/c.ex
    lib/b.ex (compile)
    lib/a.ex

This means c.ex depends on b.ex at compile time. Any compile dependency in a cycle is by definition a compile-connected dependency, which must be generally avoided, as explained earlier in the module documentation.

Shared options

Those options are shared across all modes:

  • --fail-above - generates a failure if the relevant metric is above the given threshold. Applies to all modes except mix xref graph --format stats.

  • --include-siblings - includes dependencies that have :in_umbrella set to true in the current project in the reports. This can be used to find callers or to analyze graphs between projects (it applies only to trace subcommand)

  • --no-compile - does not compile even if files require compilation

  • --no-deps-check - does not check dependencies

  • --no-archives-check - does not check archives

  • --no-elixir-version-check - does not check the Elixir version from mix.exs

Summary

Functions

calls(opts \\ []) deprecated

Returns a list of information of all the runtime function calls in the project.

Functions

This function is deprecated. Use compilation tracers described in the Code module.
@spec calls(keyword()) :: [
  %{callee: {module(), atom(), arity()}, line: integer(), file: String.t()}
]

Returns a list of information of all the runtime function calls in the project.

Each item in the list is a map with the following keys:

  • :callee - a tuple containing the module, function, and arity of the call
  • :line - an integer representing the line where the function is called
  • :file - a binary representing the file where the function is called
  • :caller_module - the module where the function is called

This function returns an empty list when used at the root of an umbrella project because there is no compile manifest to extract the function call information from. To get the function calls of each child in an umbrella, execute the function at the root of each individual application.