Behaviour for mutators: modules that produce AST replacements.
A mutator examines an AST node and returns either :skip or a list of mutations to generate at that site. Every mutator defines name/0 and at least one mutation-producing callback.
You must define name/0 to identify the mutator in reports, and at least one mutation-producing callback. The usual producer is mutate/1 (or the pipe-aware/configurable mutate/2), but a Mutare.Mutator.Structural hook or a Mutare.Mutator.MacroHost.host/2 selector host counts too — a mutator that produces only through one of those needs no mutate/1. Optionally, you may implement variants/0 and variant/2 to classify your mutations into kinds, mutate_call_option_keys?/1 to control mutations of call-option names, and argument_marks/1 to have the transform mark specific call-argument positions (e.g. timeout literals) that you then recognise with marked?/2 in mutate/2 and decline.
When both mutate/1 and mutate/2 are exported, Mutare calls mutate/2. If a mutator needs both context-free and context-aware production, call the context-free helper explicitly from mutate/2.
Structural positions use Mutare.Mutator.Structural. Macro-aware mutators use Mutare.CallRouting, and mutators that emit mutations inside hosted DSL fragments also use Mutare.Mutator.MacroHost.
Writing a mutator
Match the node shapes to mutate and rebuild them with the changed node. Reuse the original operand AST when possible so the mutation stays small:
defmodule MyApp.Mutators.AndOr do
@behaviour Mutare.Mutator
@impl true
def name, do: :and_or
@impl true
def mutate({:and, meta, [left, right]}), do: [{:or, meta, [left, right]}]
def mutate({:or, meta, [left, right]}), do: [{:and, meta, [left, right]}]
def mutate(_node), do: :skip
endTwo rules are important:
- Keep every replacement compile-safe. Mutare compiles one shared metamutant containing all emitted mutants.
- Do not choose delivery placement. The transform decides whether a mutation is delivered in place or through lifting based on where the node appears.
Build literal replacements with Mutare.AST.literal/1. Use Mutare.Calls.resolved_call_to/3 when matching aliased or imported calls.
Registering a mutator
List it under :mutators in .mutare.exs alongside, or instead of, built-in family atoms:
[mutators: [:arithmetic, :relational, MyApp.Mutators.AndOr]]Configuring a mutator ({module, opts})
Register a configurable mutator as {module, opts}. The options are available as context.opts. Node-level configurable mutators implement mutate/2:
defmodule MyApp.Mutators.MagicNumber do
@behaviour Mutare.Mutator
def name, do: :magic_number
def mutate({:__block__, _m, [n]}, %{opts: opts}) when is_integer(n) do
case Keyword.get(opts, :swaps, %{})[n] do
nil -> :skip
to -> [{:__block__, [], [to]}]
end
end
def mutate(_node, _context), do: :skip
end
# .mutare.exs
[mutators: [:arithmetic, {MyApp.Mutators.MagicNumber, swaps: %{200 => 500}}]]The reserved :as key changes the recorded family name, allowing the same module to run more than once under distinct names. It is removed before options reach the mutator. See Mutare.Mutator.Spec.
A mutator with a rich option surface implements init/1 to parse and validate its options once, when the instance is resolved — before any file is read — instead of re-reading context.opts at every offered node. The value init/1 returns reaches every context-aware callback as context.config; a typo'd option raises at startup, next to Mutare's own option validation. Without init/1, context.config is the raw options. For the common "which of my families are enabled" option, see Mutare.Mutator.Families.
A mutator that must post-process everything it produces — typically to apply that family selection and attach per-family report notes — implements finalize/2, which Mutare applies to every produced mutation on every delivery path (a mutate/1/mutate/2 return and a hosted target's :mutants) just before recording, so the funnel cannot miss a delivery site.
Structural mutators use the context-aware structural arity instead: Mutare.Mutator.Structural.return_replacements/2, Mutare.Mutator.Structural.condition_replacements/2, or Mutare.Mutator.Structural.pattern_mutations/3.
A mutator that changes atom-like keys may implement mutate_call_option_keys?/1 to decide whether to mutate a trailing call-option key such as timeout: in foo(x, timeout: 5). This policy belongs to the mutator because a context-free atom replacement may turn an option name into an unknown key, while a call-aware family may replace one legal option key with another.
[mutators: [..., {Mutare.Mutators.AtomLiteral, call_option_keys: false}]]Leaving argument positions alone
Two user-facing facilities cover "don't mutate here", and a mutator author meets both from the other side:
Routing (
call_routes:in.mutare.exs, orMutare.CallRouting.call_routes/0) is transform-enforced and positional::skipmakes a whole call an inert leaf,:rawleaves an argument as written,:interiormutates an argument's contents but not its own node, and a keyed refinement ([:expression, timeout: :raw]) reaches one option value of a literal keyword argument. No mutator is consulted — seeMutare.CallRouting.Marks (
argument_marks:in.mutare.exs, orargument_marks/1) are labels the transform stamps on positions and hands to mutators ascontext.marks; each mutator decides what a label means. This is how the built-in timeout table works —:timeoutpositions makeMutare.Mutators.IntegerLiteraldecline any integer andMutare.Mutators.AtomLiteraldecline only:infinity, while every other family proceeds — and a user extends the same table for their own functions with the same declaration shape:[argument_marks: [ {MyApp.Cache, :put, 3, [2], :timeout}, # a positional argument {MyApp.Http, :get, 2, [{:keyword, :recv_timeout}], :timeout} # a trailing-option value ]]
A mark's meaning lives entirely in the mutators that read it (marked?/2), so a configured label
must be one some enabled mutator declares positions for (declared_labels/1) — a typo fails at
startup. Reach for a route when the position should simply not mutate; reach for a mark when the
reaction should depend on the value (only a duration-shaped literal held back, a computed
duration's sub-expressions still mutating).
Targeting a macro or DSL
A mutator whose mutation depends on a macro's arguments being routed specially implements Mutare.CallRouting and registers the macros from Mutare.CallRouting.call_routes/0. Routes may be static or use :routing with Mutare.CallRouting.route_arguments/2 for shape-aware classification. Listing the mutator in :mutators auto-registers them.
Core still offers the whole registered call to mutate/2, with context.mutators carrying the run's enabled specs — so a mutator that keeps a DSL argument raw (:raw) can rewrite the call itself and sub-contract the ordinary-Elixir islands inside that raw argument back to core's generation via Mutare.Analyze.expression_mutations/3, relaying each rebuild as a Mutare.Mutator.Mutation with producer: set (see Mutare.Analyze — the island is analyzed with the full set, so another mutator's registered macro inside it is offered to its owner the same way).
A mutator that produces mutations inside a compile-time DSL additionally implements Mutare.Mutator.MacroHost, subscribes with Mutare.Mutator.MacroHost.hosted_macros/0, and delivers foreign-DSL mutations through Mutare.Mutator.MacroHost.host/2. It need not own the DSL's routing: a separate extension may declare the :hosted position, and several hosts may subscribe to it. See the "which behaviours do I implement?" table in Mutare.CallRouting.
Structural mutators at routed positions (Mutare.Mutator.Structural)
Some targets are positions rather than individual nodes: a def/defp return tail, an if/unless/cond condition, or a structural pattern position. Pattern positions include def/defp heads, clause patterns, destructuring match patterns, and routed :binding_pattern macro arguments. Those callbacks live on Mutare.Mutator.Structural. A structural mutator declares both behaviours and implements the relevant structural callback.
The transform identifies the position, asks each enabled mutator that exports the matching callback, and records each emitted mutation under that mutator's name. Built-in examples are Mutare.Mutators.ReturnValue, Mutare.Mutators.IfCondition, and Mutare.Mutators.PatternSwap.
Behaviour-targeted mutators (context.behaviours)
A mutator can depend on behaviours implemented by the enclosing module. The behaviour set is available as context.behaviours, a MapSet of module atoms gathered from direct @behaviour attributes and use-injected behaviours.
defmodule MyApp.Mutators.GenServerReply do
@behaviour Mutare.Mutator
def name, do: :genserver_reply
def mutate({:{}, m, [{:__block__, am, [:reply]}, _r, state]}, %{behaviours: bs}) do
if MapSet.member?(bs, GenServer),
do: [{:{}, m, [{:__block__, am, [:noreply]}, state]}],
else: :skip
end
def mutate(_node, _context), do: :skip
endStructural callbacks have context-aware arities carrying the same behaviour set: Mutare.Mutator.Structural.return_replacements/2, Mutare.Mutator.Structural.condition_replacements/2, and Mutare.Mutator.Structural.pattern_mutations/3. These callbacks receive context.behaviours and context.opts, so export the context-aware arity when a structural mutation depends on behaviours or configuration.
Matching aliased or imported calls (Mutare.Calls)
A mutator that targets a standard-library or remote call uses Mutare.Calls.resolved_call_to/3 with the real module atom (and optionally the function names it owns); it returns {:ok, function, arguments, rebuild} for resolved qualified, aliased, imported, and Erlang-atom module calls, and rebuild emits the replacement in the same written form as the source. For table-driven matching across modules, Mutare.Calls.resolved_call/1 returns the raw resolved tuple, keyed by Mutare.Calls.module_key/1.
Summary
Types
Context passed to mutate/2 at each runtime call site.
One argument-mark declaration: {module, function, arity, positions, label} — the shape
argument_marks/1 returns and the argument_marks: option accepts. positions lists
effective argument indices and {:keyword, key} trailing-option keys.
A call node's pipe context, as an atom: :piped (the node is a |> right-hand
side, so its effective first argument is the pipe's left side) or :unpiped.
It is what the mutate/2 context's :pipe_mode carries, and the form
effective_arity/2 and visible_index/2 take.
Callbacks
Asks the transform to mark certain argument positions of certain calls, so this mutator can
recognise them at mutate/2 and decline to mutate there (or mutate differently).
Post-processes each produced mutation before it is recorded.
Parses the instance's options into its normalized configuration, once per resolved instance.
Produces node-level mutations for node.
Produces context-aware mutations for node.
Controls mutation of trailing keyword-option keys.
Short family name, shown in reports (e.g. :arithmetic).
Declares modules that must be loadable for this mutator's routing and hosting to be valid.
Derives variant labels from an emitted {original, mutated} pair.
Declares the variant labels this mutator supports in
# mutare:ignore[family:label] filters.
Functions
Turns a list of {module, function, arity, positions} entries into argument_marks/1
declarations under label. positions is a list of effective argument indices and
{:keyword, key} option keys, exactly as in a declaration; an index is validated against the
declared arity. Raises ArgumentError with a pointed message on a malformed entry, so a typo fails
at startup rather than silently marking nothing. The one-liner for a mutator that exposes its own
"also leave these positions alone" option under its label.
The set of mark labels the given mutators declare positions for (argument_marks/1) — the
labels that mean something to some enabled family. Mutare.Options checks a configured
argument_marks: entry's label against this set, so a typo'd or orphaned label fails at startup
instead of marking positions nobody reads.
Returns the effective arity of a call under its pipe context.
Whether the node being offered carries the position mark label — i.e. sits at a position some
mutator requested via argument_marks/1. The reader half of the marking facility: a mutate/2
checks this and returns :skip (or adapts) at a marked position.
Classifies a binary operator swap for variant/2.
Validate a user-facing list of argument-mark declarations — the argument_marks: option in
.mutare.exs, whose entries are exactly what argument_marks/1 returns:
{module, function, arity, positions, label}. Shape-checked here (a bad index or a non-atom
label fails at startup); whether label is one some configured mutator declares positions for
is checked once the mutator set is known (Mutare.Options.new/1).
Converts an effective argument index to the index in the call node's visible argument list.
Types
@type context() :: %{ :pipe_mode => pipe_mode(), optional(:name) => atom(), optional(:opts) => term(), optional(:config) => term(), optional(:behaviours) => MapSet.t(module()), optional(:mutators) => [Mutare.Mutator.Spec.t()], optional(:marks) => MapSet.t(atom()) }
Context passed to mutate/2 at each runtime call site.
:pipe_mode—:pipedor:unpiped; when piped, the effective first argument is the pipe's left side and is not present in the node's own args.:opts— the configured mutator's per-instance options (theoptsof a{module, opts}entry in:mutators, with any:asname override stripped), or[]for an unconfigured mutator.:config— the mutator's normalized configuration: what itsinit/1returned for those options, or the raw options themselves when the mutator does not exportinit/1.:behaviours— the enclosing module's behaviour set: aMapSetof the modules it implements via@behaviour Foo(directly or injected by ause). Empty outside a module.:mutators— present for selector hosts (Mutare.Mutator.MacroHost.host/2) and for the whole-callmutate/2offer of a registered macro call (a call some enabled mutator or extension registered viaMutare.CallRouting): the run's enabledMutare.Mutator.Specs (hosts included), for sub-contracting ordinary-Elixir islands the macro's routing left raw back to core's generation viaMutare.Analyze.expression_mutations/3— which lowers a nested host's targets to whole-call rebuilds instead of weaving them, so hosted delivery never nests while every surface (ordinary and hosted) participates. Absent on ordinary node offers — core fully descends an unregistered node itself, so sub-contracting there would produce the same mutant twice.
The keys other than :pipe_mode are optional in the type because the base context
carries only :pipe_mode; dispatch injects the configured options, the normalized
configuration, and the behaviour set (and, at the sub-contract seams above, the enabled
specs) before calling a mutator.
@type mark_declaration() :: {module(), atom(), arity(), [non_neg_integer() | {:keyword, atom()}], atom()}
One argument-mark declaration: {module, function, arity, positions, label} — the shape
argument_marks/1 returns and the argument_marks: option accepts. positions lists
effective argument indices and {:keyword, key} trailing-option keys.
@type mutation() :: Macro.t() | Mutare.Mutator.Mutation.t()
One element of a mutate/1 or mutate/2 return list.
- A bare AST node is an ordinary replacement. A top-level bare
nilitem is rejected because it is too easy to confuse with “no replacement”; filter inapplicable entries before returning the list. To replace a node with the literalnil, returnMutare.AST.literal(nil). - A
Mutare.Mutator.Mutation.t/0carries a replacement plus metadata such as a report note, an ignore variant, or an:attribution— a report-location override (Mutare.Mutator.Mutation.at/2/at_drop/1) for a whole-node rewrite, so amutate/2that rebuilds and returns an entire registered-macro call is reported at the specific inner clause it changed rather than at the call's line (seeMutare.Mutator.Mutation).
A plain map is not treated as mutation metadata because a quoted map is also a
valid AST replacement. Selector hosts use the same forms for :mutants.
@type pipe_mode() :: :piped | :unpiped
A call node's pipe context, as an atom: :piped (the node is a |> right-hand
side, so its effective first argument is the pipe's left side) or :unpiped.
It is what the mutate/2 context's :pipe_mode carries, and the form
effective_arity/2 and visible_index/2 take.
Callbacks
@callback argument_marks(config :: term()) :: [mark_declaration()]
Asks the transform to mark certain argument positions of certain calls, so this mutator can
recognise them at mutate/2 and decline to mutate there (or mutate differently).
This is the general facility behind Mutare's "don't perturb an opaque literal" behaviour: a
mutator, not the transform, owns the knowledge of which positions are special. The transform
stays domain-agnostic — it stamps label on the resolved position and surfaces it back as
context.marks (a MapSet of atoms); Mutare.Mutator.marked?/2 reads it. For example,
Mutare.Mutators.IntegerLiteral marks the millisecond/:infinity timeout arguments of Process.sleep,
GenServer.call, Task.await, Task.async_stream's :timeout option, … and skips them, so a
near-unkillable off-by-one on a duration is never minted.
Called once per resolved instance (like init/1) with that instance's config — so a mutator
can extend its built-in marks with positions from its own options. Return a list of declarations,
each naming a resolved call and the positions to mark with a label:
@impl true
def argument_marks(_config), do: timeout_marks()A declaration is {module, function, arity, positions, label}. A position is an effective
argument index (a piped receiver counts as index 0) or a {:keyword, key} for a trailing-options
key. Arity is effective too, so an option-bearing arity (Task.async_stream/3, /5) can be
marked while a same-named arity whose trailing argument is ordinary data (/4, the MFA
callback-args list) is left alone. Marks are resolved through the same alias/import machinery as
call matching, so aliased and imported forms are covered and a shadowing alias is not. Only the
named value node is marked, never an enclosing container, so unrelated mutations there (e.g. List
collapsing an options list) are untouched.
Two mutators marking the same position union their labels; the label is a shared vocabulary, so a
family can react to a label another declared (declare it too if that must survive the declarer
being disabled). Users extend the same tables from configuration — an argument_marks: entry has
exactly this shape, and its label must be one some enabled mutator declares (declared_labels/1).
A mutator without this callback asks for no marks. argument_marks_from/2 turns a
{module, function, arity, positions} list into declarations under a label.
Post-processes each produced mutation before it is recorded.
Mutare applies this hook to every mutation the mutator produces, on both delivery
paths — each element of a mutate/1/mutate/2 return list and each element of a
hosted target's :mutants (Mutare.Mutator.MacroHost.host/2) — with the same context
the producing callback received (including context.config, see init/1). Return:
- a
mutation/0— the (possibly rewrapped) mutation to record; :skip— drop this mutation.
Because Mutare guarantees the hook runs at every delivery site, a family-rich mutator
keeps its producers pure — return Mutare.Mutator.Mutation.tagged(node, [family | finer])
everywhere — and defines the tag → filter → enrich funnel once: finalize/2 reads the
leading variant label as the family, drops mutations of disabled families (see
Mutare.Mutator.Families), and attaches the family's report note. Delivery code shrinks
to pure production, and forgetting a site cannot silently deliver unfiltered, note-less
mutants.
Finalization is part of production, not reporting: it runs before overlap suppression and
# mutare:ignore filtering, and variant labels carried by the finalized mutation win over
variant/2 derivation exactly as at production. Two kinds of mutation are never
finalized: a relayed mutation carrying an explicit :producer
(see Mutare.Mutator.Mutation — it belongs to the producing family, whose own
finalize/2 already ran when the mutation was generated), and a
Mutare.Mutator.Structural hook's bare-AST replacements (they carry no metadata to
finalize). A target whose mutants all return :skip is dropped entirely.
A mutator without finalize/2 records mutations as returned.
Parses the instance's options into its normalized configuration, once per resolved instance.
Called when a :mutators entry is resolved to a Mutare.Mutator.Spec — before
the transform reads any file — with the instance's raw options (the opts of a
{module, opts} entry, :as already stripped; [] for a bare module). Raise
for invalid options: this is where a typo'd option fails loudly, at startup,
rather than on the first mutated node.
The return value is delivered to every context-aware callback as
context.config — mutate/2, the context-aware Mutare.Mutator.Structural
arities, and Mutare.Mutator.MacroHost.host/2. context.opts continues to
carry the raw options. A mutator without init/1 gets
context.config == context.opts.
A module listed more than once (the documented multi-instance :as pattern)
runs init/1 once per instance, each call receiving that entry's own
options. Mutare.CallRouting.route_arguments/2 is not config-aware: macro
routing is shared by every mutator that meets the routed call, so its
classification stays instance-independent by design (see Mutare.CallRouting).
Produces node-level mutations for node.
Return :skip when the mutator does not apply. Otherwise return one
mutation/0 entry per mutant. This callback is optional when a mutator
produces mutations only through mutate/2 or Mutare.Mutator.Structural.
Transform-managed families such as :guard_drop and :rescue_type are
registered for configuration and reporting but do not implement this callback.
Produces context-aware mutations for node.
This callback is used for pipe-aware, configurable, and behaviour-targeted
mutators. context.pipe_mode lets a mutator compute effective arity for piped
calls; context.opts carries per-instance configuration; context.behaviours
carries the enclosing module's behaviour set.
When both mutate/1 and mutate/2 are exported, this callback takes
precedence. Mutare does not also call mutate/1. To compose them, call
mutate/1 from mutate/2 and combine the results explicitly. The return
shape is the same as mutate/1.
Controls mutation of trailing keyword-option keys.
The transform calls this after identifying a candidate that mutates a call option
key such as timeout: in foo(timeout: 5). Return true to keep the
candidate or false to suppress it. A mutator without this callback keeps the
candidate.
The callback receives the mutator instance's configured options. It is separate
from mutate/2 because option-key detection happens after the key node has
already been offered to mutators.
@callback name() :: atom()
Short family name, shown in reports (e.g. :arithmetic).
@callback required_modules() :: [module()]
Declares modules that must be loadable for this mutator's routing and hosting to be valid.
A DSL plugin has a deployment requirement Mutare cannot infer: the library
whose macros it routes (Mutare.CallRouting.call_routes/0) or hosts
(Mutare.Mutator.MacroHost.hosted_macros/0) must be loadable in the Mutare
process — otherwise its routes register against nothing and its mutations
silently fail to fire. Declaring those modules here turns the silent
degradation into a loud startup error: the check runs once, when the
:mutators entry is resolved to a Mutare.Mutator.Spec (before init/1,
before any source is read), and a missing module aborts the run with a
Mutare.EnvironmentError naming the plugin, the missing modules, and the
deployment requirement.
@impl true
def required_modules, do: [Ecto.Schema, Ecto.Query]Loadability (Code.ensure_loaded?/1) is the whole check — it does not verify
that a module's application is started or that its version is compatible. A
plugin with a requirement beyond loadability raises its own descriptive error
from init/1 (or from Mutare.CallRouting.call_routes/0).
A non-mutating extension may export the same function — capability discovery
is by export, so Mutare.Extension.validate!/1 applies the same check to
:extensions entries. A module without this callback is assumed
environment-independent.
@callback variant(original :: Macro.t(), mutated :: Macro.t()) :: String.t() | atom() | [String.t() | atom()] | nil
Derives variant labels from an emitted {original, mutated} pair.
Return one label, a list of labels, or nil. Labels must be members of
variants/0 and are matched case-insensitively by ignore directives.
Classify from both nodes, not the mutated node alone. For example, a strip
mutation such as -(a + b) → a + b emits a + node but is not an operator
swap. Operator families can use op_swap_variant/3 for this pattern:
# in Mutare.Mutators.Relational
@swap_ops [:>, :>=, :<, :<=, :==, :!=, :===, :!==]
def variants, do: Enum.map(@swap_ops, &to_string/1)
def variant(original, mutated),
do: Mutare.Mutator.op_swap_variant(original, mutated, @swap_ops)
Declares the variant labels this mutator supports in
# mutare:ignore[family:label] filters.
A single source node may produce several sibling mutants. Variant labels allow a directive to suppress one kind without suppressing the whole family.
A mutator that declares variants assigns labels in one of two ways:
- Tag at production — return a
Mutare.Mutator.Mutation.tagged(node, label)frommutate/1/mutate/2, attaching the label where the mutant is built. Best when the kind is known at construction (a value family:tagged(AST.literal(0), "zero")). - Derive afterwards — implement
variant/2, which classifies the{original, mutated}pair. Best when the label reads cleanly off the node (an operator family:op_swap_variant/3over the swapped operator).
A tagging mutator does not need variant/2. A mutator with no variant
vocabulary supports only the bare [family] filter; a qualified filter against
it is an error.
Labels must be non-empty wire-safe tokens: no whitespace, ,, (, ), ],
or ". Treat labels as public API because users write them in source comments.
Functions
@spec argument_marks_from(term(), atom()) :: [ {module(), atom(), arity(), [non_neg_integer() | {:keyword, atom()}], atom()} ]
Turns a list of {module, function, arity, positions} entries into argument_marks/1
declarations under label. positions is a list of effective argument indices and
{:keyword, key} option keys, exactly as in a declaration; an index is validated against the
declared arity. Raises ArgumentError with a pointed message on a malformed entry, so a typo fails
at startup rather than silently marking nothing. The one-liner for a mutator that exposes its own
"also leave these positions alone" option under its label.
@spec declared_labels([Mutare.Mutator.Spec.t() | module()]) :: MapSet.t(atom())
The set of mark labels the given mutators declare positions for (argument_marks/1) — the
labels that mean something to some enabled family. Mutare.Options checks a configured
argument_marks: entry's label against this set, so a typo'd or orphaned label fails at startup
instead of marking positions nobody reads.
iex> Mutare.Mutator.declared_labels([Mutare.Mutators.IntegerLiteral]) |> MapSet.member?(:timeout)
true
@spec effective_arity([Macro.t()], pipe_mode()) :: non_neg_integer()
Returns the effective arity of a call under its pipe context.
A piped call stage has one implicit argument: the left side of the pipe. That
argument is not present in the call node's own argument list, so piped arity is
length(args) + 1.
iex> Mutare.Mutator.effective_arity([:a, :b], :unpiped)
2
iex> Mutare.Mutator.effective_arity([:b], :piped)
2
Whether the node being offered carries the position mark label — i.e. sits at a position some
mutator requested via argument_marks/1. The reader half of the marking facility: a mutate/2
checks this and returns :skip (or adapts) at a marked position.
def mutate(node, context) do
if Mutare.Mutator.marked?(context, :timeout), do: :skip, else: mutate(node)
endTotal over a context with no marks (the common case).
Classifies a binary operator swap for variant/2.
When original and mutated are both two-argument operator nodes whose heads
are in ops, returns the new operator as a string. Otherwise returns nil.
Checking both nodes prevents unary strip mutations from being classified as
binary swaps.
def variant(o, m), do: Mutare.Mutator.op_swap_variant(o, m, @swap_ops)
@spec validate_argument_marks!(term()) :: [ {module(), atom(), arity(), [non_neg_integer() | {:keyword, atom()}], atom()} ]
Validate a user-facing list of argument-mark declarations — the argument_marks: option in
.mutare.exs, whose entries are exactly what argument_marks/1 returns:
{module, function, arity, positions, label}. Shape-checked here (a bad index or a non-atom
label fails at startup); whether label is one some configured mutator declares positions for
is checked once the mutator set is known (Mutare.Options.new/1).
iex> Mutare.Mutator.validate_argument_marks!([{MyApp.Http, :get, 2, [{:keyword, :recv_timeout}], :timeout}])
[{MyApp.Http, :get, 2, [{:keyword, :recv_timeout}], :timeout}]
@spec visible_index(non_neg_integer(), pipe_mode()) :: non_neg_integer() | nil
Converts an effective argument index to the index in the call node's visible argument list.
In piped calls, effective index 0 is the pipe's left side and has no visible
index, so the function returns nil. Later indexes shift down by one. In
unpiped calls, effective and visible indexes are the same.
iex> Mutare.Mutator.visible_index(2, :unpiped)
2
iex> Mutare.Mutator.visible_index(0, :piped)
nil
iex> Mutare.Mutator.visible_index(1, :piped)
0