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 ;
22
24
use PHPStan \PhpDocParser \Parser \TokenIterator ;
23
25
use PHPStan \PhpDocParser \Parser \TypeParser ;
24
26
use Symfony \Component \PropertyInfo \PhpStan \NameScope ;
25
27
use Symfony \Component \PropertyInfo \PhpStan \NameScopeFactory ;
28
+ use Symfony \Component \PropertyInfo \PropertyDescriptionExtractorInterface ;
26
29
use Symfony \Component \PropertyInfo \PropertyTypeExtractorInterface ;
27
30
use Symfony \Component \PropertyInfo \Type as LegacyType ;
28
31
use Symfony \Component \PropertyInfo \Util \PhpStanTypeHelper ;
36
39
*
37
40
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
38
41
*/
39
- final class PhpStanExtractor implements PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
42
+ final class PhpStanExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
40
43
{
41
44
private const PROPERTY = 0 ;
42
45
private const ACCESSOR = 1 ;
@@ -241,6 +244,94 @@ public function getTypeFromConstructor(string $class, string $property): ?Type
241
244
return $ this ->stringTypeResolver ->resolve ((string ) $ tagDocNode ->type , $ typeContext );
242
245
}
243
246
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
+
244
335
private function getDocBlockFromConstructor (string $ class , string $ property ): ?ParamTagValueNode
245
336
{
246
337
try {
@@ -286,7 +377,11 @@ private function getDocBlock(string $class, string $property): array
286
377
287
378
$ ucFirstProperty = ucfirst ($ property );
288
379
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
+
290
385
$ data = [$ docBlock , $ source , null , $ declaringClass ];
291
386
} elseif ([$ docBlock , $ _ , $ declaringClass ] = $ this ->getDocBlockFromMethod ($ class , $ ucFirstProperty , self ::ACCESSOR )) {
292
387
$ data = [$ docBlock , self ::ACCESSOR , null , $ declaringClass ];
@@ -300,7 +395,7 @@ private function getDocBlock(string $class, string $property): array
300
395
}
301
396
302
397
/**
303
- * @return array{PhpDocNode, int, string}|null
398
+ * @return array{?PhpDocNode, ? PhpDocNode, int, string}|null
304
399
*/
305
400
private function getDocBlockFromProperty (string $ class , string $ property ): ?array
306
401
{
@@ -323,28 +418,25 @@ private function getDocBlockFromProperty(string $class, string $property): ?arra
323
418
}
324
419
}
325
420
326
- // Type can be inside property docblock as `@var`
327
421
$ rawDocNode = $ reflectionProperty ->getDocComment ();
328
422
$ phpDocNode = $ rawDocNode ? $ this ->getPhpDocNode ($ rawDocNode ) : null ;
329
- $ source = self ::PROPERTY ;
330
423
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 ;
333
428
}
334
429
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 ) {
340
432
$ source = self ::MUTATOR ;
341
433
}
342
434
343
- if (!$ phpDocNode ) {
435
+ if (!$ phpDocNode && ! $ constructorPhpDocNode ) {
344
436
return null ;
345
437
}
346
438
347
- return [$ phpDocNode , $ source , $ reflectionProperty ->class ];
439
+ return [$ phpDocNode , $ constructorPhpDocNode , $ source , $ reflectionProperty ->class ];
348
440
}
349
441
350
442
/**
0 commit comments