diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index d28a3225de..ac8a0a8005 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -336,7 +336,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ( $functionReflection !== null && $this->rememberPossiblyImpureFunctionValues - && $parametersAcceptor !== null && $functionReflection->hasSideEffects()->maybe() && !$functionReflection->isBuiltin() ) { diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c81d35e98f..e02c18b0da 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3787,6 +3787,13 @@ private function createConditionalExpressions( ): array { $newVariableTypes = $ourExpressionTypes; + + // When our-branch type is a subtype of their-branch type, the union + // absorbs it (merged === their). Such a variable is a poor *guard* — + // asserting its our-branch type later wouldn't reliably select this + // branch — but it remains a valid conditional *target*, so only exclude + // it from guard selection instead of dropping it entirely. + $guardsToExclude = []; foreach ($theirExpressionTypes as $exprString => $holder) { if (!array_key_exists($exprString, $mergedExpressionTypes)) { continue; @@ -3804,7 +3811,7 @@ private function createConditionalExpressions( continue; } - unset($newVariableTypes[$exprString]); + $guardsToExclude[$exprString] = true; } $typeGuards = []; @@ -3818,6 +3825,9 @@ private function createConditionalExpressions( if (!$holder->getCertainty()->yes()) { continue; } + if (array_key_exists($exprString, $guardsToExclude)) { + continue; + } if ( array_key_exists($exprString, $theirExpressionTypes) diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php index f2b0f7e3a6..c7edc63cf0 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -112,6 +112,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): if ($identifier->isClass()) { $fetchedClassNode = null; + $fetchedFile = null; foreach ($files as $file) { $fetchedClassNodes = $this->fileNodesFetcher->fetchNodes($file)->getClassNodes(); @@ -121,13 +122,10 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): /** @var FetchedNode $fetchedClassNode */ $fetchedClassNode = current($fetchedClassNodes[$identifierName]); + $fetchedFile = $file; } - if ($fetchedClassNode === null) { - return null; - } - - [$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($file, $identifier); + [$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($fetchedFile, $identifier); $classReflection = $this->nodeToReflection($reflector, $fetchedClassNode); $this->cache->save($reflectionCacheKey, $variableCacheKey, $classReflection->exportToCache()); diff --git a/tests/PHPStan/Analyser/nsrt/bug-5051.php b/tests/PHPStan/Analyser/nsrt/bug-5051.php index 94ffc4711c..a91a87716b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5051.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5051.php @@ -49,7 +49,7 @@ public function testWithBooleans($data): void if ($update) { assertType('10', $data); - assertType('bool', $foo); + assertType('true', $foo); } else { assertType('1|2|3', $data); assertType('bool', $foo); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7948.php b/tests/PHPStan/Analyser/nsrt/bug-7948.php new file mode 100644 index 0000000000..2f8a488afe --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7948.php @@ -0,0 +1,71 @@ + $name + * @param mixed $value + */ + public function testMixed($name, $value): void + { + if (is_array($name)) { + $value = null; + } + + if (is_array($name)) { + assertType('null', $value); + } + } + + /** + * @param string|array $name + * @param int $value + */ + public function testInt($name, $value): void + { + if (is_array($name)) { + $value = null; + } + + if (is_array($name)) { + assertType('null', $value); + } + } + + /** + * Assigned value (5) is a subtype of the original type (int|string), + * so the merged type absorbs it just like null|mixed does. + * + * @param string|array $name + * @param int|string $value + */ + public function testSubtype($name, $value): void + { + if (is_array($name)) { + $value = 5; + } + + if (is_array($name)) { + assertType('5', $value); + } + } + + /** + * @param string|array $name + * @param mixed $value + */ + public function testMixedNegated($name, $value): void + { + if (!is_array($name)) { + $value = null; + } + + if (!is_array($name)) { + assertType('null', $value); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8467b.php b/tests/PHPStan/Analyser/nsrt/bug-8467b.php index c4971320ff..5f6e33e828 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8467b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8467b.php @@ -13,7 +13,7 @@ public function foo (?string $cwd, bool $initialClone = false): void { if ($initialClone && isset($origCwd)) { assertType('string', $origCwd); - assertType('string|null', $cwd); // could be null + assertType('null', $cwd); } } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php index 415f00c502..19d3199f12 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php @@ -112,7 +112,7 @@ public function variants(string $s) { assertType('string', $s); if (strpos($s, ':') === 5) { - assertType('string', $s); // could be non-empty-string + assertType('non-falsy-string', $s); } assertType('string', $s); if (strpos($s, ':') !== 5) { @@ -164,7 +164,7 @@ public function variants(string $s) { assertType('string', $s); if (mb_strpos($s, ':') === 5) { - assertType('string', $s); // could be non-empty-string + assertType('non-falsy-string', $s); } assertType('string', $s); if (mb_strpos($s, ':') !== 5) { diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 6709b487fe..ba95d3af40 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -172,10 +172,6 @@ public function testTypesAssignedToPropertiesExpressionNames(): void 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept string.', 69, ], - [ - 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float.', - 83, - ], [ 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float|int|string.', 97,