Repository: phpDocumentor/phpDocumentor (language: php)

Issue:
Variable length argument list syntax
## Expected behavior

This is a reopening of #3534 with a more complete example and explanation showing the issue. I don't have the ability to reopen the closed issue, and I think my comment there with additional information might have slipped passed.

The issue is the same as the original report - if `{@see ...}` is used in a parameter report that uses a variable length parameter and `...$varName` syntax in the phpDoc comment, an obscure error is thrown due to the `see` parameters not being resolved (printf gets the wrong number of parameters).

The [old phpDocumentor documentation](https://manual.phpdoc.org/HTMLSmartyConverter/HandS/phpDocumentor/tutorial_tags.param.pkg.html)  refers to `$VAR,...` syntax.

The [new documentation](https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/param.html#param) doesn't appear to make any mention of a variable number of parameters. Both PHP and phpStorm use the `...$VAR` syntax.

This might be a feature request that the `...$VAR` syntax be supported if it isn't already. However, based on @jaapio's commend in #3534 it might be supported already?

## Actual behavior

The error below is shown when using the reproduction steps below:

```
In Extension.php line 417:
                                                     
  The arguments array must contain 2 items, 0 given  
```

## Steps to reproduce the problem

1) Save the following to `test.php`

```php
<?php

/**
 * DataTables Editor base class for creating editable tables.
 */
class Editor
{
	/**
	 * Get / set field instance.
	 *
	 * The list of fields designates which columns in the table that Editor will work
	 * with (both get and set).
	 *
	 * @param Field|string ...$_ This parameter effects the return value of the
	 *                           function:
	 *
	 *      * `null` - Get an array of all fields assigned to the instance
	 * 	    * `string` - Get a specific field instance whose 'name' matches the
	 *           field passed in
	 *      * {@see Field} - Add a field to the instance's list of fields. This
	 *           can be as many fields as required (i.e. multiple arguments)
	 *      * `array` - An array of {@see Field} instances to add to the list
	 *        of fields.
	 *
	 * @return Field|Field[]|Editor The selected field, an array of fields, or
	 *                              the Editor instance for chaining, depending on the input parameter.
	 *
	 * @see {@see Field} for field documentation.
	 */
	public function field($_ = null)
	{
		var_dump($_);
	}
}

/**
 * Field class
 */
class Field
{
	private $_name = '';

	public function name($_=null)
	{
		if ($_ === null) {
			return $this->_name;
		}
	
		$this->_name = $_;
		return $this;
	}
}
```

2) Run phpDocumentor: `php phpDocumentor.phar -c phpdoc.dist.xml --sourcecode` - I get the following:

```
$ php phpDocumentor.phar -c phpdoc.dist.xml --sourcecode
phpDocumentor v3.3.1

Parsing files
 1/1 [============================] 100%
Applying transformations (can take a while)
  1/21 [=>--------------------------]   4%20:36:58 CRITICAL  [console] Error thrown while running command "-c 'phpdoc.dist.xml' --sourcecode". Message: "The arguments array must contain 2 items, 0 given" ["exception" => ValueError { …},"command" => "-c 'phpdoc.dist.xml' --sourcecode","message" => "The arguments array must contain 2 items, 0 given"]

In Extension.php line 417:
                                                     
  The arguments array must contain 2 items, 0 given  
                                                     

project:run [-t|--target [TARGET]] [--cache-folder [CACHE-FOLDER]] [-f|--filename [FILENAME]] [-d|--directory [DIRECTORY]] [--encoding [ENCODING]] [--extensions [EXTENSIONS]] [-i|--ignore [IGNORE]] [--ignore-tags [IGNORE-TAGS]] [--hidden] [--ignore-symlinks] [-m|--markers [MARKERS]] [--title [TITLE]] [--force] [--validate] [--visibility [VISIBILITY]] [--defaultpackagename [DEFAULTPACKAGENAME]] [--sourcecode] [--template [TEMPLATE]] [--examples-dir [EXAMPLES-DIR]] [-s|--setting [SETTING]] [--list-settings] [--parseprivate]
```

If I remove the `{@see Field}` then it will complete successfully:

```
$ php phpDocumentor.phar -c phpdoc.dist.xml --sourcecode
phpDocumentor v3.3.1

Parsing files
 1/1 [============================] 100%
Applying transformations (can take a while)
 21/21 [============================] 100%
All done in 0 seconds!
```

Or if I replace `$_,...` with the old style `...$_` it will also complete successfully (same output as above).


## Your environment

