From cf55128be85c031c6289784d293bc6a1d903cfe1 Mon Sep 17 00:00:00 2001 From: Brent Passmore Date: Thu, 1 Oct 2026 20:05:22 -0500 Subject: [PATCH] fix: honor a bare @pest-mutate-ignore regardless of trailing text - Trim the captured tail before testing it for empty, so a trailing space or a single-line doc block's closing `*/` no longer makes a bare annotation suppress nothing. - Reset the per-line mutator list in the line scan, so a bare annotation means all mutators instead of inheriting the list from an earlier `@pest-mutate-ignore:` line. - Add tests for both, and for an unknown mutator name still suppressing nothing. Fixes defects 1 and 2 of pestphp/pest#1942. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Support/MutationGenerator.php | 2 + src/Support/NodeVisitor.php | 6 ++- tests/Unit/MutationGeneratorTest.php | 61 ++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/Support/MutationGenerator.php b/src/Support/MutationGenerator.php index cebc859..e5642fd 100644 --- a/src/Support/MutationGenerator.php +++ b/src/Support/MutationGenerator.php @@ -47,6 +47,8 @@ public function generate( $mutatorsToIgnoreByLine = []; foreach (explode(PHP_EOL, $contents) as $lineNumber => $line) { if (str_contains($line, '@pest-mutate-ignore')) { + $mutatorsToIgnore = null; + if (Str::after($line, '@pest-mutate-ignore:') !== $line) { $mutatorsToIgnore = explode(',', Str::after($line, '@pest-mutate-ignore:')); $mutatorsToIgnore = array_map(trim(...), $mutatorsToIgnore); diff --git a/src/Support/NodeVisitor.php b/src/Support/NodeVisitor.php index a1e5fc1..ca4e6a6 100644 --- a/src/Support/NodeVisitor.php +++ b/src/Support/NodeVisitor.php @@ -33,11 +33,13 @@ public function enterNode(Node $node): Node|int|null foreach ($node->getAttribute('comments') as $comment) { // @phpstan-ignore-line preg_match('/@pest-mutate-ignore(.*)/', (string) $comment->getText(), $matches); // @phpstan-ignore-line if ($matches !== []) { - if ($matches[1] === '') { + $mutatorsToIgnore = trim($matches[1], " \t\n\r*/:"); + + if ($mutatorsToIgnore === '') { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } - if (in_array($this->mutator::name(), array_map(fn (string $mutatorToIgnore): string => trim($mutatorToIgnore, ' */:'), explode(',', $matches[1])), true)) { + if (in_array($this->mutator::name(), array_map(fn (string $mutatorToIgnore): string => trim($mutatorToIgnore, ' */:'), explode(',', $mutatorsToIgnore)), true)) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } } diff --git a/tests/Unit/MutationGeneratorTest.php b/tests/Unit/MutationGeneratorTest.php index 79c67ed..89e578f 100644 --- a/tests/Unit/MutationGeneratorTest.php +++ b/tests/Unit/MutationGeneratorTest.php @@ -286,3 +286,64 @@ function test() { expect($mutations) ->toBeEmpty(); }); + +it('ignores all mutators for a statement when the bare annotation has trailing whitespace', function (): void { + // The trailing space is built explicitly so a formatter cannot strip it from the fixture. + $mutations = ($this->generate)( + "= \$b;\n", + [GreaterOrEqualToGreater::class], + ); + + expect($mutations) + ->toBeEmpty(); +}); + +it('ignores all mutators for a function with a bare single line doc block', function (): void { + $mutations = ($this->generate)(<<<'PHP' + = $b) { + return $a = $b; + } + } + PHP, + [GreaterOrEqualToGreater::class, IfNegated::class], + ); + + expect($mutations) + ->toBeEmpty(); +}); + +it('does not carry a mutator list over to a later bare annotation', function (): void { + $mutations = ($this->generate)(<<<'PHP' + $b) { // @pest-mutate-ignore: IfNegated + $a = $b; + } + + return $a >= $b; // @pest-mutate-ignore + PHP, + [GreaterOrEqualToGreater::class, IfNegated::class], + ); + + expect($mutations) + ->toBeEmpty(); +}); + +it('does not ignore a mutator that is not named', function (string $annotation): void { + $mutations = ($this->generate)( + "= \$b;\n", + [GreaterOrEqualToGreater::class], + ); + + expect($mutations) + ->toHaveCount(1) + ->{0}->mutator->toBe(GreaterOrEqualToGreater::class); +})->with([ + 'unknown name' => ['// @pest-mutate-ignore: NotAMutator'], + 'unknown name with trailing whitespace' => ['// @pest-mutate-ignore: NotAMutator '], + 'unknown name in a doc block' => ['/** @pest-mutate-ignore: NotAMutator */'], +]);