Repository: phpactor/phpactor (php)

Issue:
Generate construtor does not work with attributes
```
#[Attribute]
class MyAttribute
{
}
````

```
#[MyAttribute('foo')]
```

:point_up: should be able to generate the constructor.

Pull request description:
Gh 2059: Generate constructor on attributes
Fixes #2059 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60374e7d..1e4d36be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,17 @@ Changelog
 
 ## master
 
+Bug fixes:
+
+  - `new class-string<Foo>` now resolves to `new Foo` #2065
+
+Improvements:
+
+  - Allow generation of constructor for Attributes.
+
+
+## 2023.01.21
+
 Bug fixes:
 
   - Allow class completion within constant declaration in class #1985 @przepompownia
diff --git a/lib/CodeTransform/Adapter/WorseReflection/Refactor/WorseGenerateConstructor.php b/lib/CodeTransform/Adapter/WorseReflection/Refactor/WorseGenerateConstructor.php
index af581524..c0db4403 100644
--- a/lib/CodeTransform/Adapter/WorseReflection/Refactor/WorseGenerateConstructor.php
+++ b/lib/CodeTransform/Adapter/WorseReflection/Refactor/WorseGenerateConstructor.php
@@ -2,6 +2,8 @@
 
 namespace Phpactor\CodeTransform\Adapter\WorseReflection\Refactor;
 
+use Microsoft\PhpParser\Node;
+use Microsoft\PhpParser\Node\Attribute;
 use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
 use Microsoft\PhpParser\Parser;
 use Phpactor\CodeBuilder\Domain\BuilderFactory;
@@ -14,9 +16,10 @@ use Phpactor\TextDocument\TextDocument;
 use Phpactor\TextDocument\TextDocumentEdits;
 use Phpactor\TextDocument\TextDocumentUri;
 use Phpactor\TextDocument\WorkspaceEdits;
-use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ReflectionObjectCreationExpression;
+use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ClassInvocation;
 use Phpactor\WorseReflection\Core\Exception\NotFound;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionArgument;
+use Phpactor\WorseReflection\Core\Reflection\ReflectionNode;
 use Phpactor\WorseReflection\Reflector;
 
 class WorseGenerateConstructor implements GenerateConstructor
@@ -31,38 +34,32 @@ class WorseGenerateConstructor implements GenerateConstructor
 
     public function generateMethod(TextDocument $document, ByteOffset $offset): WorkspaceEdits
     {
-        $node = $this->parser->parseSourceFile($document->__toString())->getDescendantNodeAtPosition($offset->toInt());
-        $node = $node->getFirstAncestor(ObjectCreationExpression::class);
+        $reflectionNode = $this->reflectionNode($document, $offset);
 
-        if (!$node instanceof ObjectCreationExpression) {
-            return WorkspaceEdits::none();
-        }
-        try {
-            $newObject = $this->reflector->reflectNode($document, $node->getStartPosition());
-        } catch (NotFound) {
+        if (null === $reflectionNode) {
             return WorkspaceEdits::none();
         }
 
-        if (!$newObject instanceof ReflectionObjectCreationExpression) {
+        if (!$reflectionNode instanceof ClassInvocation) {
             return WorkspaceEdits::none();
         }
 
         try {
-            if ($newObject->class()->methods()->has('__construct')) {
+            if ($reflectionNode->class()->methods()->has('__construct')) {
                 return WorkspaceEdits::none();
             }
         } catch (NotFound) {
             return WorkspaceEdits::none();
         }
 
-        $arguments = $newObject->arguments();
+        $arguments = $reflectionNode->arguments();
 
         if (count($arguments) === 0) {
             return WorkspaceEdits::none();
         }
 
-        $builder = $this->factory->fromSource($newObject->class()->sourceCode());
-        $class = $builder->class($newObject->class()->name()->short());
+        $builder = $this->factory->fromSource($reflectionNode->class()->sourceCode());
+        $class = $builder->class($reflectionNode->class()->name()->short());
         $method = $class->method('__construct');
 
         foreach ($arguments->named() as $name => $argument) {
@@ -77,9 +74,42 @@ class WorseGenerateConstructor implements GenerateConstructor
 
         return new WorkspaceEdits(
             new TextDocumentEdits(
-                TextDocumentUri::fromString($newObject->class()->sourceCode()->mustGetUri()),
-                $this->updater->textEditsFor($builder->build(), Code::fromString($newObject->class()->sourceCode()))
+                TextDocumentUri::fromString($reflectionNode->class()->sourceCode()->mustGetUri()),
+                $this->updater->textEditsFor($builder->build(), Code::fromString($reflectionNode->class()->sourceCode()))
             )
         );
     }
+
+    private function reflectionNode(TextDocument $document, ByteOffset $offset): ?ReflectionNode
+    {
+        $node = $this->node($document, $offset);
+        if (null === $node) {
+            return null;
+        }
+
+        try {
+            $newObject = $this->reflector->reflectNode($document, $node->getStartPosition());
+        } catch (NotFound) {
+            return null;
+        }
+
+        return $newObject;
+    }
+
+    private function node(TextDocument $document, ByteOffset $offset): ?Node
+    {
+        $node = $this->parser->parseSourceFile($document->__toString())->getDescendantNodeAtPosition($offset->toInt());
+
+        if ($node->parent instanceof Attribute) {
+            return $node->parent;
+        }
+
+        $node = $node->getFirstAncestor(ObjectCreationExpression::class);
+
+        if ($node instanceof ObjectCreationExpression) {
+            return $node;
+        }
+
+        return null;
+    }
 }
diff --git a/lib/WorseReflection/Bridge/TolerantParser/Reflection/ClassInvocation.php b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ClassInvocation.php
new file mode 100644
index 00000000..e6eb0272
--- /dev/null
+++ b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ClassInvocation.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Phpactor\WorseReflection\Bridge\TolerantParser\Reflection;
+
+use Phpactor\WorseReflection\Core\Reflection\Collection\ReflectionArgumentCollection;
+use Phpactor\WorseReflection\Core\Reflection\ReflectionClassLike;
+
+interface ClassInvocation
+{
+    public function class(): ReflectionClassLike;
+    public function arguments(): ReflectionArgumentCollection;
+}
diff --git a/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionAttribute.php b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionAttribute.php
new file mode 100644
index 00000000..f269bff1
--- /dev/null
+++ b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionAttribute.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Phpactor\WorseReflection\Bridge\TolerantParser\Reflection;
+
+use Microsoft\PhpParser\Node\Attribute;
+use Phpactor\WorseReflection\Core\Exception\CouldNotResolveNode;
+use Phpactor\WorseReflection\Core\Inference\Frame;
+use Phpactor\WorseReflection\Core\Position;
+use Phpactor\WorseReflection\Core\Reflection\Collection\ReflectionArgumentCollection;
+use Phpactor\WorseReflection\Core\Reflection\ReflectionClassLike;
+use Phpactor\WorseReflection\Core\Reflection\ReflectionObjectCreationExpression as PhpactorReflectionObjectCreationExpression;
+use Phpactor\WorseReflection\Core\Reflection\ReflectionScope;
+use Phpactor\WorseReflection\Core\ServiceLocator;
+use Phpactor\WorseReflection\Core\Type\ReflectedClassType;
+use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ReflectionScope as TolerantReflectionScope;
+
+class ReflectionAttribute implements PhpactorReflectionObjectCreationExpression, ClassInvocation
+{
+    public function __construct(
+        private ServiceLocator $locator,
+        private Frame $frame,
+        private Attribute $node
+    ) {
+    }
+
+    public function scope(): ReflectionScope
+    {
+        return new TolerantReflectionScope($this->locator->reflector(), $this->node);
+    }
+
+    public function position(): Position
+    {
+        return Position::fromFullStartStartAndEnd(
+            $this->node->getFullStartPosition(),
+            $this->node->getStartPosition(),
+            $this->node->getEndPosition()
+        );
+    }
+
+    public function class(): ReflectionClassLike
+    {
+        $type = $this->locator->symbolContextResolver()->resolveNode($this->frame, $this->node->name)->type();
+
+        if (!$type instanceof ReflectedClassType) {
+            throw new CouldNotResolveNode(sprintf('Expceted "%s" but got "%s"', ReflectedClassType::class, get_class($type)));
+        }
+
+        $reflection = $type->reflectionOrNull();
+
+        if (null === $reflection) {
+            throw new CouldNotResolveNode(
+                'Could not reflect class'
+            );
+        }
+
+        return $reflection;
+    }
+
+    public function arguments(): ReflectionArgumentCollection
+    {
+        if (null === $this->node->argumentExpressionList) {
+            return ReflectionArgumentCollection::empty();
+        }
+
+        return ReflectionArgumentCollection::fromArgumentListAndFrame(
+            $this->locator,
+            $this->node->argumentExpressionList,
+            $this->frame
+        );
+    }
+}
diff --git a/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionObjectCreationExpression.php b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionObjectCreationExpression.php
index 2d67b6a5..e39b8e07 100644
--- a/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionObjectCreationExpression.php
+++ b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionObjectCreationExpression.php
@@ -14,7 +14,7 @@ use Phpactor\WorseReflection\Core\ServiceLocator;
 use Phpactor\WorseReflection\Core\Type\ReflectedClassType;
 use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ReflectionScope as TolerantReflectionScope;
 
-class ReflectionObjectCreationExpression implements PhpactorReflectionObjectCreationExpression
+class ReflectionObjectCreationExpression implements PhpactorReflectionObjectCreationExpression, ClassInvocation
 {
     public function __construct(
         private ServiceLocator $locator,
diff --git a/lib/WorseReflection/Core/Inference/NodeReflector.php b/lib/WorseReflection/Core/Inference/NodeReflector.php
index 5b4cdb62..df65ecec 100644
--- a/lib/WorseReflection/Core/Inference/NodeReflector.php
+++ b/lib/WorseReflection/Core/Inference/NodeReflector.php
@@ -3,9 +3,11 @@
 namespace Phpactor\WorseReflection\Core\Inference;
 
 use Microsoft\PhpParser\Node;
+use Microsoft\PhpParser\Node\Attribute;
 use Microsoft\PhpParser\Node\Expression\CallExpression;
 use Microsoft\PhpParser\Node\Expression\MemberAccessExpression;
 use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
+use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ReflectionAttribute;
 use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ReflectionMethodCall;
 use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ReflectionObjectCreationExpression as PhpactorReflectionObjectCreationExpression;
 use Phpactor\WorseReflection\Core\Exception\CouldNotResolveNode;
@@ -35,6 +37,10 @@ class NodeReflector
             return $this->reflectObjectCreationExpression($frame, $node);
         }
 
+        if ($node->parent instanceof Attribute) {
+            return $this->reflectAttribute($frame, $node->parent);
+        }
+
         throw new CouldNotResolveNode(sprintf(
             'Did not know how to reflect node of type "%s"',
             get_class($node)
@@ -89,4 +95,13 @@ class NodeReflector
             $node
         );
     }
+
+    private function reflectAttribute(Frame $frame, Attribute $node): ReflectionNode
+    {
+        return new ReflectionAttribute(
+            $this->services,
+            $frame,
+            $node
+        );
+    }
 }


Test patch:
diff --git a/lib/CodeTransform/Tests/Adapter/WorseReflection/Refactor/fixtures/generateConstructor_attribute.test b/lib/CodeTransform/Tests/Adapter/WorseReflection/Refactor/fixtures/generateConstructor_attribute.test
new file mode 100644
index 00000000..2ecff1e3
--- /dev/null
+++ b/lib/CodeTransform/Tests/Adapter/WorseReflection/Refactor/fixtures/generateConstructor_attribute.test
@@ -0,0 +1,28 @@
+// File: source
+<?php
+
+namespace Phpactor;
+
+#[Attribute()]
+class Barfoo 
+{
+}
+
+#[Ba<>rfoo('foobar', 123)]
+class Foobar {}
+
+// File: expected
+<?php
+
+namespace Phpactor;
+
+#[Attribute()]
+class Barfoo 
+{
+    public function __construct(string $string, int $int)
+    {
+    }
+}
+
+#[Barfoo('foobar', 123)]
+class Foobar {}