Skip to content

Commit f2e5f22

Browse files
committed
Use our own collectors for traits. Add excluded framework base classes and custom excluded base classes.
1 parent 001815f commit f2e5f22

File tree

10 files changed

+208
-14
lines changed

10 files changed

+208
-14
lines changed

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ include:
2424
```
2525
2626
## Rules
27+
---
2728
### `UnusedClassRule`
2829
This rule scans for class declarations and use statements. If a class is declared but not used within the scanned source files, an error is generated.
2930

@@ -48,6 +49,32 @@ parameters:
4849
- 'src/MyUnusedClass.php'
4950
```
5051

52+
### Excluding Services
53+
By default, some known service and framework classes are excluded. There are a number of base classes from Symfony, Doctrine and PHPUnit that checked and, if matched, the class being analysed is ignored.
54+
55+
To disable this, set the *excludeFrameworks* property to false:
56+
```yaml
57+
# phpstan.neon
58+
parameters:
59+
unused_classes:
60+
excludeFrameworks: false
61+
```
62+
63+
This list will change as new frameworks and classes are added. Please look at the source code in src/Frameworks for a list of base classes that are excluded.
64+
65+
If you want add a custom list of base classes to ignore, use the *baseClassExcludes* property:
66+
```yaml
67+
# phpstan.neon
68+
parameters:
69+
unused_classes:
70+
baseClassExcludes:
71+
- 'App\Service\MyAbstractService'
72+
- 'App\DI\MyDIClass'
73+
```
74+
75+
Entries in *baseClassExcludes* are excluded regardless of the *excludeFrameworks* property value.
76+
77+
---
5178
### `UnusedTraitRule`
5279
This rule scans for trait declarations and use statements. If a trait is declared but not used within the scanned source files, an error is generated.
5380

