Repository: phpactor/phpactor (php)

Issue:
Variadic rendered as `array` in generated methods
When implementing a contract variadics are rendered as an array (additionally if a defauly is given it is rendered using the old `array()` syntax)

Pull request:
Variadics
Add support for variadics

Fixes #1414 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bb8d7d9..8f408d01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@ Features:
   - [wr] Type combination
   - [wr] Support for type assertions via. is_*, instanceof etc
   - [wr] Array shape type support (types and completion)
+  - [wr] Support for variadics
   - [lsp] Send rename file request to client when renaming a PSR class
       @przepompownia
   - [wr] Initial support for generics #1382
diff --git a/lib/CodeBuilder/Adapter/WorseReflection/WorseBuilderFactory.php b/lib/CodeBuilder/Adapter/WorseReflection/WorseBuilderFactory.php
index a31976fc..cbb3d136 100644
--- a/lib/CodeBuilder/Adapter/WorseReflection/WorseBuilderFactory.php
+++ b/lib/CodeBuilder/Adapter/WorseReflection/WorseBuilderFactory.php
@@ -9,7 +9,9 @@ use Phpactor\TextDocument\TextDocument;
 use Phpactor\TextDocument\TextDocumentBuilder;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionClass;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionTrait;
+use Phpactor\WorseReflection\Core\Type\ArrayType;
 use Phpactor\WorseReflection\Core\Type\ClassType;
+use Phpactor\WorseReflection\Core\Type\MissingType;
 use Phpactor\WorseReflection\Reflector;
 use Phpactor\CodeBuilder\Domain\Builder\SourceCodeBuilder;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionProperty;
@@ -134,10 +136,19 @@ class WorseBuilderFactory implements BuilderFactory
 
             $this->resolveClassMemberType($methodBuilder->end(), $method->class()->name(), $type);
 
+            if ($parameter->isVariadic()) {
+                if ($type instanceof ArrayType) {
+                    $type = $type->valueType;
+                }
+            }
             $typeName = $this->resolveTypeNameFromNameImports($type, $imports);
             $parameterBuilder->type($typeName);
         }
 
+        if ($parameter->isVariadic()) {
+            $parameterBuilder->asVariadic();
+        }
+
         if ($parameter->default()->isDefined()) {
             $parameterBuilder->defaultValue($parameter->default()->value());
         }
@@ -162,8 +173,11 @@ class WorseBuilderFactory implements BuilderFactory
         $classBuilder->end()->use($type->name()->full());
     }
 
-    private function resolveTypeNameFromNameImports(Type $type, NameImports $imports)
+    private function resolveTypeNameFromNameImports(Type $type, NameImports $imports): string
     {
+        if ($type instanceof MissingType) {
+            return '';
+        }
         $typeName = TypeUtil::short($type);
 
         foreach ($imports as $alias => $import) {
diff --git a/lib/CodeBuilder/Domain/Builder/ParameterBuilder.php b/lib/CodeBuilder/Domain/Builder/ParameterBuilder.php
index 89ad55c7..fff63a12 100644
--- a/lib/CodeBuilder/Domain/Builder/ParameterBuilder.php
+++ b/lib/CodeBuilder/Domain/Builder/ParameterBuilder.php
@@ -19,12 +19,17 @@ class ParameterBuilder extends AbstractBuilder
 
     private MethodBuilder $parent;
 
+    private bool $variadic = false;
+
     public function __construct(MethodBuilder $parent, string $name)
     {
         $this->parent = $parent;
         $this->name = $name;
     }
 
+    /**
+     * @return array{}
+     */
     public static function childNames(): array
     {
         return [];
@@ -44,14 +49,15 @@ class ParameterBuilder extends AbstractBuilder
         return $this;
     }
 
-    public function build()
+    public function build(): Parameter
     {
         return new Parameter(
             $this->name,
             $this->type,
             $this->defaultValue,
             $this->byReference,
-            UpdatePolicy::fromModifiedState($this->isModified())
+            UpdatePolicy::fromModifiedState($this->isModified()),
+            $this->variadic,
         );
     }
 
@@ -60,10 +66,17 @@ class ParameterBuilder extends AbstractBuilder
         return $this->parent;
     }
 
-    public function byReference(bool $bool)
+    public function byReference(bool $bool): self
     {
         $this->byReference = $bool;
 
         return $this;
     }
+
+    public function asVariadic(): self
+    {
+        $this->variadic = true;
+
+        return $this;
+    }
 }
