# `Mutare.Mutator`
[🔗](https://github.com/foxbenjaminfox/mutare/blob/v0.2.1/lib/mutare/mutator.ex#L1)

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 `c: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 skip.

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
    end

Two rules are important:

  * Keep every replacement compile-safe. Mutare compiles one shared metamutant containing all emitted mutants.
  * Do not choose delivery placement. A mutation is delivered in place or through lifting according to 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 `c: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 `c:finalize/2`. Mutare calls it just before recording every produced mutation, including `mutate/1`/`mutate/2` returns and hosted targets' `:mutants`.

Structural mutators use the context-aware structural arity instead: `c:Mutare.Mutator.Structural.return_replacements/2`, `c:Mutare.Mutator.Structural.condition_replacements/2`, or `c:Mutare.Mutator.Structural.pattern_mutations/3`.

A mutator that changes atom-like keys may implement `c:mutate_call_option_keys?/1` to control mutation of 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`, or `c:Mutare.CallRouting.call_routes/0`) is
    transform-enforced and positional: `:skip` makes a whole call an inert leaf, `:raw` leaves an
    argument as written, `:interior` mutates 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 — see `Mutare.CallRouting`.
  * **Marks** (`argument_marks:` in `.mutare.exs`, or `c:argument_marks/1`) are labels the
    transform attaches to positions and passes to mutators as `context.marks`. Each mutator
    implements its own handling of those labels. At `:timeout` positions,
    `Mutare.Mutators.IntegerLiteral` skips integers and `Mutare.Mutators.AtomLiteral` skips only
    `:infinity`, while other families still apply — 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 is defined by 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 `c:Mutare.CallRouting.call_routes/0`. Routes may be static or use `:routing` with `c: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 `c: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 `c:Mutare.Mutator.MacroHost.hosted_macros/0`, and delivers foreign-DSL mutations through `c: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, calls the matching callback on each enabled mutator that exports it, 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
    end

Structural callbacks have context-aware arities carrying the same behaviour set: `c:Mutare.Mutator.Structural.return_replacements/2`, `c:Mutare.Mutator.Structural.condition_replacements/2`, and `c: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 handles); 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`.

# `context`

```elixir
@type context() :: %{
  :pipe_mode =&gt; pipe_mode(),
  optional(:name) =&gt; atom(),
  optional(:opts) =&gt; term(),
  optional(:config) =&gt; term(),
  optional(:behaviours) =&gt; MapSet.t(module()),
  optional(:mutators) =&gt; [Mutare.Mutator.Spec.t()],
  optional(:marks) =&gt; MapSet.t(atom())
}
```

Context passed to `mutate/2` at each runtime call site.

  * `:pipe_mode` — `:piped` or `: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 (the `opts` of a
    `{module, opts}` entry in `:mutators`, with any `:as` name override
    stripped), or `[]` for an unconfigured mutator.
  * `:config` — the mutator's normalized configuration: what its `c:init/1`
    returned for those options, or the raw options themselves when the mutator
    does not export `init/1`.
  * `:behaviours` — the enclosing module's behaviour set: a `MapSet` of the modules it
    implements via `@behaviour Foo` (directly or injected by a `use`). Empty outside a
    module.
  * `:mutators` — present for selector hosts (`c:Mutare.Mutator.MacroHost.host/2`) and for
    the whole-call `mutate/2` offer of a **registered macro call** (a call some enabled
    mutator or extension registered via `Mutare.CallRouting`): the run's enabled
    `Mutare.Mutator.Spec`s (hosts included), for sub-contracting ordinary-Elixir islands the
    macro's routing left raw back to core's generation via
    `Mutare.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.

# `mark_declaration`

```elixir
@type mark_declaration() ::
  {module(), atom(), arity(), [non_neg_integer() | {:keyword, atom()}], atom()}
```

One argument-mark declaration: `{module, function, arity, positions, label}` — the shape
`c:argument_marks/1` returns and the `argument_marks:` option accepts. `positions` lists
effective argument indices and `{:keyword, key}` trailing-option keys.

# `mutation`

```elixir
@type mutation() :: Macro.t() | Mutare.Mutator.Mutation.t()
```

One element of a `c:mutate/1` or `c:mutate/2` return list.

  * A bare AST node is an ordinary replacement. A top-level bare `nil` item 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
    literal `nil`, return `Mutare.AST.literal(nil)`.
  * A `t:Mutare.Mutator.Mutation.t/0` carries 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 a `mutate/2` that rebuilds and returns an entire registered-macro
    call is reported at the specific inner clause it changed rather than at the
    call's line (see `Mutare.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`.

# `pipe_mode`

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

# `argument_marks`
*optional* 

```elixir
@callback argument_marks(config :: term()) :: [mark_declaration()]
```

Declares call-argument positions to **mark** for special treatment in `c:mutate/2`,
such as skipping a literal or applying a different mutation.

This is the general facility behind Mutare's "don't perturb an opaque literal" behaviour: the
special positions are defined in the mutator. 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 `c: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`).
No marks are declared for a mutator without this callback. `argument_marks_from/2` turns a
`{module, function, arity, positions}` list into declarations under a label.

