Skip to content

Feature/method params #304

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

Closed
wants to merge 2 commits into from
Closed
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
@@ -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
@@ -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<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
);
}

/**
@@ -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}>
*/
@@ -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
);
}
}
50 changes: 14 additions & 36 deletions src/DocBlock/Tags/Param.php
Original file line number Diff line number Diff line change
@@ -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,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);
}
@@ -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
@@ -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)',
],
];
}
}
11 changes: 11 additions & 0 deletions tests/unit/DocBlock/Tags/ParamTest.php
Original file line number Diff line number Diff line change
@@ -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());
}
}