Muex. Mutator behaviour
(Muex v0.7.0)
View Source
Behaviour for mutation operators that transform AST nodes.
Mutators implement specific mutation strategies (e.g., arithmetic operators, boolean operators, literals) and return a list of possible mutations for a given AST.
Each mutator declares which languages it supports via supported_languages/0.
Mutators targeting the same AST family (e.g., Elixir and Erlang both use BEAM
AST) can declare support for multiple languages.
Traversal and Pruning
walk/3 performs a context-aware, pre-order traversal of the AST. It threads
the nearest enclosing source line through context[:line] and prunes subtrees
that should never be mutated (module aliases, use/import/alias/require,
documentation and typespec attributes, and keyword/option keys). Callers can
prune additional framework DSL calls via context[:skip_calls]. See walk/3
for the full set of rules.
Equivalent Mutations
Mutators can declare that a generated mutation is semantically equivalent to the original code — meaning no test can ever kill it. This avoids polluting mutation scores with false negatives.
There are two ways to mark equivalence:
At generation time — set
equivalent: truein the mutation map returned bymutate/2. Use this when the mutator knows at generation time that the mutation is equivalent (e.g., swapping arguments to a commutative operator).Via the
equivalent?/1callback — implement this for more complex analysis that needs to inspect the full mutation map. The default implementation checks the:equivalentkey.
Example
defmodule Muex.Mutator.MyMutator do
@behaviour Muex.Mutator
@impl true
def mutate(ast, _context) do
# Return list of mutated AST variants
[mutated_ast_1, mutated_ast_2]
end
@impl true
def name, do: "My Mutator"
@impl true
def description, do: "Mutates specific AST patterns"
@impl true
def supported_languages, do: [Muex.Language.Elixir, Muex.Language.Erlang]
# Optional: override for complex equivalence detection
@impl true
def equivalent?(%{description: "swap arguments in +()" <> _}), do: true
def equivalent?(_mutation), do: false
end
Summary
Types
Represents a single mutation with its metadata.
Callbacks
Returns a description of what this mutator does.
Returns whether a mutation is semantically equivalent to the original code.
Applies mutations to the given AST.
Returns the name of the mutator.
Returns the list of language adapter modules this mutator supports.
Functions
Checks whether a mutation is equivalent, delegating to the mutator module.
Walks an AST and collects mutations from all registered mutators.
Types
@type mutation() :: %{ ast: term(), original_ast: term(), mutator: module(), description: String.t(), location: %{file: String.t(), line: non_neg_integer()} }
Represents a single mutation with its metadata.
The :equivalent key is optional. When true, the mutation is considered
semantically equivalent to the original and will be filtered out by the optimizer.
Callbacks
@callback description() :: String.t()
Returns a description of what this mutator does.
Returns whether a mutation is semantically equivalent to the original code.
Equivalent mutations can never be killed by any test and should be filtered out to avoid inflating the "survived" count.
The default implementation checks for equivalent: true in the mutation map.
Override this callback in your mutator for more sophisticated detection.
Applies mutations to the given AST.
Parameters
ast- The AST to mutatecontext- Map containing additional context (file path, line number, etc.)
Returns
List of mutation maps, each representing a possible mutation
@callback name() :: String.t()
Returns the name of the mutator.
@callback supported_languages() :: [module()]
Returns the list of language adapter modules this mutator supports.
Mutators that work with the same AST format (e.g., BEAM languages like Elixir and Erlang) can declare multiple languages. Discovery will filter mutators based on the active language.
Returns
List of language adapter modules (e.g., [Muex.Language.Elixir, Muex.Language.Erlang])
Functions
Checks whether a mutation is equivalent, delegating to the mutator module.
Falls back to checking the :equivalent key in the mutation map if the
mutator does not implement equivalent?/1.
Walks an AST and collects mutations from all registered mutators.
The traversal is context-aware: it threads the nearest enclosing source
line through context[:line] (so leaf mutators such as
Muex.Mutator.Literal can report a real location for bare literals that
carry no AST metadata of their own) and prunes subtrees that should never
be mutated.
Pruning rules
The following are skipped for every mutator, regardless of configuration:
- Module alias segments (
{:__aliases__, _, _}), e.g. thePhoenixandComponentatoms inuse Phoenix.Component. - Directives:
use,import,alias,require. - Documentation and typespec attributes:
@doc,@moduledoc,@typedoc,@type,@typep,@opaque,@spec,@callback,@macrocallback,@behaviour,@impl,@derive,@enforce_keys. - Keyword/option keys: in a
key: valuepair onlyvalueis traversed, never the atomkey.
Callers can prune additional framework DSL calls by passing a list of
call names (atoms) in context[:skip_calls]. For example,
[:attr, :slot, :scope, :pipeline] removes Phoenix component and router
DSL noise. See Muex.Config presets.
Parameters
ast- The AST to traversemutators- List of mutator modules to applycontext- Context map. Recognised keys::file- source file path, copied into each mutation's location:line- starting line (defaults to0); updated as the walk descends:skip_calls- extra call names (atoms) to prune
Returns
List of all mutations found in the AST. Each mutation is augmented with
:original_ast (the matched node), used later during application.