Skip to content

Commit

Permalink
Bugfix: resolve issue with multiline descriptions
Browse files Browse the repository at this point in the history
The phpstan parser is not consuming the full description when parsing
docblocks with a more complex description. For them it's mostlikely not an
issue as phpstan doesn't use the descriptions. But it will also parse
the descriptions into unexpected tags. This could be an advantage but is
not according to the phpdoc spec.

Our own tokenizer is already tokenizing the docblocks into the correct parts.
So all we needed to do is assume all remaining tokens in the phpstan ast belong
to the description. From there our own code is able to handle this as before in
v5.3.

fixes #365
  • Loading branch information
jaapio committed May 8, 2024
1 parent 2f7b34c commit 1aa98a9
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 11 deletions.
7 changes: 5 additions & 2 deletions src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ public function __construct(PHPStanFactory ...$factories)

public function create(string $tagLine, ?TypeContext $context = null): Tag
{
$tokens = $this->lexer->tokenize($tagLine);
$ast = $this->parser->parseTag(new TokenIterator($tokens));
$tokens = new TokenIterator($this->lexer->tokenize($tagLine));
$ast = $this->parser->parseTag($tokens);
if (property_exists($ast->value, 'description') === true) {
$ast->value->setAttribute('description', $ast->value->description . $tokens->joinUntil(Lexer::TOKEN_END));
}

if ($context === null) {
$context = new TypeContext('');
Expand Down
2 changes: 1 addition & 1 deletion src/DocBlock/Tags/Factory/ParamFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function create(PhpDocTagNode $node, Context $context): Tag
trim($tagValue->parameterName, '$'),
$this->typeResolver->createType($tagValue->type ?? new IdentifierTypeNode('mixed'), $context),
$tagValue->isVariadic,
$this->descriptionFactory->create($tagValue->description, $context),
$this->descriptionFactory->create($tagValue->getAttribute('description'), $context),
$tagValue->isReference
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/DocBlock/Tags/Factory/PropertyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function create(PhpDocTagNode $node, Context $context): Tag
return new Property(
trim($tagValue->propertyName, '$'),
$this->typeResolver->createType($tagValue->type, $context),
$this->descriptionFactory->create($tagValue->description, $context)
$this->descriptionFactory->create($tagValue->getAttribute('description'), $context)
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/DocBlock/Tags/Factory/PropertyReadFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function create(PhpDocTagNode $node, Context $context): Tag
return new PropertyRead(
trim($tagValue->propertyName, '$'),
$this->typeResolver->createType($tagValue->type, $context),
$this->descriptionFactory->create($tagValue->description, $context)
$this->descriptionFactory->create($tagValue->getAttribute('description'), $context)
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/DocBlock/Tags/Factory/PropertyWriteFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function create(PhpDocTagNode $node, Context $context): Tag
return new PropertyWrite(
trim($tagValue->propertyName, '$'),
$this->typeResolver->createType($tagValue->type, $context),
$this->descriptionFactory->create($tagValue->description, $context)
$this->descriptionFactory->create($tagValue->getAttribute('description'), $context)
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/DocBlock/Tags/Factory/ReturnFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function create(PhpDocTagNode $node, Context $context): Tag

return new Return_(
$this->typeResolver->createType($tagValue->type, $context),
$this->descriptionFactory->create($tagValue->description, $context)
$this->descriptionFactory->create($tagValue->getAttribute('description'), $context)
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/DocBlock/Tags/Factory/VarFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function create(PhpDocTagNode $node, Context $context): Tag
return new Var_(
trim($tagValue->variableName, '$'),
$this->typeResolver->createType($tagValue->type, $context),
$this->descriptionFactory->create($tagValue->description, $context)
$this->descriptionFactory->create($tagValue->getAttribute('description'), $context)
);
}

Expand Down
100 changes: 98 additions & 2 deletions tests/integration/InterpretingDocBlocksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@
use phpDocumentor\Reflection\DocBlock\Description;
use phpDocumentor\Reflection\DocBlock\StandardTagFactory;
use phpDocumentor\Reflection\DocBlock\Tag;
use phpDocumentor\Reflection\DocBlock\Tags\Generic;
use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
use phpDocumentor\Reflection\DocBlock\Tags\Method;
use phpDocumentor\Reflection\DocBlock\Tags\MethodParameter;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use phpDocumentor\Reflection\DocBlock\Tags\See;
use phpDocumentor\Reflection\DocBlock\Tags\Since;
use phpDocumentor\Reflection\PseudoTypes\ConstExpression;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Mixed_;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\Self_;
use phpDocumentor\Reflection\Types\String_;
use phpDocumentor\Reflection\Types\Void_;
Expand Down Expand Up @@ -123,8 +128,8 @@ public function testInterpretingTags(): void
$this->assertInstanceOf(See::class, $seeTags[0]);

$seeTag = $seeTags[0];
$this->assertSame('\\' . StandardTagFactory::class, (string) $seeTag->getReference());
$this->assertSame('', (string) $seeTag->getDescription());
$this->assertSame('\\' . StandardTagFactory::class, (string)$seeTag->getReference());
$this->assertSame('', (string)$seeTag->getDescription());
}

public function testDescriptionsCanEscapeAtSignsAndClosingBraces(): void
Expand Down Expand Up @@ -268,4 +273,95 @@ public function testConstantReferenceTypes(): void
$docblock->getTags()
);
}

public function testRegressionWordpressDocblocks(): void
{
$docCommment = <<<DOC
/**
* Install a package.
*
* Copies the contents of a package from a source directory, and installs them in
* a destination directory. Optionally removes the source. It can also optionally
* clear out the destination folder if it already exists.
*
* @since 2.8.0
* @since 6.2.0 Use move_dir() instead of copy_dir() when possible.
*
* @global WP_Filesystem_Base \$wp_filesystem WordPress filesystem subclass.
* @global array \$wp_theme_directories
*
* @param array|string \$args {
* Optional. Array or string of arguments for installing a package. Default empty array.
*
* @type string \$source Required path to the package source. Default empty.
* @type string \$destination Required path to a folder to install the package in.
* Default empty.
* @type bool \$clear_destination Whether to delete any files already in the destination
* folder. Default false.
* @type bool \$clear_working Whether to delete the files from the working directory
* after copying them to the destination. Default false.
* @type bool \$abort_if_destination_exists Whether to abort the installation if
* the destination folder already exists. Default true.
* @type array \$hook_extra Extra arguments to pass to the filter hooks called by
* WP_Upgrader::install_package(). Default empty array.
* }
*
* @return array|WP_Error The result (also stored in `WP_Upgrader::\$result`), or a WP_Error on failure.
*/
DOC;

$factory = DocBlockFactory::createInstance();
$docblock = $factory->create($docCommment);

self::assertEquals(
new DocBlock(
'Install a package.',
new Description(
'Copies the contents of a package from a source directory, and installs them in' . PHP_EOL .
'a destination directory. Optionally removes the source. It can also optionally' . PHP_EOL .
'clear out the destination folder if it already exists.'
),
[
new Since('2.8.0', new Description('')),
new Since('6.2.0', new Description('Use move_dir() instead of copy_dir() when possible.')),
new Generic(
'global',
new Description('WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.')
),
new Generic(
'global',
new Description('array $wp_theme_directories')
),
new Param(
'args',
new Compound([new Array_(new Mixed_()), new String_()]),
false,
new Description(
'{' . "\n" .
'Optional. Array or string of arguments for installing a package. Default empty array.' . "\n" .
"\n" .
' @type string $source Required path to the package source. Default empty.' . "\n" .
' @type string $destination Required path to a folder to install the package in.' . "\n" .
' Default empty.' . "\n" .
' @type bool $clear_destination Whether to delete any files already in the destination' . "\n" .
' folder. Default false.' . "\n" .
' @type bool $clear_working Whether to delete the files from the working directory' . "\n" .
' after copying them to the destination. Default false.' . "\n" .
' @type bool $abort_if_destination_exists Whether to abort the installation if' . "\n" .
' the destination folder already exists. Default true.' . "\n" .
' @type array $hook_extra Extra arguments to pass to the filter hooks called by' . "\n" .
' WP_Upgrader::install_package(). Default empty array.' . "\n" .
'}'
)
),
new Return_(
new Compound([new Array_(new Mixed_()), new Object_(new Fqsen('\WP_Error'))]),
new Description('The result (also stored in `WP_Upgrader::$result`), or a WP_Error on failure.')
),
],
new Context('\\')
),
$docblock
);
}
}
7 changes: 6 additions & 1 deletion tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ public function parseTag(string $tag): PhpDocTagNode
$tokens = $lexer->tokenize($tag);
$constParser = new ConstExprParser();

return (new PhpDocParser(new TypeParser($constParser), $constParser))->parseTag(new TokenIterator($tokens));
$tagNode = (new PhpDocParser(new TypeParser($constParser), $constParser))->parseTag(new TokenIterator($tokens));
if (property_exists($tagNode->value, 'description') === true) {
$tagNode->value->setAttribute('description', $tagNode->value->description);
}

return $tagNode;
}

public function giveTypeResolver(): TypeResolver
Expand Down

0 comments on commit 1aa98a9

Please sign in to comment.