Skip to content

Commit 88a07d2

Browse files
authored
Merge pull request #367 from phpDocumentor/fix/description-regression
Bugfix: resolve issue with multiline descriptions
2 parents 2f7b34c + 518be13 commit 88a07d2

File tree

10 files changed

+157
-11
lines changed

10 files changed

+157
-11
lines changed

psalm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<NoInterfaceProperties>
4545
<errorLevel type="info">
4646
<file name="src/DocBlock/Tags/Factory/ParamFactory.php"/>
47+
<file name="src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php"/>
4748
</errorLevel>
4849
</NoInterfaceProperties>
4950

src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
use PHPStan\PhpDocParser\Parser\TypeParser;
2424
use RuntimeException;
2525

26+
use function property_exists;
27+
2628
/**
2729
* Factory class creating tags using phpstan's parser
2830
*
@@ -48,8 +50,11 @@ public function __construct(PHPStanFactory ...$factories)
4850

4951
public function create(string $tagLine, ?TypeContext $context = null): Tag
5052
{
51-
$tokens = $this->lexer->tokenize($tagLine);
52-
$ast = $this->parser->parseTag(new TokenIterator($tokens));
53+
$tokens = new TokenIterator($this->lexer->tokenize($tagLine));
54+
$ast = $this->parser->parseTag($tokens);
55+
if (property_exists($ast->value, 'description') === true) {
56+
$ast->value->setAttribute('description', $ast->value->description . $tokens->joinUntil(Lexer::TOKEN_END));
57+
}
5358

5459
if ($context === null) {
5560
$context = new TypeContext('');

src/DocBlock/Tags/Factory/ParamFactory.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
2020
use Webmozart\Assert\Assert;
2121

22+
use function is_string;
2223
use function sprintf;
2324
use function trim;
2425

@@ -68,11 +69,16 @@ public function create(PhpDocTagNode $node, Context $context): Tag
6869
);
6970
}
7071

72+
$description = $tagValue->getAttribute('description');
73+
if (is_string($description) === false) {
74+
$description = $tagValue->description;
75+
}
76+
7177
return new Param(
7278
trim($tagValue->parameterName, '$'),
7379
$this->typeResolver->createType($tagValue->type ?? new IdentifierTypeNode('mixed'), $context),
7480
$tagValue->isVariadic,
75-
$this->descriptionFactory->create($tagValue->description, $context),
81+
$this->descriptionFactory->create($description, $context),
7682
$tagValue->isReference
7783
);
7884
}

src/DocBlock/Tags/Factory/PropertyFactory.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
1414
use Webmozart\Assert\Assert;
1515

16+
use function is_string;
1617
use function trim;
1718

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

38+
$description = $tagValue->getAttribute('description');
39+
if (is_string($description) === false) {
40+
$description = $tagValue->description;
41+
}
42+
3743
return new Property(
3844
trim($tagValue->propertyName, '$'),
3945
$this->typeResolver->createType($tagValue->type, $context),
40-
$this->descriptionFactory->create($tagValue->description, $context)
46+
$this->descriptionFactory->create($description, $context)
4147
);
4248
}
4349

src/DocBlock/Tags/Factory/PropertyReadFactory.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
1414
use Webmozart\Assert\Assert;
1515

16+
use function is_string;
1617
use function trim;
1718

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

38+
$description = $tagValue->getAttribute('description');
39+
if (is_string($description) === false) {
40+
$description = $tagValue->description;
41+
}
42+
3743
return new PropertyRead(
3844
trim($tagValue->propertyName, '$'),
3945
$this->typeResolver->createType($tagValue->type, $context),
40-
$this->descriptionFactory->create($tagValue->description, $context)
46+
$this->descriptionFactory->create($description, $context)
4147
);
4248
}
4349

src/DocBlock/Tags/Factory/PropertyWriteFactory.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
1414
use Webmozart\Assert\Assert;
1515

16+
use function is_string;
1617
use function trim;
1718

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

38+
$description = $tagValue->getAttribute('description');
39+
if (is_string($description) === false) {
40+
$description = $tagValue->description;
41+
}
42+
3743
return new PropertyWrite(
3844
trim($tagValue->propertyName, '$'),
3945
$this->typeResolver->createType($tagValue->type, $context),
40-
$this->descriptionFactory->create($tagValue->description, $context)
46+
$this->descriptionFactory->create($description, $context)
4147
);
4248
}
4349

src/DocBlock/Tags/Factory/ReturnFactory.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
1414
use Webmozart\Assert\Assert;
1515

16+
use function is_string;
17+
1618
/**
1719
* @internal This class is not part of the BC promise of this library.
1820
*/
@@ -32,9 +34,14 @@ public function create(PhpDocTagNode $node, Context $context): Tag
3234
$tagValue = $node->value;
3335
Assert::isInstanceOf($tagValue, ReturnTagValueNode::class);
3436

