Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/Parser/UseAliasVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php declare(strict_types = 1);

namespace PHPStan\Parser;

use Override;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\GroupUse;
use PhpParser\Node\Stmt\Use_;
use PhpParser\NodeVisitorAbstract;
use PHPStan\DependencyInjection\AutowiredService;
use function count;
use function strtolower;

#[AutowiredService]
final class UseAliasVisitor extends NodeVisitorAbstract
{

public const ATTRIBUTE_NAME = 'isExplicitUseAlias';

/** @var array<string, string> 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;
}

}
6 changes: 5 additions & 1 deletion src/Rules/ClassCaseSensitivityCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Parser\UseAliasVisitor;
use PHPStan\Reflection\ReflectionProvider;
use function sprintf;
use function strtolower;
Expand Down Expand Up @@ -38,7 +39,10 @@ public function checkClassNames(array $pairs): array
}
$realClassName = $classReflection->getName();
if (strtolower($realClassName) !== strtolower($className)) {
continue; // skip class alias
continue; // skip class_alias() where the alias is a completely different name
}
Comment on lines 41 to 43
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.

if ($pair->getNode()->getAttribute(UseAliasVisitor::ATTRIBUTE_NAME) === true) {
continue;
}
if ($realClassName === $className) {
continue;
Expand Down
5 changes: 0 additions & 5 deletions src/Rules/FunctionDefinitionCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -866,11 +866,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 [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,9 @@ public function testReadonly(): void
]);
}

public function testBug14617(): void
{
$this->analyse([__DIR__ . '/data/bug-14617.php'], []);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,9 @@ public function testRememberClassExistsFromConstructor(): void
$this->analyse([__DIR__ . '/data/remember-class-exists-from-constructor.php'], []);
}

public function testBug14617(): void
{
$this->analyse([__DIR__ . '/data/bug-14617.php'], []);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,9 @@ public function testBug8889(): void
]);
}

public function testBug14617(): void
{
$this->analyse([__DIR__ . '/data/bug-14617.php'], []);
}

}
21 changes: 21 additions & 0 deletions tests/PHPStan/Rules/Classes/data/bug-14617.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);

namespace Bug14617Classes;

class MyClass {}

interface MyInterface {}

namespace Bug14617Classes\Consumer;

use Bug14617Classes\MyClass as myclass;
use Bug14617Classes\MyInterface as myinterface;

class Foo extends myclass implements myinterface {
public myclass $prop;
}

function test(mixed $x): void {
if ($x instanceof myclass) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ public function testExistingClassInTypehint(): void
]);
}

public function testBug14617(): void
{
$this->analyse([__DIR__ . '/data/bug-14617-closure.php'], []);
}

public function testClassAliasCaseSensitivity(): void
{
$this->analyse([__DIR__ . '/data/class-alias-case-sensitivity.php'], []);
}

public function testValidTypehintPhp71(): void
{
$this->analyse([__DIR__ . '/data/closure-7.1-typehints.php'], [
Expand Down
13 changes: 13 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-14617-closure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace Bug14617Closure;

class MyClass {}

namespace Bug14617Closure\Consumer;

use Bug14617Closure\MyClass as myclass;

$callback = function (myclass $a): myclass {
return $a;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php declare(strict_types = 1);

namespace ClassAliasCaseSensitivity;

$callback = function (\ReturnTypes\FooAlias $a): \ReturnTypes\FooAlias {
return $a;
};
Original file line number Diff line number Diff line change
Expand Up @@ -662,4 +662,10 @@ public function testBug14205(): void
$this->analyse([__DIR__ . '/data/bug-14205.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug14617(): void
{
$this->analyse([__DIR__ . '/data/bug-14617.php'], []);
}

}
27 changes: 27 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-14617.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace Bug14617;

class MyClass {}

namespace Bug14617\Consumer;

use Bug14617\MyClass as myclass;

function test(): myclass {
return new myclass();
}

class Foo {
public function bar(myclass $a): myclass {
return $a;
}

public function nullable(?myclass $a): ?myclass {
return $a;
}

public function union(myclass|string $a): myclass|int {
return $a;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,9 @@ public function testIntersectionTypes(int $phpVersion, array $errors): void
$this->analyse([__DIR__ . '/data/intersection-types.php'], $errors);
}

public function testBug14617(): void
{
$this->analyse([__DIR__ . '/../Classes/data/bug-14617.php'], []);
}

}
Loading