* Version used: v3.3.1
* Install method: phar
* PHP version: 8.2.1
* Operating system and version: OpenSUSE tumbleweed
* Link to your project: N/A


Discussion / hints:

Fix patch:
diff --git a/src/phpDocumentor/Descriptor/Builder/Reflector/MethodAssembler.php b/src/phpDocumentor/Descriptor/Builder/Reflector/MethodAssembler.php
index afccbbcfc..5dddc1458 100644
--- a/src/phpDocumentor/Descriptor/Builder/Reflector/MethodAssembler.php
+++ b/src/phpDocumentor/Descriptor/Builder/Reflector/MethodAssembler.php
@@ -13,13 +13,10 @@
 
 namespace phpDocumentor\Descriptor\Builder\Reflector;
 
-use phpDocumentor\Descriptor\ArgumentDescriptor;
 use phpDocumentor\Descriptor\Collection;
-use phpDocumentor\Descriptor\DocBlock\DescriptionDescriptor;
 use phpDocumentor\Descriptor\Interfaces\MethodInterface;
 use phpDocumentor\Descriptor\MethodDescriptor;
 use phpDocumentor\Descriptor\Tag\ParamDescriptor;
-use phpDocumentor\Descriptor\ValueObjects\IsApplicable;
 use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
 use phpDocumentor\Reflection\DocBlock\Tags\Param;
 use phpDocumentor\Reflection\Php\Argument;
@@ -64,7 +61,7 @@ public function create(object $data): MethodInterface
 
         $this->assembleDocBlock($data->getDocBlock(), $methodDescriptor);
         $this->addArguments($data, $methodDescriptor);
-        $this->addVariadicArgument($data, $methodDescriptor);
+        $this->addVirualVariadicArgument($data, $methodDescriptor);
 
         return $methodDescriptor;
     }
@@ -116,10 +113,12 @@ protected function addArgument(Argument $argument, MethodDescriptor $descriptor)
     }
 
     /**
+     * Add a virtual argument to the method descriptor if the last `@param` tag is variadic.
+     *
      * Checks if there is a variadic argument in the `@param` tags and adds it to the list of Arguments in
      * the Descriptor unless there is already one present.
      */
