8000 [PropertyInfo] Add `PropertyDescriptionExtractorInterface` to `PhpStanExtractor` by mtarld · Pull Request #57632 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[PropertyInfo] Add PropertyDescriptionExtractorInterface to PhpStanExtractor #57632

New issue

Have a question about this project? Sign up for a f 8000 ree GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/PropertyInfo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

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

7.1
---
Expand Down
152 changes: 138 additions & 14 deletions src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
use phpDocumentor\Reflection\Types\ContextFactory;
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
Expand All @@ -24,6 +26,7 @@
use PHPStan\PhpDocParser\ParserConfig;
use Symfony\Component\PropertyInfo\PhpStan\NameScope;
use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper;
Expand All @@ -37,7 +40,7 @@
*
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*/
final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
final class PhpStanExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
{
private const PROPERTY = 0;
private const ACCESSOR = 1;
Expand Down Expand Up @@ -242,6 +245,126 @@ public function getTypeFromConstructor(string $class, string $property): ?Type
return $this->stringTypeResolver->resolve((string) $tagDocNode->type, $typeContext);
}

public function getShortDescription(string $class, string $property, array $context = []): ?string
{
/** @var PhpDocNode|null $docNode */
[$docNode] = $this->getDocBlockFromProperty($class, $property);
if (null === $docNode) {
return null;
}

if ($shortDescription = $this->getDescriptionsFromDocNode($docNode)[0]) {
return $shortDescription;
}

foreach ($docNode->getVarTagValues() as $var) {
if ($var->description) {
return $var->description;
}
}

return null;
}

public function getLongDescription(string $class, string $property, array $context = []): ?string
{
/** @var PhpDocNode|null $docNode */
[$docNode] = $this->getDocBlockFromProperty($class, $property);
if (null === $docNode) {
return null;
}

return $this->getDescriptionsFromDocNode($docNode)[1];
}

/**
* A docblock is splitted into a template marker, a short description, an optional long description and a tags section.
*
* - The template marker is either empty, or #@+ or #@-.
* - The short description is started from a non-tag character, and until one or multiple newlines.
* - The long description (optional), is started from a non-tag character, and until a new line is encountered followed by a tag.
* - Tags, and the remaining characters
*
* This method returns the short and the long descriptions.
*
* @return array{0: ?string, 1: ?string}
*/
private function getDescriptionsFromDocNode(PhpDocNode $docNode): array
{
$isTemplateMarker = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && ('#@+' === $node->text || '#@-' === $node->text);

$shortDescription = '';
$longDescription = '';
$shortDescriptionCompleted = false;

// BC layer for phpstan/phpdoc-parser < 2.0
if (!class_exists(ParserConfig::class)) {
$isNewLine = static fn (PhpDocChildNode $node): bool => $node instanceof PhpDocTextNode && '' === $node->text;

foreach ($docNode->children as $child) {
if (!$child instanceof PhpDocTextNode) {
break;
}

if ($isTemplateMarker($child)) {
continue;
}

if ($isNewLine($child) && !$shortDescriptionCompleted) {
if ($shortDescription) {
$shortDescriptionCompleted = true;
}

continue;
}

if (!$shortDescriptionCompleted) {
$shortDescription = \sprintf("%s\n%s", $shortDescription, $child->text);

continue;
}

$longDescription = \sprintf("%s\n%s", $longDescription, $child->text);
}
} else {
foreach ($docNode->children as $child) {
if (!$child instanceof PhpDocTextNode) {
break;
}

if ($isTemplateMarker($child)) {
continue;
}

foreach (explode("\n", $child->text) as $line) {
if ('' === $line && !$shortDescriptionCompleted) {
if ($shortDescription) {
$shortDescriptionCompleted = true;
}

continue;
}

if (!$shortDescriptionCompleted) {
$shortDescription = \sprintf("%s\n%s", $shortDescription, $line);

continue;
}

$longDescription = \sprintf("%s\n%s", $longDescription, $line);
}
}
}

$shortDescription = trim(preg_replace('/^#@[+-]{1}/m', '', $shortDescription), "\n");
$longDescription = trim($longDescription, "\n");

return [
$shortDescription ?: null,
$longDescription ?: null,
];
}

