Skip to content

Commit ed54496

Browse files
authored
Introduce IgnoreErrorExtension (#3783)
1 parent d4d7e11 commit ed54496

17 files changed

+346
-2
lines changed

Diff for: .github/workflows/e2e-tests.yml

+4
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ jobs:
247247
cd e2e/bug-12606
248248
export CONFIGTEST=test
249249
../../bin/phpstan
250+
- script: |
251+
cd e2e/ignore-error-extension
252+
composer install
253+
../../bin/phpstan
250254
251255
steps:
252256
- name: "Checkout"

Diff for: conf/config.neon

+3
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,9 @@ services:
442442
arguments:
443443
parser: @defaultAnalysisParser
444444

445+
-
446+
class: PHPStan\Analyser\IgnoreErrorExtensionProvider
447+
445448
-
446449
class: PHPStan\Analyser\LocalIgnoresProcessor
447450

Diff for: e2e/ignore-error-extension/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor
2+
/composer.lock

Diff for: e2e/ignore-error-extension/composer.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"autoload": {
3+
"psr-4": {
4+
"App\\": "src/"
5+
}
6+
}
7+
}

Diff for: e2e/ignore-error-extension/phpstan-baseline.neon

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: '#^This is an error from a rule that uses a collector$#'
5+
identifier: class.name
6+
count: 1
7+
path: src/ClassCollector.php
8+
9+
-
10+
message: '#^This is an error from a rule that uses a collector$#'
11+
identifier: class.name
12+
count: 1
13+
path: src/ClassRule.php
14+
15+
-
16+
message: '#^This is an error from a rule that uses a collector$#'
17+
identifier: class.name
18+
count: 1
19+
path: src/ControllerActionReturnTypeIgnoreExtension.php
20+
21+
-
22+
message: '#^This is an error from a rule that uses a collector$#'
23+
identifier: class.name
24+
count: 1
25+
path: src/ControllerClassNameIgnoreExtension.php
26+
27+
-
28+
message: '#^Method App\\HomepageController\:\:contactAction\(\) has parameter \$someUnrelatedError with no type specified\.$#'
29+
identifier: missingType.parameter
30+
count: 1
31+
path: src/HomepageController.php
32+
33+
-
34+
message: '#^Method App\\HomepageController\:\:getSomething\(\) return type has no value type specified in iterable type array\.$#'
35+
identifier: missingType.iterableValue
36+
count: 1
37+
path: src/HomepageController.php
38+
39+
-
40+
message: '#^Method App\\HomepageController\:\:homeAction\(\) has parameter \$someUnrelatedError with no type specified\.$#'
41+
identifier: missingType.parameter
42+
count: 1
43+
path: src/HomepageController.php
44+

Diff for: e2e/ignore-error-extension/phpstan.neon.dist

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
includes:
2+
- phpstan-baseline.neon
3+
4+
parameters:
5+
level: 9
6+
paths:
7+
- src
8+
9+
services:
10+
-
11+
class: App\ClassCollector
12+
tags:
13+
- phpstan.collector
14+
-
15+
class: App\ClassRule
16+
tags:
17+
- phpstan.rules.rule
18+
-
19+
class: App\ControllerActionReturnTypeIgnoreExtension
20+
tags:
21+
- phpstan.ignoreErrorExtension
22+
-
23+
class: App\ControllerClassNameIgnoreExtension
24+
tags:
25+
- phpstan.ignoreErrorExtension

Diff for: e2e/ignore-error-extension/src/ClassCollector.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace App;
6+
7+
use PhpParser\Node;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Collectors\Collector;
10+
11+
/**
12+
* @implements Collector<Node\Stmt\Class_, array{string, int}>
13+
*/
14+
final class ClassCollector implements Collector
15+
{
16+
public function getNodeType(): string
17+
{
18+
return Node\Stmt\Class_::class;
19+
}
20+
21+
public function processNode(Node $node, Scope $scope) : ?array
22+
{
23+
if ($node->name === null) {
24+
return null;
25+
}
26+
27+
return [$node->name->name, $node->getStartLine()];
28+
}
29+
}