@@ -61,4 +88,4 @@ parameters:
6188
```
6289

6390
#### Excluding files
64-
You can exclude directories and individual files from being scanned by this rule using the excludePaths parameter as shown above/
91+
You can exclude directories and individual files from being scanned by this rule using the excludePaths parameter as shown above.

config/extension.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ parametersSchema:
33
classes: bool()
44
traits: bool()
55
excludePaths: array()
6+
excludeFrameworks: bool()
7+
baseClassExcludes: array()
68
])
79

810
# default parameters
@@ -11,6 +13,8 @@ parameters:
1113
classes: true
1214
traits: true
1315
excludePaths: []
16+
excludeFrameworks: true
17+
baseClassExcludes: []
1418

1519
services:
1620
-
@@ -31,6 +35,14 @@ services:
3135
class: Xact\PHPStan\Collectors\DeclareClassCollector
3236
tags:
3337
- phpstan.collector
38+
-
39+
class: Xact\PHPStan\Collectors\DeclareTraitCollector
40+
tags:
41+
- phpstan.collector
42+
-
43+
class: Xact\PHPStan\Collectors\TraitUseCollector
44+
tags:
45+
- phpstan.collector
3446

3547
rules:
3648
- Xact\PHPStan\Rules\UnusedClassRule
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Xact\PHPStan\Collectors;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\TraitUse;
9+
use PhpParser\Node\Stmt\Use_ ;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Collectors\Collector;
12+
use Xact\PHPStan\Exception\InvalidNodeTypeException;
13+
14+
class TraitUseCollector implements Collector
15+
{
16+
public function getNodeType(): string
17+
{
18+
return TraitUse ::class;
19+
}
20+
21+
/**
22+
* @inheritDoc
23+
*/
24+
// phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
25+
public function processNode(Node $node, Scope $scope)
26+
{
27+
if (!$node instanceof TraitUse) {
28+
throw InvalidNodeTypeException::create($node, Use_::class);
29+
}
30+
31+
$uses = [];
32+
foreach ($node->traits as $traitNodeName) {
33+
$traitName = $traitNodeName->toString();
34+
$uses[$traitName] = $traitName;
35+
}
36+
37+
// returns an array of used trait names
38+
return $uses;
39+
}
40+
}

src/Configuration.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,22 @@ public function getExcludePaths(): array
4141

4242
return $excludePaths;
4343
}
44+
45+
public function isExcludeFrameworks(): bool
46+
{
47+
return (bool)($this->parameters['excludeFrameworks'] ?? true);
48+
}
49+
50+
/**
51+
* @return string[]
52+
*/
53+
public function getBaseClassExcludes(): array
54+
{
55+
/** @var string[] */
56+
$frameworkClasses = $this->parameters['baseClassExcludes'] ?? $this->parameters['baseClassExcludes'];
57+
58+
Assert::isArray($frameworkClasses);
59+
60+
return $frameworkClasses;
61+
}
4462
}

src/Frameworks/Doctrine.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Xact\PHPStan\Frameworks;
6+
7+
final class Doctrine
8+
{
9+
/** @var string[] */
10+
public static array $classes = [
11+
'Doctrine\Common\DataFixtures\AbstractFixture',
12+
'Doctrine\DBAL\Types\Type',
13+
'Doctrine\ORM\Query\AST\Node',
14+
];
15+
}

src/Frameworks/PHPUnit.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Xact\PHPStan\Frameworks;
6+
7+
class PHPUnit
8+
{
9+
/** @var string[] */
10+
public static array $classes = [
11+
'PHPUnit\Framework\TestCase',
12+
];
13+
}

src/Frameworks/Symfony.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Xact\PHPStan\Frameworks;
6+
7+
final class Symfony
8+
{
9+
/** @var string[] */
10+
public static array $classes = [
11+
'Symfony\Bundle\FrameworkBundle\Controller\AbstractController',
12+
'Symfony\Component\Console\Command\Command',
13+
'Symfony\Component\HttpKernel\Bundle\Bundle',
14+
'Symfony\Component\HttpKernel\Kernel',
15+
'Symfony\Component\Validator\Constraint',
16+
'Symfony\Component\Validator\ConstraintValidator',
17+
'Twig\Extension\AbstractExtension',
18+
];
19+
}

src/Rules/UnusedClassRule.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,29 @@
77
use PhpParser\Node;
88
use PHPStan\Analyser\Scope;
99
use PHPStan\Node\CollectedDataNode;
10+
use PHPStan\Reflection\ReflectionProvider;
1011
use PHPStan\Rules\Rule;
1112
use PHPStan\Rules\RuleErrorBuilder;
1213
use Xact\PHPStan\Collectors\ClassGroupUseCollector;
1314
use Xact\PHPStan\Collectors\ClassUseCollector;
1415
use Xact\PHPStan\Collectors\DeclareClassCollector;
16+
use Xact\PHPStan\Configuration;
1517
use Xact\PHPStan\Exception\InvalidNodeTypeException;
18+
use Xact\PHPStan\Frameworks\Doctrine;
19+
use Xact\PHPStan\Frameworks\PHPUnit;
20+
use Xact\PHPStan\Frameworks\Symfony;
1621

1722
class UnusedClassRule implements Rule
1823
{
24+
private Configuration $configuration;
25+
private ReflectionProvider $reflectionProvider;
26+
27+
public function __construct(Configuration $configuration, ReflectionProvider $reflectionProvider)
28+
{
29+
$this->configuration = $configuration;
30+
$this->reflectionProvider = $reflectionProvider;
31+
}
32+
1933
/**
2034
* @inheritDoc
2135
*/
@@ -65,6 +79,10 @@ public function processNode(Node $node, Scope $scope): array
6579
* @var int $line
6680
*/
6781
foreach ($declarations as [$className, $line]) {
82+
if ($this->isKnownFrameworkService($className)) {
83+
continue;
84+
}
85+
6886
if (!array_key_exists($className, $usedClasses)) {
6987
$errors[] = RuleErrorBuilder::message("Class {$className} is never used.")
7088
->file($file)
@@ -76,4 +94,33 @@ public function processNode(Node $node, Scope $scope): array
7694

7795
return $errors;
7896
}
97+
98+
private function isKnownFrameworkService(string $className): bool
99+
{
100+
if ($this->configuration->isExcludeFrameworks() === false && count($this->configuration->getBaseClassExcludes()) === 0) {
101+
return false;
102+
}
103+
if ($this->reflectionProvider->hasClass($className) === false) {
104+
return false;
105+
}
106+
107+
$excludedClasses = $this->configuration->getBaseClassExcludes();
108+
if ($this->configuration->isExcludeFrameworks() === true) {
109+
$excludedClasses = array_merge(
110+
$excludedClasses,
111+
Symfony::$classes,
112+
Doctrine::$classes,
113+
PHPUnit::$classes,
114+
);
115+
}
116+
// Does the class name extend a known framework base class that is not directly used in client code?
117+
$reflection = $this->reflectionProvider->getClass($className);
118+
foreach ($reflection->getParentClassesNames() as $parentName) {
119+
if (in_array($parentName, $excludedClasses)) {
120+
return true;
121+
}
122+
}
123+
124+
return false;
125+
}
79126
}

src/Rules/UnusedTraitRule.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
use PHPStan\Node\CollectedDataNode;
1010
use PHPStan\Rules\Rule;
1111
use PHPStan\Rules\RuleErrorBuilder;
12-
use PHPStan\Rules\Traits\TraitDeclarationCollector;
13-
use PHPStan\Rules\Traits\TraitUseCollector;
12+
use Xact\PHPStan\Collectors\DeclareTraitCollector;
13+
use Xact\PHPStan\Collectors\TraitUseCollector;
1414
use Xact\PHPStan\Exception\InvalidNodeTypeException;
1515

1616
class UnusedTraitRule implements Rule
@@ -37,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array
3737
return [];
3838
}
3939

40-
$traitDeclarationData = $node->get(TraitDeclarationCollector::class);
40+
$traitDeclarationData = $node->get(DeclareTraitCollector::class);
4141
$traitUses = $node->get(TraitUseCollector::class);
4242

4343
$usedTraits = [];

tests/UnusedClassTest.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,19 @@ public function testRule(): void
3131
// first argument: path to the example file that contains some errors that should be reported by UnusedClassRule
3232
// second argument: an array of expected errors,
3333
// each error consists of the asserted error message, and the asserted error file line
34-
$this->analyse([__DIR__ . '/UnusedClass/ClassA.php', __DIR__ . '/UnusedClass/ClassB.php', __DIR__ . '/UnusedClass/ClassC.php'], [
34+
$this->analyse(
35+
[__DIR__ . '/UnusedClass/ClassA.php', __DIR__ . '/UnusedClass/ClassB.php', __DIR__ . '/UnusedClass/ClassC.php'],
3536
[
36-
'Class Xact\PHPStan\Tests\UnusedClass\ClassA is never used.', // asserted error message
37-
9, // asserted error line
38-
],
39-
[
40-
'Class Xact\PHPStan\Tests\UnusedClass\ClassC is never used.', // asserted error message
41-
9, // asserted error line
42-
],
43-
]);
37+
[
38+
'Class Xact\PHPStan\Tests\UnusedClass\ClassA is never used.', // asserted error message
39+
9, // asserted error line
40+
],
41+
[
42+
'Class Xact\PHPStan\Tests\UnusedClass\ClassC is never used.', // asserted error message
43+
9, // asserted error line
44+
],
45+
]
46+
);
4447

4548
// the test fails, if the expected error does not occur,
4649
// or if there are other errors reported beside the expected one
@@ -49,7 +52,7 @@ public function testRule(): void
4952
protected function getRule(): Rule
5053
{
5154
// getRule() method needs to return an instance of the tested rule
52-
return new UnusedClassRule();
55+
return self::getContainer()->getByType(UnusedClassRule::class);
5356
}
5457

5558
/**

0 commit comments

Comments
 (0)