Reach translates source code into an intermediate representation, builds control/data/call relationships, and combines them into a program dependence graph.

Control flow

Control-flow graphs show branch structure inside functions. Reach keeps function CFGs acyclic and uses block-quality checks to ensure rendered blocks cover source lines without duplicates.

Call graph

The call graph connects caller and callee functions. Reach uses it for dependency trees, impact analysis, module coupling, hotspots, and why-path explanations.

Data flow

Data-flow edges track definitions, uses, parameters, returns, and cross-function value movement. Trace commands use these edges for taint and variable workflows.

Effects

Reach classifies calls into effect categories such as pure, IO, read, write, send, receive, exception, NIF, and unknown. Effect evidence powers boundaries, smells, and refactoring candidates.

Smells

Reach detects structural code smells in two layers:

Source smells use the Reach.Smell.Check.Source DSL for source-level rules. Simple rules use ExAST's ~p sigil and selectors; hot or shape-sensitive rules can use smell(..., mode: :ast) callbacks over Sourceror AST nodes. These run per-file with shared source/AST/zipper caches and inferred source prefilters.

Semantic smells use Reach's own IR with effects, data flow, call graph, and clone evidence — redundant computation, loop anti-patterns, dual key access, fixed-shape maps, behaviour candidates, return-contract drift, unsafe dynamic atom creation, and unsafe binary_to_term/1.

AST smells use Sourceror source AST when the source shape matters more than IR — compile-time file reads without @external_resource, Ecto implicit cross joins, interpolated SQL, and unpinned Ecto query values.

Source smells are declared with one smell DSL:

use Reach.Smell.Check.Source

smell ~p[Enum.reverse(_) |> hd()], :suboptimal,
      "traverses twice; use List.last/1"

smell(
  from(~p[Enum.drop(_, amount) |> Enum.take(_)]) |> where(not match?({:-, _, [_]}, ^amount)),
  :eager_pattern,
  "use Enum.slice/3"
)

smell(
  :boolean_case,
  :suboptimal,
  "case on boolean with true/false clauses; use if/else",
  mode: :ast,
  prefilter: {:all, ["case"]}
)

defp boolean_case({:case, meta, [subject, clauses]}) do
  if boolean_subject?(subject) and boolean_case_clauses?(clauses), do: {:ok, meta}
end

defp boolean_case(_node), do: nil

Semantic smells use the standard Reach.Smell.Check behaviour with IR helpers like inside_loop?/2, callback_body/1, and statement_pairs/1.

AST-backed smells can use Reach.Smell.Check.AST and implement scan_ast/2:

defmodule MyApp.ReachSmells.NoCompileTimeRead do
  use Reach.Smell.Check.AST

  alias Reach.Smell.Finding

  @impl true
  def kinds, do: [:my_app_compile_time_read]

  defp scan_ast(ast, file) do
    # inspect Sourceror AST and return Finding structs
  end
end

Projects can also define local semantic smell checks and enable them with smells: [custom_checks: [...]] in .reach.exs.

defmodule MyApp.ReachSmells.NoFoo do
  @behaviour Reach.Smell.Check

  def run(project) do
    # return Reach.Smell.Finding structs
  end
end

Clone analysis

Optional clone evidence (via ExDNA) enriches semantic smell confidence. Clone families inform structural consistency checks: return-contract drift, side-effect order drift, map-contract drift, validation drift, and behaviour extraction candidates.

Plugins

Plugins extend Reach with framework-specific semantics: effect classification, trace presets, behaviour labels, visualization filtering, and graph edges. Built-in plugins auto-detect Phoenix, Ecto, Oban, Ash, GenStage, Jido, and OpenTelemetry. Language frontends (JavaScript/Gleam) are also plugin-discovered.

OTP analysis

OTP checks identify GenServer state access, GenStatem transitions, missing handlers, dead replies, supervision, process dictionary/ETS coupling, and cross-process resource sharing.