Skip to content

Commit 82ee0bf

Browse files
phpstan-botclaude
andcommitted
Check for impure sub-expressions in TypeSpecifier::createForExpr
Instead of only checking for `new` in the expression chain at type resolution time, prevent TypeSpecifier from storing narrowed types when the receiver chain or arguments contain impure calls. This generalizes the fix to handle named constructor patterns like `Repository::create()->getAll()` where `create()` is impure, and impure arguments like `$repo->getAll(Repository::create())`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 16ab672 commit 82ee0bf

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2405,6 +2405,14 @@ private function createForExpr(
24052405
}
24062406
}
24072407

2408+
if ($this->subExpressionsHaveSideEffects($expr, $scope)) {
2409+
if (isset($containsNull) && !$containsNull) {
2410+
return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
2411+
}
2412+
2413+
return new SpecifiedTypes([], []);
2414+
}
2415+
24082416
$sureTypes = [];
24092417
$sureNotTypes = [];
24102418
if ($context->false()) {
@@ -2433,6 +2441,149 @@ private function createForExpr(
24332441
return $types;
24342442
}
24352443

2444+
private function subExpressionsHaveSideEffects(Expr $expr, Scope $scope): bool
2445+
{
2446+
if (
2447+
$expr instanceof MethodCall
2448+
|| $expr instanceof Expr\NullsafeMethodCall
2449+
|| $expr instanceof PropertyFetch
2450+
|| $expr instanceof Expr\NullsafePropertyFetch
2451+
|| $expr instanceof ArrayDimFetch
2452+
) {
2453+
if ($this->expressionHasSideEffects($expr->var, $scope)) {
2454+
return true;
2455+
}
2456+
} elseif (
2457+
$expr instanceof StaticCall
2458+
|| $expr instanceof StaticPropertyFetch
2459+
) {
2460+
if ($expr->class instanceof Expr && $this->expressionHasSideEffects($expr->class, $scope)) {
2461+
return true;
2462+
}
2463+
}
2464+
2465+
if ($expr instanceof Expr\CallLike && !$expr->isFirstClassCallable()) {
2466+
foreach ($expr->getArgs() as $arg) {
2467+
if ($this->expressionHasSideEffects($arg->value, $scope)) {
2468+
return true;
2469+
}
2470+
}
2471+
}
2472+
2473+
return false;
2474+
}
2475+
2476+
private function expressionHasSideEffects(Expr $expr, Scope $scope): bool
2477+
{
2478+
if ($expr instanceof Expr\New_) {
2479+
return true;
2480+
}
2481+
2482+
if ($expr instanceof FuncCall) {
2483+
if ($expr->isFirstClassCallable()) {
2484+
return false;
2485+
}
2486+
if ($expr->name instanceof Name) {
2487+
if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) {
2488+
return true;
2489+
}
2490+
$functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
2491+
$hasSideEffects = $functionReflection->hasSideEffects();
2492+
if ($hasSideEffects->yes()) {
2493+
return true;
2494+
}
2495+
if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
2496+
return true;
2497+
}
2498+
} else {
2499+
return true;
2500+
}
2501+
foreach ($expr->getArgs() as $arg) {
2502+
if ($this->expressionHasSideEffects($arg->value, $scope)) {
2503+
return true;
2504+
}
2505+
}
2506+
return false;
2507+
}
2508+
2509+
if ($expr instanceof MethodCall || $expr instanceof Expr\NullsafeMethodCall) {
2510+
if ($expr->isFirstClassCallable()) {
2511+
return $this->expressionHasSideEffects($expr->var, $scope);
2512+
}
2513+
if ($expr->name instanceof Node\Identifier) {
2514+
$calledOnType = $scope->getType($expr->var);
2515+
$methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString());
2516+
if (
2517+
$methodReflection === null
2518+
|| $methodReflection->hasSideEffects()->yes()
2519+
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
2520+
) {
2521+
return true;
2522+
}
2523+
} else {
2524+
return true;
2525+
}
2526+
foreach ($expr->getArgs() as $arg) {
2527+
if ($this->expressionHasSideEffects($arg->value, $scope)) {
2528+
return true;
2529+
}
2530+
}
2531+
return $this->expressionHasSideEffects($expr->var, $scope);
2532+
}
2533+
2534+
if ($expr instanceof StaticCall) {
2535+
if ($expr->isFirstClassCallable()) {
2536+
if ($expr->class instanceof Expr) {
2537+
return $this->expressionHasSideEffects($expr->class, $scope);
2538+
}
2539+
return false;
2540+
}
2541+
if ($expr->name instanceof Node\Identifier) {
2542+
if ($expr->class instanceof Name) {
2543+
$calledOnType = $scope->resolveTypeByName($expr->class);
2544+
} else {
2545+
$calledOnType = $scope->getType($expr->class);
2546+
}
2547+
$methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString());
2548+
if (
2549+
$methodReflection === null
2550+
|| $methodReflection->hasSideEffects()->yes()
2551+
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
2552+
) {
2553+
return true;
2554+
}
2555+
} else {
2556+
return true;
2557+
}
2558+
foreach ($expr->getArgs() as $arg) {
2559+
if ($this->expressionHasSideEffects($arg->value, $scope)) {
2560+
return true;
2561+
}
2562+
}
2563+
if ($expr->class instanceof Expr) {
2564+
return $this->expressionHasSideEffects($expr->class, $scope);
2565+
}
2566+
return false;
2567+
}
2568+
2569+
if ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) {
2570+
return $this->expressionHasSideEffects($expr->var, $scope);
2571+
}
2572+
2573+
if ($expr instanceof ArrayDimFetch) {
2574+
return $this->expressionHasSideEffects($expr->var, $scope);
2575+
}
2576+
2577+
if ($expr instanceof StaticPropertyFetch) {
2578+
if ($expr->class instanceof Expr) {
2579+
return $this->expressionHasSideEffects($expr->class, $scope);
2580+
}
2581+
return false;
2582+
}
2583+
2584+
return false;
2585+
}
2586+
24362587
private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierContext $context, ?Type $type): SpecifiedTypes
24372588
{
24382589
if ($expr instanceof Expr\NullsafePropertyFetch) {

0 commit comments

Comments
 (0)