14
14
use phpDocumentor \Reflection \Types \ContextFactory ;
15
15
use PHPStan \PhpDocParser \Ast \PhpDoc \InvalidTagValueNode ;
16
16
use PHPStan \PhpDocParser \Ast \PhpDoc \ParamTagValueNode ;
17
+ use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocChildNode ;
17
18
use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocNode ;
18
19
use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTagNode ;
20
+ use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTextNode ;
19
21
use PHPStan \PhpDocParser \Lexer \Lexer ;
20
22
use PHPStan \PhpDocParser \Parser \ConstExprParser ;
21
23
use PHPStan \PhpDocParser \Parser \PhpDocParser ;
24
26
use PHPStan \PhpDocParser \ParserConfig ;
25
27
use Symfony \Component \PropertyInfo \PhpStan \NameScope ;
26
28
use Symfony \Component \PropertyInfo \PhpStan \NameScopeFactory ;
29
+ use Symfony \Component \PropertyInfo \PropertyDescriptionExtractorInterface ;
27
30
use Symfony \Component \PropertyInfo \PropertyTypeExtractorInterface ;
28
31
use Symfony \Component \PropertyInfo \Type as LegacyType ;
29
32
use Symfony \Component \PropertyInfo \Util \PhpStanTypeHelper ;
37
40
*
38
41
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
39
42
*/
40
- final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
43
+ final class PhpStanExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
41
44
{
42
45
private const PROPERTY = 0 ;
43
46
private const ACCESSOR = 1 ;
@@ -242,6 +245,126 @@ public function getTypeFromConstructor(string $class, string $property): ?Type
242
245
return $ this ->stringTypeResolver ->resolve ((string ) $ tagDocNode ->type , $ typeContext );
243
246
}
244
247
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
+
245
368
private function getDocBlockFromConstructor (string $ class , string $ property ): ?ParamTagValueNode
246
369
{
247
370
try {
@@ -287,7 +410,11 @@ private function getDocBlock(string $class, string $property): array
287
410
288
411
$ ucFirstProperty = ucfirst ($ property );
289
412
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
+
291
418
$ data = [$ docBlock , $ source , null , $ declaringClass ];
292
419
} elseif ([$ docBlock , $ _ , $ declaringClass ] = $ this ->getDocBlockFromMethod ($ class , $ ucFirstProperty , self ::ACCESSOR )) {
293
420
$ data = [$ docBlock , self ::ACCESSOR , null , $ declaringClass ];
@@ -301,7 +428,7 @@ private function getDocBlock(string $class, string $property): array
301
428
}
302
429
303
430
/**
304
- * @return array{PhpDocNode, int, string}|null
431
+ * @return array{?PhpDocNode, ? PhpDocNode, int, string}|null
305
432
*/
306
433
private function getDocBlockFromProperty (string $ class , string $ property ): ?array
307
434
{
@@ -324,28 +451,25 @@ private function getDocBlockFromProperty(string $class, string $property): ?arra
324
451
}
325
452
}
326
453
327
- // Type can be inside property docblock as `@var`
328
454
$ rawDocNode = $ reflectionProperty ->getDocComment ();
329
455
$ phpDocNode = $ rawDocNode ? $ this ->getPhpDocNode ($ rawDocNode ) : null ;
330
- $ source = self ::PROPERTY ;
331
456
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 ;
334
461
}
335
462
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 ) {
341
465
$ source = self ::MUTATOR ;
342
466
}
343
467
344
- if (!$ phpDocNode ) {
468
+ if (!$ phpDocNode && ! $ constructorPhpDocNode ) {
345
469
return null ;
346
470
}
347
471
348
- return [$ phpDocNode , $ source , $ reflectionProperty ->class ];
472
+ return [$ phpDocNode , $ constructorPhpDocNode , $ source , $ reflectionProperty ->class ];
349
473
}
350
474
351
475
/**
0 commit comments