Skip to content

Commit bd80f29

Browse files
committed
[PropertyInfo] Add PropertyDescriptionExtractorInterface to PhpStanExtractor
1 parent 7d6b9ad commit bd80f29

File tree

5 files changed

+167
-16
lines changed

5 files changed

+167
-16
lines changed

src/Symfony/Component/PropertyInfo/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add support for `non-positive-int`, `non-negative-int` and `non-zero-int` PHPStan types to `PhpStanExtractor`
8+
* Add `PropertyDescriptionExtractorInterface` to `PhpStanExtractor`
89

910
7.1
1011
---

src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php

+138-14
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
use phpDocumentor\Reflection\Types\ContextFactory;
1515
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
1616
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
17+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
1718
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
1819
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
20+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
1921
use PHPStan\PhpDocParser\Lexer\Lexer;
2022
use PHPStan\PhpDocParser\Parser\ConstExprParser;
2123
use PHPStan\PhpDocParser\Parser\PhpDocParser;
@@ -24,6 +26,7 @@
2426
use PHPStan\PhpDocParser\ParserConfig;
2527
use Symfony\Component\PropertyInfo\PhpStan\NameScope;
2628
use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory;
29+
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
2730
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
2831
use Symfony\Component\PropertyInfo\Type as LegacyType;
2932
use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
@@ -37,7 +40,7 @@
3740
*
3841
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
3942
*/
40-
final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
43+
final class PhpStanExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
4144
{
4245
private const PROPERTY = 0;
4346
private const ACCESSOR = 1;
@@ -242,6 +245,126 @@ public function getTypeFromConstructor(string $class, string $property): ?Type
242245
return $this->stringTypeResolver->resolve((string) $tagDocNode->type, $typeContext);
243246
}
244247