37+
$description = $tagValue->getAttribute('description');
38+
if (is_string($description) === false) {
39+
$description = $tagValue->description;
40+
}
41+
3542
return new Return_(
3643
$this->typeResolver->createType($tagValue->type, $context),
37-
$this->descriptionFactory->create($tagValue->description, $context)
44+
$this->descriptionFactory->create($description, $context)
3845
);
3946
}
4047

src/DocBlock/Tags/Factory/VarFactory.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
1414
use Webmozart\Assert\Assert;
1515

16+
use function is_string;
1617
use function trim;
1718

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

38+
$description = $tagValue->getAttribute('description');
39+
if (is_string($description) === false) {
40+
$description = $tagValue->description;
41+
}
42+
3743
return new Var_(
3844
trim($tagValue->variableName, '$'),
3945
$this->typeResolver->createType($tagValue->type, $context),
40-
$this->descriptionFactory->create($tagValue->description, $context)
46+
$this->descriptionFactory->create($description, $context)
4147
);
4248
}
4349

tests/integration/InterpretingDocBlocksTest.php

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@
1717
use phpDocumentor\Reflection\DocBlock\Description;
1818
use phpDocumentor\Reflection\DocBlock\StandardTagFactory;
1919
use phpDocumentor\Reflection\DocBlock\Tag;
20+
use phpDocumentor\Reflection\DocBlock\Tags\Generic;
2021
use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
2122
use phpDocumentor\Reflection\DocBlock\Tags\Method;
2223
use phpDocumentor\Reflection\DocBlock\Tags\MethodParameter;
2324
use phpDocumentor\Reflection\DocBlock\Tags\Param;
2425
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
2526
use phpDocumentor\Reflection\DocBlock\Tags\See;
27+
use phpDocumentor\Reflection\DocBlock\Tags\Since;
2628
use phpDocumentor\Reflection\PseudoTypes\ConstExpression;
2729
use phpDocumentor\Reflection\Types\Array_;
30+
use phpDocumentor\Reflection\Types\Compound;
31+
use phpDocumentor\Reflection\Types\Context;
2832
use phpDocumentor\Reflection\Types\Integer;
2933
use phpDocumentor\Reflection\Types\Mixed_;
34+
use phpDocumentor\Reflection\Types\Object_;
3035
use phpDocumentor\Reflection\Types\Self_;
3136
use phpDocumentor\Reflection\Types\String_;
3237
use phpDocumentor\Reflection\Types\Void_;
@@ -123,8 +128,8 @@ public function testInterpretingTags(): void
123128
$this->assertInstanceOf(See::class, $seeTags[0]);
124129

125130
$seeTag = $seeTags[0];
126-
$this->assertSame('\\' . StandardTagFactory::class, (string) $seeTag->getReference());
127-
$this->assertSame('', (string) $seeTag->getDescription());
131+
$this->assertSame('\\' . StandardTagFactory::class, (string)$seeTag->getReference());
132+
$this->assertSame('', (string)$seeTag->getDescription());
128133
}
129134