# `finalize`
*optional* 

```elixir
@callback finalize(mutation(), context()) :: mutation() | :skip
```

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 `c:mutate/1`/`c:mutate/2` return list and each element of a
hosted target's `:mutants` (`c:Mutare.Mutator.MacroHost.host/2`) — with the same context
the producing callback received (including `context.config`, see `c:init/1`). Return:

  * a `t: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
`c: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.

# `init`
*optional* 

```elixir
@callback init(opts :: term()) :: term()
```

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` — `c:mutate/2`, the context-aware `Mutare.Mutator.Structural`
arities, and `c: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. `c: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`).

# `mutate`
*optional* 

```elixir
@callback mutate(Macro.t()) :: :skip | [mutation()]
```

Produces node-level mutations for `node`.

Return `:skip` when the mutator does not apply. Otherwise return one
`t: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.

# `mutate`
*optional* 

```elixir
@callback mutate(Macro.t(), context()) :: :skip | [mutation()]
```

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

# `mutate_call_option_keys?`
*optional* 

```elixir
@callback mutate_call_option_keys?(opts :: term()) :: boolean()
```

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 `c:mutate/2` because option-key detection happens after the key node has
already been offered to mutators.

# `name`

```elixir
@callback name() :: atom()
```

Short family name, shown in reports (e.g. `:arithmetic`).

# `required_modules`
*optional* 

```elixir
@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 (`c:Mutare.CallRouting.call_routes/0`) or hosts
(`c: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 `c: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 `c:init/1` (or from `c: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.

# `variant`
*optional* 

```elixir
@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
`c: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)

# `variants`
*optional* 

```elixir
@callback variants() :: [String.t() | atom()]
```

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)` from
    `c:mutate/1`/`c: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 `c:variant/2`, which classifies the `{original, mutated}`
    pair. Best when the label reads cleanly off the node (an operator family:
    `op_swap_variant/3` over the swapped operator).

A tagging mutator does not need `c: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.

# `argument_marks_from`

```elixir
@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 `c: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.

# `declared_labels`

```elixir
@spec declared_labels([Mutare.Mutator.Spec.t() | module()]) :: MapSet.t(atom())
```

The set of mark labels the given mutators declare positions for (`c: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 with an unused label.

    iex> Mutare.Mutator.declared_labels([Mutare.Mutators.IntegerLiteral]) |> MapSet.member?(:timeout)
    true

# `effective_arity`

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

# `marked?`

```elixir
@spec marked?(context(), atom()) :: boolean()
```

Whether the node being offered carries the position mark `label` — i.e. sits at a position some
mutator requested via `c: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)
    end

Total over a context with no marks (the common case).

# `op_swap_variant`

```elixir
@spec op_swap_variant(Macro.t(), Macro.t(), [atom()]) :: String.t() | nil
```

Classifies a binary operator swap for `c: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)

# `validate_argument_marks!`

```elixir
@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 `c: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}]

# `visible_index`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
