From 3dc999783226de0fe1205fcd16159cb67f22a8a0 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Mon, 27 Sep 2021 22:13:20 +0200 Subject: [PATCH 1/2] Allow whitespace between variable and modifiers The original code didn't allow a whitespace between `&` and `$` or `...` this patch makes that possible. --- src/DocBlock/Tags/Param.php | 50 ++++++++------------------ tests/unit/DocBlock/Tags/ParamTest.php | 11 ++++++ 2 files changed, 25 insertions(+), 36 deletions(-) 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/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()); + } } From 686ed1ed4dfb240e842de17b6c86b28ec9ac9697 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Mon, 27 Sep 2021 22:17:53 +0200 Subject: [PATCH 2/2] Allow more advanced @method notations The original code was limited by the simple notation of `@method` arguments the arguments did not support variadic and by reference notations. Both were just parsed into the argument name. The new getParameters will expose an Param object which includes all information like we have in native php methods. --- src/DocBlock/Tags/Method.php | 70 +++++++++++++------ tests/unit/DocBlock/Tags/MethodTest.php | 92 +++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 20 deletions(-) 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/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)', + ], + ]; + } }