Skip to content

Skip class name case check for type hints using explicit use ... as aliases#5671

Open
phpstan-bot wants to merge 10 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-rsul8dt
Open

Skip class name case check for type hints using explicit use ... as aliases#5671
phpstan-bot wants to merge 10 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-rsul8dt

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a class is imported with an explicit alias via use Foo\MyClass as myclass, PHPStan incorrectly reported "Class Foo\MyClass referenced with incorrect case: Foo\myclass" on type hints using the alias. The alias name is intentionally chosen by the user and should not trigger a case sensitivity error.

Changes

  • Added src/Parser/UseAliasVisitor.php — a new parser visitor that:
    • Tracks explicit use ... as aliases (both regular and group use statements)
    • Marks resolved Name nodes with an isExplicitUseAlias attribute when the original name matches an explicit alias case-sensitively
    • Resets alias tracking on namespace boundaries
  • Modified src/Rules/FunctionDefinitionCheck.php to check the isExplicitUseAlias attribute in getOriginalClassNamePairsFromTypeNode() and skip the case sensitivity check for explicit aliases

Root cause

FunctionDefinitionCheck::getOriginalClassNamePairsFromTypeNode() reconstructs the "original case" class name by combining the namespace prefix from the resolved name with the original name parts. For use Foo\MyClass as myclass, the original name myclass was combined with prefix Foo to produce Foo\myclass. Since strtolower('Foo\myclass') === strtolower('Foo\MyClass'), the existing alias detection (which only catches aliases with entirely different names) did not fire, and the name was incorrectly flagged as a case mismatch.

The fix distinguishes explicit use ... as aliases from wrong-case references by tracking which aliases were explicitly defined via the as keyword in use statements.

Analogous cases probed

The following contexts were tested and confirmed to NOT be affected (they pass the fully-resolved class name to ClassCaseSensitivityCheck, bypassing the problematic reconstruction logic):

  • new expressions (InstantiationRule)
  • instanceof expressions (ExistingClassInInstanceOfRule)
  • catch clauses (CaughtExceptionExistenceRule)
  • Class constant access (ClassConstantRule)
  • Static property access (AccessStaticPropertiesCheck)
  • Static method calls (StaticMethodCallCheck)
  • Class extends/implements/trait use declarations
  • Property type declarations (ExistingClassesInPropertiesRule)
  • PHPDoc types (@param, @return)

Test

Added tests/PHPStan/Rules/Methods/data/bug-14617.php with a regression test covering:

  • Function return type with explicit alias
  • Class method parameter type with explicit alias
  • Class method return type with explicit alias
  • new expression with explicit alias (via InstantiationRule, verified no error)

Fixes phpstan/phpstan#14617

