Skip to content

Navigation Menu

Sign in
Appearance settings

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 8348a29

Browse filesBrowse files
committed
[PropertyInfo] Add PropertyDescriptionExtractorInterface to PhpStanExtractor
1 parent 0f4cf9b commit 8348a29
Copy full SHA for 8348a29

File tree

5 files changed

+139
-16
lines changed
Filter options

5 files changed

+139
-16
lines changed

‎src/Symfony/Component/PropertyInfo/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/CHANGELOG.md
+5Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php
+106-14Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php
+2-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
+18Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php
+8Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.