8000 [PropertyInfo] Add `PropertyDescriptionExtractorInterface` to `PhpSta… · symfony/symfony@833c6eb · GitHub
[go: up one dir, main page]

Skip to content
Sign in

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 833c6eb

Browse files
committed
[PropertyInfo] Add PropertyDescriptionExtractorInterface to PhpStanExtractor
1 parent 8c841e3 commit 833c6eb

File tree

5 files changed

+139
-16
lines changed

5 files changed

+139
-16
lines changed

src/Symfony/Component/PropertyInfo/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Add `PropertyDescriptionExtractorInterface` to `PhpStanExtractor`
8+
49
7.1
510
---
611

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

Lines changed: 106 additions & 14 deletions
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,94 @@ 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+
$isNewLine = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && '' === $node->text;
295+
$isTemplateMarker = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && ('#@+' === $node->text || '#@-' === $node->text);
296+
297+
$shortDescription = '';
298+
$longDescription = '';
299+
$shortDescriptionCompleted = false;
300+
301+
foreach ($docNode->children as $child) {
302+
if (!$child instanceof PhpDocTextNode) {
303+
break;
304+
}
305+
306+
if ($isTemplateMarker($child)) {
307+
continue;
308+
}
309+
310+
if ($isNewLine($child) && !$shortDescriptionCompleted) {
311+
if ($shortDescription) {
312+
$shortDescriptionCompleted = true;
313+
}
314+
315+
continue;
316+
}
317+
318+
if (!$shortDescriptionCompleted) {
319+
$shortDescription = sprintf("%s\n%s", $shortDescription, $child->text);
320+
321+
continue;
322+
}
323+
324+
$longDescription = sprintf("%s\n%s", $longDescription, $child->text);
325+
}
326+
327+
$shortDescription = trim(preg_replace('/^#@[+-]{1}/m', '', $shortDescription), "\n");
328+
$longDescription = trim($longDescription, "\n");
329+
330+
return [
331+
$shortDescription ?: null,
332+
$longDescription ?: null,
333+
];
334+
}
335+
245336
private function getDocBlockFromConstructor(string $class, string $property): ?ParamTagValueNode
246337
{
247338
try {
@@ -287,7 +378,11 @@ private function getDocBlock(string $class, string $property): array
287378

288379
$ucFirstProperty = ucfirst($property);
289380

290-
if ([$docBlock, $source, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
381+
if ([$docBlock, $constructorDocBlock, $source, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
382+
if (!$docBlock?->getTagsByName('@var') && $constructorDocBlock) {
383+
$docBlock = $constructorDocBlock;
384+
}
385+
291386
$data = [$docBlock, $source, null, $declaringClass];
292387
} elseif ([$docBlock, $_, $declaringClass] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR)) {
293388
$data = [$docBlock, self::ACCESSOR, null, $declaringClass];
@@ -301,7 +396,7 @@ private function getDocBlock(string $class, string $property): array
301396
}
302397

303398
/**
304-
* @return array{PhpDocNode, int, string}|null
399+
* @return array{?PhpDocNode, ?PhpDocNode, int, string}|null
305400
*/
306401
private function getDocBlockFromProperty(string $class, string $property): ?array
307402
{
@@ -324,28 +419,25 @@ private function getDocBlockFromProperty(string $class, string $property): ?arra
324419
}
325420
}
326421

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

332-
if (!$phpDocNode?->getTagsByName('@var')) {
333-
$phpDocNode = null;
425+
$constructorPhpDocNode = null;
426+
if ($reflectionProperty->isPromoted()) {
427+
$constructorRawDocNode = (new \ReflectionMethod($class, '__construct'))->getDocComment();
428+
$constructorPhpDocNode = $constructorRawDocNode ? $this->getPhpDocNode($constructorRawDocNode) : null;
334429
}
335430

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;
431+
$source = self::PROPERTY;
432+
if (!$phpDocNode?->getTagsByName('@var') && $constructorPhpDocNode) {
341433
$source = self::MUTATOR;
342434
}
343435

344-
if (!$phpDocNode) {
436+
if (!$phpDocNode && !$constructorPhpDocNode) {
345437
return null;
346438
}
347439

348-
return [$phpDocNode, $source, $reflectionProperty->class];
440+
return [$phpDocNode, $constructorPhpDocNode, $source, $reflectionProperty->class];
349441
}
350442

351443
/**

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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,24 @@ public function testGenericInterface()
970970
{
971971
$this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface'));
972972
}
973+
974+
/**
975+
* @dataProvider descriptionsProvider
976+
*/
977+
public function testGetDescriptions(string $property, ?string $shortDescription, ?string $longDescription)
978+
{
979+
$this->assertEquals($shortDescription, $this->extractor->getShortDescription(Dummy::class, $property));
980+
$this->assertEquals($longDescription, $this->extractor->getLongDescription(Dummy::class, $property));
981+
}
982+
983+
public static function descriptionsProvider(): iterable
984+
{
985+
yield ['foo', 'Short description.', 'Long description.'];
986+
yield ['bar', 'This is bar', null];
987+
yield ['baz', 'Should be used.', null];
988+
yield ['bal', 'A short description ignoring template.', "A long description...\n\n...over several lines."];
989+
yield ['foo2', null, null];
990+
}
973991
}
974992

975993
class PhpStanOmittedParamTagTypeDocBlock

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

Lines changed: 8 additions & 0 deletions
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)
0