Skip to content
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

Support multiple anonymous class definitions on the same line #3328

Merged
merged 4 commits into from
Aug 22, 2024

Conversation

tscni
Copy link
Contributor

@tscni tscni commented Aug 17, 2024

@tscni tscni force-pushed the fix/issue-11511 branch 2 times, most recently from c5f05a6 to 70a8982 Compare August 17, 2024 02:21
@tscni
Copy link
Contributor Author

tscni commented Aug 17, 2024

It would probably be suitable to adjust this as well:

sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine()),

But adding the file position there isn't very helpful to users.
PHP itself uses a 0-indexed increment suffix after the line (:1$0, :1$1, :1$2). Reusing that approach would seem the most ideal. I'd just have to figure out how to collect & carry that state to the respective usages.

Maybe a stateful node visitor might be a good approach for that.

@tscni tscni marked this pull request as draft August 19, 2024 01:11
@ondrejmirtes
Copy link
Member

The problem here is that the ClassReflection::getCacheKey consists of the display name. Which would be the same for two anonymous classes on the same line. Thus mangling the cache.

My idea for the fix: a node visitor that would add some kind of attribute on Class_ if there are two anonymous classes on the same line.

If there was just a single class on the same line, the name generated by AnonymousClassNameHelper would not change. If there were two, the name would be line:order, so 24:1 and 24:2 for example.

The same would apply to the display name in ClassReflection.

@tscni tscni marked this pull request as ready for review August 19, 2024 16:08
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

Copy link
Contributor Author

@tscni tscni left a comment

Choose a reason for hiding this comment

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

This should work now. I left some notes / questions around possible improvements.

Comment on lines +217 to +223
/** @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);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If there was just a single class on the same line, the name generated by AnonymousClassNameHelper would not change. If there were two, the name would be line:order, so 24:1 and 24:2 for example.

The same would apply to the display name in ClassReflection.

I've done this now and am personally happy with it.

But a small notes on this:
E.g. path/to/file.php:12:34 would usually indicate that something is located in file path/to/file.php at line 12 at column / character 34
In that regard this seems slightly unusual and might potentially lead to some confusion. If we do care to solve this (I don't mind it), maybe $ as a separator, like the one PHP uses, might be a good alternative.
We could alternatively also use the character position of course, but that seems a bit more cumbersome to add in contexts without the reflection logic.

Copy link
Member

Choose a reason for hiding this comment

The 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.

@@ -201,7 +202,6 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $
$scopeFile,
);
$classNode->name = new Node\Identifier($className);
$classNode->setAttribute('anonymousClass', true);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moving this into AnonymousClassVisitor felt like a reasonable step to take now that it exists.

In theory the node visitor would also offer the possibility to fix Node\Stmt\Class_::isAnonymous() by implementing an AnonymousClass extends Node\Stmt\Class_ and replacing the original node with that.
But maybe that's more complexity than desired?

Copy link
Member

Choose a reason for hiding this comment

The 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 InClassNode and ClassReflection so that they don't have to make any differences between traditional and anonymous classes, in theory someone could ask about Class_::isAnonymous() and it's always better to give the right answer :) Please take a note and send a follow up PR :)

Comment on lines +15 to +16
/** @var array<int, non-empty-list<Node\Stmt\Class_>> */
private array $nodesPerLine = [];
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
It seems unlikely that a node visitor might replace class nodes though (except that I propose exactly that in another comment.. :P)

Ultimately it's a performance optimization to avoid having to traverse through all nodes in afterTraverse().

Comment on lines 45 to -37
'AnonymousClass%s',
md5(sprintf('%s:%s', $filename, $classNode->getStartLine())),
Copy link
Contributor Author

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?

Copy link
Contributor Author

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.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

This is almost perfect. I admire that as your first contribution, you really found your way around this complex codebase so easily. You didn't need any hand-holding and figured everything by yourself. Thanks!

I have one more request: there isn't really any test proving the anonymous class display name with ATTRIBUTE_LINE_INDEX. We just see the resulting hash.

A solution would be to create a dummy rule just for test purposes, like the one in VirtualNullsafeMethodCallTest. Its goal would be to report anonymous class display name, and to show that a standalone class has just :line and more classes with :line:index.

Thank you once more!

@tscni
Copy link
Contributor Author

tscni commented Aug 22, 2024

Thanks for the kind words :)

I've now added the test.

@ondrejmirtes ondrejmirtes merged commit 0c22f13 into phpstan:1.11.x Aug 22, 2024
453 of 463 checks passed
@ondrejmirtes
Copy link
Member

Awesome, thank you!

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