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

Skip to content

Commit 8348a29

Browse files
committed
[PropertyInfo] Add PropertyDescriptionExtractorInterface to PhpStanExtractor
1 parent 0f4cf9b commit 8348a29

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.2
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,15 +14,18 @@
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;
2224
use PHPStan\PhpDocParser\Parser\TokenIterator;
2325
use PHPStan\PhpDocParser\Parser\TypeParser;
2426
use Symfony\Component\PropertyInfo\PhpStan\NameScope;
2527
use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory;
28+
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
2629
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
2730
use Symfony\Component\PropertyInfo\Type as LegacyType;
2831
use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
@@ -36,7 +39,7 @@
3639
*
3740
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
3841
*/
39-
final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
42+
final class PhpStanExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
4043
{
4144
private const PROPERTY = 0;
4245
private const ACCESSOR = 1;
@@ -241,6 +244,94 @@ public function getTypeFromConstructor(string $class, string $property): ?Type
241244
return $this->stringTypeResolver->resolve((string) $tagDocNode->type, $typeContext);
242245
}
243246

247+
public function getShortDescription(string $class, string $property, array $context = []): ?string
248+
{
249+
/** @var PhpDocNode|null $docNode */
250+
[$docNode] = $this->getDocBlockFromProperty($class, $property);
251+
if (null === $docNode) {
252+
return null;
253+
}
254+
255+
if ($shortDescription = $this->getDescriptionsFromDocNode($docNode)[0]) {
256+
return $shortDescription;
257+
}
258+
259+
foreach ($docNode->getVarTagValues() as $var) {
260+
if ($var->description) {
261+
return $var->description;
262+
}
263+
}
264+
265+
return null;
266+
}
267+
268+
public function getLongDescription(string $class, string $property, array $context = []): ?string
269+
{
270+
/** @var PhpDocNode|null $docNode */
271+
[$docNode] = $this->getDocBlockFromProperty($class, $property);
272+
if (null === $docNode) {
273+
return null;
274+
}
275+
276+
return $this->getDescriptionsFromDocNode($docNode)[1];
277+
}
278+
279+
/**
280+
* A docblock is splitted into a template marker, a short description, an optional long description and a tags section.
281+
*
282+
* - The template marker is either empty, or #@+ or #@-.
283+
* - The short description is started from a non-tag character, and until one or multiple newlines.
284+
* - The long description (optional), is started from a non-tag character, and until a new line is encountered followed by a tag.
285+
* - Tags, and the remaining characters
286+
*
287+
* This method returns the short and the long descriptions.
288+
*
289+
* @return array{0: ?string, 1: ?string}
290+
*/
291+
private function getDescriptionsFromDocNode(PhpDocNode $docNode): array
292+
{
293+
$isNewLine = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && '' === $node->text;
294+
$isTemplateMarker = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && ('#@+' === $node->text || '#@-' === $node->text);
295+
296+
$shortDescription = '';
297+
$longDescription = '';
298+
$shortDescriptionCompleted = false;
299+
300+
foreach ($docNode->children as $child) {
301+
if (!$child instanceof PhpDocTextNode) {
302+
break;
303+
}
304+
305+
if ($isTemplateMarker($child)) {
306+
continue;
307+
}
308+
309+
if ($isNewLine($child) && !$shortDescriptionCompleted) {
310+
if ($shortDescription) {
311+
$shortDescriptionCompleted = true;
312+
}
313+
314+
continue;
315+
}
316+
317+
if (!$shortDescriptionCompleted) {
318+
$shortDescription = sprintf("%s\n%s", $shortDescription, $child->text);
319+
320+
continue;
321+
}
322+
323+
$longDescription = sprintf("%s\n%s", $longDescription, $child->text);
324+
}
325+
326+
$shortDescription = trim(preg_replace('/^#@[+-]{1}/m', '', $shortDescription), "\n");
327+
$longDescription = trim($longDescription, "\n");
328+
329+
return [
330+
$shortDescription ?: null,
331+
$longDescription ?: null,
332+
];
333+
}
334+
244335
private function getDocBlockFromConstructor(string $class, string $property): ?ParamTagValueNode
245336
{
246337
try {
@@ -286,7 +377,11 @@ private function getDocBlock(string $class, string $property): array
286377

287378
$ucFirstProperty = ucfirst($property);
288379

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

302397
/**
303-
* @return array{PhpDocNode, int, string}|null
398+
* @return array{?PhpDocNode, ?PhpDocNode, int, string}|null
304399
*/
305400
private function getDocBlockFromProperty(string $class, string $property): ?array
306401
{
@@ -323,28 +418,25 @@ private function getDocBlockFromProperty(string $class, string $property): ?arra
323418
}
324419
}
325420

326-
// Type can be inside property docblock as `@var`
327421
$rawDocNode = $reflectionProperty->getDocComment();
328422
$phpDocNode = $rawDocNode ? $this->getPhpDocNode($rawDocNode) : null;
329-
$source = self::PROPERTY;
330423

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

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

343-
if (!$phpDocNode) {
435+
if (!$phpDocNode && !$constructorPhpDocNode) {
344436
return null;
345437
}
346438

347-
return [$phpDocNode, $source, $reflectionProperty->class];
439+
return [$phpDocNode, $constructorPhpDocNode, $source, $reflectionProperty->class];
348440
}
349441

350442
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public static function provideLegacyTypes()
134134
null,
135135
null,
136136
],
137-
['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null],
137+
['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], 'A short description ignoring template.', "A long description...\n\n...over several lines."],
138138
['parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null],
139139
['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],
140140
['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],
@@ -541,7 +541,7 @@ public static function typeProvider(): iterable
541541
yield ['foo4', Type::null(), null, null];
542542
yield ['foo5', Type::mixed(), null, null];
543543
yield ['files', Type::union(Type::list(Type::object(\SplFileInfo::class)), Type::resource()), null, null];
544-
yield ['bal', Type::object(\DateTimeImmutable::class), null, null];
544+
yield ['bal', Type::object(\DateTimeImmutable::class), 'A short description ignoring template.', "A long description...\n\n...over several lines."];
545545
yield ['parent', Type::object(ParentDummy::class), null, null];
546546
yield ['collection', Type::list(Type::object(\DateTimeImmutable::class)), null, null];
547547
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
@@ -959,6 +959,24 @@ public function testGenericInterface()
959959
{
960960
$this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface'));
961961
}
962+
963+
/**
964+
* @dataProvider descriptionsProvider
965+
*/
966+
public function testGetDescriptions(string $property, ?string $shortDescription, ?string $longDescription)
967+
{
968+
$this->assertEquals($shortDescription, $this->extractor->getShortDescription(Dummy::class, $property));
969+
$this->assertEquals($longDescription, $this->extractor->getLongDescription(Dummy::class, $property));
970+
}
971+
972+
public static function descriptionsProvider(): iterable
973+
{
974+
yield ['foo', 'Short description.', 'Long description.'];
975+
yield ['bar', 'This is bar', null];
976+
yield ['baz', 'Should be used.', null];
977+
yield ['bal', 'A short description ignoring template.', "A long description...\n\n...over several lines."];
978+
yield ['foo2', null, null];
979+
}
962980
}
963981

964982
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