-    protected function addVariadicArgument(Method $data, MethodDescriptor $methodDescriptor): void
+    protected function addVirualVariadicArgument(Method $data, MethodDescriptor $methodDescriptor): void
     {
         if (! $data->getDocBlock()) {
             return;
@@ -135,21 +134,26 @@ protected function addVariadicArgument(Method $data, MethodDescriptor $methodDes
 
         if (
             ! $lastParamTag->isVariadic()
-            || ! array_key_exists($lastParamTag->getVariableName(), $methodDescriptor->getArguments()->getAll())
+            || array_key_exists($lastParamTag->getVariableName(), $methodDescriptor->getArguments()->getAll())
         ) {
             return;
         }
 
         $types = $lastParamTag->getType();
 
-        $argument = new ArgumentDescriptor();
-        $argument->setName($lastParamTag->getVariableName());
-        $argument->setType($types);
-        $argument->setDescription(new DescriptionDescriptor($lastParamTag->getDescription(), []));
-        $argument->setStartLocation($methodDescriptor->getStartLocation());
-        $argument->setEndLocation($methodDescriptor->getEndLocation());
-        $argument->setVariadic(IsApplicable::true());
+        $argument = new Argument(
+            $lastParamTag->getVariableName(),
+            $types,
+            null,
+            $lastParamTag->isReference(),
+            $lastParamTag->isVariadic(),
+        );
+
+        $argumentDescriptor = $this->argumentAssembler->create(
+            $argument,
+            $methodDescriptor->getTags()->fetch('param', new Collection())->filter(ParamDescriptor::class),
+        );
 
-        $methodDescriptor->getArguments()->set($argument->getName(), $argument);
+        $methodDescriptor->getArguments()->set($argumentDescriptor->getName(), $argumentDescriptor);
     }
 }


Test patch:
diff --git a/tests/unit/phpDocumentor/Descriptor/Builder/Reflector/MethodAssemblerTest.php b/tests/unit/phpDocumentor/Descriptor/Builder/Reflector/MethodAssemblerTest.php
index 91a89d671..28967e089 100644
--- a/tests/unit/phpDocumentor/Descriptor/Builder/Reflector/MethodAssemblerTest.php
+++ b/tests/unit/phpDocumentor/Descriptor/Builder/Reflector/MethodAssemblerTest.php
@@ -49,8 +49,8 @@ protected function setUp(): void
         $this->builderMock->buildDescriptor(ProphecyArgument::any(), ProphecyArgument::any())->willReturn(null);
 
         $this->argumentAssemblerMock = $this->prophesize(ArgumentAssembler::class);
-        $this->argumentAssemblerMock->getBuilder()->shouldBeCalledOnce()->willReturn(null);
-        $this->argumentAssemblerMock->setBuilder(ProphecyArgument::any())->shouldBeCalledOnce();
+        $this->argumentAssemblerMock->getBuilder()->willReturn(null);
+        $this->argumentAssemblerMock->setBuilder(ProphecyArgument::any())->hasReturnVoid();
 
         $this->fixture = new MethodAssembler($this->argumentAssemblerMock->reveal());
         $this->fixture->setBuilder($this->builderMock->reveal());
@@ -62,7 +62,7 @@ protected function setUp(): void
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::mapReflectorToDescriptor
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addArguments
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addArgument
-     * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addVariadicArgument
+     * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addVirualVariadicArgument
      */
     public function testCreateMethodDescriptorFromReflector(): void
     {
@@ -76,8 +76,8 @@ public function testCreateMethodDescriptorFromReflector(): void
         $column = 25;
         $endLine = 264;
         $endColumn = 9950;
-        $startLocation = $this->givenALocation(10, 25);
-        $endLocation = $this->givenALocation(264, 9950);
+        $startLocation = $this->givenALocation($line, $column);
+        $endLocation = $this->givenALocation($endLine, $endColumn);
         $methodReflectorMock = $this->givenAMethodReflector(
             $namespace,
             $methodName,
@@ -122,7 +122,7 @@ public function testCreateMethodDescriptorFromReflector(): void
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::mapReflectorToDescriptor
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addArguments
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addArgument
-     * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addVariadicArgument
+     * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addVirualVariadicArgument
      */
     public function testCreateMethodDescriptorFromReflectorWhenDocblockIsAbsent(): void
     {
@@ -160,7 +160,7 @@ public function testCreateMethodDescriptorFromReflectorWhenDocblockIsAbsent(): v
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::mapReflectorToDescriptor
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addArguments
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addArgument
-     * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addVariadicArgument
+     * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addVirualVariadicArgument
      */
     public function testCreateMethodDescriptorFromReflectorWhenParamTagsAreAbsent(): void
     {
@@ -199,7 +199,7 @@ public function testCreateMethodDescriptorFromReflectorWhenParamTagsAreAbsent():
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::mapReflectorToDescriptor
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addArguments
      * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addArgument
-     * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addVariadicArgument
+     * @covers \phpDocumentor\Descriptor\Builder\Reflector\MethodAssembler::addVirualVariadicArgument
      */
     public function testCreateMethodDescriptorFromReflectorWithReturnByReference(): void
     {
@@ -232,13 +232,43 @@ public function testCreateMethodDescriptorFromReflectorWithReturnByReference():
         $this->assertTrue($descriptor->getHasReturnByReference());
     }
 
+    public function testMethodWillContainArgumentCreatedFromVaridictParamInDocblock(): void
+    {
+        // Arrange
+        $namespace = 'Namespace';
+        $methodName = 'goodbyeWorld';
+        $argumentName = 'variableName';
+
+        $methodReflectorMock = $this->givenAMethodReflector(
+            $namespace,
+            $methodName,
+            null,
+            $this->givenADocBlockObject(true),
+            false,
+        );
+
+        $argumentDescriptor = new ArgumentDescriptor();
+        $argumentDescriptor->setName($argumentName);
+
+        $this->argumentAssemblerMock
+            ->create(ProphecyArgument::any(), ProphecyArgument::any())
+            ->shouldBeCalled()
+            ->willReturn($argumentDescriptor);
+
+        // Act
+        $descriptor = $this->fixture->create($methodReflectorMock);
+
+        // Assert
+        $this->assertArrayHasKey($argumentName, $descriptor->getArguments()->getAll());
+    }
+
     /**
      * Creates a sample method reflector for the tests with the given data.
      */
     protected function givenAMethodReflector(
         string $namespace,
         string $methodName,
-        Argument $argumentMock,
+        Argument|null $argumentMock,
         DocBlock|null $docBlockMock = null,
         bool $hasReturnByReference = false,
         Location|null $location = null,
@@ -257,7 +287,9 @@ protected function givenAMethodReflector(
             $hasReturnByReference,
         );
 
-        $method->addArgument($argumentMock);
+        if ($argumentMock !== null) {
+            $method->addArgument($argumentMock);
+        }
 
         return $method;
     }