diff --git a/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php index 71673e8e43b..9e7f17bce37 100644 --- a/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php +++ b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php @@ -19,6 +19,10 @@ final class CallToConstructorStatementWithoutImpurePointsRule implements Rule { + public function __construct(private PossiblyPureCallTransitivePurityResolver $purityResolver) + { + } + public function getNodeType(): string { return CollectedDataNode::class; @@ -26,9 +30,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + $pureKeys = $this->purityResolver->getPureCallableKeys($node); + $classesWithConstructors = []; - foreach ($node->get(ConstructorWithoutImpurePointsCollector::class) as [$class]) { - $classesWithConstructors[strtolower($class)] = $class; + foreach ($node->get(ConstructorWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$class]) { + if (!isset($pureKeys[PossiblyPureCallTransitivePurityResolver::methodKey($class, '__construct')])) { + continue; + } + + $classesWithConstructors[strtolower($class)] = $class; + } } $errors = []; diff --git a/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php index 06d710a83a7..bd0467f46ee 100644 --- a/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php +++ b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php @@ -19,6 +19,10 @@ final class CallToFunctionStatementWithoutImpurePointsRule implements Rule { + public function __construct(private PossiblyPureCallTransitivePurityResolver $purityResolver) + { + } + public function getNodeType(): string { return CollectedDataNode::class; @@ -26,9 +30,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + $pureKeys = $this->purityResolver->getPureCallableKeys($node); + $functions = []; - foreach ($node->get(FunctionWithoutImpurePointsCollector::class) as [$functionName]) { - $functions[strtolower($functionName)] = $functionName; + foreach ($node->get(FunctionWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$functionName]) { + if (!isset($pureKeys[PossiblyPureCallTransitivePurityResolver::functionKey($functionName)])) { + continue; + } + + $functions[strtolower($functionName)] = $functionName; + } } $errors = []; diff --git a/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php index a04d151ad9e..b3a6720bbf4 100644 --- a/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php +++ b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php @@ -19,6 +19,10 @@ final class CallToMethodStatementWithoutImpurePointsRule implements Rule { + public function __construct(private PossiblyPureCallTransitivePurityResolver $purityResolver) + { + } + public function getNodeType(): string { return CollectedDataNode::class; @@ -26,11 +30,16 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + $pureKeys = $this->purityResolver->getPureCallableKeys($node); + $methods = []; foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { foreach ($collected as [$className, $methodName, $classDisplayName]) { - $className = strtolower($className); - $methods[$className][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + if (!isset($pureKeys[PossiblyPureCallTransitivePurityResolver::methodKey($className, $methodName)])) { + continue; + } + + $methods[strtolower($className)][strtolower($methodName)] = $classDisplayName . '::' . $methodName; } } diff --git a/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php index f8938547b5a..25284799ce2 100644 --- a/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php +++ b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php @@ -19,6 +19,10 @@ final class CallToStaticMethodStatementWithoutImpurePointsRule implements Rule { + public function __construct(private PossiblyPureCallTransitivePurityResolver $purityResolver) + { + } + public function getNodeType(): string { return CollectedDataNode::class; @@ -26,11 +30,16 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + $pureKeys = $this->purityResolver->getPureCallableKeys($node); + $methods = []; foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { foreach ($collected as [$className, $methodName, $classDisplayName]) { - $lowerClassName = strtolower($className); - $methods[$lowerClassName][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + if (!isset($pureKeys[PossiblyPureCallTransitivePurityResolver::methodKey($className, $methodName)])) { + continue; + } + + $methods[strtolower($className)][strtolower($methodName)] = $classDisplayName . '::' . $methodName; } } diff --git a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php index 86d93e1c4ae..f0d0a9523d4 100644 --- a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php +++ b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php @@ -10,12 +10,16 @@ use function count; /** - * @implements Collector + * @implements Collector}> */ #[RegisteredCollector(level: 4)] final class ConstructorWithoutImpurePointsCollector implements Collector { + public function __construct(private PossiblyPureCallTransitivePurityResolver $purityResolver) + { + } + public function getNodeType(): string { return MethodReturnStatementsNode::class; @@ -32,14 +36,6 @@ public function processNode(Node $node, Scope $scope) return null; } - if (count($node->getImpurePoints()) !== 0) { - return null; - } - - if (count($node->getStatementResult()->getThrowPoints()) !== 0) { - return null; - } - foreach ($method->getParameters() as $parameter) { if (!$parameter->passedByReference()->createsNewVariable()) { continue; @@ -52,7 +48,15 @@ public function processNode(Node $node, Scope $scope) return null; } - return $method->getDeclaringClass()->getName(); + $dependencies = $this->purityResolver->resolveDependencies( + $node->getImpurePoints(), + $node->getStatementResult()->getThrowPoints(), + ); + if ($dependencies === null) { + return null; + } + + return [$method->getDeclaringClass()->getName(), $dependencies]; } } diff --git a/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php index d7abebe3eb9..e32c8efe04f 100644 --- a/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php +++ b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php @@ -10,12 +10,16 @@ use function count; /** - * @implements Collector + * @implements Collector}> */ #[RegisteredCollector(level: 4)] final class FunctionWithoutImpurePointsCollector implements Collector { + public function __construct(private PossiblyPureCallTransitivePurityResolver $purityResolver) + { + } + public function getNodeType(): string { return FunctionReturnStatementsNode::class; @@ -31,14 +35,6 @@ public function processNode(Node $node, Scope $scope) return null; } - if (count($node->getImpurePoints()) !== 0) { - return null; - } - - if (count($node->getStatementResult()->getThrowPoints()) !== 0) { - return null; - } - foreach ($function->getParameters() as $parameter) { if (!$parameter->passedByReference()->createsNewVariable()) { continue; @@ -51,7 +47,15 @@ public function processNode(Node $node, Scope $scope) return null; } - return $function->getName(); + $dependencies = $this->purityResolver->resolveDependencies( + $node->getImpurePoints(), + $node->getStatementResult()->getThrowPoints(), + ); + if ($dependencies === null) { + return null; + } + + return [$function->getName(), $dependencies]; } } diff --git a/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php index 776e55969c1..66297f61bf1 100644 --- a/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php +++ b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php @@ -10,12 +10,16 @@ use function count; /** - * @implements Collector + * @implements Collector}> */ #[RegisteredCollector(level: 4)] final class MethodWithoutImpurePointsCollector implements Collector { + public function __construct(private PossiblyPureCallTransitivePurityResolver $purityResolver) + { + } + public function getNodeType(): string { return MethodReturnStatementsNode::class; @@ -31,14 +35,6 @@ public function processNode(Node $node, Scope $scope) return null; } - if (count($node->getImpurePoints()) !== 0) { - return null; - } - - if (count($node->getStatementResult()->getThrowPoints()) !== 0) { - return null; - } - foreach ($method->getParameters() as $parameter) { if (!$parameter->passedByReference()->createsNewVariable()) { continue; @@ -55,7 +51,15 @@ public function processNode(Node $node, Scope $scope) return null; } - return [$method->getDeclaringClass()->getName(), $method->getName(), $method->getDeclaringClass()->getDisplayName()]; + $dependencies = $this->purityResolver->resolveDependencies( + $node->getImpurePoints(), + $node->getStatementResult()->getThrowPoints(), + ); + if ($dependencies === null) { + return null; + } + + return [$method->getDeclaringClass()->getName(), $method->getName(), $method->getDeclaringClass()->getDisplayName(), $dependencies]; } } diff --git a/src/Rules/DeadCode/PossiblyPureCallTransitivePurityResolver.php b/src/Rules/DeadCode/PossiblyPureCallTransitivePurityResolver.php new file mode 100644 index 00000000000..627f49e27f4 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureCallTransitivePurityResolver.php @@ -0,0 +1,256 @@ + */ + private array $cachedResult = []; + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public static function functionKey(string $functionName): string + { + return 'f' . "\0" . strtolower($functionName); + } + + public static function methodKey(string $className, string $methodName): string + { + return 'm' . "\0" . strtolower($className) . "\0" . strtolower($methodName); + } + + /** + * Resolves the dependencies of a declaration from its impure and throw points. + * + * Returns the list of callable keys the declaration depends on, or null when the + * declaration can never be effect-free (it has a non-call impure point or an explicit + * throw point). Implicit throw points are ignored, mirroring how noop expression + * statements are detected in NodeScopeResolver. + * + * @param ImpurePoint[] $impurePoints + * @param ThrowPoint[] $throwPoints + * @return list|null + */ + public function resolveDependencies(array $impurePoints, array $throwPoints): ?array + { + foreach ($throwPoints as $throwPoint) { + if ($throwPoint->isExplicit()) { + return null; + } + } + + $dependencies = []; + foreach ($impurePoints as $impurePoint) { + $keys = $this->resolveCall($impurePoint->getNode(), $impurePoint->getScope()); + if ($keys === null) { + return null; + } + + foreach ($keys as $key) { + $dependencies[$key] = true; + } + } + + return array_keys($dependencies); + } + + /** + * @return array + */ + public function getPureCallableKeys(CollectedDataNode $node): array + { + if ($this->cachedNode === $node) { + return $this->cachedResult; + } + + /** @var array> $declarations */ + $declarations = []; + + foreach ($node->get(FunctionWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$functionName, $dependencies]) { + $declarations[self::functionKey($functionName)] = $dependencies; + } + } + + foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$className, $methodName, , $dependencies]) { + $declarations[self::methodKey($className, $methodName)] = $dependencies; + } + } + + foreach ($node->get(ConstructorWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$className, $dependencies]) { + $declarations[self::methodKey($className, '__construct')] = $dependencies; + } + } + + $pure = []; + do { + $changed = false; + foreach ($declarations as $key => $dependencies) { + if (isset($pure[$key])) { + continue; + } + + $allPure = true; + foreach ($dependencies as $dependency) { + if (!isset($pure[$dependency])) { + $allPure = false; + break; + } + } + + if (!$allPure) { + continue; + } + + $pure[$key] = true; + $changed = true; + } + } while ($changed); + + $this->cachedNode = $node; + $this->cachedResult = $pure; + + return $pure; + } + + /** + * Resolves a call expression to the keys of the callables it targets, or null when + * the call cannot be guaranteed effect-free (unknown callee, overridable method, ...). + * + * @return list|null + */ + private function resolveCall(Node $expr, Scope $scope): ?array + { + if ($expr instanceof Node\Expr\FuncCall) { + if ($expr->isFirstClassCallable()) { + return null; + } + if (!$expr->name instanceof Node\Name) { + return null; + } + if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) { + return null; + } + + return [self::functionKey($this->reflectionProvider->getFunction($expr->name, $scope)->getName())]; + } + + if ($expr instanceof Node\Expr\MethodCall || $expr instanceof Node\Expr\NullsafeMethodCall) { + if ($expr->isFirstClassCallable()) { + return null; + } + if (!$expr->name instanceof Node\Identifier) { + return null; + } + + $methodName = $expr->name->toString(); + $calledOnType = $scope->getType($expr->var); + if (!$calledOnType->hasMethod($methodName)->yes()) { + return null; + } + + $keys = []; + foreach ($calledOnType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->hasMethod($methodName)) { + return null; + } + + $methodReflection = $classReflection->getMethod($methodName, $scope); + if ( + !$methodReflection->isPrivate() + && !$methodReflection->isFinal()->yes() + && !$methodReflection->getDeclaringClass()->isFinal() + ) { + if (!$classReflection->isFinal()) { + return null; + } + } + + $keys[] = self::methodKey($methodReflection->getDeclaringClass()->getName(), $methodReflection->getName()); + } + + if (count($keys) === 0) { + return null; + } + + return $keys; + } + + if ($expr instanceof Node\Expr\StaticCall) { + if ($expr->isFirstClassCallable()) { + return null; + } + if (!$expr->name instanceof Node\Identifier) { + return null; + } + if (!$expr->class instanceof Node\Name) { + return null; + } + + $methodName = $expr->name->toString(); + $calledOnType = $scope->resolveTypeByName($expr->class); + $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection === null) { + return null; + } + + return [self::methodKey($methodReflection->getDeclaringClass()->getName(), $methodReflection->getName())]; + } + + if ($expr instanceof Node\Expr\New_) { + if (!$expr->class instanceof Node\Name) { + return null; + } + + $className = $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; + } + + return [self::methodKey($constructor->getDeclaringClass()->getName(), '__construct')]; + } + + return null; + } + +} diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 707599c92f1..418b57601d2 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1274,9 +1274,10 @@ public function testBug13987(): void public function testBug13982(): void { - // false positive $errors = $this->runAnalyse(__DIR__ . '/data/bug-13982.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Call to method class@anonymous/tests/PHPStan/Analyser/data/bug-13982.php:6::test2() on a separate line has no effect.', $errors[0]->getMessage()); + $this->assertSame(15, $errors[0]->getLine()); } #[RequiresPhp('>= 8.1.0')] diff --git a/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php index c4dd2583f3d..9f1fca8e7f6 100644 --- a/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php @@ -13,7 +13,7 @@ class CallToConstructorStatementWithoutImpurePointsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToConstructorStatementWithoutImpurePointsRule(); + return new CallToConstructorStatementWithoutImpurePointsRule(new PossiblyPureCallTransitivePurityResolver(self::createReflectionProvider())); } public function testRule(): void @@ -26,11 +26,25 @@ public function testRule(): void ]); } + public function testTransitive(): void + { + $this->analyse([__DIR__ . '/data/call-to-constructor-without-impure-points-transitive.php'], [ + [ + 'Call to new CallToConstructorWithoutImpurePointsTransitive\PureCtor() on a separate line has no effect.', + 42, + ], + ]); + } + protected function getCollectors(): array { + $purityResolver = new PossiblyPureCallTransitivePurityResolver(self::createReflectionProvider()); + return [ new PossiblyPureNewCollector(self::createReflectionProvider()), - new ConstructorWithoutImpurePointsCollector(), + new ConstructorWithoutImpurePointsCollector($purityResolver), + new MethodWithoutImpurePointsCollector($purityResolver), + new FunctionWithoutImpurePointsCollector($purityResolver), ]; } diff --git a/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php index 41c7e9915c3..9d158674172 100644 --- a/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php @@ -14,7 +14,7 @@ class CallToFunctionStatementWithoutImpurePointsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToFunctionStatementWithoutImpurePointsRule(); + return new CallToFunctionStatementWithoutImpurePointsRule(new PossiblyPureCallTransitivePurityResolver(self::createReflectionProvider())); } public function testRule(): void @@ -27,6 +27,24 @@ public function testRule(): void ]); } + public function testTransitive(): void + { + $this->analyse([__DIR__ . '/data/call-to-function-without-impure-points-transitive.php'], [ + [ + 'Call to function CallToFunctionWithoutImpurePointsTransitive\pureBase() on a separate line has no effect.', + 32, + ], + [ + 'Call to function CallToFunctionWithoutImpurePointsTransitive\pureTransitive() on a separate line has no effect.', + 33, + ], + [ + 'Call to function CallToFunctionWithoutImpurePointsTransitive\pureTransitive2() on a separate line has no effect.', + 34, + ], + ]); + } + #[RequiresPhp('>= 8.5.0')] public function testPipeOperator(): void { @@ -44,9 +62,13 @@ public function testPipeOperator(): void protected function getCollectors(): array { + $purityResolver = new PossiblyPureCallTransitivePurityResolver(self::createReflectionProvider()); + return [ new PossiblyPureFuncCallCollector(self::createReflectionProvider()), - new FunctionWithoutImpurePointsCollector(), + new FunctionWithoutImpurePointsCollector($purityResolver), + new MethodWithoutImpurePointsCollector($purityResolver), + new ConstructorWithoutImpurePointsCollector($purityResolver), ]; } diff --git a/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php index b3486eb9bc2..bd84f0b1287 100644 --- a/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php @@ -14,7 +14,7 @@ class CallToMethodStatementWithoutImpurePointsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToMethodStatementWithoutImpurePointsRule(); + return new CallToMethodStatementWithoutImpurePointsRule(new PossiblyPureCallTransitivePurityResolver(self::createReflectionProvider())); } public function testRule(): void @@ -75,6 +75,20 @@ public function testRule(): void ]); } + public function testTransitive(): void + { + $this->analyse([__DIR__ . '/data/call-to-method-without-impure-points-transitive.php'], [ + [ + 'Call to method CallToMethodWithoutImpurePointsTransitive\Foo::pureBase() on a separate line has no effect.', + 37, + ], + [ + 'Call to method CallToMethodWithoutImpurePointsTransitive\Foo::transitive() on a separate line has no effect.', + 38, + ], + ]); + } + #[RequiresPhp('>= 8.0.0')] public function testBug11011(): void { @@ -109,9 +123,13 @@ public function testPipeOperator(): void protected function getCollectors(): array { + $purityResolver = new PossiblyPureCallTransitivePurityResolver(self::createReflectionProvider()); + return [ new PossiblyPureMethodCallCollector(), - new MethodWithoutImpurePointsCollector(), + new MethodWithoutImpurePointsCollector($purityResolver), + new FunctionWithoutImpurePointsCollector($purityResolver), + new ConstructorWithoutImpurePointsCollector($purityResolver), ]; } diff --git a/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php index 4a0ac6b38f9..dd6dde275e0 100644 --- a/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php @@ -14,7 +14,7 @@ class CallToStaticMethodStatementWithoutImpurePointsRuleTest extends RuleTestCas protected function getRule(): Rule { - return new CallToStaticMethodStatementWithoutImpurePointsRule(); + return new CallToStaticMethodStatementWithoutImpurePointsRule(new PossiblyPureCallTransitivePurityResolver(self::createReflectionProvider())); } public function testRule(): void @@ -44,6 +44,18 @@ public function testRule(): void 'Call to CallToStaticMethodWithoutImpurePoints\SubSubY::mySubSubFunc() on a separate line has no effect.', 21, ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\SubSubY::mySubSubCallSelfFunc() on a separate line has no effect.', + 22, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\SubSubY::mySubSubCallParentFunc() on a separate line has no effect.', + 23, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\SubSubY::mySubSubCallStaticFunc() on a separate line has no effect.', + 24, + ], [ 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', 48, @@ -76,9 +88,13 @@ public function testPipeOperator(): void protected function getCollectors(): array { + $purityResolver = new PossiblyPureCallTransitivePurityResolver(self::createReflectionProvider()); + return [ new PossiblyPureStaticCallCollector(), - new MethodWithoutImpurePointsCollector(), + new MethodWithoutImpurePointsCollector($purityResolver), + new FunctionWithoutImpurePointsCollector($purityResolver), + new ConstructorWithoutImpurePointsCollector($purityResolver), ]; } diff --git a/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points-transitive.php b/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points-transitive.php new file mode 100644 index 00000000000..b234d6a48b7 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points-transitive.php @@ -0,0 +1,44 @@ +pureBase() + pureFunc(); + } + + /** @phpstan-impure */ + public function impureBase(): void + { + echo 'x'; + } + + public function callsImpure(): void + { + $this->impureBase(); + } + +} + +function (Foo $foo): void { + $foo->pureBase(); + $foo->transitive(); + $foo->impureBase(); + $foo->callsImpure(); +};