130135
public function testDescriptionsCanEscapeAtSignsAndClosingBraces(): void
@@ -268,4 +273,95 @@ public function testConstantReferenceTypes(): void
268273
$docblock->getTags()
269274
);
270275
}
276+
277+
public function testRegressionWordpressDocblocks(): void
278+
{
279+
$docCommment = <<<DOC
280+
/**
281+
* Install a package.
282+
*
283+
* Copies the contents of a package from a source directory, and installs them in
284+
* a destination directory. Optionally removes the source. It can also optionally
285+
* clear out the destination folder if it already exists.
286+
*
287+
* @since 2.8.0
288+
* @since 6.2.0 Use move_dir() instead of copy_dir() when possible.
289+
*
290+
* @global WP_Filesystem_Base \$wp_filesystem WordPress filesystem subclass.
291+
* @global array \$wp_theme_directories
292+
*
293+
* @param array|string \$args {
294+
* Optional. Array or string of arguments for installing a package. Default empty array.
295+
*
296+
* @type string \$source Required path to the package source. Default empty.
297+
* @type string \$destination Required path to a folder to install the package in.
298+
* Default empty.
299+
* @type bool \$clear_destination Whether to delete any files already in the destination
300+
* folder. Default false.
301+
* @type bool \$clear_working Whether to delete the files from the working directory
302+
* after copying them to the destination. Default false.
303+
* @type bool \$abort_if_destination_exists Whether to abort the installation if
304+
* the destination folder already exists. Default true.
305+
* @type array \$hook_extra Extra arguments to pass to the filter hooks called by
306+
* WP_Upgrader::install_package(). Default empty array.
307+
* }
308+
*
309+
* @return array|WP_Error The result (also stored in `WP_Upgrader::\$result`), or a WP_Error on failure.
310+
*/
311+
DOC;
312+
313+
$factory = DocBlockFactory::createInstance();
314+
$docblock = $factory->create($docCommment);
315+
316+
self::assertEquals(
317+
new DocBlock(
318+
'Install a package.',
319+
new Description(
320+
'Copies the contents of a package from a source directory, and installs them in' . PHP_EOL .
321+
'a destination directory. Optionally removes the source. It can also optionally' . PHP_EOL .
322+
'clear out the destination folder if it already exists.'
323+
),
324+
[
325+
new Since('2.8.0', new Description('')),
326+
new Since('6.2.0', new Description('Use move_dir() instead of copy_dir() when possible.')),
327+
new Generic(
328+
'global',
329+
new Description('WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.')
330+
),
331+
new Generic(
332+
'global',
333+
new Description('array $wp_theme_directories')
334+
),
335+
new Param(
336+
'args',
337+
new Compound([new Array_(new Mixed_()), new String_()]),
338+
false,
339+
new Description(
340+
'{' . "\n" .
341+
'Optional. Array or string of arguments for installing a package. Default empty array.' . "\n" .
342+
"\n" .
343+
' @type string $source Required path to the package source. Default empty.' . "\n" .
344+
' @type string $destination Required path to a folder to install the package in.' . "\n" .
345+
' Default empty.' . "\n" .
346+
' @type bool $clear_destination Whether to delete any files already in the destination' . "\n" .
347+
' folder. Default false.' . "\n" .
348+
' @type bool $clear_working Whether to delete the files from the working directory' . "\n" .
349+
' after copying them to the destination. Default false.' . "\n" .
350+
' @type bool $abort_if_destination_exists Whether to abort the installation if' . "\n" .
351+
' the destination folder already exists. Default true.' . "\n" .
352+
' @type array $hook_extra Extra arguments to pass to the filter hooks called by' . "\n" .
353+
' WP_Upgrader::install_package(). Default empty array.' . "\n" .
354+
'}'
355+
)
356+
),
357+
new Return_(
358+
new Compound([new Array_(new Mixed_()), new Object_(new Fqsen('\WP_Error'))]),
359+
new Description('The result (also stored in `WP_Upgrader::$result`), or a WP_Error on failure.')
360+
),
361+
],
362+
new Context('\\')
363+
),
364+
$docblock
365+
);
366+
}
271367
}

tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
use PHPStan\PhpDocParser\Parser\TypeParser;
2727
use PHPUnit\Framework\TestCase;
2828

29+
use function property_exists;
30+
2931
abstract class TagFactoryTestCase extends TestCase
3032
{
3133
public function parseTag(string $tag): PhpDocTagNode
@@ -34,7 +36,12 @@ public function parseTag(string $tag): PhpDocTagNode
3436
$tokens = $lexer->tokenize($tag);
3537
$constParser = new ConstExprParser();
3638

37-
return (new PhpDocParser(new TypeParser($constParser), $constParser))->parseTag(new TokenIterator($tokens));
39+
$tagNode = (new PhpDocParser(new TypeParser($constParser), $constParser))->parseTag(new TokenIterator($tokens));
40+
if (property_exists($tagNode->value, 'description') === true) {
41+
$tagNode->value->setAttribute('description', $tagNode->value->description);
42+
}
43+
44+
return $tagNode;
3845
}
3946

4047
public function giveTypeResolver(): TypeResolver

0 commit comments

Comments
 (0)