diff --git a/lib/CodeBuilder/Domain/Prototype/Parameter.php b/lib/CodeBuilder/Domain/Prototype/Parameter.php
index b0fcb6f5..6c117319 100644
--- a/lib/CodeBuilder/Domain/Prototype/Parameter.php
+++ b/lib/CodeBuilder/Domain/Prototype/Parameter.php
@@ -12,18 +12,23 @@ final class Parameter extends Prototype
 
     private DefaultValue $defaultValue;
 
+    private bool $isVariadic;
+
     public function __construct(
         string $name,
         Type $type = null,
         DefaultValue $defaultValue = null,
         bool $byReference = false,
-        UpdatePolicy $updatePolicy = null
+        UpdatePolicy $updatePolicy = null,
+        bool $isVariadic = false
     ) {
         parent::__construct($updatePolicy);
         $this->name = $name;
         $this->type = $type ?: Type::none();
         $this->defaultValue = $defaultValue ?: DefaultValue::none();
         $this->byReference = $byReference;
+        $this->updatePolicy = $updatePolicy;
+        $this->isVariadic = $isVariadic;
     }
 
     public function name(): string
@@ -45,4 +50,9 @@ final class Parameter extends Prototype
     {
         return $this->byReference;
     }
+
+    public function isVariadic(): bool
+    {
+        return $this->isVariadic;
+    }
 }
diff --git a/lib/CodeTransform/Adapter/WorseReflection/Transformer/ImplementContracts.php b/lib/CodeTransform/Adapter/WorseReflection/Transformer/ImplementContracts.php
index b9c0d176..860d75f7 100644
--- a/lib/CodeTransform/Adapter/WorseReflection/Transformer/ImplementContracts.php
+++ b/lib/CodeTransform/Adapter/WorseReflection/Transformer/ImplementContracts.php
@@ -84,10 +84,6 @@ class ImplementContracts implements Transformer
                     $missingMethod->declaringClass()->name()->short()
                 )->method($missingMethod->name());
 
