diff --git a/src/DocBlock/Tags/Method.php b/src/DocBlock/Tags/Method.php index f08bfffd..a7db4122 100644 --- a/src/DocBlock/Tags/Method.php +++ b/src/DocBlock/Tags/Method.php @@ -57,6 +57,9 @@ final class Method extends BaseTag implements Factory\StaticMethod /** @var Type */ private $returnType; + /** @var Param[] */ + private $parameters; + /** * @param array> $arguments * @phpstan-param array $arguments @@ -66,7 +69,8 @@ public function __construct( array $arguments = [], ?Type $returnType = null, bool $static = false, - ?Description $description = null + ?Description $description = null, + array $parameters = [] ) { Assert::stringNotEmpty($methodName); @@ -79,6 +83,7 @@ public function __construct( $this->returnType = $returnType; $this->isStatic = $static; $this->description = $description; + $this->parameters = $this->fromLegacyArguments($parameters, $this->arguments); } public static function create( @@ -150,29 +155,29 @@ public static function create( $returnType = $typeResolver->resolve($returnType, $context); $description = $descriptionFactory->create($description, $context); - /** @phpstan-var array $arguments */ - $arguments = []; + $parameters = []; if ($argumentLines !== '') { $argumentsExploded = explode(',', $argumentLines); foreach ($argumentsExploded as $argument) { - $argument = explode(' ', self::stripRestArg(trim($argument)), 2); - if (strpos($argument[0], '$') === 0) { - $argumentName = substr($argument[0], 1); - $argumentType = new Mixed_(); - } else { - $argumentType = $typeResolver->resolve($argument[0], $context); - $argumentName = ''; - if (isset($argument[1])) { - $argument[1] = self::stripRestArg($argument[1]); - $argumentName = substr($argument[1], 1); - } - } - - $arguments[] = ['name' => $argumentName, 'type' => $argumentType]; + $parameters[] = Param::create(trim($argument), $typeResolver, $descriptionFactory, $context); } } - return new static($methodName, $arguments, $returnType, $static, $description); + return new static($methodName, self::toLegacyArguments($parameters), $returnType, $static, $description, $parameters); + } + + /** @return array */ + private static function toLegacyArguments(array $parameters): array + { + return array_map( + static function (Param $param): array { + return [ + 'name' => $param->getVariableName(), + 'type' => $param->getType() + ]; + }, + $parameters + ); } /** @@ -184,6 +189,8 @@ public function getMethodName(): string } /** + * @deprecated arguments are a limited way to express method arguments, use {@see self::getParameters} to have full + * featured method parameters. This method will be removed in v6.0 * @return array> * @phpstan-return array */ @@ -192,6 +199,12 @@ public function getArguments(): array return $this->arguments; } + /** @return Param[] */ + public function getParameters(): array + { + return $this->parameters; + } + /** * Checks whether the method tag describes a static method or not. * @@ -210,8 +223,11 @@ public function getReturnType(): Type public function __toString(): string { $arguments = []; - foreach ($this->arguments as $argument) { - $arguments[] = $argument['type'] . ' $' . $argument['name']; + foreach ($this->parameters as $parameter) { + $arguments[] = ($parameter->getType() ?? new Mixed_()) . ' ' . + ($parameter->isReference() ? '&' : '') . + ($parameter->isVariadic() ? '...' : '') . + '$' . $parameter->getVariableName(); } $argumentStr = '(' . implode(', ', $arguments) . ')'; @@ -276,4 +292,18 @@ private static function stripRestArg(string $argument): string return $argument; } + + private function fromLegacyArguments(array $parameters, array $arguments) : array + { + if (!empty($parameters)) { + return $parameters; + } + + return array_map( + static function ($argument) { + return new Param($argument['name'], $argument['type']); + }, + $arguments + ); + } } diff --git a/src/DocBlock/Tags/Param.php b/src/DocBlock/Tags/Param.php index 3399649b..5ba611aa 100644 --- a/src/DocBlock/Tags/Param.php +++ b/src/DocBlock/Tags/Param.php @@ -34,6 +34,8 @@ */ final class Param extends TagWithType implements Factory\StaticMethod { + private const VARIABLE_PATTERN = '^\s*((?>&\s*)?(?>\.{3}\s*)?\$[^\s]+)'; + /** @var string|null */ private $variableName; @@ -63,15 +65,14 @@ public static function create( ?TypeResolver $typeResolver = null, ?DescriptionFactory $descriptionFactory = null, ?TypeContext $context = null - ): self { + ): ?self { Assert::stringNotEmpty($body); Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); - [$firstPart, $body] = self::extractTypeFromBody($body); + [$firstPart, $bodyWithoutType] = self::extractTypeFromBody($body); $type = null; - $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); $variableName = ''; $isVariadic = false; $isReference = false; @@ -79,36 +80,19 @@ public static function create( // if the first item that is encountered is not a variable; it is a type if ($firstPart && !self::strStartsWithVariable($firstPart)) { $type = $typeResolver->resolve($firstPart, $context); - } else { - // first part is not a type; we should prepend it to the parts array for further processing - array_unshift($parts, $firstPart); + $body = $bodyWithoutType; } - // if the next item starts with a $ or ...$ or &$ or &...$ it must be the variable name - if (isset($parts[0]) && self::strStartsWithVariable($parts[0])) { - $variableName = array_shift($parts); - if ($type) { - array_shift($parts); - } - - Assert::notNull($variableName); - - if (strpos($variableName, '$') === 0) { - $variableName = substr($variableName, 1); - } elseif (strpos($variableName, '&$') === 0) { - $isReference = true; - $variableName = substr($variableName, 2); - } elseif (strpos($variableName, '...$') === 0) { - $isVariadic = true; - $variableName = substr($variableName, 4); - } elseif (strpos($variableName, '&...$') === 0) { - $isVariadic = true; - $isReference = true; - $variableName = substr($variableName, 5); - } + $parts = []; + preg_match('/'. self::VARIABLE_PATTERN . '?(.*)$/Su', $body, $parts); + $var = $parts[1] ?? ''; + if ($var !== '') { + $variableName = substr($var, strpos($var, '$') + 1); + $isReference = strpos($var, '&') !== false; + $isVariadic = strpos($var, '...') !== false; } - $description = $descriptionFactory->create(implode('', $parts), $context); + $description = $descriptionFactory->create(trim($parts[2]), $context); return new static($variableName, $type, $isVariadic, $description, $isReference); } @@ -163,12 +147,6 @@ public function __toString(): string private static function strStartsWithVariable(string $str): bool { - return strpos($str, '$') === 0 - || - strpos($str, '...$') === 0 - || - strpos($str, '&$') === 0 - || - strpos($str, '&...$') === 0; + return preg_match('/' . self::VARIABLE_PATTERN . '/Su', $str) === 1; } } diff --git a/tests/unit/DocBlock/Tags/MethodTest.php b/tests/unit/DocBlock/Tags/MethodTest.php index 1f068dbb..99c6ff50 100644 --- a/tests/unit/DocBlock/Tags/MethodTest.php +++ b/tests/unit/DocBlock/Tags/MethodTest.php @@ -324,6 +324,10 @@ public function testFactoryMethod(): void ->with('My Description', $context) ->andReturn($description); + $descriptionFactory->shouldReceive('create') + ->with('', $context) + ->andReturn(new Description('')); + $fixture = Method::create( 'static void myMethod(string $argument1, $argument2) My Description', $resolver, @@ -656,4 +660,92 @@ public function testCreateWithMixedReturnTypes(): void $fixture->getReturnType() ); } + + /** @dataProvider parameterNotationProvider */ + public function testMethodWithParameters(string $body, array $parameters, array $arguments, string $expectedBody): void + { + $descriptionFactory = m::mock(DescriptionFactory::class); + $resolver = new TypeResolver(); + $context = new Context(''); + + $descriptionFactory->shouldReceive('create')->andReturn(new Description('')); + + $fixture = Method::create( + $body, + $resolver, + $descriptionFactory, + $context + ); + + self::assertSame($expectedBody, (string) $fixture); + self::assertEquals($arguments, $fixture->getArguments()); + self::assertEquals($parameters, $fixture->getParameters()); + + } + + public function parameterNotationProvider(): array + { + return [ + 'no parameters' => [ + 'int myMethod()', + [], + [], + 'int myMethod()' + ], + 'simple arguments' => [ + 'int myMethod($arg1, $arg2)', + [ + new Param('arg1', null, false, new Description(''), false), + new Param('arg2', null, false, new Description(''), false) + ], + [ + [ + 'name' => 'arg1', + 'type' => new Mixed_() + ], + [ + 'name' => 'arg2', + 'type' => new Mixed_() + ], + ], + 'int myMethod(mixed $arg1, mixed $arg2)', + ], + 'with by reference argument' => [ + 'int myMethod($arg1, &$arg2)', + [ + new Param('arg1', null, false, new Description(''), false), + new Param('arg2', null, false, new Description(''), true) + ], + [ + [ + 'name' => 'arg1', + 'type' => new Mixed_() + ], + [ + 'name' => 'arg2', + 'type' => new Mixed_() + ], + ], + 'int myMethod(mixed $arg1, mixed &$arg2)', + ], + 'with variadic argument' => [ + 'int myMethod($arg1, string & ... $arg2)', + [ + new Param('arg1', null, false, new Description(''), false), + new Param('arg2', new String_(), true, new Description(''), true) + ], + [ + [ + 'name' => 'arg1', + 'type' => new Mixed_() + ], + [ + 'name' => 'arg2', + 'type' => new String_() + ], + ], + 'int myMethod(mixed $arg1, string &...$arg2)', + ], + ]; + } } diff --git a/tests/unit/DocBlock/Tags/ParamTest.php b/tests/unit/DocBlock/Tags/ParamTest.php index e677140e..6eede093 100644 --- a/tests/unit/DocBlock/Tags/ParamTest.php +++ b/tests/unit/DocBlock/Tags/ParamTest.php @@ -428,4 +428,15 @@ public function testFactoryMethodFailsIfDescriptionFactoryIsNull(): void $this->expectException('InvalidArgumentException'); Param::create('body', new TypeResolver()); } + + public function testSpacedNotations(): void + { + $descriptionFactory = m::mock(DescriptionFactory::class); + $descriptionFactory->shouldReceive('create')->andReturn(new Description('Description')); + $param = Param::create('array & ... $var description', new TypeResolver(), $descriptionFactory); + + self::assertSame('var', $param->getVariableName()); + self::assertTrue($param->isVariadic()); + self::assertTrue($param->isReference()); + } }