diff --git a/conf/config.level4.neon b/conf/config.level4.neon index 8c027372da..d68be91709 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -32,6 +32,12 @@ conditionalTags: phpstan.rules.rule: %featureToggles.paramOutType% PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule: phpstan.rules.rule: %featureToggles.paramOutType% + PHPStan\Rules\DeadCode\CallToConstructorStatementWithoutImpurePointsRule: + phpstan.rules.rule: %featureToggles.pure% + PHPStan\Rules\DeadCode\PossiblyPureNewCollector: + phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector: + phpstan.collector: %featureToggles.pure% parameters: checkAdvancedIsset: true @@ -79,6 +85,15 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\DeadCode\CallToConstructorStatementWithoutImpurePointsRule + + - + class: PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector + + - + class: PHPStan\Rules\DeadCode\PossiblyPureNewCollector + - class: PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule arguments: diff --git a/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..ab14e74d36 --- /dev/null +++ b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php @@ -0,0 +1,54 @@ + + */ +class CallToConstructorStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classesWithConstructors = []; + foreach ($node->get(ConstructorWithoutImpurePointsCollector::class) as [$class]) { + $classesWithConstructors[strtolower($class)] = $class; + } + + $errors = []; + foreach ($node->get(PossiblyPureNewCollector::class) as $filePath => $data) { + foreach ($data as [$class, $line]) { + $lowerClass = strtolower($class); + if (!array_key_exists($lowerClass, $classesWithConstructors)) { + continue; + } + + $originalClassName = $classesWithConstructors[$lowerClass]; + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to new %s() on a separate line has no effect.', + $originalClassName, + ))->file($filePath) + ->line($line) + ->identifier('new.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..dae162500a --- /dev/null +++ b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php @@ -0,0 +1,59 @@ + + */ +class ConstructorWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $method = $node->getMethodReflection(); + if (strtolower($method->getName()) !== '__construct') { + return null; + } + + if (!$method->isPure()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + foreach ($variant->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($method->getAsserts()->getAll()) !== 0) { + return null; + } + + return $method->getDeclaringClass()->getName(); + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureNewCollector.php b/src/Rules/DeadCode/PossiblyPureNewCollector.php new file mode 100644 index 0000000000..e2fabe49ca --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureNewCollector.php @@ -0,0 +1,60 @@ + + */ +class PossiblyPureNewCollector implements Collector +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\New_) { + return null; + } + + if (!$node->expr->class instanceof Node\Name) { + return null; + } + + $className = $node->expr->class->toString(); + + if (!$this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$classReflection->hasConstructor()) { + return null; + } + + $constructor = $classReflection->getConstructor(); + if (strtolower($constructor->getName()) !== '__construct') { + return null; + } + + if (!$constructor->isPure()->maybe()) { + return null; + } + + return [$constructor->getDeclaringClass()->getName(), $node->getStartLine()]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..ac630c4880 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,37 @@ + + */ +class CallToConstructorStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToConstructorStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-constructor-without-impure-points.php'], [ + [ + 'Call to new CallToConstructorWithoutImpurePoints\Foo() on a separate line has no effect.', + 15, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureNewCollector($this->createReflectionProvider()), + new ConstructorWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points.php new file mode 100644 index 0000000000..10b9622a2d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points.php @@ -0,0 +1,16 @@ +