Skip to content

Commit

Permalink
Improve mutate ignore annotation to allow for usage on classes, metho…
Browse files Browse the repository at this point in the history
…ds and statements. Add support to ignore only specific mutators
  • Loading branch information
gehrisandro committed Nov 29, 2023
1 parent 20ef452 commit 95322ce
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 31 deletions.
51 changes: 47 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,12 +405,55 @@ vendor/bin/pest --mutate --parallel

## Ignoring Mutations

Sometimes, you may want to ignore a specific mutation or line of code. To do so, you may use the `@pest-ignore-mutation` annotation:
### Ignore for a single line

Sometimes, you may want to prevent a line from being mutated. To do so, you may use the `@pest-mutate-ignore` annotation:

```php
if($age >= 18) // @pest-mutate-ignore
// ...
];
```

If you want to ignore only a specific mutator, you can add a comma separated list of mutator names:

```php
if($age >= 18) // @pest-mutate-ignore: GreaterOrEqualToGreater
// ...
];
```

### Ignore for multiple lines

To ignore mutations on large parts of the code you can add the annotation to a class, method or statement to ignore all mutations within the elements scope.

To ignore only specific mutators, you can add a comma separated list of mutator names: `@pest-mutate-ignore: GreaterOrEqualToGreater,IfNegated`

#### Class level
```php
/**
* @pest-mutate-ignore
*/
class Test {
// ...
}
```

#### Method or function level
```php
if($age >= 18) // @pest-ignore-mutation
// ...
];
/**
* @pest-mutate-ignore
*/
public function test() {
// ...
}
```

#### Statement level
```php
/** @pest-mutate-ignore */
for($i = 0; $i < 10; $i++) {
// ...
}
```

Expand Down
10 changes: 5 additions & 5 deletions WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@
# Known Bugs

# Backlog Prio 1
- [ ] Properly support xdebug
- [ ] Run test that killed a mutation before first
- [ ] Run mutations in a reasonable order: New, Survived, NotCovered, Skipped, Killed (Survived first, if --stop-on-survived or --bail; NotCovered first, if --stop-on-uncovered)
- [ ] Automatically empty cache when package version changes / Maybe there is another approach: Use the same cache key per php file, but store a hash of the file content and the package version in the cache. If the hash changes, the cache is invalid.
- [ ] Automatically skip "Arch" and "Stressless" tests - wait for arch() and stress() aliases for test()
- [ ] Properly support xdebug
- [ ] What should we do with interfaces? ignore them completely?
- [ ] Finish: Disable mutations by annotation
- [ ] Run mutations in a reasonable order: New, Survived, NotCovered, Skipped, Killed (Survived first, if --stop-on-survived or --bail; NotCovered first, if --stop-on-uncovered)
- [ ] Run test that killed a mutation before first
- [ ] Log to file
- [ ] Automatically skip "Arch" and "Stressless" tests - wait for arch() and stress() aliases for test()

# Backlog Prio 2
- [ ] Add mutator to unwrap idn_to_ascii() and idn_to_utf8()
- [ ] Add array declaration mutators: https://stryker-mutator.io/docs/mutation-testing-elements/supported-mutators/#array-declaration
- [ ] Add empty block statement mutator?: https://stryker-mutator.io/docs/mutation-testing-elements/supported-mutators/#array-declaration
- [ ] Check if we have mutators which do the same mutation. For example: "true" to "false", and "return true" to "return false"
Expand Down
51 changes: 47 additions & 4 deletions docs/mutation-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,12 +395,55 @@ vendor/bin/pest --mutate --parallel

## Ignoring Mutations

Sometimes, you may want to ignore a specific mutation or line of code. To do so, you may use the `@pest-ignore-mutation` annotation:
### Ignore for a single line

Sometimes, you may want to prevent a line from being mutated. To do so, you may use the `@pest-mutate-ignore` annotation:

```php
if($age >= 18) // @pest-mutate-ignore
// ...
];
```

If you want to ignore only a specific mutator, you can add a comma separated list of mutator names:

```php
if($age >= 18) // @pest-mutate-ignore: GreaterOrEqualToGreater
// ...
];
```

### Ignore for multiple lines

To ignore mutations on large parts of the code you can add the annotation to a class, method or statement to ignore all mutations within the elements scope.

To ignore only specific mutators, you can add a comma separated list of mutator names: `@pest-mutate-ignore: GreaterOrEqualToGreater,IfNegated`

