From 90c68c1d74a9566f35e618093771872d654e66b8 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Fri, 15 May 2026 16:02:59 +0000 Subject: [PATCH 1/2] Skip class name case check for type hints using explicit `use ... as` aliases - Add `UseAliasVisitor` parser visitor that tracks explicit `use ... as` aliases and sets an `isExplicitUseAlias` attribute on resolved Name nodes whose original name matches an explicit alias exactly (case-sensitively) - In `FunctionDefinitionCheck::getOriginalClassNamePairsFromTypeNode()`, skip the case sensitivity check when the Name node is marked as an explicit alias - The false positive occurred because the method reconstructed the original-case class name by combining the resolved namespace prefix with the original alias name, producing e.g. `Foo\myclass` when the alias was `myclass` for `Foo\MyClass`, which was then incorrectly flagged as a case mismatch - Verified that analogous contexts (new, instanceof, catch, extends, implements, trait use, static access, PHPDoc types) are not affected because they pass the fully-resolved class name directly to `ClassCaseSensitivityCheck` --- src/Parser/UseAliasVisitor.php | 73 +++++++++++++++++++ src/Rules/FunctionDefinitionCheck.php | 5 ++ .../ExistingClassesInTypehintsRuleTest.php | 5 ++ .../PHPStan/Rules/Methods/data/bug-14617.php | 19 +++++ 4 files changed, 102 insertions(+) create mode 100644 src/Parser/UseAliasVisitor.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-14617.php diff --git a/src/Parser/UseAliasVisitor.php b/src/Parser/UseAliasVisitor.php new file mode 100644 index 00000000000..6ddf78c723a --- /dev/null +++ b/src/Parser/UseAliasVisitor.php @@ -0,0 +1,73 @@ + alias name (original case) keyed by lowercase alias name */ + private array $explicitAliases = []; + + #[Override] + public function enterNode(Node $node): ?Node + { + if ($node instanceof Node\Stmt\Namespace_) { + $this->explicitAliases = []; + } + + if ($node instanceof Use_ && $node->type === Use_::TYPE_NORMAL) { + foreach ($node->uses as $use) { + if ($use->alias === null) { + continue; + } + + $this->explicitAliases[strtolower($use->alias->name)] = $use->alias->name; + } + } + + if ($node instanceof GroupUse) { + foreach ($node->uses as $use) { + if ($use->type !== Use_::TYPE_NORMAL && $node->type !== Use_::TYPE_NORMAL) { + continue; + } + if ($use->alias === null) { + continue; + } + + $this->explicitAliases[strtolower($use->alias->name)] = $use->alias->name; + } + } + + if ($node instanceof Name) { + $originalName = $node->getAttribute('originalName'); + if ($originalName instanceof Name) { + $originalParts = $originalName->getParts(); + if (count($originalParts) === 1) { + $lowerOriginal = strtolower($originalParts[0]); + if ( + isset($this->explicitAliases[$lowerOriginal]) + && $this->explicitAliases[$lowerOriginal] === $originalParts[0] + ) { + $node->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + } + } + + return null; + } + +} diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index 6ef8f74b158..0f6594afadb 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -19,6 +19,7 @@ use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Printer\NodeTypePrinter; +use PHPStan\Parser\UseAliasVisitor; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\ExtendedParametersAcceptor; @@ -852,6 +853,10 @@ private function getOriginalClassNamePairsFromTypeNode(Identifier|Name|ComplexTy return []; } + if ($typeNode->getAttribute(UseAliasVisitor::ATTRIBUTE_NAME) === true) { + return []; + } + $resolvedName = $typeNode->toString(); $originalParts = $originalName->getParts(); $resolvedParts = $typeNode->getParts(); diff --git a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index 75d808498f4..dd08b8562da 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -662,4 +662,9 @@ public function testBug14205(): void $this->analyse([__DIR__ . '/data/bug-14205.php'], []); } + public function testBug14617(): void + { + $this->analyse([__DIR__ . '/data/bug-14617.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14617.php b/tests/PHPStan/Rules/Methods/data/bug-14617.php new file mode 100644 index 00000000000..8a94720469f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14617.php @@ -0,0 +1,19 @@ + Date: Fri, 15 May 2026 16:21:14 +0000 Subject: [PATCH 2/2] Remove redundant strtolower alias detection heuristic The UseAliasVisitor now explicitly tracks `use ... as` aliases, making the old heuristic that compared lowercase class names unnecessary. The old check was a broad approximation; the new attribute-based approach is precise. Co-Authored-By: Claude Opus 4.6 --- src/Rules/FunctionDefinitionCheck.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index 0f6594afadb..d08f598a779 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -871,11 +871,6 @@ private function getOriginalClassNamePairsFromTypeNode(Identifier|Name|ComplexTy $originalCaseClassName = $originalName->toString(); } - if (strtolower($originalCaseClassName) !== strtolower($resolvedName)) { - // use alias, not just a case difference - return []; - } - if ($originalCaseClassName === $resolvedName) { return []; }