1414use phpDocumentor \Reflection \Types \ContextFactory ;
1515use PHPStan \PhpDocParser \Ast \PhpDoc \InvalidTagValueNode ;
1616use PHPStan \PhpDocParser \Ast \PhpDoc \ParamTagValueNode ;
17+ use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocChildNode ;
1718use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocNode ;
1819use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTagNode ;
20+ use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTextNode ;
1921use PHPStan \PhpDocParser \Lexer \Lexer ;
2022use PHPStan \PhpDocParser \Parser \ConstExprParser ;
2123use PHPStan \PhpDocParser \Parser \PhpDocParser ;
2426use PHPStan \PhpDocParser \ParserConfig ;
2527use Symfony \Component \PropertyInfo \PhpStan \NameScope ;
2628use Symfony \Component \PropertyInfo \PhpStan \NameScopeFactory ;
29+ use Symfony \Component \PropertyInfo \PropertyDescriptionExtractorInterface ;
2730use Symfony \Component \PropertyInfo \PropertyTypeExtractorInterface ;
2831use Symfony \Component \PropertyInfo \Type as LegacyType ;
2932use Symfony \Component \PropertyInfo \Util \PhpStanTypeHelper ;
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 /**
0 commit comments