-                if ($missingMethod->docblock()->isDefined()) {
-                    $methodBuilder->docblock('{@inheritDoc}');
-                }
-
                 $missingMethodReturnType = $missingMethod->returnType();
                 foreach (TypeUtil::unwrapClassTypes($missingMethodReturnType) as $type) {
                     $sourceCodeBuilder->use($type->name());
diff --git a/lib/Extension/LanguageServerWorseReflection/Workspace/WorkspaceIndexListener.php b/lib/Extension/LanguageServerWorseReflection/Workspace/WorkspaceIndexListener.php
index 0e311162..5192fc56 100644
--- a/lib/Extension/LanguageServerWorseReflection/Workspace/WorkspaceIndexListener.php
+++ b/lib/Extension/LanguageServerWorseReflection/Workspace/WorkspaceIndexListener.php
@@ -21,9 +21,6 @@ class WorkspaceIndexListener implements ListenerProviderInterface
         $this->index = $index;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     public function getListenersForEvent(object $event): iterable
     {
         if ($event instanceof TextDocumentUpdated) {
diff --git a/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionParameter.php b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionParameter.php
index 6be415c7..0f4ffb99 100644
--- a/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionParameter.php
+++ b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionParameter.php
@@ -11,8 +11,9 @@ use Phpactor\WorseReflection\Core\DefaultValue;
 use Phpactor\WorseReflection\Core\Inference\Frame;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionParameter as CoreReflectionParameter;
 use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\TypeResolver\DeclaredMemberTypeResolver;
-use Phpactor\WorseReflection\Core\TypeFactory;
 use Phpactor\WorseReflection\Core\Reflection\TypeResolver\ParameterTypeResolver;
+use Phpactor\WorseReflection\Core\Type\ArrayType;
+use Phpactor\WorseReflection\Core\Type\IntType;
 use Phpactor\WorseReflection\TypeUtil;
 
 class ReflectionParameter extends AbstractReflectedNode implements CoreReflectionParameter
@@ -58,7 +59,7 @@ class ReflectionParameter extends AbstractReflectedNode implements CoreReflectio
         );
 
         if ($this->parameter->dotDotDotToken) {
-            return TypeFactory::array($type->__toString());
+            return new ArrayType(new IntType(), $type);
         }
 
         return $type;
@@ -102,6 +103,11 @@ class ReflectionParameter extends AbstractReflectedNode implements CoreReflectio
         return $this->parameter->visibilityToken !== null;
     }
 
+    public function isVariadic(): bool
+    {
+        return $this->parameter->dotDotDotToken !== null;
+    }
+
     protected function node(): Node
     {
         return $this->parameter;
diff --git a/lib/WorseReflection/Core/Inference/Resolver/ParameterResolver.php b/lib/WorseReflection/Core/Inference/Resolver/ParameterResolver.php
index 2174b497..aaf566d9 100644
--- a/lib/WorseReflection/Core/Inference/Resolver/ParameterResolver.php
+++ b/lib/WorseReflection/Core/Inference/Resolver/ParameterResolver.php
@@ -34,6 +34,9 @@ class ParameterResolver implements Resolver
         $typeDeclaration = $node->typeDeclarationList;
 
         $type = NodeUtil::typeFromQualfiedNameLike($resolver->reflector(), $node, $node->typeDeclarationList);
+        if ($node->dotDotDotToken) {
+            $type = TypeFactory::array($type);
+        }
 
         if ($node->questionToken) {
             $type = TypeFactory::nullable($type);
diff --git a/lib/WorseReflection/Core/Reflection/ReflectionParameter.php b/lib/WorseReflection/Core/Reflection/ReflectionParameter.php
index bd0bd0e0..3cd3c974 100644
--- a/lib/WorseReflection/Core/Reflection/ReflectionParameter.php
+++ b/lib/WorseReflection/Core/Reflection/ReflectionParameter.php
@@ -28,4 +28,6 @@ interface ReflectionParameter extends ReflectionNode
     public function byReference(): bool;
 
     public function isPromoted(): bool;
+
+    public function isVariadic(): bool;
 }
diff --git a/lib/WorseReflection/Core/Virtual/VirtualReflectionParameter.php b/lib/WorseReflection/Core/Virtual/VirtualReflectionParameter.php
index 9483a8dd..5f4a013a 100644
--- a/lib/WorseReflection/Core/Virtual/VirtualReflectionParameter.php
+++ b/lib/WorseReflection/Core/Virtual/VirtualReflectionParameter.php
@@ -96,4 +96,9 @@ class VirtualReflectionParameter implements ReflectionParameter
     {
         return false;
     }
+
+    public function isVariadic(): bool
+    {
+        return false;
+    }
 }
diff --git a/templates/code/Parameter.php.twig b/templates/code/Parameter.php.twig
index 17581839..ccc222be 100644
--- a/templates/code/Parameter.php.twig
+++ b/templates/code/Parameter.php.twig
@@ -1,3 +1,3 @@
 {% if prototype.type.notNone %}{{ prototype.type }} {% endif %}
-{% if prototype.byReference %}&{% endif %}{% if true %}${{ prototype.name }}{% endif %}
+{% if prototype.byReference %}&{% endif %}{% if prototype.isVariadic %}...{% endif %}{% if true %}${{ prototype.name }}{% endif %}
 {% if prototype.defaultValue.notNone %} = {{ prototype.defaultValue.export|raw }}{% endif %}


Test patch:
diff --git a/lib/CodeBuilder/Tests/Adapter/UpdaterTestCase.php b/lib/CodeBuilder/Tests/Adapter/UpdaterTestCase.php
index 0f9b03f8..0b70709e 100644
--- a/lib/CodeBuilder/Tests/Adapter/UpdaterTestCase.php
+++ b/lib/CodeBuilder/Tests/Adapter/UpdaterTestCase.php
@@ -1291,6 +1291,32 @@ abstract class UpdaterTestCase extends TestCase
                         }
                     }
                     EOT
