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,94 @@ 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
+ $ isNewLine = static fn (PhpDocChildNode $ node ): bool => $ node instanceof PhpDocTextNode && '' === $ node ->text ;
295
+ $ isTemplateMarker = static fn (PhpDocChildNode $ node ): bool => $ node instanceof PhpDocTextNode && ('#@+ ' === $ node ->text || '#@- ' === $ node ->text );
296
+
297
+ $ shortDescription = '' ;
298
+ $ longDescription = '' ;
299
+ $ shortDescriptionCompleted = false ;
300
+
301
+ foreach ($ docNode ->children as $ child ) {
302
+ if (!$ child instanceof PhpDocTextNode) {
303
+ break ;
304
+ }
305
+
306
+ if ($ isTemplateMarker ($ child )) {
307
+ continue ;
308
+ }
309
+
310
+ if ($ isNewLine ($ child ) && !$ shortDescriptionCompleted ) {
311
+ if ($ shortDescription ) {
312
+ $ shortDescriptionCompleted = true ;
313
+ }
314
+
315
+ continue ;
316
+ }
317
+
318
+ if (!$ shortDescriptionCompleted ) {
319
+ $ shortDescription = sprintf ("%s \n%s " , $ shortDescription , $ child ->text );
320
+
321
+ continue ;
322
+ }
323
+
324
+ $ longDescription = sprintf ("%s \n%s " , $ longDescription , $ child ->text );
325
+ }
326
+
327
+ $ shortDescription = trim (preg_replace ('/^#@[+-]{1}/m ' , '' , $ shortDescription ), "\n" );
328
+ $ longDescription = trim ($ longDescription , "\n" );
329
+
330
+ return [
331
+ $ shortDescription ?: null ,
332
+ $ longDescription ?: null ,
333
+ ];
334
+ }
335
+
245
336
private function getDocBlockFromConstructor (string $ class , string $ property ): ?ParamTagValueNode
246
337
{
247
338
try {
@@ -287,7 +378,11 @@ private function getDocBlock(string $class, string $property): array
287
378
288
379
$ ucFirstProperty = ucfirst ($ property );
289
380
290
- if ([$ docBlock , $ source , $ declaringClass ] = $ this ->getDocBlockFromProperty ($ class , $ property )) {
381
+ if ([$ docBlock , $ constructorDocBlock , $ source , $ declaringClass ] = $ this ->getDocBlockFromProperty ($ class , $ property )) {
382
+ if (!$ docBlock ?->getTagsByName('@var ' ) && $ constructorDocBlock ) {
383
+ $ docBlock = $ constructorDocBlock ;
384
+ }
385
+
291
386
$ data = [$ docBlock , $ source , null , $ declaringClass ];
292
387
} elseif ([$ docBlock , $ _ , $ declaringClass ] = $ this ->getDocBlockFromMethod ($ class , $ ucFirstProperty , self ::ACCESSOR )) {
293
388
$ data = [$ docBlock , self ::ACCESSOR , null , $ declaringClass ];
@@ -301,7 +396,7 @@ private function getDocBlock(string $class, string $property): array
301
396
}
302
397
303
398
/**
304
- * @return array{PhpDocNode, int, string}|null
399
+ * @return array{?PhpDocNode, ? PhpDocNode, int, string}|null
305
400
*/
306
401
private function getDocBlockFromProperty (string $ class , string $ property ): ?array
307
402
{
@@ -324,28 +419,25 @@ private function getDocBlockFromProperty(string $class, string $property): ?arra
324
419
}
325
420
}
326
421
327
- // Type can be inside property docblock as `@var`
328
422
$ rawDocNode = $ reflectionProperty ->getDocComment ();
329
423
$ phpDocNode = $ rawDocNode ? $ this ->getPhpDocNode ($ rawDocNode ) : null ;
330
- $ source = self ::PROPERTY ;
331
424
332
- if (!$ phpDocNode ?->getTagsByName('@var ' )) {
333
- $ phpDocNode = null ;
425
+ $ constructorPhpDocNode = null ;
426
+ if ($ reflectionProperty ->isPromoted ()) {
427
+ $ constructorRawDocNode = (new \ReflectionMethod ($ class , '__construct ' ))->getDocComment ();
428
+ $ constructorPhpDocNode = $ constructorRawDocNode ? $ this ->getPhpDocNode ($ constructorRawDocNode ) : null ;
334
429
}
335
430
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 ;
431
+ $ source = self ::PROPERTY ;
432
+ if (!$ phpDocNode ?->getTagsByName('@var ' ) && $ constructorPhpDocNode ) {
341
433
$ source = self ::MUTATOR ;
342
434
}
343
435
344
- if (!$ phpDocNode ) {
436
+ if (!$ phpDocNode && ! $ constructorPhpDocNode ) {
345
437
return null ;
346
438
}
347
439
348
- return [$ phpDocNode , $ source , $ reflectionProperty ->class ];
440
+ return [$ phpDocNode , $ constructorPhpDocNode , $ source , $ reflectionProperty ->class ];
349
441
}
350
442
351
443
/**
0 commit comments