Diff for: e2e/ignore-error-extension/src/ClassRule.php

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App;
6+
7+
use Override;
8+
use PhpParser\Node;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Node\CollectedDataNode;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
14+
/**
15+
* @implements Rule<CollectedDataNode>
16+
*/
17+
final class ClassRule implements Rule
18+
{
19+
#[Override]
20+
public function getNodeType() : string
21+
{
22+
return CollectedDataNode::class;
23+
}
24+
25+
#[Override]
26+
public function processNode(Node $node, Scope $scope) : array
27+
{
28+
$errors = [];
29+
30+
foreach ($node->get(ClassCollector::class) as $file => $data) {
31+
foreach ($data as [$className, $line]) {
32+
$errors[] = RuleErrorBuilder::message('This is an error from a rule that uses a collector')
33+
->file($file)
34+
->line($line)
35+
->identifier('class.name')
36+
->build();
37+
}
38+
}
39+
40+
return $errors;
41+
}
42+
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App;
6+
7+
use PhpParser\Node;
8+
use PHPStan\Analyser\Error;
9+
use PHPStan\Analyser\IgnoreErrorExtension;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Node\InClassMethodNode;
12+
13+
// This extension will ignore "missingType.iterableValue" errors for public Action methods inside Controller classes.
14+
final class ControllerActionReturnTypeIgnoreExtension implements IgnoreErrorExtension
15+
{
16+
public function shouldIgnore(Error $error, Node $node, Scope $scope) : bool
17+
{
18+
if ($error->getIdentifier() !== 'missingType.iterableValue') {
19+
return false;
20+
}
21+
22+
// @phpstan-ignore phpstanApi.instanceofAssumption
23+
if (! $node instanceof InClassMethodNode) {
24+
return false;
25+
}
26+
27+
if (! str_ends_with($node->getClassReflection()->getName(), 'Controller')) {
28+
return false;
29+
}
30+
31+
if (! str_ends_with($node->getMethodReflection()->getName(), 'Action')) {
32+
return false;
33+
}
34+
35+
if (! $node->getMethodReflection()->isPublic()) {
36+
return false;
37+
}
38+
39+
return true;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App;
6+
7+
use PhpParser\Node;
8+
use PHPStan\Analyser\Error;
9+
use PHPStan\Analyser\IgnoreErrorExtension;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Node\CollectedDataNode;
12+
13+
// This extension will ignore "class.name" errors for classes with names ending with "Controller".
14+
// These errors are reported by the ClassRule which triggers on CollectedDataNode coming from ClassCollector.
15+
final class ControllerClassNameIgnoreExtension implements IgnoreErrorExtension
16+
{
17+
public function shouldIgnore(Error $error, Node $node, Scope $scope) : bool
18+
{
19+
if ($error->getIdentifier() !== 'class.name') {
20+
return false;
21+
}
22+
23+
// @phpstan-ignore phpstanApi.instanceofAssumption
24+
if (!$node instanceof CollectedDataNode) {
25+
return false;
26+
}
27+
28+
if (!str_ends_with($error->getFile(), 'Controller.php')) {
29+
return false;
30+
}
31+
32+
return true;
33+
}
34+
}
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App;
6+
7+
final class HomepageController
8+
{
9+
public function homeAction($someUnrelatedError = false): array
10+
{
11+
return [
12+
'title' => 'Homepage',
13+
'something' => $this->getSomething(),
14+
];
15+
}
16+
17+
public function contactAction($someUnrelatedError): array
18+
{
19+
return [
20+
'title' => 'Contact',
21+
'something' => $this->getSomething(),
22+
];
23+
}
24+
25+
private function getSomething(): array
26+
{
27+
return [];
28+
}
29+
}

Diff for: src/Analyser/AnalyserResultFinalizer.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ final class AnalyserResultFinalizer
1919

2020
public function __construct(
2121
private RuleRegistry $ruleRegistry,
22+
private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider,
2223
private RuleErrorTransformer $ruleErrorTransformer,
2324
private ScopeFactory $scopeFactory,
2425
private LocalIgnoresProcessor $localIgnoresProcessor,
@@ -88,7 +89,17 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
8889
}
8990

9091
foreach ($ruleErrors as $ruleError) {
91-
$tempCollectorErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine());
92+
$error = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine());
93+
94+
if ($error->canBeIgnored()) {
95+
foreach ($this->ignoreErrorExtensionProvider->getExtensions() as $ignoreErrorExtension) {
96+
if ($ignoreErrorExtension->shouldIgnore($error, $node, $scope)) {
97+
continue 2;
98+
}
99+
}
100+
}
101+
102+
$tempCollectorErrors[] = $error;
92103
}
93104
}
94105

Diff for: src/Analyser/FileAnalyser.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public function __construct(
5151
private NodeScopeResolver $nodeScopeResolver,
5252
private Parser $parser,
5353
private DependencyResolver $dependencyResolver,
54+
private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider,
5455
private RuleErrorTransformer $ruleErrorTransformer,
5556
private LocalIgnoresProcessor $localIgnoresProcessor,
5657
)
@@ -142,7 +143,17 @@ public function analyseFile(
142143
}
143144

144145
foreach ($ruleErrors as $ruleError) {
145-
$temporaryFileErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine());
146+
$error = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine());
147+
148+
if ($error->canBeIgnored()) {
149+
foreach ($this->ignoreErrorExtensionProvider->getExtensions() as $ignoreErrorExtension) {
150+
if ($ignoreErrorExtension->shouldIgnore($error, $node, $scope)) {
151+
continue 2;
152+
}
153+
}
154+
}
155+
156+
$temporaryFileErrors[] = $error;
146157
}
147158
}
148159

Diff for: src/Analyser/IgnoreErrorExtension.php

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PhpParser\Node;
6+
7+
/**
8+
* This is the extension interface to implement if you want to ignore errors
9+
* based on the node and scope.
10+
*
11+
* To register it in the configuration file use the `phpstan.ignoreErrorExtension` service tag:
12+
*
13+
* ```
14+
* services:
15+
* -
16+
* class: App\PHPStan\MyExtension
17+
* tags:
18+
* - phpstan.ignoreErrorExtension
19+
* ```
20+
*
21+
* Learn more: https://phpstan.org
22+
*
23+
* @api
24+
*/
25+
interface IgnoreErrorExtension
26+
{
27+
28+
public const EXTENSION_TAG = 'phpstan.ignoreErrorExtension';
29+
30+
public function shouldIgnore(Error $error, Node $node, Scope $scope): bool;
31+
32+
}

Diff for: src/Analyser/IgnoreErrorExtensionProvider.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\DependencyInjection\Container;
6+
7+
final class IgnoreErrorExtensionProvider
8+
{
9+
10+
public function __construct(private Container $container)
11+
{
12+
}
13+
14+
/**
15+
* @return IgnoreErrorExtension[]
16+
*/
17+
public function getExtensions(): array
18+
{
19+
return $this->container->getServicesByTag(IgnoreErrorExtension::EXTENSION_TAG);
20+
}
21+
22+
}

0 commit comments

Comments
 (0)