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

Improving support for anonymous classes #2613

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c597a53
First try at implementing it
mamazu Mar 22, 2024
5cc2e4d
Fixups
mamazu Mar 22, 2024
71bf377
Skip real classes in `ObjectCreationExpression`s
mamazu Mar 22, 2024
f76d62f
Improvements
mamazu Mar 23, 2024
d98a9f8
Removing unused properties
mamazu Mar 23, 2024
5e5389a
Making assertions assert the correct type
mamazu Mar 23, 2024
4889c1c
Merge branch 'master' into anonymous_classes
mamazu Mar 23, 2024
836d962
Using the reflected class resolver
mamazu Mar 23, 2024
8e24b48
Using a softer exception
mamazu Mar 24, 2024
f32ad1b
More fixup
mamazu Mar 24, 2024
7a53a6b
Adding support for extending anonymous classes
mamazu Mar 25, 2024
701944a
Improving the typing
mamazu Mar 27, 2024
8b69ecb
Adding an `isAnonymous` method to the class
mamazu Mar 30, 2024
168f3e2
Improving it some more
mamazu Mar 30, 2024
cbeb726
More sensible default
mamazu Mar 31, 2024
07dd87c
Adding tests back
mamazu Mar 31, 2024
c79260d
First try at implementing it
mamazu Mar 22, 2024
f91f1b4
Fixups
mamazu Mar 22, 2024
ae72ace
Skip real classes in `ObjectCreationExpression`s
mamazu Mar 22, 2024
046ff0c
Improvements
mamazu Mar 23, 2024
f8044f4
Removing unused properties
mamazu Mar 23, 2024
10d4180
Making assertions assert the correct type
mamazu Mar 23, 2024
202087e
Using the reflected class resolver
mamazu Mar 23, 2024
175266d
Using a softer exception
mamazu Mar 24, 2024
bc3b1b4
More fixup
mamazu Mar 24, 2024
0ea9d3e
Adding support for extending anonymous classes
mamazu Mar 25, 2024
14802b4
Improving the typing
mamazu Mar 27, 2024
0ea17a9
Adding an `isAnonymous` method to the class
mamazu Mar 30, 2024
f197e6f
Improving it some more
mamazu Mar 30, 2024
e5ff010
More sensible default
mamazu Mar 31, 2024
c30b479
Adding tests back
mamazu Mar 31, 2024
4f76488
Merge remote-tracking branch 'origin/anonymous_classes' into anonymou…
mamazu Mar 31, 2024
9249c6a
Fixing type errors
mamazu Mar 31, 2024
6640629
Reverting changes
mamazu Mar 31, 2024
caa112a
Fix
mamazu Mar 31, 2024
da577f9
Moving the ReflectionClassLikeCollection creation to the reflector
mamazu Apr 9, 2024
011b095
Merge branch 'master' into anonymous_classes
mamazu Apr 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -5,6 +5,7 @@
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\Node\ClassBaseClause;
use Microsoft\PhpParser\Node\ClassInterfaceClause;
use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
use Microsoft\PhpParser\Node\QualifiedName;
use Microsoft\PhpParser\Node\Statement\ClassDeclaration;
use Microsoft\PhpParser\TokenKind;
Expand Down Expand Up @@ -57,14 +58,15 @@
public function __construct(
private ServiceLocator $serviceLocator,
private TextDocument $sourceCode,
private ClassDeclaration $node,
private array $visited = []
private ClassDeclaration|ObjectCreationExpression $node,
private array $visited = [],
private bool $anonymous = false

Check failure on line 63 in lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionClass.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Property Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ReflectionClass::$anonymous is never read, only written.
) {
}

