-
Notifications
You must be signed in to change notification settings - Fork 510
Support multiple anonymous class definitions on the same line #3328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
27476be
d04bd26
ab3d1bd
d764925
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Parser; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\NodeVisitorAbstract; | ||
use function count; | ||
|
||
class AnonymousClassVisitor extends NodeVisitorAbstract | ||
{ | ||
|
||
public const ATTRIBUTE_ANONYMOUS_CLASS = 'anonymousClass'; | ||
public const ATTRIBUTE_LINE_INDEX = 'anonymousClassLineIndex'; | ||
|
||
/** @var array<int, non-empty-list<Node\Stmt\Class_>> */ | ||
private array $nodesPerLine = []; | ||
Comment on lines
+15
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm slightly concerned about keeping the node reference, especially considering that nodes could be replaced. Ultimately it's a performance optimization to avoid having to traverse through all nodes in |
||
|
||
public function beforeTraverse(array $nodes): ?array | ||
{ | ||
$this->nodesPerLine = []; | ||
return null; | ||
} | ||
|
||
public function enterNode(Node $node): ?Node | ||
{ | ||
if (!$node instanceof Node\Stmt\Class_ || !$node->isAnonymous()) { | ||
return null; | ||
} | ||
|
||
$node->setAttribute(self::ATTRIBUTE_ANONYMOUS_CLASS, true); | ||
$this->nodesPerLine[$node->getStartLine()][] = $node; | ||
|
||
return null; | ||
} | ||
|
||
public function afterTraverse(array $nodes): ?array | ||
{ | ||
foreach ($this->nodesPerLine as $nodesOnLine) { | ||
if (count($nodesOnLine) === 1) { | ||
continue; | ||
} | ||
for ($i = 0; $i < count($nodesOnLine); $i++) { | ||
$nodesOnLine[$i]->setAttribute(self::ATTRIBUTE_LINE_INDEX, $i + 1); | ||
} | ||
} | ||
|
||
$this->nodesPerLine = []; | ||
return null; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ | |
use PHPStan\File\FileHelper; | ||
use PHPStan\File\FileReader; | ||
use PHPStan\File\RelativePathHelper; | ||
use PHPStan\Parser\AnonymousClassVisitor; | ||
use PHPStan\Php\PhpVersion; | ||
use PHPStan\PhpDoc\PhpDocInheritanceResolver; | ||
use PHPStan\PhpDoc\StubPhpDocProvider; | ||
|
@@ -201,7 +202,6 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ | |
$scopeFile, | ||
); | ||
$classNode->name = new Node\Identifier($className); | ||
$classNode->setAttribute('anonymousClass', true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moving this into In theory the node visitor would also offer the possibility to fix There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good idea for a follow up PR! Although rules should better rely on |
||
|
||
if (isset(self::$anonymousClasses[$className])) { | ||
return self::$anonymousClasses[$className]; | ||
|
@@ -214,6 +214,14 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ | |
null, | ||
); | ||
|
||
/** @var int|null $classLineIndex */ | ||
$classLineIndex = $classNode->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX); | ||
if ($classLineIndex === null) { | ||
$displayName = sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine()); | ||
} else { | ||
$displayName = sprintf('class@anonymous/%s:%s:%d', $filename, $classNode->getStartLine(), $classLineIndex); | ||
} | ||
Comment on lines
+217
to
+223
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I've done this now and am personally happy with it. But a small notes on this: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm aware of this, but it's fine, the number should be low enough not to cause confusion with columns. |
||
|
||
self::$anonymousClasses[$className] = new ClassReflection( | ||
$this->reflectionProviderProvider->getReflectionProvider(), | ||
$this->initializerExprTypeResolver, | ||
|
@@ -227,7 +235,7 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ | |
$this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), | ||
$this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), | ||
$this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), | ||
sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine()), | ||
$displayName, | ||
new ReflectionClass($reflectionClass), | ||
$scopeFile, | ||
null, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<?php | ||
|
||
namespace AnonymousClassNameSameLine; | ||
|
||
$foo = new class {}; $bar = new class {}; $baz = new class {}; die; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php declare(strict_types = 1); // lint >= 8.0 | ||
|
||
namespace Bug11511; | ||
|
||
$myObject = new class (new class { public string $bar = 'test'; }) { | ||
public function __construct(public object $foo) | ||
{ | ||
} | ||
}; | ||
echo $myObject->foo->bar; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php declare(strict_types = 1); // lint >= 8.0 | ||
|
||
namespace Bug5597; | ||
|
||
interface InterfaceA {} | ||
|
||
class ClassA implements InterfaceA {} | ||
|
||
class ClassB | ||
{ | ||
public function __construct( | ||
private InterfaceA $parameterA, | ||
) { | ||
} | ||
|
||
public function test() : InterfaceA | ||
{ | ||
return $this->parameterA; | ||
} | ||
} | ||
|
||
$classA = new class() extends ClassA {}; | ||
$thisWorks = new class($classA) extends ClassB {}; | ||
|
||
$thisFailsWithTwoErrors = new class(new class() extends ClassA {}) extends ClassB {}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Reflection; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Stmt\Class_; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Parser\AnonymousClassVisitor; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\Testing\RuleTestCase; | ||
use function implode; | ||
use function sprintf; | ||
|
||
/** | ||
* @extends RuleTestCase<Rule<Class_>> | ||
*/ | ||
class AnonymousClassReflectionTest extends RuleTestCase | ||
{ | ||
|
||
/** | ||
* @return Rule<Class_> | ||
*/ | ||
protected function getRule(): Rule | ||
{ | ||
return new /** @implements Rule<Class_> */ class (self::createReflectionProvider()) implements Rule { | ||
|
||
public function __construct(private ReflectionProvider $reflectionProvider) | ||
{ | ||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return Class_::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (!(bool) $node->getAttribute(AnonymousClassVisitor::ATTRIBUTE_ANONYMOUS_CLASS)) { | ||
return []; | ||
} | ||
|
||
$classReflection = $this->reflectionProvider->getAnonymousClassReflection($node, $scope); | ||
|
||
return [ | ||
RuleErrorBuilder::message(sprintf( | ||
"name: %s\ndisplay name: %s", | ||
$classReflection->getName(), | ||
$classReflection->getDisplayName(), | ||
))->identifier('test.anonymousClassReflection')->build(), | ||
]; | ||
} | ||
|
||
}; | ||
} | ||
|
||
public function testReflection(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/anonymous-classes.php'], [ | ||
[ | ||
implode("\n", [ | ||
'name: AnonymousClass0c307d7b8501323d1d30b0afea7e0578', | ||
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:5', | ||
]), | ||
5, | ||
], | ||
[ | ||
implode("\n", [ | ||
'name: AnonymousClassa16017c480192f8fbf3c03e17840e99c', | ||
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:1', | ||
]), | ||
7, | ||
], | ||
[ | ||
implode("\n", [ | ||
'name: AnonymousClassd68d75f1cdac379350e3027c09a7c5a0', | ||
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:2', | ||
]), | ||
7, | ||
], | ||
[ | ||
implode("\n", [ | ||
'name: AnonymousClass75aa798fed4f30306c14dcf03a50878c', | ||
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:3', | ||
]), | ||
7, | ||
], | ||
[ | ||
implode("\n", [ | ||
'name: AnonymousClass4fcabdc52bfed5f8c101f3f89b2180bd', | ||
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:1', | ||
]), | ||
9, | ||
], | ||
[ | ||
implode("\n", [ | ||
'name: AnonymousClass0e77d7995f4c47dcd5402817970fd7e0', | ||
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:2', | ||
]), | ||
9, | ||
], | ||
]); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's not clear to me, why differ between the class name (
AnonymousClass<hash>
) and the display name (class@anonymous<location>
) at all?Is it a memory optimization, avoiding problematic characters in some context, or just an artifact? Or in other words, would it be a problem to align them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the same turn I was thinking about setting the name already in the node visitor, but then stumbled upon
PHPStan\Parser\Parser::parseString()
.In theory that could be amended with an optional file name argument.
That could then lead to some minor simplifications in the handling of anonymous class names.