Muex.Mutator behaviour (Muex v0.6.1)

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.

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:

  1. At generation time — set equivalent: true in the mutation map returned by mutate/2. Use this when the mutator knows at generation time that the mutation is equivalent (e.g., swapping arguments to a commutative operator).

  2. Via the equivalent?/1 callback — implement this for more complex analysis that needs to inspect the full mutation map. The default implementation checks the :equivalent key.

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 through an AST and applies all registered mutators.

Types

mutation()

@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

description()

@callback description() :: String.t()

Returns a description of what this mutator does.

equivalent?(mutation)

(optional)
@callback equivalent?(mutation :: mutation()) :: boolean()

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.

mutate(ast, context)

@callback mutate(ast :: term(), context :: map()) :: [mutation()]

Applies mutations to the given AST.

Parameters

  • ast - The AST to mutate
  • context - Map containing additional context (file path, line number, etc.)

Returns

List of mutation maps, each representing a possible mutation

name()

@callback name() :: String.t()

Returns the name of the mutator.

supported_languages()

@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

equivalent?(mutation)

@spec equivalent?(mutation()) :: boolean()

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.

walk(ast, mutators, context)

@spec walk(ast :: term(), mutators :: [module()], context :: map()) :: [mutation()]

Walks through an AST and applies all registered mutators.

Parameters

  • ast - The AST to traverse
  • mutators - List of mutator modules to apply
  • context - Context map with file information

Returns

List of all possible mutations found in the AST