Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 281a87d

Browse files
committedApr 17, 2024··
Detect unused new on a separate line with possibly pure constructor
1 parent 6026869 commit 281a87d

6 files changed

+241
-0
lines changed
 

‎conf/config.level4.neon

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ conditionalTags:
3232
phpstan.rules.rule: %featureToggles.paramOutType%
3333
PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule:
3434
phpstan.rules.rule: %featureToggles.paramOutType%
35+
PHPStan\Rules\DeadCode\CallToConstructorStatementWithoutImpurePointsRule:
36+
phpstan.rules.rule: %featureToggles.pure%
37+
PHPStan\Rules\DeadCode\PossiblyPureNewCollector:
38+
phpstan.collector: %featureToggles.pure%
39+
PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector:
40+
phpstan.collector: %featureToggles.pure%
3541

3642
parameters:
3743
checkAdvancedIsset: true
@@ -79,6 +85,15 @@ services:
7985
tags:
8086
- phpstan.rules.rule
8187

88+
-
89+
class: PHPStan\Rules\DeadCode\CallToConstructorStatementWithoutImpurePointsRule
90+
91+
-
92+
class: PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector
93+
94+
-
95+
class: PHPStan\Rules\DeadCode\PossiblyPureNewCollector
96+
8297
-
8398
class: PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule
8499
arguments:
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\DeadCode;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\CollectedDataNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use function array_key_exists;
11+
use function sprintf;
12+
use function strtolower;
13+
14+
/**
15+
* @implements Rule<CollectedDataNode>
16+
*/
17+
class CallToConstructorStatementWithoutImpurePointsRule implements Rule
18+
{
19+
20+
public function getNodeType(): string
21+
{
22+
return CollectedDataNode::class;
23+
}
24+
25+
public function processNode(Node $node, Scope $scope): array
26+
{
27+
$classesWithConstructors = [];
28+
foreach ($node->get(ConstructorWithoutImpurePointsCollector::class) as [$class]) {
29+
$classesWithConstructors[strtolower($class)] = $class;
30+
}
31+
32+
$errors = [];
33+
foreach ($node->get(PossiblyPureNewCollector::class) as $filePath => $data) {
34+
foreach ($data as [$class, $line]) {
35+
$lowerClass = strtolower($class);
36+
if (!array_key_exists($lowerClass, $classesWithConstructors)) {
37+
continue;
38+
}
39+
40+
$originalClassName = $classesWithConstructors[$lowerClass];
41+
$errors[] = RuleErrorBuilder::message(sprintf(
42+
'Call to new %s() on a separate line has no effect.',
43+
$originalClassName,
44+
))->file($filePath)
45+
->line($line)
46+
->identifier('new.resultUnused')
47+
->build();
48+
}
49+
}
50+
51+
return $errors;
52+
}
53+
54+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\DeadCode;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Collectors\Collector;
8+
use PHPStan\Node\MethodReturnStatementsNode;
9+
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use function count;
11+
use function strtolower;
12+
13+
/**
14+
* @implements Collector<MethodReturnStatementsNode, string>
15+
*/
16+
class ConstructorWithoutImpurePointsCollector implements Collector
17+
{
18+
19+
public function getNodeType(): string
20+
{
21+
return MethodReturnStatementsNode::class;
22+
}
23+
24+
public function processNode(Node $node, Scope $scope)
25+
{
26+
$method = $node->getMethodReflection();
27+
if (strtolower($method->getName()) !== '__construct') {
28+
return null;
29+
}
30+
31+
if (!$method->isPure()->maybe()) {
32+
return null;
33+
}
34+
35+
if (count($node->getImpurePoints()) !== 0) {
36+
return null;
37+
}
38+
39+
if (count($node->getStatementResult()->getThrowPoints()) !== 0) {
40+
return null;
41+
}
42+
43+
$variant = ParametersAcceptorSelector::selectSingle($method->getVariants());
44+
foreach ($variant->getParameters() as $parameter) {
45+
if (!$parameter->passedByReference()->createsNewVariable()) {
46+
continue;
47+
}
48+
49+
return null;
50+
}
51+
52+
if (count($method->getAsserts()->getAll()) !== 0) {
53+
return null;
54+
}
55+
56+
return $method->getDeclaringClass()->getName();
57+
}
58+
59+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\DeadCode;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Stmt\Expression;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Collectors\Collector;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use function strtolower;
11+
12+
/**
13+
* @implements Collector<Expression, array{string, int}>
14+
*/
15+
class PossiblyPureNewCollector implements Collector
16+
{
17+
18+
public function __construct(private ReflectionProvider $reflectionProvider)
19+
{
20+
}
21+
22+
public function getNodeType(): string
23+
{
24+
return Expression::class;
25+
}
26+
27+
public function processNode(Node $node, Scope $scope)
28+
{
29+
if (!$node->expr instanceof Node\Expr\New_) {
30+
return null;
31+
}
32+
33+
if (!$node->expr->class instanceof Node\Name) {
34+
return null;
35+
}
36+
37+
$className = $node->expr->class->toString();
38+
39+
if (!$this->reflectionProvider->hasClass($className)) {
40+
return null;
41+
}
42+
43+
$classReflection = $this->reflectionProvider->getClass($className);
44+
if (!$classReflection->hasConstructor()) {
45+
return null;
46+
}
47+
48+
$constructor = $classReflection->getConstructor();
49+
if (strtolower($constructor->getName()) !== '__construct') {
50+
return null;
51+
}
52+
53+
if (!$constructor->isPure()->maybe()) {
54+
return null;
55+
}
56+
57+
return [$constructor->getDeclaringClass()->getName(), $node->getStartLine()];
58+
}
59+
60+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\DeadCode;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<CallToConstructorStatementWithoutImpurePointsRule>
10+
*/
11+
class CallToConstructorStatementWithoutImpurePointsRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new CallToConstructorStatementWithoutImpurePointsRule();
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/call-to-constructor-without-impure-points.php'], [
22+
[
23+
'Call to new CallToConstructorWithoutImpurePoints\Foo() on a separate line has no effect.',
24+
15,
25+
],
26+
]);
27+
}
28+
29+
protected function getCollectors(): array
30+
{
31+
return [
32+
new PossiblyPureNewCollector($this->createReflectionProvider()),
33+
new ConstructorWithoutImpurePointsCollector(),
34+
];
35+
}
36+
37+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace CallToConstructorWithoutImpurePoints;
4+
5+
class Foo
6+
{
7+
8+
public function __construct()
9+
{
10+
}
11+
12+
}
13+
14+
function (): void {
15+
new Foo();
16+
};

0 commit comments

Comments
 (0)
Please sign in to comment.