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..485152e --- /dev/null +++ b/src/Infection/LooseBooleanMutator.php @@ -0,0 +1,75 @@ + + */ +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 (count($node->getArgs()) !== 0) { + 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..9f77336 --- /dev/null +++ b/tests/Infection/LooseBooleanMutatorTest.php @@ -0,0 +1,99 @@ +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 +, + ]; + + yield 'skip isTrue() with arguments' => [ + <<<'PHP' + isTrue($b); + PHP +, + ]; + + yield 'skip isFalse() with arguments' => [ + <<<'PHP' + isFalse($b, $c); + 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 },