Skip to content

Commit 452a3c8

Browse files
phpstan-botclaude
andcommitted
Return TrinaryLogic from expressionContainsNonPureCall
Refactor expressionContainsNonPureCall to return TrinaryLogic instead of bool, separating the purity determination from the rememberPossiblyImpureFunctionValues policy decision. Extract callNodeHasSideEffects helper that returns the raw TrinaryLogic purity of each call node. Replace NodeFinder::findFirst (which requires a bool callback) with NodeFinder::find to collect all call nodes, then aggregate their purity via TrinaryLogic::or with early exit on yes. Unknown methods and dynamic call names return maybe (we genuinely don't know their purity). Unknown named functions keep returning yes for backward compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e731e88 commit 452a3c8

1 file changed

Lines changed: 63 additions & 60 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2538,12 +2538,15 @@ private function createForExpr(
25382538
}
25392539
}
25402540

2541-
if (!($expr instanceof AlwaysRememberedExpr) && $this->expressionContainsNonPureCall($expr, $scope)) {
2542-
if (isset($containsNull) && !$containsNull) {
2543-
return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
2544-
}
2541+
if (!($expr instanceof AlwaysRememberedExpr)) {
2542+
$containsNonPureCall = $this->expressionContainsNonPureCall($expr, $scope);
2543+
if ($containsNonPureCall->yes() || (!$this->rememberPossiblyImpureFunctionValues && !$containsNonPureCall->no())) {
2544+
if (isset($containsNull) && !$containsNull) {
2545+
return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
2546+
}
25452547

2546-
return new SpecifiedTypes([], []);
2548+
return new SpecifiedTypes([], []);
2549+
}
25472550
}
25482551

25492552
$sureTypes = [];
@@ -2574,76 +2577,76 @@ private function createForExpr(
25742577
return $types;
25752578
}
25762579

2577-
private function expressionContainsNonPureCall(Expr $expr, Scope $scope): bool
2580+
private function expressionContainsNonPureCall(Expr $expr, Scope $scope): TrinaryLogic
25782581
{
25792582
$nodeFinder = new NodeFinder();
2580-
$found = $nodeFinder->findFirst([$expr], function (Node $node) use ($scope): bool {
2581-
if (!$node instanceof Expr) {
2582-
return false;
2583+
$callNodes = $nodeFinder->find([$expr], static fn (Node $node): bool => $node instanceof FuncCall || $node instanceof MethodCall || $node instanceof StaticCall);
2584+
2585+
$result = TrinaryLogic::createNo();
2586+
foreach ($callNodes as $callNode) {
2587+
$result = $result->or($this->callNodeHasSideEffects($callNode, $scope));
2588+
if ($result->yes()) {
2589+
return $result;
25832590
}
2591+
}
25842592

2585-
if ($node instanceof FuncCall) {
2586-
if ($node->name instanceof Name) {
2587-
if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
2588-
return true;
2589-
}
2590-
$hasSideEffects = $this->reflectionProvider->getFunction($node->name, $scope)->hasSideEffects();
2591-
return $hasSideEffects->yes()
2592-
|| (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no());
2593-
}
2593+
return $result;
2594+
}
25942595

2595-
$nameType = $scope->getType($node->name);
2596-
if ($nameType->isCallable()->yes()) {
2597-
$isPure = null;
2598-
foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
2599-
$variantIsPure = $variant->isPure();
2600-
$isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
2601-
}
2602-
if ($isPure !== null) {
2603-
return $isPure->no()
2604-
|| (!$this->rememberPossiblyImpureFunctionValues && !$isPure->yes());
2605-
}
2596+
private function callNodeHasSideEffects(Node $node, Scope $scope): TrinaryLogic
2597+
{
2598+
if ($node instanceof FuncCall) {
2599+
if ($node->name instanceof Name) {
2600+
if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
2601+
return TrinaryLogic::createYes();
26062602
}
2607-
2608-
return false;
2603+
return $this->reflectionProvider->getFunction($node->name, $scope)->hasSideEffects();
26092604
}
26102605

2611-
if ($node instanceof MethodCall) {
2612-
if ($node->name instanceof Identifier) {
2613-
$calledOnType = $scope->getType($node->var);
2614-
$methodReflection = $scope->getMethodReflection($calledOnType, $node->name->name);
2615-
if ($methodReflection === null) {
2616-
return true;
2617-
}
2618-
$hasSideEffects = $methodReflection->hasSideEffects();
2619-
return $hasSideEffects->yes()
2620-
|| (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no());
2606+
$nameType = $scope->getType($node->name);
2607+
if ($nameType->isCallable()->yes()) {
2608+
$isPure = null;
2609+
foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
2610+
$variantIsPure = $variant->isPure();
2611+
$isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
2612+
}
2613+
if ($isPure !== null) {
2614+
return $isPure->negate();
26212615
}
2622-
return true;
26232616
}
26242617

2625-
if ($node instanceof StaticCall) {
2626-
if ($node->name instanceof Identifier) {
2627-
if ($node->class instanceof Name) {
2628-
$calledOnType = $scope->resolveTypeByName($node->class);
2629-
} else {
2630-
$calledOnType = $scope->getType($node->class);
2631-
}
2632-
$methodReflection = $scope->getMethodReflection($calledOnType, $node->name->name);
2633-
if ($methodReflection === null) {
2634-
return true;
2635-
}
2636-
$hasSideEffects = $methodReflection->hasSideEffects();
2637-
return $hasSideEffects->yes()
2638-
|| (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no());
2618+
return TrinaryLogic::createNo();
2619+
}
2620+
2621+
if ($node instanceof MethodCall) {
2622+
if ($node->name instanceof Identifier) {
2623+
$calledOnType = $scope->getType($node->var);
2624+
$methodReflection = $scope->getMethodReflection($calledOnType, $node->name->name);
2625+
if ($methodReflection === null) {
2626+
return TrinaryLogic::createMaybe();
26392627
}
2640-
return true;
2628+
return $methodReflection->hasSideEffects();
26412629
}
2630+
return TrinaryLogic::createMaybe();
2631+
}
26422632

2643-
return false;
2644-
});
2633+
if ($node instanceof StaticCall) {
2634+
if ($node->name instanceof Identifier) {
2635+
if ($node->class instanceof Name) {
2636+
$calledOnType = $scope->resolveTypeByName($node->class);
2637+
} else {
2638+
$calledOnType = $scope->getType($node->class);
2639+
}
2640+
$methodReflection = $scope->getMethodReflection($calledOnType, $node->name->name);
2641+
if ($methodReflection === null) {
2642+
return TrinaryLogic::createMaybe();
2643+
}
2644+
return $methodReflection->hasSideEffects();
2645+
}
2646+
return TrinaryLogic::createMaybe();
2647+
}
26452648

2646-
return $found !== null;
2649+
return TrinaryLogic::createNo();
26472650
}
26482651

26492652
private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierContext $context, ?Type $type): SpecifiedTypes

0 commit comments

Comments
 (0)