private function getDocBlockFromConstructor(string $class, string $property): ?ParamTagValueNode
{
try {
Expand Down Expand Up @@ -287,7 +410,11 @@ private function getDocBlock(string $class, string $property): array

$ucFirstProperty = ucfirst($property);

if ([$docBlock, $source, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
if ([$docBlock, $constructorDocBlock, $source, $declaringClass] = $this->getDocBlockFromProperty($class, $property)) {
if (!$docBlock?->getTagsByName('@var') && $constructorDocBlock) {
$docBlock = $constructorDocBlock;
}

$data = [$docBlock, $source, null, $declaringClass];
} elseif ([$docBlock, $_, $declaringClass] = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR)) {
$data = [$docBlock, self::ACCESSOR, null, $declaringClass];
Expand All @@ -301,7 +428,7 @@ private function getDocBlock(string $class, string $property): array
}

/**
* @return array{PhpDocNode, int, string}|null
* @return array{?PhpDocNode, ?PhpDocNode, int, string}|null
*/
private function getDocBlockFromProperty(string $class, string $property): ?array
{
Expand All @@ -324,28 +451,25 @@ private function getDocBlockFromProperty(string $class, string $property): ?arra
}
}

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

if (!$phpDocNode?->getTagsByName('@var')) {
$phpDocNode = null;
$constructorPhpDocNode = null;
if ($reflectionProperty->isPromoted()) {
$constructorRawDocNode = (new \ReflectionMethod($class, '__construct'))->getDocComment();
$constructorPhpDocNode = $constructorRawDocNode ? $this->getPhpDocNode($constructorRawDocNode) : null;
}

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

if (!$phpDocNode) {
if (!$phpDocNode && !$constructorPhpDocNode) {
return null;
}

return [$phpDocNode, $source, $reflectionProperty->class];
return [$phpDocNode, $constructorPhpDocNode, $source, $reflectionProperty->class];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public static function provideLegacyTypes()
null,
null,
],
['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null],
['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], 'A short description ignoring template.', "A long description...\n\n...over several lines."],
['parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null],
['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],
['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],
Expand Down Expand Up @@ -545,7 +545,7 @@ public static function typeProvider(): iterable
yield ['foo4', Type::null(), null, null];
yield ['foo5', Type::mixed(), null, null];
yield ['files', Type::union(Type::list(Type::object(\SplFileInfo::class)), Type::resource()), null, null];
yield ['bal', Type::object(\DateTimeImmutable::class), null, null];
yield ['bal', Type::object(\DateTimeImmutable::class), 'A short description ignoring template.', "A long description...\n\n...over several lines."];
yield ['parent', Type::object(ParentDummy::class), null, null];
yield ['collection', Type::list(Type::object(\DateTimeImmutable::class)), null, null];
yield ['nestedCollection', Type::list(Type::list(Type::string())), null, null];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,24 @@ public static function genericsProvider(): iterable
Type::nullable(Type::generic(Type::object(IFace::class), Type::object(Dummy::class))),
];
}

/**
* @dataProvider descriptionsProvider
*/
public function testGetDescriptions(string $property, ?string $shortDescription, ?string $longDescription)
{
$this->assertEquals($shortDescription, $this->extractor->getShortDescription(Dummy::class, $property));
$this->assertEquals($longDescription, $this->extractor->getLongDescription(Dummy::class, $property));
}

public static function descriptionsProvider(): iterable
{
yield ['foo', 'Short description.', 'Long description.'];
yield ['bar', 'This is bar', null];
yield ['baz', 'Should be used.', null];
yield ['bal', 'A short description ignoring template.', "A long description...\n\n...over several lines."];
yield ['foo2', null, null];
}
}

class PhpStanOmittedParamTagTypeDocBlock
Expand Down
8 changes: 8 additions & 0 deletions src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ class Dummy extends ParentDummy
protected $baz;

/**
* #@+
* A short description ignoring template.
*
*
* A long description...
*
* ...over several lines.
*
* @var \DateTimeImmutable
*/
public $bal;
Expand Down
Loading
0