#### Class level
```php
/**
* @pest-mutate-ignore
*/
class Test {
// ...
}
```

#### Method or function level
```php
/**
* @pest-mutate-ignore
*/
public function test() {
// ...
}
```

#### Statement level
```php
if($age >= 18) // @pest-ignore-mutation
// ...
];
/** @pest-mutate-ignore */
for($i = 0; $i < 10; $i++) {
// ...
}
```

Expand Down
32 changes: 19 additions & 13 deletions src/Support/MutationGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Pest\Mutate\Contracts\Mutator;
use Pest\Mutate\Factories\NodeTraverserFactory;
use Pest\Mutate\Mutation;
use Pest\Support\Str;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
Expand Down Expand Up @@ -44,10 +45,14 @@ public function generate(
return $mutations;
}

$ignoreComments = [];
$mutatorsToIgnoreByLine = [];
foreach (explode(PHP_EOL, $contents) as $lineNumber => $line) {
if (str_contains($line, '// @pest-mutate-ignore')) {
$ignoreComments[] = ['line' => $lineNumber + 1, 'comment' => '// @pest-mutate-ignore'];
if (str_contains($line, '@pest-mutate-ignore')) {
if (Str::after($line, '@pest-mutate-ignore:') !== $line) {
$mutatorsToIgnore = explode(',', Str::after($line, '@pest-mutate-ignore:'));
$mutatorsToIgnore = array_map(fn (string $mutator): string => trim($mutator), $mutatorsToIgnore);
}
$mutatorsToIgnoreByLine[$lineNumber + 1] = $mutatorsToIgnore ?? ['all'];
}
}

Expand All @@ -72,6 +77,7 @@ public function generate(
mutator: $mutator,
linesToMutate: $linesToMutate,
offset: $this->offset,
mutatorsToIgnoreByLine: $mutatorsToIgnoreByLine,
hasAlreadyMutated: $this->hasMutated(...),
trackMutation: $this->trackMutation(...),
));
Expand Down Expand Up @@ -100,16 +106,16 @@ public function generate(
];
}

// filter out mutations that are ignored
$mutations = array_filter($mutations, function (Mutation $mutation) use ($ignoreComments): bool {
foreach ($ignoreComments as $comment) {
if ($comment['line'] === $mutation->startLine) {
return false;
}
}

return true;
});
// // filter out mutations that are ignored
// $mutations = array_filter($mutations, function (Mutation $mutation) use ($ignoreComments): bool {
// foreach ($ignoreComments as $comment) {
// if ($comment['line'] === $mutation->startLine) {
// return false;
// }
// }
//
// return true;
// });

// sort mutations by line number
usort($mutations, fn (Mutation $a, Mutation $b): int => $a->startLine <=> $b->startLine);
Expand Down
33 changes: 33 additions & 0 deletions src/Support/NodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,40 @@ class NodeVisitor extends NodeVisitorAbstract

/**
* @param array<int, int> $linesToMutate
* @param array<int, array<int, string>> $mutatorsToIgnoreByLine
* @param callable $hasAlreadyMutated
* @param callable $trackMutation
*/
public function __construct(
private readonly string $mutator,
private readonly int $offset,
private readonly array $linesToMutate,
private readonly array $mutatorsToIgnoreByLine,
private $hasAlreadyMutated, // @pest-ignore-type
private $trackMutation, // @pest-ignore-type
) {
}

public function enterNode(Node $node): Node|int|null
{
if ($node->getAttribute('comments') !== 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] === '') {
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)) {
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
}
}
}

return null;
}

public function leaveNode(Node $node): Node|int|null
{
if (($this->hasAlreadyMutated)()) {
Expand All @@ -40,6 +62,17 @@ public function leaveNode(Node $node): Node|int|null
return null;
}

if (isset($this->mutatorsToIgnoreByLine[$node->getStartLine()])) {
foreach ($this->mutatorsToIgnoreByLine[$node->getStartLine()] as $ignore) {
if ($ignore === 'all') {
return null;
}
if ($ignore === $this->mutator::name()) {
return null;
}
}
}

if ($this->mutator::can($node)) {
$originalNode = clone $node;
$mutatedNode = $this->mutator::mutate($node);
Expand Down
2 changes: 1 addition & 1 deletion tests/Fixtures/Classes/SizeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ class SizeHelper
{
public static function isBig(int $size): bool
{
return $size >= 100; // @pest-mutate-ignore
return $size >= 100;
}
}
Loading

0 comments on commit 95322ce

Please sign in to comment.