diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f7b1c64ee0..a6d38c503c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -87,7 +87,7 @@ parameters: - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType - count: 4 + count: 5 path: src/Analyser/TypeSpecifier.php - diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 3a1f8bd98e..9a577ee53f 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -920,6 +920,52 @@ public function specifyTypesInCondition( return $exprType; } + if ( + $issetExpr instanceof ArrayDimFetch + && $issetExpr->dim !== null + ) { + $varType = $scope->getType($issetExpr->var); + if (!$varType instanceof MixedType) { + $dimType = $scope->getType($issetExpr->dim); + + if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { + $types = $varType instanceof UnionType ? $varType->getTypes() : [$varType]; + $typesToRemove = []; + foreach ($types as $innerType) { + $hasOffset = $innerType->hasOffsetValueType($dimType); + if (!$hasOffset->yes() || !$innerType->getOffsetValueType($dimType)->isNull()->no()) { + continue; + } + + $typesToRemove[] = $innerType; + } + + if ($typesToRemove !== []) { + $typeToRemove = TypeCombinator::union(...$typesToRemove); + $result = $this->create( + $issetExpr->var, + $typeToRemove, + TypeSpecifierContext::createFalse(), + $scope, + )->setRootExpr($expr); + + if ($scope->hasExpressionType($issetExpr->var)->maybe()) { + $result = $result->unionWith( + $this->create( + new IssetExpr($issetExpr->var), + new NullType(), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + + return $result; + } + } + } + } + return new SpecifiedTypes(); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-10544.php b/tests/PHPStan/Analyser/nsrt/bug-10544.php new file mode 100644 index 0000000000..22b9ed2636 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10544.php @@ -0,0 +1,19 @@ + 'string']; + } + + if (isset($a['bar'])) { + $a['bar'] = 1; + } + + assertType("array{}|array{bar: 1}", $a); + } + + /** + * @param array{bar?: int} $foo + */ + public function sayHello(array $foo): void + { + echo 'Hello' . print_r($foo, true); + } + + public function test2(): void + { + $a = []; + if (rand() % 2) { + $a = ['bar' => 'string']; + } + + if (isset($a['bar'])) { + $a['bar'] = 1; + } + + $this->sayHello($a); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/tagged-unions.php b/tests/PHPStan/Analyser/nsrt/tagged-unions.php index 9926467123..45e7e6e98b 100644 --- a/tests/PHPStan/Analyser/nsrt/tagged-unions.php +++ b/tests/PHPStan/Analyser/nsrt/tagged-unions.php @@ -55,7 +55,7 @@ public function doFoo4(array $foo) if (isset($foo['C'])) { assertType("array{A: string, C: 1}", $foo); } else { - assertType("array{A: int, B: 1}|array{A: string, C: 1}", $foo); // could be array{A: int, B: 1} + assertType("array{A: int, B: 1}", $foo); } assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo);