Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/method params #304

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 50 additions & 20 deletions src/DocBlock/Tags/Method.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ final class Method extends BaseTag implements Factory\StaticMethod
/** @var Type */
private $returnType;

/** @var Param[] */
private $parameters;

/**
* @param array<int, array<string, Type|string>> $arguments
* @phpstan-param array<int, array{name: string, type: Type}|string> $arguments
Expand All @@ -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);

Expand All @@ -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(
Expand Down Expand Up @@ -150,29 +155,29 @@ public static function create(
$returnType = $typeResolver->resolve($returnType, $context);
$description = $descriptionFactory->create($description, $context);

/** @phpstan-var array<int, array{name: string, type: Type}> $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<int, array{name: string, type: Type}> */
private static function toLegacyArguments(array $parameters): array
{
return array_map(
static function (Param $param): array {
return [
'name' => $param->getVariableName(),
'type' => $param->getType()
];
},
$parameters
);
}

/**
Expand All @@ -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<int, array<string, Type|string>>
* @phpstan-return array<int, array{name: string, type: Type}>
*/
Expand All @@ -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.
*
Expand All @@ -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) . ')';
Expand Down Expand Up @@ -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
);
}
}
50 changes: 14 additions & 36 deletions src/DocBlock/Tags/Param.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -63,52 +65,34 @@ 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;

// 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);
}
Expand Down Expand Up @@ -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;
}
}
92 changes: 92 additions & 0 deletions tests/unit/DocBlock/Tags/MethodTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)',
],
];
}
}
11 changes: 11 additions & 0 deletions tests/unit/DocBlock/Tags/ParamTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}