… 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`
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition

if (strtolower($originalCaseClassName) !== strtolower($resolvedName)) {
				// use alias, not just a case difference
				return [];
			}

should not be needed anymore

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 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Removed the redundant strtolower alias detection heuristic in FunctionDefinitionCheck.php. The UseAliasVisitor now handles all explicit use ... as alias detection precisely via the isExplicitUseAlias attribute, making the old lowercase-comparison check unnecessary. All tests (12,075) and static analysis pass.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the fix be in the ClassCaseSensitivityCheck which already has the incomplete check

if (strtolower($realClassName) !== strtolower($className)) {
				continue; // skip class alias
			}

The isExplicitUseAlias attribute check belongs in ClassCaseSensitivityCheck
(where the case error is generated) rather than in FunctionDefinitionCheck
(where class name pairs are constructed). This centralizes the alias
handling alongside the existing strtolower alias skip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Moved the isExplicitUseAlias attribute check from FunctionDefinitionCheck::getOriginalClassNamePairsFromTypeNode() to ClassCaseSensitivityCheck::checkClassNames(), as the reviewer suggested. This centralizes the explicit alias handling in the same class that already has the incomplete strtolower alias skip — right where the "referenced with incorrect case" error is generated. All 12,075 tests pass and make phpstan reports no errors.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add more test case which wasn't fixed by the previous fix in FunctionDefinitionCheck

@VincentLanglet VincentLanglet self-assigned this May 16, 2026
Comment on lines 41 to 43
if (strtolower($realClassName) !== strtolower($className)) {
continue; // skip class alias
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should/Could this if be removed too ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a summary of what was done to address the review:

The strtolower check cannot be removed. I verified this by temporarily removing it and running the full test suite — ExistingClassesInClosureTypehintsRuleTest::testExistingClassInTypehint fails because line 35 of closure-typehints.php uses \ReturnTypes\FooAliaS, which is a class_alias() for ReturnTypes\Foo. Without the strtolower check, PHPStan incorrectly reports "Class ReturnTypes\Foo referenced with incorrect case: ReturnTypes\FooAliaS" — a false positive because FooAliaS is a completely different name (a class_alias), not a case mismatch.

The UseAliasVisitor only tracks use ... as aliases in the parser. The strtolower check handles class_alias() scenarios where the reflection-resolved class name is entirely different from the written name.

Changes made:

  • Clarified the comment on the strtolower check to explain it handles class_alias() specifically
  • Added closure test coverage (bug-14617-closure.php + test in ExistingClassesInClosureTypehintsRuleTest)
  • Added nullable and union type test cases to the existing bug-14617.php

All 12,076 tests pass and make phpstan reports no errors.

…eof, and property types

These contexts go through ClassCaseSensitivityCheck but not through
FunctionDefinitionCheck, so they weren't covered by the previous
strtolower heuristic fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Added test cases for explicit use ... as aliases in contexts that go through ClassCaseSensitivityCheck but not through FunctionDefinitionCheck:

  • extends (ExistingClassInClassExtendsRuleTest)
  • implements (ExistingClassesInClassImplementsRuleTest)
  • instanceof (ExistingClassInInstanceOfRuleTest)
  • property types (ExistingClassesInPropertiesRuleTest)

These contexts weren't covered by the previous strtolower heuristic in FunctionDefinitionCheck since they use separate rules. The shared test data file (tests/PHPStan/Rules/Classes/data/bug-14617.php) exercises all four cases with use Bug14617Classes\MyClass as myclass and use Bug14617Classes\MyInterface as myinterface. All 12,079 tests pass and make phpstan reports no errors.

The strtolower check in ClassCaseSensitivityCheck cannot be removed
because it handles class_alias() scenarios where the alias name is
completely different from the real class name (e.g., FooAlias for Foo).
UseAliasVisitor only tracks `use ... as` aliases, not class_alias().
Removing it causes false positives like "Class Foo referenced with
incorrect case: FooAliaS" for class_alias targets.

Clarified the comment to explain what the check handles. Added test
coverage for closures using explicit use aliases, and for nullable/union
type hints with aliases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it mean we don't avoid false positive with something like ?

class_alias('Foo', 'foo');

?

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we already have test for the

if (strtolower($realClassName) !== strtolower($className)) {
				continue; // skip class_alias() where the alias is a completely different name
			}

case ?

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skip ExistingClassesInTypehintsRuleTest::testBug14617 for PHP < 8

phpstan-bot and others added 2 commits May 16, 2026 10:18
Adds a dedicated test that verifies the strtolower check in
ClassCaseSensitivityCheck correctly skips class_alias() names
that are completely different from the original class name.

Previously this was only implicitly covered by line 35 of
closure-typehints.php using \ReturnTypes\FooAliaS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test data uses union types (myclass|string, myclass|int) which
require PHP 8.0+ to parse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Added #[RequiresPhp('>= 8.0.0')] to ExistingClassesInTypehintsRuleTest::testBug14617 since the test data uses union types which require PHP 8.0+. All tests pass and make phpstan reports no errors.

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

All 12,081 tests pass (79,707 assertions). The commit has been pushed.

Summary: Added an explicit test (testClassAliasCaseSensitivity) that verifies the strtolower check in ClassCaseSensitivityCheck correctly skips class_alias() names that are completely different from the original class name. This was previously only implicitly covered by closure-typehints.php line 35. The strtolower check remains necessary for class_alias() scenarios (e.g., ReturnTypes\FooAliasReturnTypes\Foo) and cannot be removed.

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

The branch is already up to date with the remote — no new commits to push.

Summary: The reviewer asked whether class_alias('Foo', 'foo') avoids false positives. The answer is no — it's a pre-existing limitation that this PR neither introduces nor worsens. PHP's case-insensitive class resolution means (new ReflectionClass('foo'))->getName() returns 'Foo' identically whether class_alias('Foo', 'foo') was called or not, making it impossible to distinguish a case-only alias from a genuine case typo using PHP's reflection API. Fixing this would require tracking class_alias() calls explicitly (like the UseAliasVisitor does for use ... as), which is a separate concern.

Comment thread tests/PHPStan/Rules/Functions/data/class-alias-case-sensitivity.php Outdated
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add back test case but just with

$callback = function (\ReturnTypes\FooAlias $a): \ReturnTypes\FooAlias {
	return $a;
};

and not the callback2

Re-adds the test for class_alias() with FooAlias using correct case,
without locking in the wrong-case behavior that may be tightened later.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Added back the class-alias-case-sensitivity.php test with only the correct-case FooAlias callback, omitting the wrong-case callback2 that locked in behavior the reviewer wants to keep open for future tightening. All 12,081 tests pass and make phpstan reports no errors.

@VincentLanglet VincentLanglet requested a review from staabm May 16, 2026 11:51
Comment thread src/Parser/UseAliasVisitor.php
Covers the GroupUse code path in UseAliasVisitor that was not
exercised by existing tests. Tests use `use Ns\{A as a, B as b}`
syntax in both closure and method typehint contexts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants