Skip to main content

guides/concepts.md

# Concepts

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:

```elixir
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`:

```elixir
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`.

```elixir
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.