From 95e62ef22733ffc6d74a17e7b15d14355948e345 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 3 Nov 2025 09:34:22 +0100 Subject: [PATCH 1/4] Implement LooseBooleanMutator --- resources/infection.json5 | 1 + src/Infection/LooseBooleanMutator.php | 70 ++++++++++++++++++ tests/Infection/LooseBooleanMutatorTest.php | 81 +++++++++++++++++++++ tests/phpt/infection-config-default.phpt | 1 + tests/phpt/infection-config.phpt | 1 + 5 files changed, 154 insertions(+) create mode 100644 src/Infection/LooseBooleanMutator.php create mode 100644 tests/Infection/LooseBooleanMutatorTest.php diff --git a/resources/infection.json5 b/resources/infection.json5 index 52ebb55..c26e837 100644 --- a/resources/infection.json5 +++ b/resources/infection.json5 @@ -12,6 +12,7 @@ }, "mutators": { "@default": false, + "PHPStan\\Infection\\LooseBooleanMutator": true, "PHPStan\\Infection\\TrinaryLogicMutator": true }, "bootstrap": "build-infection/vendor/autoload.php" diff --git a/src/Infection/LooseBooleanMutator.php b/src/Infection/LooseBooleanMutator.php new file mode 100644 index 0000000..4f2351a --- /dev/null +++ b/src/Infection/LooseBooleanMutator.php @@ -0,0 +1,70 @@ + + */ +final class LooseBooleanMutator implements Mutator +{ + + public static function getDefinition(): Definition + { + return new Definition( + <<<'TXT' + Replaces boolean Type->isTrue()/isFalse() with Type->toBoolean()->isTrue()/isFalse() to check loose comparisons coverage. + TXT + , + MutatorCategory::ORTHOGONAL_REPLACEMENT, + null, + <<<'DIFF' + - $type->isFalse()->yes(); + + $type->isBoolean()->isFalse()->yes(); + DIFF, + ); + } + + public function getName(): string + { + return self::class; + } + + public function canMutate(Node $node): bool + { + if (!$node instanceof Node\Expr\MethodCall) { + return false; + } + + if (!$node->name instanceof Node\Identifier) { + return false; + } + + if (!in_array($node->name->name, ['isTrue', 'isFalse'], true)) { + return false; + } + + if ($node->var instanceof Node\Expr\MethodCall) { + if ( + $node->var->name instanceof Node\Identifier + && in_array($node->var->name->name, ['toBoolean'], true) + ) { + return false; + } + } + + return true; + } + + public function mutate(Node $node): iterable + { + $node->var = new Node\Expr\MethodCall($node->var, 'toBoolean'); + yield $node; + } + +} diff --git a/tests/Infection/LooseBooleanMutatorTest.php b/tests/Infection/LooseBooleanMutatorTest.php new file mode 100644 index 0000000..de6468a --- /dev/null +++ b/tests/Infection/LooseBooleanMutatorTest.php @@ -0,0 +1,81 @@ +assertMutatesInput($input, $expected); + } + + /** + * @return iterable + */ + public static function mutationsProvider(): iterable + { + yield 'It mutates isFalse() into loose comparison' => [ + <<<'PHP' + isFalse()->yes(); + PHP +, + <<<'PHP' + toBoolean()->isFalse()->yes(); + PHP +, + ]; + + yield 'It mutates isTrue() into loose comparison' => [ + <<<'PHP' + isTrue()->yes(); + PHP +, + <<<'PHP' + toBoolean()->isTrue()->yes(); + PHP +, + ]; + + yield 'It skips already toBoolean() calls to prevent double repetition' => [ + <<<'PHP' + toBoolean()->isTrue()->yes(); + PHP +, + ]; + + yield 'It skips non boolean Type calls' => [ + <<<'PHP' + isObject()->yes(); + PHP +, + ]; + } + + protected function getTestedMutatorClassName(): string + { + return LooseBooleanMutator::class; + } + +} diff --git a/tests/phpt/infection-config-default.phpt b/tests/phpt/infection-config-default.phpt index cd698df..ddb44db 100644 --- a/tests/phpt/infection-config-default.phpt +++ b/tests/phpt/infection-config-default.phpt @@ -19,6 +19,7 @@ echo shell_exec($bin); }, "mutators": { "@default": false, + "PHPStan\\Infection\\LooseBooleanMutator": true, "PHPStan\\Infection\\TrinaryLogicMutator": true }, "bootstrap": "build-infection\/vendor\/autoload.php" diff --git a/tests/phpt/infection-config.phpt b/tests/phpt/infection-config.phpt index 763087b..99db7ff 100644 --- a/tests/phpt/infection-config.phpt +++ b/tests/phpt/infection-config.phpt @@ -20,6 +20,7 @@ echo shell_exec($bin." --source-directory='more/files/' --timeout=180 --mutator- }, "mutators": { "@default": false, + "PHPStan\\Infection\\LooseBooleanMutator": true, "PHPStan\\Infection\\TrinaryLogicMutator": true, "My\\Class": true }, From c0a9e2b7685ab49ed18e6e0843e5bb09871cc7d1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 3 Nov 2025 09:42:47 +0100 Subject: [PATCH 2/4] ignore isTrue()/isFalse() with arguments --- src/Infection/LooseBooleanMutator.php | 5 +++++ tests/Infection/LooseBooleanMutatorTest.php | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Infection/LooseBooleanMutator.php b/src/Infection/LooseBooleanMutator.php index 4f2351a..485152e 100644 --- a/src/Infection/LooseBooleanMutator.php +++ b/src/Infection/LooseBooleanMutator.php @@ -6,6 +6,7 @@ use Infection\Mutator\Mutator; use Infection\Mutator\MutatorCategory; use PhpParser\Node; +use function count; use function in_array; /** @@ -49,6 +50,10 @@ public function canMutate(Node $node): bool return false; } + if (count($node->getArgs()) !== 0) { + return false; + } + if ($node->var instanceof Node\Expr\MethodCall) { if ( $node->var->name instanceof Node\Identifier diff --git a/tests/Infection/LooseBooleanMutatorTest.php b/tests/Infection/LooseBooleanMutatorTest.php index de6468a..247d163 100644 --- a/tests/Infection/LooseBooleanMutatorTest.php +++ b/tests/Infection/LooseBooleanMutatorTest.php @@ -69,6 +69,22 @@ public static function mutationsProvider(): iterable $type->isObject()->yes(); PHP +, + ]; + + yield 'skip isTrue() with arguments' => [ + <<<'PHP' + isTrue($b, $c); + PHP +, + ]; + + yield 'skip isFalse() with arguments' => [ + <<<'PHP' + isFalse($b, $c); + PHP , ]; } From b6c171df2dea3fa79d0226f80605dc895850c173 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 3 Nov 2025 09:44:27 +0100 Subject: [PATCH 3/4] Update LooseBooleanMutatorTest.php --- tests/Infection/LooseBooleanMutatorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Infection/LooseBooleanMutatorTest.php b/tests/Infection/LooseBooleanMutatorTest.php index 247d163..ea50008 100644 --- a/tests/Infection/LooseBooleanMutatorTest.php +++ b/tests/Infection/LooseBooleanMutatorTest.php @@ -75,7 +75,7 @@ public static function mutationsProvider(): iterable yield 'skip isTrue() with arguments' => [ <<<'PHP' isTrue($b, $c); + $a->isTrue($b); PHP , ]; From 49d572e2a13e7452a875bab313e469f01f5b19a7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 3 Nov 2025 09:45:07 +0100 Subject: [PATCH 4/4] cs --- tests/Infection/LooseBooleanMutatorTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Infection/LooseBooleanMutatorTest.php b/tests/Infection/LooseBooleanMutatorTest.php index ea50008..9f77336 100644 --- a/tests/Infection/LooseBooleanMutatorTest.php +++ b/tests/Infection/LooseBooleanMutatorTest.php @@ -75,6 +75,7 @@ public static function mutationsProvider(): iterable yield 'skip isTrue() with arguments' => [ <<<'PHP' isTrue($b); PHP , @@ -83,6 +84,7 @@ public static function mutationsProvider(): iterable yield 'skip isFalse() with arguments' => [ <<<'PHP' isFalse($b, $c); PHP ,