Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
/CONTRIBUTING.md export-ignore
/bin/ export-ignore
/docs/ export-ignore
/src/PhpStan/ export-ignore
/tests/ export-ignore
9 changes: 8 additions & 1 deletion Build/phpstan/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ parameters:
- %currentWorkingDirectory%/src/
- %currentWorkingDirectory%/tests/

treatPhpDocTypesAsCertain: false
excludePaths:
- %currentWorkingDirectory%/tests/fixtures/phpstan/

type_perfect:
no_mixed_property: true
Expand All @@ -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
48 changes: 48 additions & 0 deletions src/PhpStan/IgnoreBooleanAlways.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Sabberworm\CSS\PhpStan;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Error;
use PHPStan\Analyser\IgnoreErrorExtension;
use PHPStan\Analyser\Scope;

/**
* Ignore PHPStan warnings where the DocBlocks indicate that a conditional expression would always be true (or false),
* but a programming mistake elsewhere could lead to that not being the case, for the following:
* - `assert($object instanceof Class);`.
*
* @internal
*/
final class IgnoreBooleanAlways implements IgnoreErrorExtension
{
public function shouldIgnore(Error $error, Node $node, Scope $scope): bool
{
$shouldIgnore = false;

switch ($error->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;
}
}
54 changes: 54 additions & 0 deletions tests/Unit/PhpStan/IgnoreBooleanAlwaysImpossibleAssertTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Sabberworm\CSS\Tests\Unit\PhpStan;

use PHPStan\Analyser\IgnoreErrorExtension;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Rules\Comparison\ImpossibleCheckTypeFunctionCallRule;
use PHPStan\Rules\Rule;

/**
* This covers `function.alreadyNarrowedType` error handling in the `IgnoreBooleanAlways` PHPStan extension class.
* `RuleTestCase` allows only one `Rule` class, so separate `TestCase`s are needed for errors generated by other rules.
* Note that the base class covers testing the suppression of the warning, and is included via `extends`.
* This class covers the non-suppression of warnings in other contexts, which requires detailing the expected warnings.
*
* @covers \Sabberworm\CSS\PhpStan\IgnoreBooleanAlways
*/
final class IgnoreBooleanAlwaysImpossibleAssertTest extends IgnoreBooleanAlwaysTestBase
{
/**
* @return ImpossibleCheckTypeFunctionCallRule
*/
protected function getRule(): Rule
{
// If the class is renamed or removed in the next major release of PHPStan, we'll deal with it then.
// @phpstan-ignore phpstanApi.classConstant
return self::getContainer()->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,
],
]
);
}
}
54 changes: 54 additions & 0 deletions tests/Unit/PhpStan/IgnoreBooleanAlwaysImpossibleInstanceofTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Sabberworm\CSS\Tests\Unit\PhpStan;

use PHPStan\Analyser\IgnoreErrorExtension;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Rules\Classes\ImpossibleInstanceOfRule;
use PHPStan\Rules\Rule;

/**
* This covers `instanceof.alwaysTrue` error handling in the `IgnoreBooleanAlways` PHPStan extension class.
* `RuleTestCase` allows only one `Rule` class, so separate `TestCase`s are needed for errors generated by other rules.
* Note that the base class covers testing the suppression of the warning, and is included via `extends`.
* This class covers the non-suppression of warnings in other contexts, which requires detailing the expected warnings.
*
* @covers \Sabberworm\CSS\PhpStan\IgnoreBooleanAlways
*/
final class IgnoreBooleanAlwaysImpossibleInstanceofTest extends IgnoreBooleanAlwaysTestBase
{
/**
* @return ImpossibleInstanceOfRule
*/
protected function getRule(): Rule
{
// If the class is renamed or removed in the next major release of PHPStan, we'll deal with it then.
// @phpstan-ignore phpstanApi.classConstant
return self::getContainer()->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,
],
]
);
}
}
50 changes: 50 additions & 0 deletions tests/Unit/PhpStan/IgnoreBooleanAlwaysTestBase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Sabberworm\CSS\Tests\Unit\PhpStan;

use PHPStan\Analyser\IgnoreErrorExtension;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Rules\Rule;

/**
* Only one `Rule` can seemingly be covered by classes extending `RuleTestCase`.
* This provides some common functionality and settings for `TestCase`s covering `IgnoreBooleanAlways`,
* which involves more than one `Rule`.
*
* @extends RuleTestCase<Rule>
*/
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<string>
*/
public static function getAdditionalConfigFiles(): array
{
return \array_merge(
parent::getAdditionalConfigFiles(),
[self::FIXTURES_DIR . 'ignorebooleanalways.neon']
);
}
}
6 changes: 6 additions & 0 deletions tests/fixtures/phpstan/alwaystrue-instanceof-inassert.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

declare(strict_types=1);

$e = new \Exception();
\assert($e instanceof \Exception);
8 changes: 8 additions & 0 deletions tests/fixtures/phpstan/alwaystrue-instanceof-notinassert.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

$e = new \Exception();
if ($e instanceof \Exception) {
$theTest = 'passed';
}
8 changes: 8 additions & 0 deletions tests/fixtures/phpstan/alwaystrue-pointlessassert.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

function pointless(int $value): void
{
\assert(\is_int($value));
}
5 changes: 5 additions & 0 deletions tests/fixtures/phpstan/ignorebooleanalways.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
-
class: \Sabberworm\CSS\PhpStan\IgnoreBooleanAlways
tags:
- phpstan.ignoreErrorExtension
Loading