public function isAbstract(): bool
{
$modifier = $this->node->abstractOrFinalModifier;

Check failure on line 69 in lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionClass.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Access to an undefined property Microsoft\PhpParser\Node\Expression\ObjectCreationExpression|Microsoft\PhpParser\Node\Statement\ClassDeclaration::$abstractOrFinalModifier.

/** @phpstan-ignore-next-line */
if (!$modifier) {
Expand Down Expand Up @@ -109,7 +111,7 @@

// we need to account for traits renaming aliases
if ($reflectionClassLike instanceof ReflectionTrait) {
$traitImports = TraitImports::forClassDeclaration($this->node);

Check failure on line 114 in lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionClass.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Parameter #1 $classDeclaration of static method Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\TraitImport\TraitImports::forClassDeclaration() expects Microsoft\PhpParser\Node\Statement\ClassDeclaration, Microsoft\PhpParser\Node\Expression\ObjectCreationExpression|Microsoft\PhpParser\Node\Statement\ClassDeclaration given.
/** @phpstan-ignore-next-line collection IS compatible */
$members = $members->merge($this->resolveTraitMethods($traitImports, $this, $this->traits()));
continue;
Expand Down Expand Up @@ -250,8 +252,8 @@
public function memberListPosition(): ByteOffsetRange
{
return ByteOffsetRange::fromInts(
$this->node->classMembers->openBrace->start,

Check failure on line 255 in lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionClass.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Cannot access property $openBrace on Microsoft\PhpParser\Node\ClassMembersNode|null.
$this->node->classMembers->openBrace->start + $this->node->classMembers->openBrace->length

Check failure on line 256 in lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionClass.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Cannot access property $openBrace on Microsoft\PhpParser\Node\ClassMembersNode|null.

Check failure on line 256 in lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionClass.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Cannot access property $openBrace on Microsoft\PhpParser\Node\ClassMembersNode|null.
);
}

Expand All @@ -260,7 +262,13 @@
if ($this->name) {
return $this->name;
}
$this->name = ClassName::fromString((string) $this->node->getNamespacedName());

if ($this->node instanceof ObjectCreationExpression){
$this->name = ClassName::fromString('class@anonymous:'.$this->node->getStartPosition());
} else {
$this->name = ClassName::fromString((string) $this->node->getNamespacedName());
}

return $this->name;
}

Expand Down Expand Up @@ -340,7 +348,7 @@

public function isFinal(): bool
{
$modifier = $this->node->abstractOrFinalModifier;

Check failure on line 351 in lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionClass.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Access to an undefined property Microsoft\PhpParser\Node\Expression\ObjectCreationExpression|Microsoft\PhpParser\Node\Statement\ClassDeclaration::$abstractOrFinalModifier.

/** @phpstan-ignore-next-line */
if (!$modifier) {
Expand Down
Expand Up @@ -4,6 +4,7 @@

use Microsoft\PhpParser\ClassLike;
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Node\Statement\CompoundStatementNode;
use Microsoft\PhpParser\TokenKind;
Expand Down Expand Up @@ -60,7 +61,10 @@ public function nameRange(): ByteOffsetRange

public function declaringClass(): ReflectionClassLike
{
$classDeclaration = $this->node->getFirstAncestor(ClassLike::class);
$classDeclaration = $this->node->getFirstAncestor(ClassLike::class, ObjectCreationExpression::class);
if ($classDeclaration instanceof ObjectCreationExpression) {
return $this->class ?? $this->serviceLocator->reflector()->reflectClassLike('class@anonymous:'.$classDeclaration->getStartPosition());
}

assert($classDeclaration instanceof NamespacedNameInterface);
$class = $classDeclaration->getNamespacedName();
Expand Down
Expand Up @@ -10,8 +10,10 @@
use Phpactor\WorseReflection\Core\Inference\FunctionArguments;
use Phpactor\WorseReflection\Core\Inference\GenericMapResolver;
use Phpactor\WorseReflection\Core\Inference\NodeContext;
use Phpactor\WorseReflection\Core\Inference\NodeContextFactory;
use Phpactor\WorseReflection\Core\Inference\Resolver;
use Phpactor\WorseReflection\Core\Inference\NodeContextResolver;
use Phpactor\WorseReflection\Core\Inference\Symbol;
use Phpactor\WorseReflection\Core\Type;
use Phpactor\WorseReflection\Core\TypeFactory;
use Phpactor\WorseReflection\Core\Type\ClassStringType;
Expand All @@ -27,12 +29,13 @@ public function __construct(private GenericMapResolver $resolver)
public function resolve(NodeContextResolver $resolver, Frame $frame, Node $node): NodeContext
{
assert($node instanceof ObjectCreationExpression);
if (false === $node->classTypeDesignator instanceof Node) {
throw new CouldNotResolveNode(sprintf('Could not create object from "%s"', get_class($node)));
}

$className = $node->classTypeDesignator;
if (false === $className instanceof Node) {
mamazu marked this conversation as resolved.
Show resolved Hide resolved
return $this->resolveAnonymousClass($resolver, $node);
}

$classContext = $resolver->resolveNode($frame, $node->classTypeDesignator);
$classContext = $resolver->resolveNode($frame, $className);
$classType = $classContext->type();

if ($classType instanceof ClassStringType) {
Expand Down Expand Up @@ -74,4 +77,20 @@ private function resolveClassType(NodeContextResolver $resolver, Frame $frame, O
);
return new GenericClassType($resolver->reflector(), $classType->name(), $templateMap->toArguments());
}

private function resolveAnonymousClass(NodeContextResolver $resolver, Node $node): NodeContext
{
return NodeContextFactory::create(
'testing',
dantleech marked this conversation as resolved.
Show resolved Hide resolved
$node->getStartPosition(),
$node->getEndPosition(),
[
'symbol_type' => Symbol::CLASS_,
'type' => TypeFactory::fromStringWithReflector(
mamazu marked this conversation as resolved.
Show resolved Hide resolved
'class@anonymous:'.$node->getStartPosition(),
$resolver->reflector(),
)
]
);
}
}
Expand Up @@ -7,6 +7,7 @@
use Microsoft\PhpParser\Node\ClassConstDeclaration;
use Microsoft\PhpParser\Node\EnumCaseDeclaration;
use Microsoft\PhpParser\Node\Expression\AssignmentExpression;
use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
use Microsoft\PhpParser\Node\Expression\Variable;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Node\Parameter;
Expand Down Expand Up @@ -68,13 +69,13 @@

public static function fromClassMemberDeclarations(
ServiceLocator $serviceLocator,
ClassDeclaration $class,
ClassDeclaration|ObjectCreationExpression $class,
ReflectionClass $reflectionClass
): self {
return self::fromDeclarations(
$serviceLocator,
$reflectionClass,
$class->classMembers->classMemberDeclarations,

Check failure on line 78 in lib/WorseReflection/Core/Reflection/Collection/ClassLikeReflectionMemberCollection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Cannot access property $classMemberDeclarations on Microsoft\PhpParser\Node\ClassMembersNode|null.
);
}

Expand Down
Expand Up @@ -2,6 +2,7 @@

namespace Phpactor\WorseReflection\Core\Reflection\Collection;

use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
use Microsoft\PhpParser\Node\Statement\ClassDeclaration;
use Microsoft\PhpParser\Node\Statement\EnumDeclaration;
use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ReflectionEnum;
Expand Down Expand Up @@ -30,11 +31,11 @@ public static function fromNode(ServiceLocator $serviceLocator, TextDocument $so
$items = [];

$nodeCollection = $node->getDescendantNodes(function (Node $node) {
return false === $node instanceof ClassLike;
return false === $node instanceof ClassLike && false === $node instanceof ObjectCreationExpression;
});

foreach ($nodeCollection as $child) {
if (false === $child instanceof ClassLike) {
if (false === $child instanceof ClassLike && !$child instanceof ObjectCreationExpression) {
continue;
}

Expand All @@ -55,6 +56,12 @@ public static function fromNode(ServiceLocator $serviceLocator, TextDocument $so

if ($child instanceof ClassDeclaration) {
$items[(string) $child->getNamespacedName()] = new ReflectionClass($serviceLocator, $source, $child, $visited);
continue;
}

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.

$items['class@anonymous:'.$child->getStartPosition()] = new ReflectionClass($serviceLocator, $source, $child, $visited, anonymous: true);
}
}

Expand Down
Expand Up @@ -3,6 +3,7 @@
namespace Phpactor\WorseReflection\Core\Reflection\Collection;

use Microsoft\PhpParser\Node\ClassInterfaceClause;
use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
use Microsoft\PhpParser\Node\InterfaceBaseClause;
use Phpactor\WorseReflection\Core\Exception\NotFound;
use Phpactor\WorseReflection\Core\Reflection\ReflectionInterface;
Expand All @@ -25,8 +26,10 @@ public static function fromInterfaceDeclaration(ServiceLocator $serviceLocator,
return self::fromBaseClause($serviceLocator, $interface->interfaceBaseClause, $visited);
}

public static function fromClassDeclaration(ServiceLocator $serviceLocator, ClassDeclaration $class): self
{
public static function fromClassDeclaration(
ServiceLocator $serviceLocator,
ClassDeclaration|ObjectCreationExpression $class,
): self {
return self::fromBaseClause($serviceLocator, $class->classInterfaceClause, []);
}

Expand Down
Expand Up @@ -2,6 +2,7 @@

namespace Phpactor\WorseReflection\Core\Reflection\Collection;

use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
use Microsoft\PhpParser\Node\Statement\EnumDeclaration;
use Microsoft\PhpParser\Node\Statement\TraitDeclaration;
use Phpactor\WorseReflection\Core\Exception\NotFound;
Expand All @@ -17,10 +18,12 @@
*/
class ReflectionTraitCollection extends AbstractReflectionCollection
{
public static function fromClassDeclaration(ServiceLocator $serviceLocator, ClassDeclaration $class): self
{
public static function fromClassDeclaration(
ServiceLocator $serviceLocator,
ClassDeclaration|ObjectCreationExpression $class,
): self {
$items = [];
foreach ($class->classMembers->classMemberDeclarations as $memberDeclaration) {

Check failure on line 26 in lib/WorseReflection/Core/Reflection/Collection/ReflectionTraitCollection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1)

Cannot access property $classMemberDeclarations on Microsoft\PhpParser\Node\ClassMembersNode|null.
if (false === $memberDeclaration instanceof TraitUseClause) {
continue;
}
Expand Down
Expand Up @@ -2,6 +2,7 @@

namespace Phpactor\WorseReflection\Tests\Unit\Core\Reflector\SourceCode;

use Generator;
use PHPUnit\Framework\TestCase;
use Phpactor\TextDocument\TextDocumentBuilder;
use Phpactor\WorseReflection\Core\SourceCodeLocator\TemporarySourceLocator;
Expand All @@ -19,8 +20,6 @@ class ContextualSourceCodeReflectorTest extends TestCase

private ContextualSourceCodeReflector $reflector;

private $code;

private TemporarySourceLocator $locator;

public function setUp(): void
Expand All @@ -31,8 +30,6 @@ public function setUp(): void
ReflectorBuilder::create()->build(),
$this->locator
);

$this->code = TextDocumentBuilder::create(self::TEST_SOURCE_CODE)->build();
}

public function testReflectsClassesIn(): void
Expand All @@ -51,4 +48,33 @@ public function testReflectMethodCall(): void
$call = $this->reflector->reflectMethodCall(TextDocumentBuilder::fromUnknown('<?php class One { function bar() {} } $f = new One();$f->bar();'), 59);
self::assertInstanceOf(ReflectionMethodCall::class, $call);
}

/**
* @dataProvider provideReflectAnonymousClass
*/
public function testReflectAnonymousClass(string $sourceCode, int $expectedCount): void
{
$result = $this->reflector->reflectClassesIn(TextDocumentBuilder::fromUnknown($sourceCode));

self::assertCount($expectedCount, $result);
}

/**
* @return Generator<string,array{string,int}>
*/
public function provideReflectAnonymousClass(): Generator
{
yield 'one class' => [
'<?php $formatter = new class() {};', 1
];

yield 'one class with implements' => [
'<?php $formatter = new class implements Test () {};', 1
];

yield 'two classes' => [
'<?php $billow = new class() {}; $formatter = new class() {};', 2
];
}

}
50 changes: 46 additions & 4 deletions tests/System/Extension/Completion/Application/CompleteTest.php
Expand Up @@ -295,7 +295,49 @@ class Foobar
'short_description' => 'pub $foobar',
],
],
]
],

'Anonoymous class properties' => [
<<<'EOT'
<?php

$foobar = new class(){
public string $test;
};
$foobar-><>

EOT
, [
[
'type' => 'property',
'name' => 'test',
'short_description' => 'pub $test: string',
],
],
],
'Anonoymous class methods' => [
<<<'EOT'
<?php

$foobar = new class() {
public function doSomething(): bool {}
};
$foobar-><>

EOT
, [
[
'type' => 'method',
'name' => 'doSomething',
'short_description' => 'pub doSomething(): bool',
'documentation' => '### class@anonymous:17::doSomething

```php
<?php public function doSomething(): bool
```'
],
],
],
];
}
/**
Expand All @@ -304,9 +346,9 @@ class Foobar
private function complete(string $source): array
{
[$source, $offset] = ExtractOffset::fromSource($source);
$complete = $this->container()->get('application.complete');
assert($complete instanceof Complete);
$result = $complete->complete($source, $offset);
$result = $this->container()
->expect('application.complete', Complete::class)
->complete($source, $offset);

return $result;
}
Expand Down