diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 0d90c4116c..5e574eb6d2 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -247,6 +247,10 @@ jobs: cd e2e/bug-12606 export CONFIGTEST=test ../../bin/phpstan + - script: | + cd e2e/ignore-error-extension + composer install + ../../bin/phpstan steps: - name: "Checkout" diff --git a/conf/config.neon b/conf/config.neon index b9f6445b08..b7e91bd3d9 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -442,6 +442,9 @@ services: arguments: parser: @defaultAnalysisParser + - + class: PHPStan\Analyser\IgnoreErrorExtensionProvider + - class: PHPStan\Analyser\LocalIgnoresProcessor diff --git a/e2e/ignore-error-extension/.gitignore b/e2e/ignore-error-extension/.gitignore new file mode 100644 index 0000000000..de4a392c33 --- /dev/null +++ b/e2e/ignore-error-extension/.gitignore @@ -0,0 +1,2 @@ +/vendor +/composer.lock diff --git a/e2e/ignore-error-extension/composer.json b/e2e/ignore-error-extension/composer.json new file mode 100644 index 0000000000..f8a4e6ebed --- /dev/null +++ b/e2e/ignore-error-extension/composer.json @@ -0,0 +1,7 @@ +{ + "autoload": { + "psr-4": { + "App\\": "src/" + } + } +} diff --git a/e2e/ignore-error-extension/phpstan-baseline.neon b/e2e/ignore-error-extension/phpstan-baseline.neon new file mode 100644 index 0000000000..8c53510373 --- /dev/null +++ b/e2e/ignore-error-extension/phpstan-baseline.neon @@ -0,0 +1,44 @@ +parameters: + ignoreErrors: + - + message: '#^This is an error from a rule that uses a collector$#' + identifier: class.name + count: 1 + path: src/ClassCollector.php + + - + message: '#^This is an error from a rule that uses a collector$#' + identifier: class.name + count: 1 + path: src/ClassRule.php + + - + message: '#^This is an error from a rule that uses a collector$#' + identifier: class.name + count: 1 + path: src/ControllerActionReturnTypeIgnoreExtension.php + + - + message: '#^This is an error from a rule that uses a collector$#' + identifier: class.name + count: 1 + path: src/ControllerClassNameIgnoreExtension.php + + - + message: '#^Method App\\HomepageController\:\:contactAction\(\) has parameter \$someUnrelatedError with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/HomepageController.php + + - + message: '#^Method App\\HomepageController\:\:getSomething\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/HomepageController.php + + - + message: '#^Method App\\HomepageController\:\:homeAction\(\) has parameter \$someUnrelatedError with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/HomepageController.php + diff --git a/e2e/ignore-error-extension/phpstan.neon.dist b/e2e/ignore-error-extension/phpstan.neon.dist new file mode 100644 index 0000000000..bc04b24e67 --- /dev/null +++ b/e2e/ignore-error-extension/phpstan.neon.dist @@ -0,0 +1,25 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 9 + paths: + - src + +services: + - + class: App\ClassCollector + tags: + - phpstan.collector + - + class: App\ClassRule + tags: + - phpstan.rules.rule + - + class: App\ControllerActionReturnTypeIgnoreExtension + tags: + - phpstan.ignoreErrorExtension + - + class: App\ControllerClassNameIgnoreExtension + tags: + - phpstan.ignoreErrorExtension diff --git a/e2e/ignore-error-extension/src/ClassCollector.php b/e2e/ignore-error-extension/src/ClassCollector.php new file mode 100644 index 0000000000..03011e44fc --- /dev/null +++ b/e2e/ignore-error-extension/src/ClassCollector.php @@ -0,0 +1,29 @@ + + */ +final class ClassCollector implements Collector +{ + public function getNodeType(): string + { + return Node\Stmt\Class_::class; + } + + public function processNode(Node $node, Scope $scope) : ?array + { + if ($node->name === null) { + return null; + } + + return [$node->name->name, $node->getStartLine()]; + } +} diff --git a/e2e/ignore-error-extension/src/ClassRule.php b/e2e/ignore-error-extension/src/ClassRule.php new file mode 100644 index 0000000000..17283bafe5 --- /dev/null +++ b/e2e/ignore-error-extension/src/ClassRule.php @@ -0,0 +1,43 @@ + + */ +final class ClassRule implements Rule +{ + #[Override] + public function getNodeType() : string + { + return CollectedDataNode::class; + } + + #[Override] + public function processNode(Node $node, Scope $scope) : array + { + $errors = []; + + foreach ($node->get(ClassCollector::class) as $file => $data) { + foreach ($data as [$className, $line]) { + $errors[] = RuleErrorBuilder::message('This is an error from a rule that uses a collector') + ->file($file) + ->line($line) + ->identifier('class.name') + ->build(); + } + } + + return $errors; + } + +} diff --git a/e2e/ignore-error-extension/src/ControllerActionReturnTypeIgnoreExtension.php b/e2e/ignore-error-extension/src/ControllerActionReturnTypeIgnoreExtension.php new file mode 100644 index 0000000000..dc7b0dab5a --- /dev/null +++ b/e2e/ignore-error-extension/src/ControllerActionReturnTypeIgnoreExtension.php @@ -0,0 +1,41 @@ +getIdentifier() !== 'missingType.iterableValue') { + return false; + } + + // @phpstan-ignore phpstanApi.instanceofAssumption + if (! $node instanceof InClassMethodNode) { + return false; + } + + if (! str_ends_with($node->getClassReflection()->getName(), 'Controller')) { + return false; + } + + if (! str_ends_with($node->getMethodReflection()->getName(), 'Action')) { + return false; + } + + if (! $node->getMethodReflection()->isPublic()) { + return false; + } + + return true; + } +} diff --git a/e2e/ignore-error-extension/src/ControllerClassNameIgnoreExtension.php b/e2e/ignore-error-extension/src/ControllerClassNameIgnoreExtension.php new file mode 100644 index 0000000000..b52b4f7ef1 --- /dev/null +++ b/e2e/ignore-error-extension/src/ControllerClassNameIgnoreExtension.php @@ -0,0 +1,34 @@ +getIdentifier() !== 'class.name') { + return false; + } + + // @phpstan-ignore phpstanApi.instanceofAssumption + if (!$node instanceof CollectedDataNode) { + return false; + } + + if (!str_ends_with($error->getFile(), 'Controller.php')) { + return false; + } + + return true; + } +} diff --git a/e2e/ignore-error-extension/src/HomepageController.php b/e2e/ignore-error-extension/src/HomepageController.php new file mode 100644 index 0000000000..d55c955157 --- /dev/null +++ b/e2e/ignore-error-extension/src/HomepageController.php @@ -0,0 +1,29 @@ + 'Homepage', + 'something' => $this->getSomething(), + ]; + } + + public function contactAction($someUnrelatedError): array + { + return [ + 'title' => 'Contact', + 'something' => $this->getSomething(), + ]; + } + + private function getSomething(): array + { + return []; + } +} diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php index b88b3e0d31..56fde0132e 100644 --- a/src/Analyser/AnalyserResultFinalizer.php +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -19,6 +19,7 @@ final class AnalyserResultFinalizer public function __construct( private RuleRegistry $ruleRegistry, + private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider, private RuleErrorTransformer $ruleErrorTransformer, private ScopeFactory $scopeFactory, private LocalIgnoresProcessor $localIgnoresProcessor, @@ -88,7 +89,17 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ } foreach ($ruleErrors as $ruleError) { - $tempCollectorErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); + $error = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); + + if ($error->canBeIgnored()) { + foreach ($this->ignoreErrorExtensionProvider->getExtensions() as $ignoreErrorExtension) { + if ($ignoreErrorExtension->shouldIgnore($error, $node, $scope)) { + continue 2; + } + } + } + + $tempCollectorErrors[] = $error; } } diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index 570c637092..969154c3c8 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -51,6 +51,7 @@ public function __construct( private NodeScopeResolver $nodeScopeResolver, private Parser $parser, private DependencyResolver $dependencyResolver, + private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider, private RuleErrorTransformer $ruleErrorTransformer, private LocalIgnoresProcessor $localIgnoresProcessor, ) @@ -142,7 +143,17 @@ public function analyseFile( } foreach ($ruleErrors as $ruleError) { - $temporaryFileErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); + $error = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); + + if ($error->canBeIgnored()) { + foreach ($this->ignoreErrorExtensionProvider->getExtensions() as $ignoreErrorExtension) { + if ($ignoreErrorExtension->shouldIgnore($error, $node, $scope)) { + continue 2; + } + } + } + + $temporaryFileErrors[] = $error; } } diff --git a/src/Analyser/IgnoreErrorExtension.php b/src/Analyser/IgnoreErrorExtension.php new file mode 100644 index 0000000000..54ff6d2422 --- /dev/null +++ b/src/Analyser/IgnoreErrorExtension.php @@ -0,0 +1,32 @@ +container->getServicesByTag(IgnoreErrorExtension::EXTENSION_TAG); + } + +} diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index b40a8ebca0..e48e757bfe 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -7,6 +7,7 @@ use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\IgnoreErrorExtensionProvider; use PHPStan\Analyser\InternalError; use PHPStan\Analyser\LocalIgnoresProcessor; use PHPStan\Analyser\NodeScopeResolver; @@ -113,6 +114,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser $nodeScopeResolver, $this->getParser(), self::getContainer()->getByType(DependencyResolver::class), + new IgnoreErrorExtensionProvider(self::getContainer()), new RuleErrorTransformer(), new LocalIgnoresProcessor(), ); @@ -192,6 +194,7 @@ public function gatherAnalyserErrors(array $files): array $finalizer = new AnalyserResultFinalizer( $ruleRegistry, + new IgnoreErrorExtensionProvider(self::getContainer()), new RuleErrorTransformer(), $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier()), new LocalIgnoresProcessor(), diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 6162106ac5..bacd85a967 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use Nette\DI\Container; use PhpParser\Lexer; use PhpParser\NodeVisitor\NameResolver; use PhpParser\Parser\Php7; @@ -10,6 +11,7 @@ use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\ExportedNodeResolver; +use PHPStan\DependencyInjection\Nette\NetteContainer; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; @@ -666,6 +668,7 @@ private function runAnalyser( $finalizer = new AnalyserResultFinalizer( new DirectRuleRegistry([]), + new IgnoreErrorExtensionProvider(new NetteContainer(new Container([]))), new RuleErrorTransformer(), $this->createScopeFactory( $this->createReflectionProvider(), @@ -742,6 +745,7 @@ private function createAnalyser(): Analyser new IgnoreLexer(), ), new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), + new IgnoreErrorExtensionProvider(new NetteContainer(new Container([]))), new RuleErrorTransformer(), new LocalIgnoresProcessor(), );