diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a35d133b6fc..ba9b388d532 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3143,6 +3143,13 @@ public function enterForeachKey(Expr $iteratee, string $keyName): self $scope = $this->assignVariable($keyName, $iterateeType->getIterableKeyType()); $scope->nativeExpressionTypes[sprintf('$%s', $keyName)] = $nativeIterateeType->getIterableKeyType(); + if ($iterateeType->isArray()->yes()) { + $scope = $scope->specifyExpressionType( + new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), + $iterateeType->getIterableValueType(), + ); + } + return $scope; } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index fc657c9aa3d..a01b06900b7 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -118,11 +118,11 @@ public function getType(Expr $expr, InitializerExprContext $context): Type } if ($expr instanceof File) { $file = $context->getFile(); - return $file !== null ? new ConstantStringType($file) : new StringType(); + return $file !== null ? (new ConstantStringType($file))->generalize(GeneralizePrecision::moreSpecific()) : new StringType(); } if ($expr instanceof Dir) { $file = $context->getFile(); - return $file !== null ? new ConstantStringType(dirname($file)) : new StringType(); + return $file !== null ? (new ConstantStringType(dirname($file)))->generalize(GeneralizePrecision::moreSpecific()) : new StringType(); } if ($expr instanceof Line) { return new ConstantIntegerType($expr->getLine()); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 612a8dc09f4..fa06a05bdf0 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateType; @@ -120,6 +121,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function unsetOffset(Type $offsetType): Type { + if ($this->subtractedType !== null) { + return new self($this->isExplicitMixed, TypeCombinator::remove($this->subtractedType, new ConstantArrayType([], []))); + } return $this; } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 24e02420255..58fa78166c8 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2308,11 +2308,11 @@ public function dataBinaryOperations(): array '$line', ], [ - (new ConstantStringType(__DIR__ . '/data'))->describe(VerbosityLevel::precise()), + 'literal-string&non-falsy-string', '$dir', ], [ - (new ConstantStringType(__DIR__ . '/data/binary.php'))->describe(VerbosityLevel::precise()), + 'literal-string&non-falsy-string', '$file', ], [ diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 961cd2708fe..7c623c1a667 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -980,6 +980,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/ctype-digit.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7788.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7809.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/composer-non-empty-array-after-unset.php'); } /** diff --git a/tests/PHPStan/Analyser/data/composer-non-empty-array-after-unset.php b/tests/PHPStan/Analyser/data/composer-non-empty-array-after-unset.php new file mode 100644 index 00000000000..b0e9226df82 --- /dev/null +++ b/tests/PHPStan/Analyser/data/composer-non-empty-array-after-unset.php @@ -0,0 +1,46 @@ +config['authors'])) { + assertType("mixed~0|0.0|''|'0'|array{}|false|null", $this->config['authors']); + foreach ($this->config['authors'] as $key => $author) { + assertType("mixed~0|0.0|''|'0'|false|null", $this->config['authors']); + if (!is_array($author)) { + unset($this->config['authors'][$key]); + assertType("mixed~0|0.0|''|'0'|false|null", $this->config['authors']); + continue; + } + foreach (['homepage', 'email', 'name', 'role'] as $authorData) { + if (isset($author[$authorData]) && !is_string($author[$authorData])) { + unset($this->config['authors'][$key][$authorData]); + } + } + if (isset($author['homepage'])) { + unset($this->config['authors'][$key]['homepage']); + } + if (isset($author['email']) && !filter_var($author['email'], FILTER_VALIDATE_EMAIL)) { + unset($this->config['authors'][$key]['email']); + } + if (empty($this->config['authors'][$key])) { + assertType("mixed~0|0.0|''|'0'|false|null", $this->config['authors']); + unset($this->config['authors'][$key]); + assertType("mixed~0|0.0|''|'0'|false|null", $this->config['authors']); + } + assertType("mixed~0|0.0|''|'0'|false|null", $this->config['authors']); + } + assertType("mixed~0|0.0|''|'0'|false|null", $this->config['authors']); + } + } + +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php index 91464b46f31..ac465f8f429 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php @@ -10,7 +10,6 @@ use PHPStan\Type\VerbosityLevel; use SingleFileSourceLocatorTestClass; use TestSingleFileSourceLocator\AFoo; -use function str_replace; use const PHP_VERSION_ID; class OptimizedSingleFileSourceLocatorTest extends PHPStanTestCase @@ -119,7 +118,7 @@ public function dataConst(): array ], [ 'const_with_dir_const', - "'" . str_replace('\\', '/', __DIR__ . '/data') . "'", + 'literal-string&non-falsy-string', ], [ 'OPTIMIZED_SFSL_OBJECT_CONSTANT', diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 971334ae0f1..3c8beee5047 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -429,12 +429,7 @@ public function testBug7142(): void public function testBug6000(): void { $this->checkExplicitMixed = true; - $this->analyse([__DIR__ . '/data/bug-6000.php'], [ - [ - 'Offset \'classmap\' does not exist on array{psr-4?: array|string>, classmap?: array}.', - 12, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-6000.php'], []); } public function testBug5743(): void @@ -508,4 +503,9 @@ public function testBug7763(): void $this->analyse([__DIR__ . '/data/bug-7763.php'], []); } + public function testSpecifyExistentOffsetWhenEnteringForeach(): void + { + $this->analyse([__DIR__ . '/data/specify-existent-offset-when-entering-foreach.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/specify-existent-offset-when-entering-foreach.php b/tests/PHPStan/Rules/Arrays/data/specify-existent-offset-when-entering-foreach.php new file mode 100644 index 00000000000..18cfef7aead --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/specify-existent-offset-when-entering-foreach.php @@ -0,0 +1,22 @@ + 0, 'lib-' => 0, 'php' => 99, 'composer' => 99]; + foreach ($hintsToFind as $hintPrefix => $hintCount) { + if (str_starts_with($s, $hintPrefix)) { + if ($hintCount === 0 || $hintCount >= 99) { + $hintsToFind[$hintPrefix]++; + } elseif ($hintCount === 1) { + unset($hintsToFind[$hintPrefix]); + } + } + } + } + +} diff --git a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php index d155a13d96c..ab79952fcf8 100644 --- a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php @@ -50,7 +50,13 @@ public function testFile(): void public function testBug6000(): void { - $this->analyse([__DIR__ . '/../Arrays/data/bug-6000.php'], []); + $this->analyse([__DIR__ . '/../Arrays/data/bug-6000.php'], [ + [ + // shouldn't be happening... + 'Parameter #2 $array of function implode expects array, array|string> given.', + 12, + ], + ]); } }