Skip to content

Improving support for anonymous classes#2613

Open
mamazu wants to merge 2 commits intophpactor:masterfrom
mamazu:anonymous_classes
Open

Improving support for anonymous classes#2613
mamazu wants to merge 2 commits intophpactor:masterfrom
mamazu:anonymous_classes

Conversation

@mamazu
Copy link
Contributor

@mamazu mamazu commented Mar 22, 2024

Things that have been done

  • Completion for it works (tests added)
  • Check if there is a better way to generate the name of the anonymous classes

Limitations:

  • The current implementation uses the byteoffset of the class. Meaning that the classname could change if there are textedits that change the text before the class
  • This doesn't go into ClassDeclarationNodes so anoymous classes inside a class wouldn't be recognized?

@mamazu mamazu marked this pull request as draft March 22, 2024 19:54
@mamazu mamazu changed the title First try at implementing it Improving support for anonymous classes Mar 22, 2024
}

if ($child instanceof ObjectCreationExpression) {
// TODO: come up with a good way to generate a class name
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need a better way to generate class names as the code is currently duplicated everywhere.

Copy link
Collaborator

Choose a reason for hiding this comment

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

PHPStan does it like this: https://github.com/dantleech/phpstan-src/blob/f7b89d47cfa1fa636fb571b4315841a9af0e976f/src/Broker/AnonymousClassNameHelper.php#L22-L25

but I don't think this is the correct place to do it.

We can create the reflection class in the resolver?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, technically using the line number is not 100% garanteed to be unique but who has two anon classes on the same line. :D

But what you mean with "creating the reflection class in the resolver"? I wanted to try to avoid making a ReflectionAnonymousClass class as anon classes should be the same except for their weird name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes you are right. A different approach for this would probably make more sense but that would require a lot of refactoring as the fromNode function is used quite a lot.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think it needs refactoring and would also fix the issue Thomas raised below, but would need to show an example.

@mamazu mamazu force-pushed the anonymous_classes branch from f8c99ee to 8e24b48 Compare March 24, 2024 18:14
@mamazu mamazu marked this pull request as ready for review March 24, 2024 23:15
@mamazu mamazu force-pushed the anonymous_classes branch from 4235fc4 to f32ad1b Compare March 24, 2024 23:34
@przepompownia
Copy link
Contributor

From #2612 perspective this seems to be one step ahead but:

image

@mamazu
Copy link
Contributor Author

mamazu commented Mar 25, 2024

What were you trying to do? Generate a class?

@przepompownia
Copy link
Contributor

"Implement contracts" like in #2612

@mamazu
Copy link
Contributor Author

mamazu commented Mar 25, 2024

Now it should work (at least the test for it is green) and it looks like the rest broke.

@mamazu mamazu force-pushed the anonymous_classes branch from 681f9c8 to 7a53a6b Compare March 25, 2024 23:07
@przepompownia
Copy link
Contributor

Works for me:

image

return $classNode->classMembers;
if ($classNode instanceof ClassDeclaration || $classNode instanceof ObjectCreationExpression) {
$classNode = $classNode->classMembers;
Assert::isInstanceOf( $classNode, ClassMembersNode::class);
Copy link
Collaborator

@dantleech dantleech Mar 29, 2024

Choose a reason for hiding this comment

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

is it expected that this is NOT a ClassMembersNode? pm am ObjectCreationExpression it can be NULL, and for ClassDeclaration tolerant parser often lies about nullability. this will cause a runtime exception, which is probably not what we want.

if it should never happen then this should be an exception and I'd prefer not to use Assertion libraries.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, the problem in this case is that it could also be a "normal" Object creation with a class name which would then crash here. But in this case I'm not sure what the best way to go about this is.

if ($nodeOrToken instanceof Token) {
return (string)$nodeOrToken->getText($node->getFileContents());
}
if ($nodeOrToken instanceof ObjectCreationExpression) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is method is explicitly about getting the name frm a Node or Token, it's not about annonymous classes. you can add a separate method or probably there is something better.

public function memberListPosition(): ByteOffsetRange
{
$classMembers = $this->node->classMembers;
assert($classMembers instanceof ClassMembersNode, 'ObjectCreationExpression does not contain anonymous class');
Copy link
Collaborator

@dantleech dantleech Mar 29, 2024

Choose a reason for hiding this comment

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

presumably thid can happen and this will crash? can we return defaults instead based on the node position?

$classDeclaration = $this->node->getFirstAncestor(ClassLike::class, ObjectCreationExpression::class);
if ($classDeclaration instanceof ObjectCreationExpression) {
return $this->class ?? $this->serviceLocator->reflector()
->reflectClassLike(NodeUtil::nameFromTokenOrNode($classDeclaration, $classDeclaration));
Copy link
Collaborator

@dantleech dantleech Mar 29, 2024

Choose a reason for hiding this comment

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

the reflector has no way to know how to locate this, so this is working based on caching? does it work without the cache?

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 don't know how the reflector does it. But I assume it cashes the classes that it has seen yes. Where would the logic be for finding it properly? Do we need to index anonymous classes for that?

$node->getEndPosition(),
[
'symbol_type' => Symbol::CLASS_,
'type' => TypeFactory::reflectedClass($resolver->reflector(), NodeUtil::nameFromTokenOrNode($node, $node))
Copy link
Collaborator

Choose a reason for hiding this comment

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

again, i'm not sure how this works, and assume it works based on the anonymous class previously being parsed? we should avoid depending on the cache.

see Roave/BetterReflection#300

probably the reflector should support accepting the Node instead of a "name".

continue;
}

if ($child instanceof ObjectCreationExpression && !($child->classTypeDesignator instanceof Node)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this may cause unexpected issues. "reflect classes in" reflects locatable symbols and also it should not need to deeply traverse the AST.

we could consider removing this and instead adding support to $reflector->reflect{Node,AnnonymousClass,Object}($node) for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem is this approach won't find all classes anyways. Because we actually have to traverse into class definitions as well. Otherwise we would miss anonymous classes inside other classes. But I don't have an idea how to do improve performance here.

Copy link
Collaborator

@dantleech dantleech Apr 9, 2024

Choose a reason for hiding this comment

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

yes, they need probably to be interpreted inline as types as with the above comment. doing this will mean that "implement contracts" will not automatically work, but at the same time it's only working because of the cache effect in this PR

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 just saw that reflectClassesIn should probably better be named reflectClassLikeIn 'cause it also returns interfaces enums etc. which are completely irrelevant for the "Implement contracts". But that's not a problem of this PR.

However I get what you're saying, I'm not sure if its based on cache in this situation because the source code builder reflects the same structure again and should also build the same list of ClassLike nodes. On the other hand I'm quite familiar with the concept of inline types and how I would go about and implement this. Do we already have an example for this?

@mamazu mamazu force-pushed the anonymous_classes branch from b7acb99 to da577f9 Compare April 9, 2024 09:08
@przepompownia
Copy link
Contributor

przepompownia commented Apr 19, 2024

At the moment I have no occasion to work with anonymous classes, but have merged the recent version of this PR into my experimental branch and can report possible side effects.

@dantleech
Copy link
Collaborator

just to be clear, it's unlikely to be merged with it's current implementation. but hopefully I can find some time to look more into it.

@przepompownia
Copy link
Contributor

Regardless of merging into master, this change stopped working after merge de10e22 here (incompatible declarartion of reflectClassesIn in lib/WorseReflection/Bridge/TolerantParser/Reflector/TolerantSourceCodeReflector.php).

@mamazu
Copy link
Contributor Author

mamazu commented Oct 18, 2024

Should work again now.

@przepompownia
Copy link
Contributor

Works again, thank you

@mamazu mamazu force-pushed the anonymous_classes branch from 95cf6a5 to e2fec03 Compare December 1, 2024 12:33
@mamazu mamazu force-pushed the anonymous_classes branch from e2fec03 to 3a2108e Compare December 1, 2024 12:37
@mamazu
Copy link
Contributor Author

mamazu commented Dec 1, 2024

I had to rebase this because I messed up the merging of this branch.

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