+        ];
+
+        yield 'It adds parameterized method with array shape' => [
+                <<<'EOT'
+                    class Aardvark
+                    {
+                    }
+                    EOT
+                , SourceCodeBuilder::create()
+                    ->class('Aardvark')
+                        ->method('methodOne')
+                            ->parameter('sniff')
+                                ->asVariadic()
+                                ->type('Snort')
+                            ->end()
+                        ->end()
+                    ->end()
+                    ->build(),
+                <<<'EOT'
+                    class Aardvark
+                    {
+                        public function methodOne(Snort ...$sniff)
+                        {
+                        }
+                    }
+                    EOT
             ];
 
         yield 'It is idempotent' => [
diff --git a/lib/CodeBuilder/Tests/Adapter/WorseReflection/WorseBuilderFactoryTest.php b/lib/CodeBuilder/Tests/Adapter/WorseReflection/WorseBuilderFactoryTest.php
index 515798b4..c6436f67 100644
--- a/lib/CodeBuilder/Tests/Adapter/WorseReflection/WorseBuilderFactoryTest.php
+++ b/lib/CodeBuilder/Tests/Adapter/WorseReflection/WorseBuilderFactoryTest.php
@@ -159,6 +159,18 @@ class WorseBuilderFactoryTest extends TestCase
         $this->assertEquals('string', (string) $source->classes()->first()->methods()->first()->parameters()->first()->type());
     }
 
+    public function testMethodWithVariadicParameter(): void
+    {
+        $source = $this->build('<?php class Foobar { public function method(string ...$param) {} }');
+        $this->assertEquals('string', (string) $source->classes()->first()->methods()->first()->parameters()->first()->type());
+    }
+
+    public function testMethodWithMissingParameterType(): void
+    {
+        $source = $this->build('<?php class Foobar { public function method(...$param) {} }');
+        $this->assertEquals('', (string) $source->classes()->first()->methods()->first()->parameters()->first()->type());
+    }
+
     public function testMethodWithAliasedParameter(): void
     {
         $source = $this->build('<?php use Foobar as Barfoo; class Foobar { public function method(Barfoo $param) {} }');
diff --git a/lib/CodeTransform/Tests/Adapter/WorseReflection/Transformer/ImplementContractsTest.php b/lib/CodeTransform/Tests/Adapter/WorseReflection/Transformer/ImplementContractsTest.php
index ec135412..b6af879e 100644
--- a/lib/CodeTransform/Tests/Adapter/WorseReflection/Transformer/ImplementContractsTest.php
+++ b/lib/CodeTransform/Tests/Adapter/WorseReflection/Transformer/ImplementContractsTest.php
@@ -189,9 +189,6 @@ class ImplementContractsTest extends WorseTestCase
 
                     class Foobar implements Bird
                     {
-                        /**
-                         * {@inheritDoc}
-                         */
                         public function chirp()
                         {
                         }
diff --git a/lib/WorseReflection/Tests/Inference/type/variadic.test b/lib/WorseReflection/Tests/Inference/type/variadic.test
new file mode 100644
index 00000000..17372c42
--- /dev/null
+++ b/lib/WorseReflection/Tests/Inference/type/variadic.test
@@ -0,0 +1,5 @@
+<?php
+
+function foo(string ...$foo) {
+    wrAssertType('string[]', $foo);
+}
diff --git a/lib/WorseReflection/Tests/Integration/Bridge/TolerantParser/Reflection/ReflectionTraitTest.php b/lib/WorseReflection/Tests/Integration/Bridge/TolerantParser/Reflection/ReflectionTraitTest.php
index de847768..13615c7e 100644
--- a/lib/WorseReflection/Tests/Integration/Bridge/TolerantParser/Reflection/ReflectionTraitTest.php
+++ b/lib/WorseReflection/Tests/Integration/Bridge/TolerantParser/Reflection/ReflectionTraitTest.php
@@ -105,9 +105,6 @@ class ReflectionTraitTest extends IntegrationTestCase
 
                     trait Int1
                     {
-                        /**
-                         * {@inheritDoc()
-                         */
                         public function foo()
                         {
                         }