Skip to content

Commit

Permalink
Merge pull request #367 from phpDocumentor/fix/description-regression
Browse files Browse the repository at this point in the history
Bugfix: resolve issue with multiline descriptions
  • Loading branch information
jaapio committed May 8, 2024
2 parents 2f7b34c + 518be13 commit 88a07d2
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 11 deletions.
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<NoInterfaceProperties>
<errorLevel type="info">
<file name="src/DocBlock/Tags/Factory/ParamFactory.php"/>
<file name="src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php"/>
</errorLevel>
</NoInterfaceProperties>

Expand Down
9 changes: 7 additions & 2 deletions src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
use PHPStan\PhpDocParser\Parser\TypeParser;
use RuntimeException;

use function property_exists;

/**
* Factory class creating tags using phpstan's parser
*
Expand All @@ -48,8 +50,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
8 changes: 7 additions & 1 deletion src/DocBlock/Tags/Factory/ParamFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use Webmozart\Assert\Assert;

use function is_string;
use function sprintf;
use function trim;

Expand Down Expand Up @@ -68,11 +69,16 @@ public function create(PhpDocTagNode $node, Context $context): Tag
);
}

$description = $tagValue->getAttribute('description');
if (is_string($description) === false) {
$description = $tagValue->description;
}

return new Param(
trim($tagValue->parameterName, '$'),
$this->typeResolver->createType($tagValue->type ?? new IdentifierTypeNode('mixed'), $context),
$tagValue->isVariadic,
$this->descriptionFactory->create($tagValue->description, $context),
$this->descriptionFactory->create($description, $context),
$tagValue->isReference
);
}
Expand Down
8 changes: 7 additions & 1 deletion src/DocBlock/Tags/Factory/PropertyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use Webmozart\Assert\Assert;

use function is_string;
use function trim;

/**
Expand All @@ -34,10 +35,15 @@ public function create(PhpDocTagNode $node, Context $context): Tag
$tagValue = $node->value;
Assert::isInstanceOf($tagValue, PropertyTagValueNode::class);

$description = $tagValue->getAttribute('description');
if (is_string($description) === false) {
$description = $tagValue->description;
}

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

Expand Down
8 changes: 7 additions & 1 deletion src/DocBlock/Tags/Factory/PropertyReadFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use Webmozart\Assert\Assert;

use function is_string;
use function trim;

/**
Expand All @@ -34,10 +35,15 @@ public function create(PhpDocTagNode $node, Context $context): Tag
$tagValue = $node->value;
Assert::isInstanceOf($tagValue, PropertyTagValueNode::class);

$description = $tagValue->getAttribute('description');
if (is_string($description) === false) {
$description = $tagValue->description;
}

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

Expand Down
8 changes: 7 additions & 1 deletion src/DocBlock/Tags/Factory/PropertyWriteFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use Webmozart\Assert\Assert;

use function is_string;
use function trim;

/**
Expand All @@ -34,10 +35,15 @@ public function create(PhpDocTagNode $node, Context $context): Tag
$tagValue = $node->value;
Assert::isInstanceOf($tagValue, PropertyTagValueNode::class);

$description = $tagValue->getAttribute('description');
if (is_string($description) === false) {
$description = $tagValue->description;
}

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

Expand Down
9 changes: 8 additions & 1 deletion src/DocBlock/Tags/Factory/ReturnFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use Webmozart\Assert\Assert;

use function is_string;

/**
* @internal This class is not part of the BC promise of this library.
*/
Expand All @@ -32,9 +34,14 @@ public function create(PhpDocTagNode $node, Context $context): Tag
$tagValue = $node->value;
Assert::isInstanceOf($tagValue, ReturnTagValueNode::class);

$description = $tagValue->getAttribute('description');
if (is_string($description) === false) {
$description = $tagValue->description;
}

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

Expand Down
8 changes: 7 additions & 1 deletion src/DocBlock/Tags/Factory/VarFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use Webmozart\Assert\Assert;

use function is_string;
use function trim;

/**
Expand All @@ -34,10 +35,15 @@ public function create(PhpDocTagNode $node, Context $context): Tag
$tagValue = $node->value;
Assert::isInstanceOf($tagValue, VarTagValueNode::class);

$description = $tagValue->getAttribute('description');
if (is_string($description) === false) {
$description = $tagValue->description;
}

return new Var_(
trim($tagValue->variableName, '$'),
$this->typeResolver->createType($tagValue->type, $context),
$this->descriptionFactory->create($tagValue->description, $context)
$this->descriptionFactory->create($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
);
}
}
9 changes: 8 additions & 1 deletion tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPUnit\Framework\TestCase;

use function property_exists;

abstract class TagFactoryTestCase extends TestCase
{
public function parseTag(string $tag): PhpDocTagNode
Expand All @@ -34,7 +36,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 88a07d2

Please sign in to comment.