diff --git a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php index dfcc91a501f..6fa732c2c17 100644 --- a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php @@ -12,7 +12,9 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\Type\NullType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; /** * @implements ExprHandler @@ -42,7 +44,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex public function resolveType(MutatingScope $scope, Expr $expr): Type { - return $scope->getType($expr->getVar())->getOffsetValueType($scope->getType($expr->getDim())); + $varType = $scope->getType($expr->getVar()); + $dimType = $scope->getType($expr->getDim()); + $offsetValueType = $varType->getOffsetValueType($dimType); + if (!$varType->isArray()->no() && !$varType->hasOffsetValueType($dimType)->yes()) { + $offsetValueType = TypeCombinator::union($offsetValueType, new NullType()); + } + + return $offsetValueType; } } diff --git a/src/Node/CollectedDataNode.php b/src/Node/CollectedDataNode.php index 14a12815fe1..700600d4158 100644 --- a/src/Node/CollectedDataNode.php +++ b/src/Node/CollectedDataNode.php @@ -27,7 +27,7 @@ public function __construct(private array $collectedData, private bool $onlyFile * @template TCollector of Collector * @template TValue * @param class-string $collectorType - * @return array> + * @return array> */ public function get(string $collectorType): array { diff --git a/src/Reflection/ConstructorsHelper.php b/src/Reflection/ConstructorsHelper.php index fc38d722982..8986b82c06a 100644 --- a/src/Reflection/ConstructorsHelper.php +++ b/src/Reflection/ConstructorsHelper.php @@ -52,7 +52,7 @@ public function getConstructors(ClassReflection $classReflection): array $nativeReflection = $classReflection->getNativeReflection(); foreach ($this->additionalConstructors as $additionalConstructor) { [$className, $methodName] = explode('::', $additionalConstructor); - if (!$nativeReflection->hasMethod($methodName)) { + if ($methodName === null || !$nativeReflection->hasMethod($methodName)) { continue; } $nativeMethod = $nativeReflection->getMethod($methodName); diff --git a/tests/PHPStan/Analyser/nsrt/array-destructuring.php b/tests/PHPStan/Analyser/nsrt/array-destructuring.php index 39b4f2830a1..c8ca09e8b56 100644 --- a/tests/PHPStan/Analyser/nsrt/array-destructuring.php +++ b/tests/PHPStan/Analyser/nsrt/array-destructuring.php @@ -109,18 +109,18 @@ function (\stdClass $obj) { assertType('*ERROR*', $foreachNestedNeverList); assertType('1|4', $u1); assertType('2|\'bar\'', $u2); - assertType('3', $u3); + assertType('3|null', $u3); assertType('1|4', $foreachU1); assertType('2|\'bar\'', $foreachU2); - assertType('3', $foreachU3); - assertType('string', $firstStringArray); - assertType('string', $secondStringArray); + assertType('3|null', $foreachU3); + assertType('string|null', $firstStringArray); + assertType('string|null', $secondStringArray); assertType('non-empty-string', $thirdStringArray); - assertType('string', $fourthStringArray); - assertType('string', $firstStringArrayList); - assertType('string', $secondStringArrayList); + assertType('string|null', $fourthStringArray); + assertType('string|null', $firstStringArrayList); + assertType('string|null', $secondStringArrayList); assertType('non-empty-string', $thirdStringArrayList); - assertType('string', $fourthStringArrayList); + assertType('string|null', $fourthStringArrayList); assertType('non-empty-string', $firstStringArrayForeach); assertType('non-empty-string', $secondStringArrayForeach); assertType('non-empty-string', $thirdStringArrayForeach); @@ -130,7 +130,7 @@ function (\stdClass $obj) { assertType('non-empty-string', $thirdStringArrayForeachList); assertType('non-empty-string', $fourthStringArrayForeachList); assertType('lowercase-string&uppercase-string', $dateArray['Y']); - assertType('lowercase-string&uppercase-string', $dateArray['m']); + assertType('(lowercase-string&uppercase-string)|null', $dateArray['m']); assertType('int', $dateArray['d']); assertType('lowercase-string&uppercase-string', $intArrayForRewritingFirstElement[0]); assertType('int', $intArrayForRewritingFirstElement[1]); @@ -141,12 +141,12 @@ function (\stdClass $obj) { assertType('1', $assocOne); assertType('*ERROR*', $assocNonExistent); assertType('true', $dynamicAssocKey); - assertType('\'123\'|true', $dynamicAssocStrings); - assertType('1|\'123\'|\'foo\'|true', $dynamicAssocMixed); + assertType('\'123\'|true|null', $dynamicAssocStrings); + assertType('1|\'123\'|\'foo\'|true|null', $dynamicAssocMixed); assertType('true', $dynamicAssocKeyForeach); - assertType('\'123\'|true', $dynamicAssocStringsForeach); - assertType('1|\'123\'|\'foo\'|true', $dynamicAssocMixedForeach); - assertType('string', $stringFromIterable); + assertType('\'123\'|true|null', $dynamicAssocStringsForeach); + assertType('1|\'123\'|\'foo\'|true|null', $dynamicAssocMixedForeach); + assertType('string|null', $stringFromIterable); assertType('string', $stringWithVarAnnotation); assertType('string', $stringWithVarAnnotationInForeach); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10854.php b/tests/PHPStan/Analyser/nsrt/bug-10854.php new file mode 100644 index 00000000000..0a98dc22c95 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10854.php @@ -0,0 +1,77 @@ + $list + */ +function listFromGenericList(array $list): void +{ + [$a, $b] = $list; + assertType('string|null', $a); + assertType('string|null', $b); +} + +/** + * @param array $arr + */ +function listFromGenericArray(array $arr): void +{ + [$a, $b] = $arr; + assertType('string|null', $a); + assertType('string|null', $b); +} + +/** + * @param non-empty-list $list + */ +function listFromNonEmptyList(array $list): void +{ + [$a, $b] = $list; + assertType('string', $a); + assertType('string|null', $b); +} + +function listFromConstantArray(): void +{ + $arr = [1, 'foo', true]; + [$a, $b, $c] = $arr; + assertType('1', $a); + assertType("'foo'", $b); + assertType('true', $c); +} + +/** + * @param array{0: string, 1?: string} $arr + */ +function listFromOptionalKeys(array $arr): void +{ + [$a, $b] = $arr; + assertType('string', $a); + assertType('string|null', $b); +} + +function nullCoalesceAfterList(string $input): void +{ + [$a, $b] = explode('-', $input); + $x = $a ?? 'default'; + $y = $b ?? 'default'; + assertType('string', $x); + assertType('string', $y); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8127.php b/tests/PHPStan/Analyser/nsrt/bug-8127.php index 6e38769e6fe..9a3485e5e6d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8127.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8127.php @@ -45,7 +45,7 @@ public function getNodeType(): string public function processNode(\PhpParser\Node $node, Scope $scope): array { $sinkCollectorData = $node->get(SinkCollector::class); - assertType("array>", $sinkCollectorData); + assertType("array>", $sinkCollectorData); return []; } diff --git a/tests/PHPStan/Analyser/nsrt/collected-data.php b/tests/PHPStan/Analyser/nsrt/collected-data.php index 10c6d5fd8ce..ef7dd03fabe 100644 --- a/tests/PHPStan/Analyser/nsrt/collected-data.php +++ b/tests/PHPStan/Analyser/nsrt/collected-data.php @@ -30,7 +30,7 @@ class Foo public function doFoo(CollectedDataNode $node): void { - assertType('array>', $node->get(TestCollector::class)); + assertType('array>', $node->get(TestCollector::class)); } } diff --git a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php index c9f990d1cdf..00d751ab2fb 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php @@ -77,7 +77,7 @@ private static function generateSymbolDescription(string $symbol): string { [$type, $name] = explode(' ', $symbol); - if ($name === '') { + if ($name === null || $name === '') { throw new ShouldNotHappenException(); } @@ -288,6 +288,10 @@ private static function generateClassMethodDescription(string $classMethodName): { [$className, $methodName] = explode('::', $classMethodName); + if ($methodName === null) { + throw new ShouldNotHappenException(); + } + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); if (! $reflectionProvider->hasClass($className)) { @@ -429,6 +433,11 @@ private static function generateVariantsDescription(string $name, array $variant private static function generateClassPropertyDescription(string $propertyName): string { [$className, $propertyName] = explode('::', $propertyName); + + if ($propertyName === null) { + throw new ShouldNotHappenException(); + } + // remove $ $propertyName = substr($propertyName, 1); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index d7a4d6766d8..83478879e0c 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2493,7 +2493,7 @@ public function testRectorDoWhileVarIssue(): void $this->checkUnionTypes = true; $this->analyse([__DIR__ . '/data/rector-do-while-var-issue.php'], [ [ - 'Parameter #1 $cls of method RectorDoWhileVarIssue\Foo::processCharacterClass() expects string, int|string given.', + 'Parameter #1 $cls of method RectorDoWhileVarIssue\Foo::processCharacterClass() expects string, int|string|null given.', 24, ], ]); diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index b8ed61a1d96..cf1f07299e2 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -459,4 +459,26 @@ public function testBug14393(): void ]); } + public function testBug10854(): void + { + $this->analyse([__DIR__ . '/data/bug-10854.php'], [ + [ + 'Variable $a on left side of ?? always exists and is not nullable.', + 10, + ], + [ + 'Variable $a on left side of ?? always exists and is not nullable.', + 17, + ], + [ + 'Variable $a on left side of ?? always exists and is not nullable.', + 37, + ], + [ + 'Variable $a on left side of ??= always exists and is not nullable.', + 44, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-10854.php b/tests/PHPStan/Rules/Variables/data/bug-10854.php new file mode 100644 index 00000000000..73505ad0201 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10854.php @@ -0,0 +1,55 @@ + $list + */ + public function doBaz(array $list): void + { + [$a, $b] = $list; + $x = $a ?? 'default'; // no error + $y = $b ?? 'default'; // no error + } + + /** + * @param array{0: string, 1?: string} $arr + */ + public function doQux(array $arr): void + { + [$a, $b] = $arr; + $x = $a ?? 'default'; // $a is always string from required key 0 + $y = $b ?? 'default'; // no error - key 1 is optional + } + + public function coalesceAssign(string $input): void + { + [$a, $b] = explode('-', $input); + $a ??= 'default'; // $a is always string from non-empty-list index 0 + $b ??= 'default'; // no error - $b might be null + } + + public function issetAfterList(string $input): void + { + [$a, $b] = explode('-', $input); + if (isset($b)) { // no error - $b might be null/undefined + echo $b; + } + } +} diff --git a/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php b/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php index 054094eebbd..2eec83366f5 100644 --- a/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php +++ b/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Testing; use PHPStan\File\FileHelper; +use PHPStan\ShouldNotHappenException; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Attributes\DataProvider; use function array_values; @@ -117,6 +118,10 @@ public function testVariableOrOffsetDescription(): void [$variableAssert, $offsetAssert] = array_values(self::gatherAssertTypes($filePath)); + if ($variableAssert === null || $offsetAssert === null) { + throw new ShouldNotHappenException(); + } + $this->assertSame('variable $context', $variableAssert[4]); $this->assertSame("offset 'email'", $offsetAssert[4]); }