Repository navigation
Fix crash on blocks using the it parameter - #1674
Merged
Merged
Conversation
Ruby 3.4 parses a block using the `it` parameter, such as `bar { it }`,
into an itblock node. Mutant had no structure for that node type. The
block mutator walks the body of a block without parameters to find
`break` and `next`, so mutating `foo { bar { it } }` failed with:
KeyError: key not found: :itblock
Before: KeyError
After: nil, foo, foo {}, foo { raise }, foo { nil }, bar { it }
* itblock nodes now have a structure, so walking a body that contains
one works.
* itblock nodes now get their own mutator instead of the generic one.
The generic mutator mutates every child, so it emits invalid
mutations, such as `nil { it.price }`, and body mutations that
remove every `it`, such as `items.map { nil }`. That prints as a
block without parameters and parses back as a block node, not an
itblock. A run discards invalid mutations silently, but meta examples
reject a mutator that emits them, so the generic mutator could not be
covered by meta examples.
* The new mutator emits what the numblock mutator emits, nil and the
mutations of the receiving send, plus the body mutations that still
use `it`. An `it` in a nested block or method definition belongs to
that block or method, so for those only the first child counts: the
receiving send of a block, the receiver of `def obj.bar`, and nothing
for `def bar`, whose first child is its name. Ruby allows `it` in a
nested block only as that block's own parameter: it is a syntax
error next to other parameters or numbered parameters, and a block
without parameters that uses `it` becomes an itblock itself.
foo { it.bar(1) { it } } emits foo { it.bar { it } }
filters out foo { self.bar(1) { it } }
foo { it.bar { |it| it } } filters out foo { self.bar { |it| it } }
foo { it; def bar(it) = it } filters out foo { def bar(it) = it }
* class, module and `class << obj` bodies start a new scope too, but
the filter still searches them. An `it` block containing
`class Bar; it = 1; it; end` emits 2 mutations that remove the
outer `it`, such as `foo { class Bar; it = 1; it; end }`, which no
longer parse back as an itblock. `mutant run` discards them silently
and `mutant util mutation` reports them as invalid.
Compared to the generic mutator, the valid mutations are the same
plus nil:
items.map { it.price * 2 }
Before: 11 mutations, such as items.each { it.price * 2 } and
items.map { it.price * 3 }
After: the same 11, plus nil
* The mutator, and the examples, load only when RUBY_VERSION is above
3.4.0. Unparser parses with the parser gem up to Ruby 3.4.0 and with
Prism after it, and the parser gem never builds itblock nodes, so
there the mutator would never run. Loaded anyway, nothing exercises
it, and the `mutant run --since HEAD~1` CI job on ruby-3.3 would
report its mutations alive. Ignoring the subject in mutant.yml would
stop that check on Ruby 3.4.1 and later too, and such entries outlive
their reason: the "alive for ruby < 3.1" entries remain although the
gemspec requires Ruby >= 3.3.
mbj
approved these changes
Oct 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
foo { bar { it } }raisedKeyError: key not found: :itblock. Now it mutates tonil,foo,foo {},foo { raise },foo { nil }andbar { it }.itblocks get their own mutator. It emitsniland the mutations of the receiving send. It also emits body mutations, but only those that still useit.items.map { it.price * 2 }keeps its 11 mutations and gainsnil.Why
Without its own mutator, an
itblock falls back to the generic one. The generic mutator mutates every child. Some results are invalid, such asnil { it.price }. Meta examples reject a mutator that emits invalid mutations.A body without
itturns the block into a plain block.items.map { nil }is one example. So the filter keeps only body mutations that useit.An
itinside a nested block belongs to that block. The same goes for a method definition. So the filter skips their bodies:foo { it.bar(1) { it } }foo { it.bar { it } }foo { it.bar(1) { it } }foo { self.bar(1) { it } }foo { it.bar { |it| it } }foo { self.bar { |it| it } }foo { it; def bar(it) = it }foo { def bar(it) = it }Up to Ruby 3.4.0, Unparser parses with the parser gem. The parser gem never builds
itblock nodes. The mutator would never run there. The ruby-3.3mutant run --sincejob would then report its mutations alive.