Reach.Plugin behaviour (Reach v2.6.1)

Copy Markdown View Source

Behaviour for library-specific analysis plugins.

Plugins extend Reach in three ways:

  1. Graph edgesanalyze/2 and analyze_project/3 add domain-specific edges to the dependence graph (framework dispatch, message routing, etc.)

  2. Effect classificationclassify_effect/1 teaches the effect classifier about framework-specific calls (Ecto queries are pure, Repo writes are :write, etc.)

  3. Source frontends — optional source_extensions/0 and parse_file/2 callbacks let plugins own language frontends for optional ecosystems.

  4. Elixir AST loweringlower_elixir_ast/2 translates framework DSL forms, sigils, or template syntax into analysis-friendly Elixir AST.

  5. Embedded IRanalyze_embedded/2 extracts code from string literals (e.g. JS inside QuickBEAM.eval) and returns additional IR nodes plus cross-language edges.

  6. Framework presentation/patterns — optional callbacks provide framework-specific trace presets, behaviour labels, and visualization edge filtering.

Implementing a plugin

defmodule MyPlugin do
  @behaviour Reach.Plugin

  @impl true
  def analyze(all_nodes, _opts), do: []

  @impl true
  def classify_effect(%Reach.IR.Node{type: :call, meta: %{function: :my_pure_fn}}), do: :pure
  def classify_effect(_), do: nil
end

Built-in plugins

Plugins for Phoenix, Ecto, Oban, GenStage, Jido, and OpenTelemetry are included and auto-detected at runtime. Override with the :plugins option:

Reach.Project.from_mix_project(plugins: [Reach.Plugins.Ecto])

Disable auto-detection:

Reach.string_to_graph!(source, plugins: [])

Summary

Callbacks

Analyzes IR nodes from a single module and returns edges to add.

Extracts embedded code from IR nodes (e.g. JS strings passed to QuickBEAM.eval) and returns additional IR nodes plus edges connecting them to the host graph.

Analyzes IR nodes across all modules in a project.

Classifies the effect of a call node.

Lowers framework-specific Elixir AST into ordinary Elixir AST before Reach IR translation.

Functions

Infers a framework-specific behaviour label from callback names.

Asks each plugin to classify a call node's effect.

Returns the list of auto-detected plugins based on loaded dependencies.

Returns evidence providers contributed by plugins.

Returns true when a plugin marks a call-graph edge as visualization noise.

Asks plugins to lower a framework-specific Elixir AST node.

Parses a source file with the first plugin frontend that accepts its extension.

Lets plugins annotate or adjust evidence facts without owning user-facing policy.

Resolves plugins from options, falling back to auto-detection.

Runs module-local analysis hooks for the configured plugins.

Runs embedded-node analysis hooks for plugins that provide them.

Runs project-level analysis hooks for plugins that provide them.

Returns smell checks contributed by plugins.

Returns source extensions handled by plugins.

Returns the source language for an extension handled by a plugin.

Compiles a framework-specific trace pattern, if a plugin recognizes it.

Types

edge_spec()

@type edge_spec() :: {Reach.IR.Node.id(), Reach.IR.Node.id(), term()}

embedded_result()

@type embedded_result() :: {[Reach.IR.Node.t()], [edge_spec()]}

Callbacks

analyze(all_nodes, opts)

@callback analyze(all_nodes :: [Reach.IR.Node.t()], opts :: keyword()) :: [edge_spec()]

Analyzes IR nodes from a single module and returns edges to add.

analyze_embedded(all_nodes, opts)

(optional)
@callback analyze_embedded(all_nodes :: [Reach.IR.Node.t()], opts :: keyword()) ::
  embedded_result()

Extracts embedded code from IR nodes (e.g. JS strings passed to QuickBEAM.eval) and returns additional IR nodes plus edges connecting them to the host graph.

analyze_project(modules, all_nodes, opts)

(optional)
@callback analyze_project(
  modules :: %{required(module()) => map()},
  all_nodes :: [Reach.IR.Node.t()],
  opts :: keyword()
) :: [edge_spec()]

Analyzes IR nodes across all modules in a project.

Only needed for cross-module patterns like router→controller dispatch or job enqueue→perform flow.

behaviour_label(callbacks)

(optional)
@callback behaviour_label(callbacks :: [atom()]) :: String.t() | nil

classify_effect(node)

(optional)
@callback classify_effect(node :: Reach.IR.Node.t()) :: atom() | nil

Classifies the effect of a call node.

Return an effect atom (:pure, :read, :write, :io, :send, :exception) or nil to defer to the next classifier.

evidence_providers()

(optional)
@callback evidence_providers() :: [module()]

ignore_call_edge?(t)

(optional)
@callback ignore_call_edge?(Graph.Edge.t()) :: boolean()

lower_elixir_ast(ast, opts)

(optional)
@callback lower_elixir_ast(ast :: Macro.t(), opts :: keyword()) ::
  {:ok, Macro.t()} | :ignore | {:error, term()}

Lowers framework-specific Elixir AST into ordinary Elixir AST before Reach IR translation.

Return :ignore when the plugin does not own the AST node. Returned AST may carry %Reach.Source.Origin{} metadata under the :reach metadata key.

parse_file(path, opts)

(optional)
@callback parse_file(path :: Path.t(), opts :: keyword()) ::
  {:ok, [Reach.IR.Node.t()]} | {:error, term()}

refine_evidence(evidence, context)

(optional)
@callback refine_evidence(evidence :: struct() | map(), context :: map()) ::
  struct() | map() | :unchanged

smell_checks()

(optional)
@callback smell_checks() :: [module()]

source_extensions()

(optional)
@callback source_extensions() :: [String.t()]

source_language(extension)

(optional)
@callback source_language(extension :: String.t()) :: atom() | nil

trace_pattern(pattern)

(optional)
@callback trace_pattern(pattern :: String.t()) :: (Reach.IR.Node.t() -> boolean()) | nil

Functions

behaviour_label(plugins, callbacks)

Infers a framework-specific behaviour label from callback names.

classify_effect(plugins, node)

Asks each plugin to classify a call node's effect.

Returns the first non-nil result, or nil if no plugin matches.

detect()

Returns the list of auto-detected plugins based on loaded dependencies.

evidence_providers(plugins)

Returns evidence providers contributed by plugins.

ignore_call_edge?(plugins, edge)

Returns true when a plugin marks a call-graph edge as visualization noise.

lower_elixir_ast(plugins, ast, opts)

Asks plugins to lower a framework-specific Elixir AST node.

parse_file(plugins, path, opts)

Parses a source file with the first plugin frontend that accepts its extension.

refine_evidence(plugins, evidence, context \\ %{})

Lets plugins annotate or adjust evidence facts without owning user-facing policy.

resolve(opts)

Resolves plugins from options, falling back to auto-detection.

run_analyze(plugins, all_nodes, opts)

Runs module-local analysis hooks for the configured plugins.

run_analyze_embedded(plugins, all_nodes, opts)

Runs embedded-node analysis hooks for plugins that provide them.

run_analyze_project(plugins, modules, all_nodes, opts)

Runs project-level analysis hooks for plugins that provide them.

smell_checks(plugins)

Returns smell checks contributed by plugins.

source_extensions(plugins)

Returns source extensions handled by plugins.

source_language(plugins, extension)

Returns the source language for an extension handled by a plugin.

trace_pattern(plugins, pattern)

Compiles a framework-specific trace pattern, if a plugin recognizes it.