248+
public function getShortDescription(string $class, string $property, array $context = []): ?string
249+
{
250+
/** @var PhpDocNode|null $docNode */
251+
[$docNode] = $this->getDocBlockFromProperty($class, $property);
252+
if (null === $docNode) {
253+
return null;
254+
}
255+
256+
if ($shortDescription = $this->getDescriptionsFromDocNode($docNode)[0]) {
257+
return $shortDescription;
258+
}
259+
260+
foreach ($docNode->getVarTagValues() as $var) {
261+
if ($var->description) {
262+
return $var->description;
263+
}
264+
}
265+
266+
return null;
267+
}
268+
269+
public function getLongDescription(string $class, string $property, array $context = []): ?string
270+
{
271+
/** @var PhpDocNode|null $docNode */
272+
[$docNode] = $this->getDocBlockFromProperty($class, $property);
273+
if (null === $docNode) {
274+
return null;
275+
}
276+
277+
return $this->getDescriptionsFromDocNode($docNode)[1];
278+
}
279+
280+
/**
281+
* A docblock is splitted into a template marker, a short description, an optional long description and a tags section.
282+
*
283+
* - The template marker is either empty, or #@+ or #@-.
284+
* - The short description is started from a non-tag character, and until one or multiple newlines.
285+
* - The long description (optional), is started from a non-tag character, and until a new line is encountered followed by a tag.
286+
* - Tags, and the remaining characters
287+
*
288+
* This method returns the short and the long descriptions.
289+
*
290+
* @return array{0: ?string, 1: ?string}
291+
*/
292+
private function getDescriptionsFromDocNode(PhpDocNode $docNode): array
293+
{
294+
$isTemplateMarker = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && ('#@+' === $node->text || '#@-' === $node->text);
295+
296+
$shortDescription = '';
297+
$longDescription = '';
298+
$shortDescriptionCompleted = false;
299+
300+
// BC layer for phpstan/phpdoc-parser < 2.0
301+
if (!class_exists(ParserConfig::class)) {
302+
$isNewLine = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && '' === $node->text;
303+
304+
foreach ($docNode->children as $child) {
305+
if (!$child instanceof PhpDocTextNode) {
306+
break;
307+
}
308+
309+
if ($isTemplateMarker($child)) {
310+
continue;
311+
}
312+
313+
if ($isNewLine($child) && !$shortDescriptionCompleted) {
314+
if ($shortDescription) {
315+
$shortDescriptionCompleted = true;
316+
}
317+
318+
continue;
319+
}
320+
321+
if (!$shortDescriptionCompleted) {
322+
$shortDescription = \sprintf("%s\n%s", $shortDescription, $child->text);
323+
324+
continue;
325+
}
326+
327+
$longDescription = \sprintf("%s\n%s", $longDescription, $child->text);
328+
}
329+
} else {
330+
foreach ($docNode->children as $child) {
331+
if (!$child instanceof PhpDocTextNode) {
332+
break;
333+
}
334+
335+
if ($isTemplateMarker($child)) {
336+
continue;
337+
}
338+
339+
foreach (explode("\n", $child->text) as $line) {
340+
if ('' === $line && !$shortDescriptionCompleted) {
341+
if ($shortDescription) {
342+
$shortDescriptionCompleted = true;
343+
}
344+
345+
continue;
346+
}
347+
348+
if (!$shortDescriptionCompleted) {
349+
$shortDescription = \sprintf("%s\n%s", $shortDescription, $line);
350+
351+
continue;
352+
}
353+
354+
$longDescription = \sprintf("%s\n%s", $longDescription, $line);
355+
}
356+
}
357+
}
358+
359+
$shortDescription = trim(preg_replace('/^#@[+-]{1}/m', '', $shortDescription), "\n");
360+
$longDescription = trim($longDescription, "\n");
361+
362+
return [
363+
$shortDescription ?: null,
364+
$longDescription ?: null,
365+
];
366+
}
367+
245368
private function getDocBlockFromConstructor(string $class, string $property): ?ParamTagValueNode
246369
{
247370
try {
@@ -287,7 +410,11 @@ private function getDocBlock(string $class, string $property): array
287410

288411
$ucFirstProperty = ucfirst($property);
289412

290-
if ([$docBlock, $source, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
413+
if ([$docBlock, $constructorDocBlock, $source, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
414+
if (!$docBlock?->getTagsByName('@var') && $constructorDocBlock) {
415+
$docBlock = $constructorDocBlock;
416+
}
417+
291418
$data = [$docBlock, $source, null, $declaringClass];
292419
} elseif ([$docBlock, $_, $declaringClass] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR)) {
293420
$data = [$docBlock, self::ACCESSOR, null, $declaringClass];
@@ -301,7 +428,7 @@ private function getDocBlock(string $class, string $property): array
301428
}
302429

303430
/**
304-
* @return array{PhpDocNode, int, string}|null
431+
* @return array{?PhpDocNode, ?PhpDocNode, int, string}|null
305432
*/
306433
private function getDocBlockFromProperty(string $class, string $property): ?array
307434
{
@@ -324,28 +451,25 @@ private function getDocBlockFromProperty(string $class, string $property): ?arra
324451
}
325452
}
326453

327-
// Type can be inside property docblock as `@var`
328454
$rawDocNode = $reflectionProperty->getDocComment();
329455
$phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null;
330-
$source = self::PROPERTY;
331456

332-
if (!$phpDocNode?->getTagsByName('@var')) {
333-
$phpDocNode = null;
457+
$constructorPhpDocNode = null;
458+
if ($reflectionProperty->isPromoted()) {
459+
$constructorRawDocNode = (new \ReflectionMethod($class, '__construct'))->getDocComment();
460+
$constructorPhpDocNode = $constructorRawDocNode ? $this->getPhpDocNode($constructorRawDocNode) : null;
334461
}
335462

336-
// or in the constructor as `@param` for promoted properties
337-
if (!$phpDocNode && $reflectionProperty->isPromoted()) {
338-
$constructor = new \ReflectionMethod($class, '__construct');
339-
$rawDocNode = $constructor->getDocComment();
340-
$phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null;
463+
$source = self::PROPERTY;
464+
if (!$phpDocNode?->getTagsByName('@var') && $constructorPhpDocNode) {
341465
$source = self::MUTATOR;
342466
}
343467

344-
if (!$phpDocNode) {
468+
if (!$phpDocNode && !$constructorPhpDocNode) {
345469
return null;
346470
}
347471

348-
return [$phpDocNode, $source, $reflectionProperty->class];
472+
return [$phpDocNode, $constructorPhpDocNode, $source, $reflectionProperty->class];
349473
}
350474

351475
/**

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public static function provideLegacyTypes()
136136
null,
137137
null,
138138
],
139-
['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null],
139+
['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], 'A short description ignoring template.', "A long description...\n\n...over several lines."],
140140
['parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null],
141141
['collection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null],
142142
['nestedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false)))], null, null],
@@ -545,7 +545,7 @@ public static function typeProvider(): iterable
545545
yield ['foo4', Type::null(), null, null];
546546
yield ['foo5', Type::mixed(), null, null];
547547
yield ['files', Type::union(Type::list(Type::object(\SplFileInfo::class)), Type::resource()), null, null];
548-
yield ['bal', Type::object(\DateTimeImmutable::class), null, null];
548+
yield ['bal', Type::object(\DateTimeImmutable::class), 'A short description ignoring template.', "A long description...\n\n...over several lines."];
549549
yield ['parent', Type::object(ParentDummy::class), null, null];
550550
yield ['collection', Type::list(Type::object(\DateTimeImmutable::class)), null, null];
551551
yield ['nestedCollection', Type::list(Type::list(Type::string())), null, null];

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php

+18
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,24 @@ public static function genericsProvider(): iterable
10811081
Type::nullable(Type::generic(Type::object(IFace::class), Type::object(Dummy::class))),
10821082
];
10831083
}
1084+
1085+
/**
1086+
* @dataProvider descriptionsProvider
1087+
*/
1088+
public function testGetDescriptions(string $property, ?string $shortDescription, ?string $longDescription)
1089+
{
1090+
$this->assertEquals($shortDescription, $this->extractor->getShortDescription(Dummy::class, $property));
1091+
$this->assertEquals($longDescription, $this->extractor->getLongDescription(Dummy::class, $property));
1092+
}
1093+
1094+
public static function descriptionsProvider(): iterable
1095+
{
1096+
yield ['foo', 'Short description.', 'Long description.'];
1097+
yield ['bar', 'This is bar', null];
1098+
yield ['baz', 'Should be used.', null];
1099+
yield ['bal', 'A short description ignoring template.', "A long description...\n\n...over several lines."];
1100+
yield ['foo2', null, null];
1101+
}
10841102
}
10851103

10861104
class PhpStanOmittedParamTagTypeDocBlock

src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ class Dummy extends ParentDummy
3131
protected $baz;
3232

3333
/**
34+
* #@+
35+
* A short description ignoring template.
36+
*
37+
*
38+
* A long description...
39+
*
40+
* ...over several lines.
41+
*
3442
* @var \DateTimeImmutable
3543
*/
3644
public $bal;

0 commit comments

Comments
 (0)