MixAffected.Graph (MixAffected v0.1.0)

Copy Markdown View Source

File-level dependency graph of a compiled Mix project.

Every path in a graph is relative to the project root (for an umbrella, that is the umbrella root, so paths look like apps/web/lib/web/page.ex). An edge from -> to of kind kind/0 means from depends on to:

  • :compile - from needs to at compile time (use, require plus a macro call, import, module body calls)
  • :export - from depends on to's exports, typically its struct
  • :runtime - from calls to at runtime

When xref reports more than one kind for a pair, the strongest is kept (compile, then export, then runtime), as Mix itself does.

Building

build/2 shells out to mix xref graph in the project directory with MIX_ENV=test (by default), so files the project compiles only in the test env, such as test/support, are part of the graph. See build/2 for the options and error/0 for every way it fails open.

Selecting

dependents/2 returns the changed files plus every file that reaches one of them through any chain of edges of any kind. Cycles are fine.

Summary

Types

Outgoing edges per file: %{from => %{to => kind}}.

Why a graph or a selection could not be produced. Every one of them means "run the full suite"; none is ever an empty selection.

How from depends on to in an edge from -> to.

A project-root-relative file path, e.g. "lib/my_app/user.ex".

t()

Functions

Builds the graph of the compiled Mix project at project_dir.

Returns the changed files plus every file that transitively depends on any of them, over edges of every kind.

The kind of the edge from -> to, or nil when from does not depend on to directly.

Every edge as {from, to, kind}, sorted.

Every source file in the graph.

Whether path (root-relative or absolute under the root) is a source in the graph.

Builds a graph from root-relative adjacency. Files that only appear as edge targets become files of the graph too.

Adds edges {from, to, kind} (keeping the stronger kind when from -> to exists), for dependencies only the caller knows about.

Types

adjacency()

@type adjacency() :: %{optional(path()) => %{optional(path()) => kind()}}

Outgoing edges per file: %{from => %{to => kind}}.

error()

@type error() ::
  {:xref_failed, non_neg_integer(), String.t()}
  | :mix_not_found
  | {:output_unreadable, File.posix()}
  | {:parse_error, term()}
  | :empty_graph
  | {:unresolved_paths, [path()]}
  | {:ambiguous_paths, [path()]}
  | {:unknown_files, [path()]}

Why a graph or a selection could not be produced. Every one of them means "run the full suite"; none is ever an empty selection.

  • {:xref_failed, exit_status, output} - mix xref graph exited non-zero (compile error, unsupported --format, missing deps, ...)
  • :mix_not_found - no mix executable on PATH
  • {:output_unreadable, posix} - xref succeeded but its output file could not be read
  • {:parse_error, detail} - xref output was not in the expected shape
  • :empty_graph - xref reported no source files at all
  • {:unresolved_paths, paths} - xref reported paths that are not files under the project root (or, for an umbrella, under any of its apps; usually an umbrella built without :apps_paths)
  • {:ambiguous_paths, paths} - umbrella children share these relative paths; xref merges same-named files and drops edges, so the graph cannot be trusted
  • {:unknown_files, paths} - dependents/2 was asked about files that are not compiled sources in the graph (new, deleted, or non-source files)

kind()

@type kind() :: :compile | :export | :runtime

How from depends on to in an edge from -> to.

path()

@type path() :: String.t()

A project-root-relative file path, e.g. "lib/my_app/user.ex".

t()

@type t() :: %MixAffected.Graph{
  dependents: %{optional(path()) => MapSet.t(path())},
  edges: adjacency(),
  files: MapSet.t(path()),
  root: Path.t(),
  source: module() | nil
}
  • :root - absolute project root every path is relative to
  • :source - the MixAffected.Graph.Source adapter that produced it (nil for new/2)
  • :files - every source file in the graph
  • :edges - outgoing edges, %{from => %{to => kind}}
  • :dependents - reverse edges, %{to => MapSet of from}

Functions

build(project_dir, opts \\ [])

@spec build(Path.t(), keyword()) :: {:ok, t()} | {:error, error()}

Builds the graph of the compiled Mix project at project_dir.

Options:

  • :source - the MixAffected.Graph.Source adapter; defaults to MixAffected.Graph.Source.default(System.version())
  • :apps_paths - for an umbrella, %{app => relative_app_path} as returned by Mix.Project.apps_paths/0. Defaults to Mix.Project.apps_paths() when project_dir is the current Mix project, otherwise nil (not an umbrella)
  • :mix_env - MIX_ENV for the xref run, default "test"
  • :mix_executable - path to mix, default the one on PATH

The project is compiled first if it is stale (xref does that itself).

dependents(graph, changed)

@spec dependents(t(), [Path.t()]) :: {:ok, MapSet.t(path())} | {:error, error()}

Returns the changed files plus every file that transitively depends on any of them, over edges of every kind.

changed may be root-relative or absolute paths under the root; the result is always root-relative. If any changed file is not a source in the graph the answer is {:error, {:unknown_files, sorted_paths}}: callers decide what non-source changes mean, the graph never guesses.

edge(graph, from, to)

@spec edge(t(), path(), path()) :: kind() | nil

The kind of the edge from -> to, or nil when from does not depend on to directly.

edges(graph)

@spec edges(t()) :: [{path(), path(), kind()}]

Every edge as {from, to, kind}, sorted.

files(graph)

@spec files(t()) :: MapSet.t(path())

Every source file in the graph.

member?(graph, path)

@spec member?(t(), Path.t()) :: boolean()

Whether path (root-relative or absolute under the root) is a source in the graph.

new(root, adjacency)

@spec new(Path.t(), adjacency()) :: t()

Builds a graph from root-relative adjacency. Files that only appear as edge targets become files of the graph too.

put_edges(graph, edges)

@spec put_edges(t(), [{path(), path(), kind()}]) :: t()

Adds edges {from, to, kind} (keeping the stronger kind when from -> to exists), for dependencies only the caller knows about.