Skip to content

Commit c9b56e6

Browse files
phpstan-botgithub-actions[bot]claude
committed
Fix phpstan/phpstan#12686: Unable to mark an anonymous function as impure (phpstan#5348)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 973ff7c commit c9b56e6

File tree

5 files changed

+156
-0
lines changed

5 files changed

+156
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,30 @@ private function createForExpr(
22312231
}
22322232
}
22332233

2234+
if (
2235+
$expr instanceof FuncCall
2236+
&& !$expr->name instanceof Name
2237+
) {
2238+
$nameType = $scope->getType($expr->name);
2239+
if ($nameType->isCallable()->yes()) {
2240+
$isPure = null;
2241+
foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
2242+
$variantIsPure = $variant->isPure();
2243+
$isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
2244+
}
2245+
2246+
if ($isPure !== null) {
2247+
if ($isPure->no()) {
2248+
return new SpecifiedTypes([], []);
2249+
}
2250+
2251+
if (!$this->rememberPossiblyImpureFunctionValues && !$isPure->yes()) {
2252+
return new SpecifiedTypes([], []);
2253+
}
2254+
}
2255+
}
2256+
}
2257+
22342258
if (
22352259
$expr instanceof MethodCall
22362260
&& $expr->name instanceof Node\Identifier
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12686;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/** @phpstan-impure */
8+
$f = function (): bool {
9+
return (bool) rand(0,1);
10+
};
11+
12+
if ($f()) {
13+
assertType('bool', $f());
14+
}
15+
16+
// Pure closure should still have narrowing
17+
$h = function (): bool {
18+
return true;
19+
};
20+
21+
if ($h()) {
22+
assertType('true', $h());
23+
}
24+
25+
// Multiple callable parameter acceptors (union of closures)
26+
// When one variant is impure, the combined result should be impure
27+
/** @phpstan-impure */
28+
$impure = function (): bool {
29+
return (bool) rand(0, 1);
30+
};
31+
32+
$pure = function (): bool {
33+
return true;
34+
};
35+
36+
if (rand(0, 1)) {
37+
$g = $impure;
38+
} else {
39+
$g = $pure;
40+
}
41+
42+
if ($g()) {
43+
assertType('bool', $g());
44+
}
45+
46+
// Multiple callable parameter acceptors where all are pure
47+
$pure1 = function (): bool {
48+
return true;
49+
};
50+
51+
$pure2 = function (): bool {
52+
return true;
53+
};
54+
55+
if (rand(0, 1)) {
56+
$p = $pure1;
57+
} else {
58+
$p = $pure2;
59+
}
60+
61+
if ($p()) {
62+
assertType('true', $p());
63+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug3770;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
// PHPDoc on closures should be respected for purity
8+
9+
/** @phpstan-impure */
10+
$f = static function (string $input): bool {
11+
return strlen($input) > rand(0, 10);
12+
};
13+
14+
if ($f('hello')) {
15+
// Should not narrow to true because closure is impure
16+
assertType('bool', $f('hello'));
17+
}
18+
19+
// Closure with @phpstan-pure should allow narrowing
20+
/** @phpstan-pure */
21+
$g = static function (string $input): bool {
22+
return strlen($input) > 5;
23+
};
24+
25+
if ($g('hello world')) {
26+
assertType('true', $g('hello world'));
27+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6822;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
// Closures marked as @phpstan-impure should not have their return type narrowed
8+
9+
/** @phpstan-impure */
10+
$closure = function (): bool {
11+
return (bool) rand(0, 1);
12+
};
13+
14+
assertType('bool', $closure());
15+
16+
if ($closure()) {
17+
assertType('bool', $closure());
18+
19+
if ($closure()) { // should not be reported as "always true"
20+
echo 'yes';
21+
}
22+
}
23+
24+
// Same with an explicit impure closure assigned to a variable
25+
/** @phpstan-impure */
26+
$impureFn = function (): int {
27+
return rand(0, 100);
28+
};
29+
30+
if ($impureFn() > 50) {
31+
assertType('int<0, 100>', $impureFn());
32+
33+
if ($impureFn() > 50) { // should not be reported as "always true"
34+
echo 'yes';
35+
}
36+
}

tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,10 @@ public function testBug4284(): void
231231
]);
232232
}
233233

234+
public function testBug6822(): void
235+
{
236+
$this->treatPhpDocTypesAsCertain = true;
237+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6822.php'], []);
238+
}
239+
234240
}

0 commit comments

Comments
 (0)