diff --git a/.gitattributes b/.gitattributes index c0c0e12f..c26292bd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,4 +8,5 @@ /CONTRIBUTING.md export-ignore /bin/ export-ignore /docs/ export-ignore +/src/PhpStan/ export-ignore /tests/ export-ignore diff --git a/Build/phpstan/phpstan.neon b/Build/phpstan/phpstan.neon index 46e8153d..a21758dd 100644 --- a/Build/phpstan/phpstan.neon +++ b/Build/phpstan/phpstan.neon @@ -11,7 +11,8 @@ parameters: - %currentWorkingDirectory%/src/ - %currentWorkingDirectory%/tests/ - treatPhpDocTypesAsCertain: false + excludePaths: + - %currentWorkingDirectory%/tests/fixtures/phpstan/ type_perfect: no_mixed_property: true @@ -24,3 +25,9 @@ parameters: - message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) .* will always evaluate to#' path: '%currentWorkingDirectory%/tests/' + +services: + - + class: \Sabberworm\CSS\PhpStan\IgnoreBooleanAlways + tags: + - phpstan.ignoreErrorExtension diff --git a/src/PhpStan/IgnoreBooleanAlways.php b/src/PhpStan/IgnoreBooleanAlways.php new file mode 100644 index 00000000..dbcd499f --- /dev/null +++ b/src/PhpStan/IgnoreBooleanAlways.php @@ -0,0 +1,48 @@ +getIdentifier()) { + case 'function.alreadyNarrowedType': + // For an `assert()` that the DocBlocks say cannot fail. + if ($node instanceof FuncCall) { + $nameNode = $node->name; + if ($nameNode instanceof Name && $nameNode->name === 'assert') { + $shouldIgnore = true; + } + } + break; + case 'instanceof.alwaysTrue': + // For `instanceof` within an `assert()` that the DocBlocks say cannot fail. + $functionCallStack = $scope->getFunctionCallStack(); + if (isset($functionCallStack[0]) && $functionCallStack[0]->getName() === 'assert') { + $shouldIgnore = true; + } + break; + } + + return $shouldIgnore; + } +} diff --git a/tests/Unit/PhpStan/IgnoreBooleanAlwaysImpossibleAssertTest.php b/tests/Unit/PhpStan/IgnoreBooleanAlwaysImpossibleAssertTest.php new file mode 100644 index 00000000..24061629 --- /dev/null +++ b/tests/Unit/PhpStan/IgnoreBooleanAlwaysImpossibleAssertTest.php @@ -0,0 +1,54 @@ +getByType(ImpossibleCheckTypeFunctionCallRule::class); + } + + /** + * @test + */ + public function warningIsRetainedForPointlessAssert(): void + { + // Skip the test in PHP/PHPStan configurations that don't have the required components. + // It is good enough to test for those that do. + if (!\interface_exists(IgnoreErrorExtension::class)) { + self::markTestSkipped('This is testing the testers, and only needs to run whenever possible.'); + } + + // Second argument is array of expected warnings. + $this->analyse( + [self::FIXTURES_DIR . 'alwaystrue-pointlessassert.php'], + [ + [ + 'Call to function is_int() with int will always evaluate to true.', + 7, + ], + ] + ); + } +} diff --git a/tests/Unit/PhpStan/IgnoreBooleanAlwaysImpossibleInstanceofTest.php b/tests/Unit/PhpStan/IgnoreBooleanAlwaysImpossibleInstanceofTest.php new file mode 100644 index 00000000..2f7058f6 --- /dev/null +++ b/tests/Unit/PhpStan/IgnoreBooleanAlwaysImpossibleInstanceofTest.php @@ -0,0 +1,54 @@ +getByType(ImpossibleInstanceOfRule::class); + } + + /** + * @test + */ + public function warningIsRetainedForInstanceofNotInAssert(): void + { + // Skip the test in PHP/PHPStan configurations that don't have the required components. + // It is good enough to test for those that do. + if (!\interface_exists(IgnoreErrorExtension::class)) { + self::markTestSkipped('This is testing the testers, and only needs to run whenever possible.'); + } + + // Second argument is array of expected warnings. + $this->analyse( + [self::FIXTURES_DIR . 'alwaystrue-instanceof-notinassert.php'], + [ + [ + 'Instanceof between Exception and Exception will always evaluate to true.', + 6, + ], + ] + ); + } +} diff --git a/tests/Unit/PhpStan/IgnoreBooleanAlwaysTestBase.php b/tests/Unit/PhpStan/IgnoreBooleanAlwaysTestBase.php new file mode 100644 index 00000000..59614864 --- /dev/null +++ b/tests/Unit/PhpStan/IgnoreBooleanAlwaysTestBase.php @@ -0,0 +1,50 @@ + + */ +abstract class IgnoreBooleanAlwaysTestBase extends RuleTestCase +{ + /** + * @var non-empty-string + */ + protected const FIXTURES_DIR = __DIR__ . '/../../fixtures/phpstan/'; + + /** + * @test + */ + public function warningIsIgnoredInAssertInstanceOf(): void + { + // Skip the test in PHP/PHPStan configurations that don't have the required components. + // It is good enough to test for those that do. + if (!\interface_exists(IgnoreErrorExtension::class)) { + self::markTestSkipped('This is testing the testers, and only needs to run whenever possible.'); + } + + // Second argument is array of expected warnings. + $this->analyse([self::FIXTURES_DIR . 'alwaystrue-instanceof-inassert.php'], []); + } + + /** + * @return non-empty-array + */ + public static function getAdditionalConfigFiles(): array + { + return \array_merge( + parent::getAdditionalConfigFiles(), + [self::FIXTURES_DIR . 'ignorebooleanalways.neon'] + ); + } +} diff --git a/tests/fixtures/phpstan/alwaystrue-instanceof-inassert.php b/tests/fixtures/phpstan/alwaystrue-instanceof-inassert.php new file mode 100644 index 00000000..4b979791 --- /dev/null +++ b/tests/fixtures/phpstan/alwaystrue-instanceof-inassert.php @@ -0,0 +1,6 @@ +