From e6b84f1b6cf0d1107a0955ba19fbf4c4d4bf1f42 Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Sun, 30 Aug 2026 11:48:33 +0200 Subject: [PATCH] Hint type blind spots --- .../ExprHandler/Helper/ClosureTypeResolver.php | 2 ++ src/Analyser/NodeScopeResolver.php | 12 ++++++------ src/Analyser/ResultCache/ResultCacheManager.php | 12 ++++++++++++ src/Analyser/ScopeOps.php | 4 +++- src/Command/CommandHelper.php | 2 ++ src/Command/WorkerCommand.php | 2 ++ src/Dependency/ExportedNode/ExportedClassNode.php | 1 + src/Dependency/ExportedNode/ExportedEnumNode.php | 1 + .../ExportedNode/ExportedInterfaceNode.php | 1 + src/Dependency/ExportedNode/ExportedTraitNode.php | 1 + src/DependencyInjection/NeonAdapter.php | 1 + src/Parallel/ParallelAnalyser.php | 1 + src/PhpDoc/StubValidator.php | 2 ++ src/Reflection/ParametersAcceptorSelector.php | 1 + src/Rules/Debug/FileAssertRule.php | 1 + src/Testing/PHPUnit/ContainerInitializer.php | 4 ++++ src/Testing/TypeInferenceTestCase.php | 3 +++ src/Type/FileTypeMapper.php | 1 + src/Type/Regex/RegexGroupParser.php | 1 + src/Type/UnionType.php | 3 +++ 20 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php index cb8411fed35..ac525d29a20 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php @@ -801,7 +801,9 @@ private function buildParametersAndAcceptors( $callableParameters = null; $nativeCallableParameters = null; + /** @var Node\Arg[]|null $arrayMapArgs */ $arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + /** @var Node\Arg[]|null $immediatelyInvokedArgs */ $immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME); if ($arrayMapArgs !== null) { $callableParameters = []; diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 4ed253d91a6..a1c2ce3e062 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1873,7 +1873,9 @@ public function processArgs( $processingOrder = array_keys($args); usort($processingOrder, static function (int $a, int $b) use ($args): int { + /** @var Node\Arg|null $aOriginalArg */ $aOriginalArg = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + /** @var Node\Arg|null $bOriginalArg */ $bOriginalArg = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); $aValue = $aOriginalArg !== null ? $aOriginalArg->value : $args[$a]->value; $bValue = $bOriginalArg !== null ? $bOriginalArg->value : $args[$b]->value; @@ -1885,19 +1887,17 @@ public function processArgs( return $aIsClosure ? 1 : -1; } - $aOriginal = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - $bOriginal = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - if ($aOriginal === null && $bOriginal === null) { + if ($aOriginalArg === null && $bOriginalArg === null) { return $a <=> $b; } - if ($aOriginal === null) { + if ($aOriginalArg === null) { return 1; } - if ($bOriginal === null) { + if ($bOriginalArg === null) { return -1; } - return $aOriginal->getStartTokenPos() <=> $bOriginal->getStartTokenPos(); + return $aOriginalArg->getStartTokenPos() <=> $bOriginalArg->getStartTokenPos(); }); $countStableMetadataAcceptor = null; diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 53407bed071..46e05be5412 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -856,6 +856,18 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $projectConfigArray = $this->getPathTransformer()->relativizeProjectConfig($projectConfigArray); $meta['projectConfig'] = Neon::encode($projectConfigArray); } + /** + * @param array> $errorsByFile + * @param array> $locallyIgnoredErrorsByFile + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores + * @param CollectorData $collectedDataByFile + * @param array>|null $dependencies + * @param array>|null $usedTraitDependencies + * @param array>|null $packageDependencies + * @param array> $exportedNodes + * @param array $projectExtensionFiles + */ $doSave = function (array $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, ?array $dependencies, ?array $usedTraitDependencies, ?array $packageDependencies, array $exportedNodes, array $projectExtensionFiles) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { if ($onlyFiles) { if ($output->isVeryVerbose()) { diff --git a/src/Analyser/ScopeOps.php b/src/Analyser/ScopeOps.php index 69eb65a4a94..362b374e641 100644 --- a/src/Analyser/ScopeOps.php +++ b/src/Analyser/ScopeOps.php @@ -72,7 +72,9 @@ public static function nodeKey(Expr $node, ExprPrinter $exprPrinter): string && (($attributes['startFilePos'] ?? null) !== null) ) { $key .= '/*' . $attributes['startFilePos']; - foreach ($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] as $arg) { + /** @var Node\Arg[] $arrayMapArgs */ + $arrayMapArgs = $attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME]; + foreach ($arrayMapArgs as $arg) { $key .= ':' . $exprPrinter->printExpr($arg->value); } $key .= '*/'; diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 5f89864098e..84a8492aedf 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -31,6 +31,7 @@ use PHPStan\Internal\ComposerHelper; use PHPStan\Internal\DirectoryCreator; use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\Parser\PathRoutingParser; use PHPStan\Process\ForkedChildCrashReporter; use PHPStan\ShouldNotHappenException; use ReflectionClass; @@ -602,6 +603,7 @@ public static function begin( /** @var FileFinder $fileFinder */ $fileFinder = $container->getService('fileFinderAnalyse'); + /** @var PathRoutingParser $pathRoutingParser */ $pathRoutingParser = $container->getService('pathRoutingParser'); /** @var string[] $configStubFiles */ diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index 4ebaa02439f..da84533d0fa 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -6,6 +6,7 @@ use PHPStan\Cache\ArenaCache; use PHPStan\File\PathNotFoundException; use PHPStan\Parallel\WorkerRunner; +use PHPStan\Parser\PathRoutingParser; use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -127,6 +128,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // routing is unused either way.) $analysedFiles = ArenaCache::lookup('analysed-files'); if (is_array($analysedFiles)) { + /** @var PathRoutingParser $pathRoutingParser */ $pathRoutingParser = $container->getService('pathRoutingParser'); $pathRoutingParser->setAnalysedFiles($analysedFiles); } else { diff --git a/src/Dependency/ExportedNode/ExportedClassNode.php b/src/Dependency/ExportedNode/ExportedClassNode.php index 58c3d17fd46..34f88c2baec 100644 --- a/src/Dependency/ExportedNode/ExportedClassNode.php +++ b/src/Dependency/ExportedNode/ExportedClassNode.php @@ -158,6 +158,7 @@ public static function decode(array $data): self return ExportedTraitUseAdaptation::decode($traitUseAdaptationData['data']); }, $data['traitUseAdaptations']), array_map(static function (array $node): ExportedNode { + /** @var class-string $nodeType */ $nodeType = $node['type']; return $nodeType::decode($node['data']); diff --git a/src/Dependency/ExportedNode/ExportedEnumNode.php b/src/Dependency/ExportedNode/ExportedEnumNode.php index 1378b5ea809..728d774cb93 100644 --- a/src/Dependency/ExportedNode/ExportedEnumNode.php +++ b/src/Dependency/ExportedNode/ExportedEnumNode.php @@ -121,6 +121,7 @@ public static function decode(array $data): self $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, $data['implements'], array_map(static function (array $node): ExportedNode { + /** @var class-string $nodeType */ $nodeType = $node['type']; return $nodeType::decode($node['data']); diff --git a/src/Dependency/ExportedNode/ExportedInterfaceNode.php b/src/Dependency/ExportedNode/ExportedInterfaceNode.php index c6f9aad58a4..d6d2554b4b6 100644 --- a/src/Dependency/ExportedNode/ExportedInterfaceNode.php +++ b/src/Dependency/ExportedNode/ExportedInterfaceNode.php @@ -96,6 +96,7 @@ public static function decode(array $data): self $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, $data['extends'], array_map(static function (array $node): ExportedNode { + /** @var class-string $nodeType */ $nodeType = $node['type']; return $nodeType::decode($node['data']); diff --git a/src/Dependency/ExportedNode/ExportedTraitNode.php b/src/Dependency/ExportedNode/ExportedTraitNode.php index 1dfed9a9ca0..8ce229a45b3 100644 --- a/src/Dependency/ExportedNode/ExportedTraitNode.php +++ b/src/Dependency/ExportedNode/ExportedTraitNode.php @@ -137,6 +137,7 @@ public static function decode(array $data): self return ExportedTraitUseAdaptation::decode($traitUseAdaptationData['data']); }, $data['traitUseAdaptations']), array_map(static function (array $node): ExportedNode { + /** @var class-string $nodeType */ $nodeType = $node['type']; return $nodeType::decode($node['data']); diff --git a/src/DependencyInjection/NeonAdapter.php b/src/DependencyInjection/NeonAdapter.php index 1f8f93db5c2..916c62bbfee 100644 --- a/src/DependencyInjection/NeonAdapter.php +++ b/src/DependencyInjection/NeonAdapter.php @@ -98,6 +98,7 @@ public function process(array $arr, string $fileKey, string $file): array if ($val->value === Neon::CHAIN) { $tmp = null; foreach ($this->process($val->attributes, $fileKeyToPass, $file) as $st) { + /** @var Statement $st */ $tmp = new Statement( $tmp === null ? $st->getEntity() : [$tmp, ltrim(implode('::', (array) $st->getEntity()), ':')], $st->arguments, diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index e3aa2ca36c9..97c68cf6be3 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -344,6 +344,7 @@ public function analyse( continue; } $exportedNodes[$file] = array_map(static function (array $node): RootExportedNode { + /** @var class-string $class */ $class = $node['type']; return $class::decode($node['data']); diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index d15b89bcdc0..ce6f6db664b 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -11,6 +11,7 @@ use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ContainerFactory; use PHPStan\DependencyInjection\DerivativeContainerFactory; +use PHPStan\Parser\PathRoutingParser; use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; use Throwable; use function array_fill_keys; @@ -53,6 +54,7 @@ public function validate(array $stubFiles, bool $debug): array $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); $nodeScopeResolver->setAnalysedFiles($stubFiles); + /** @var PathRoutingParser $pathRoutingParser */ $pathRoutingParser = $container->getService('pathRoutingParser'); $pathRoutingParser->setAnalysedFiles($stubFiles); diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index d3d1773f802..a9f98c1318c 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -203,6 +203,7 @@ public static function applyIntrinsicArgOverrides( count($args) > 0 && count($parametersAcceptors) > 0 ) { + /** @var Node\Arg[]|null $arrayMapArgs */ $arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); if ($arrayMapArgs !== null) { $callbackParameters = []; diff --git a/src/Rules/Debug/FileAssertRule.php b/src/Rules/Debug/FileAssertRule.php index b4223955278..9c2faab5e9b 100644 --- a/src/Rules/Debug/FileAssertRule.php +++ b/src/Rules/Debug/FileAssertRule.php @@ -200,6 +200,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra ]; } + /** @var TrinaryLogic $expectedCertaintyValue */ // @phpstan-ignore staticMethod.dynamicName $expectedCertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); $variable = $args[1]->value; diff --git a/src/Testing/PHPUnit/ContainerInitializer.php b/src/Testing/PHPUnit/ContainerInitializer.php index 8f29c59aa90..170b5b550aa 100644 --- a/src/Testing/PHPUnit/ContainerInitializer.php +++ b/src/Testing/PHPUnit/ContainerInitializer.php @@ -3,10 +3,14 @@ namespace PHPStan\Testing\PHPUnit; use PHPStan\DependencyInjection\InvalidIgnoredErrorExceptionTest; +use PHPStan\Testing\PHPStanTestCase; final class ContainerInitializer { + /** + * @param class-string $testClassName + */ public static function initialize(string $testClassName): void { // This test expects an exception during container initialization diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 51541bfa411..3cd9b32c5b0 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -134,6 +134,7 @@ public function assertFileAsserts( $expectedType = $args[0]; $this->assertInstanceOf(ConstantScalarType::class, $expectedType); $expected = $expectedType->getValue(); + /** @var Type $actualType */ $actualType = $args[1]; $actual = $actualType->describe(VerbosityLevel::precise()); } else { @@ -182,7 +183,9 @@ public function assertFileAsserts( $failureMessage, ); } elseif ($assertType === 'variableCertainty') { + /** @var TrinaryLogic $expectedCertainty */ $expectedCertainty = $args[0]; + /** @var TrinaryLogic $actualCertainty */ $actualCertainty = $args[1]; $variableName = $args[2]; diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 3ccd03e06c0..7bc50d78c6d 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -377,6 +377,7 @@ private function loadCachedPhpDocNodeMap(string $cacheKey, string $variableCache $cached = $this->cache->load($cacheKey, $variableCacheKey); if ($cached !== null) { /** + * @var array $nameScopeMap * @var array $filesWithHashes */ [$nameScopeMap, $filesWithHashes] = $cached; diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 048b2a3beb3..b2566c0538f 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -152,6 +152,7 @@ private function createEmptyTokenTreeNode(TreeNode $parentAst): TreeNode private function updateAlternationAstRemoveVerticalBarsAndAddEmptyToken(TreeNode $ast): void { + /** @var TreeNode[] $children */ $children = $ast->getChildren(); foreach ($children as $i => $child) { diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index f1b7f932237..2d2a6faa6de 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -499,6 +499,9 @@ public function describe(VerbosityLevel $level): string if (isset($this->cachedDescriptions[$level->getLevelValue()])) { return $this->cachedDescriptions[$level->getLevelValue()]; } + /** + * @param Type[] $types + */ $joinTypes = static function (array $types) use ($level): string { $typeNames = []; foreach ($types as $i => $type) {