1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\PHPDoc; |
4: | |
5: | use Salient\Contract\Core\Immutable; |
6: | use Salient\Core\Concern\ImmutableTrait; |
7: | use Salient\PHPDoc\Tag\AbstractTag; |
8: | use Salient\PHPDoc\Tag\ErrorTag; |
9: | use Salient\PHPDoc\Tag\GenericTag; |
10: | use Salient\PHPDoc\Tag\MethodParam; |
11: | use Salient\PHPDoc\Tag\MethodTag; |
12: | use Salient\PHPDoc\Tag\ParamTag; |
13: | use Salient\PHPDoc\Tag\PropertyTag; |
14: | use Salient\PHPDoc\Tag\ReturnTag; |
15: | use Salient\PHPDoc\Tag\TemplateTag; |
16: | use Salient\PHPDoc\Tag\VarTag; |
17: | use Salient\Utility\Exception\ShouldNotHappenException; |
18: | use Salient\Utility\Arr; |
19: | use Salient\Utility\Regex; |
20: | use Salient\Utility\Str; |
21: | use InvalidArgumentException; |
22: | use ReflectionClass; |
23: | use ReflectionClassConstant; |
24: | use ReflectionMethod; |
25: | use ReflectionProperty; |
26: | use Stringable; |
27: | |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | |
34: | class PHPDoc implements Immutable, Stringable |
35: | { |
36: | use ImmutableTrait; |
37: | |
38: | private const PHP_DOCBLOCK = '`^' . PHPDocRegex::PHP_DOCBLOCK . '$`D'; |
39: | private const PHPDOC_TAG = '`^' . PHPDocRegex::PHPDOC_TAG . '`D'; |
40: | private const BLANK_OR_PHPDOC_TAG = '`^(?:$|' . PHPDocRegex::PHPDOC_TAG . ')`D'; |
41: | |
42: | private const PARAM_TAG = '`^' |
43: | . '(?:(?<param_type>' . PHPDocRegex::PHPDOC_TYPE . ')\h++)?' |
44: | . '(?<param_reference>&\h*+)?' |
45: | . '(?<param_variadic>\.\.\.\h*+)?' |
46: | . '\$(?<param_name>[^\s]++)' |
47: | . '\s*+(?<param_description>(?s).++)?' |
48: | . '`'; |
49: | |
50: | private const RETURN_TAG = '`^' |
51: | . '(?<return_type>' . PHPDocRegex::PHPDOC_TYPE . ')' |
52: | . '\s*+(?<return_description>(?s).++)?' |
53: | . '`'; |
54: | |
55: | private const VAR_TAG = '`^' |
56: | . '(?<var_type>' . PHPDocRegex::PHPDOC_TYPE . ')' |
57: | . '(?:\h++\$(?<var_name>[^\s]++))?' |
58: | . '\s*+(?<var_description>(?s).++)?' |
59: | . '`'; |
60: | |
61: | private const METHOD_TAG = '`^' |
62: | . '(?<method_static>static\h++)?' |
63: | . '(?:(?<method_type>' . PHPDocRegex::PHPDOC_TYPE . ')\h++)?' |
64: | . '(?<method_name>[^\s(]++)' |
65: | . '\h*+\(\h*+(?:(?<method_params>' |
66: | . '(?<method_param>(?:(?&method_type)\h++)?(?:\.\.\.\h*+)?\$[^\s=,)$]++(?:\h*+=\h*+' . PHPDocRegex::PHPDOC_VALUE . ')?)' |
67: | . '(?:\h*+,\h*+(?&method_param))*+' |
68: | . ')\h*+(?:,\h*+)?)?\)' |
69: | . '\s*+(?<method_description>(?s).++)?' |
70: | . '`'; |
71: | |
72: | private const METHOD_PARAM = '`^' |
73: | . '(?:(?<param_type>' . PHPDocRegex::PHPDOC_TYPE . ')\h++)?' |
74: | . '(?<param_variadic>\.\.\.\h*+)?' |
75: | . '\$(?<param_name>[^\s=,)$]++)' |
76: | . '(?:\h*+=\h*+(?<param_default>' . PHPDocRegex::PHPDOC_VALUE . '))?' |
77: | . '`'; |
78: | |
79: | private const PROPERTY_TAG = '`^' |
80: | . '(?:(?<property_type>' . PHPDocRegex::PHPDOC_TYPE . ')\h++)?' |
81: | . '\$(?<property_name>[^\s]++)' |
82: | . '\s*+(?<property_description>(?s).++)?' |
83: | . '`'; |
84: | |
85: | private const TEMPLATE_TAG = '`^' |
86: | . '(?<template_name>[^\s]++)' |
87: | . '(?:\h++(?:of|as)\h++(?<template_type>' . PHPDocRegex::PHPDOC_TYPE . '))?' |
88: | . '(?:\h++=\h++(?<template_default>(?&template_type)))?' |
89: | . '`'; |
90: | |
91: | |
92: | |
93: | |
94: | protected const DEFAULT_TAG_PREFIXES = [ |
95: | '', |
96: | 'phpstan-', |
97: | 'psalm-', |
98: | ]; |
99: | |
100: | |
101: | |
102: | |
103: | |
104: | |
105: | protected const INHERITABLE_TAGS = [ |
106: | |
107: | 'author' => false, |
108: | 'copyright' => false, |
109: | 'version' => false, |
110: | 'package' => false, |
111: | 'param' => true, |
112: | 'return' => true, |
113: | 'throws' => true, |
114: | 'var' => true, |
115: | |
116: | 'method' => true, |
117: | 'property' => true, |
118: | 'property-read' => ['phpstan-', 'psalm-'], |
119: | 'property-write' => ['phpstan-', 'psalm-'], |
120: | 'mixin' => true, |
121: | |
122: | 'param-out' => true, |
123: | 'param-immediately-invoked-callable' => ['', 'phpstan-'], |
124: | 'param-later-invoked-callable' => ['', 'phpstan-'], |
125: | 'param-closure-this' => ['', 'phpstan-'], |
126: | |
127: | 'deprecated' => false, |
128: | 'readonly' => false, |
129: | ]; |
130: | |
131: | |
132: | |
133: | |
134: | |
135: | |
136: | protected const INHERITABLE_BY_CLASS_TAGS = [ |
137: | 'method' => true, |
138: | 'property' => true, |
139: | 'property-read' => ['phpstan-', 'psalm-'], |
140: | 'property-write' => ['phpstan-', 'psalm-'], |
141: | 'mixin' => true, |
142: | ]; |
143: | |
144: | |
145: | |
146: | |
147: | protected const MERGED_TAGS = [ |
148: | 'param' => true, |
149: | 'return' => true, |
150: | 'var' => true, |
151: | 'method' => true, |
152: | 'property' => true, |
153: | ]; |
154: | |
155: | |
156: | |
157: | |
158: | protected const STANDARD_TAGS = [ |
159: | 'param', |
160: | 'return', |
161: | 'throws', |
162: | 'var', |
163: | 'template', |
164: | 'template-covariant', |
165: | 'template-contravariant', |
166: | 'api', |
167: | 'internal', |
168: | 'inheritDoc', |
169: | 'readonly', |
170: | ]; |
171: | |
172: | protected ?string $Summary = null; |
173: | protected ?string $Description = null; |
174: | |
175: | protected array $Tags = []; |
176: | |
177: | protected array $Params = []; |
178: | protected ?ReturnTag $Return = null; |
179: | |
180: | protected array $Vars = []; |
181: | |
182: | protected array $Methods = []; |
183: | |
184: | protected array $Properties = []; |
185: | |
186: | protected array $Templates = []; |
187: | |
188: | protected array $InheritedTemplates = []; |
189: | |
190: | protected array $Errors = []; |
191: | |
192: | protected ?string $Class; |
193: | protected ?string $Member; |
194: | |
195: | protected ?string $Static; |
196: | |
197: | protected ?string $Self; |
198: | |
199: | protected self $Original; |
200: | |
201: | private array $Lines; |
202: | private ?string $NextLine; |
203: | |
204: | private static array $InheritableTagIndex; |
205: | |
206: | private static array $InheritableByClassTagIndex; |
207: | |
208: | |
209: | |
210: | |
211: | |
212: | |
213: | |
214: | |
215: | |
216: | |
217: | final public function __construct( |
218: | ?string $docBlock = null, |
219: | $classDocBlock = null, |
220: | ?string $class = null, |
221: | ?string $member = null, |
222: | ?string $static = null, |
223: | ?string $self = null, |
224: | array $aliases = [] |
225: | ) { |
226: | $this->Class = $class; |
227: | $this->Member = $member; |
228: | $this->Static = $static ?? $class; |
229: | $this->Self = $self ?? $class; |
230: | $this->Original = $this; |
231: | |
232: | if ($docBlock !== null) { |
233: | if (!Regex::match(self::PHP_DOCBLOCK, $docBlock, $matches)) { |
234: | throw new InvalidArgumentException('Invalid DocBlock'); |
235: | } |
236: | $this->parse($matches['content'], $aliases, $tags); |
237: | if ($tags) { |
238: | $this->updateTags(); |
239: | } |
240: | } |
241: | |
242: | |
243: | if ($classDocBlock !== null && $class !== null && $member !== null) { |
244: | $phpDoc = $classDocBlock instanceof self |
245: | ? $classDocBlock |
246: | : new static($classDocBlock, null, $class, null, $static, $self, $aliases); |
247: | foreach ($phpDoc->Templates as $name => $tag) { |
248: | $this->Templates[$name] ??= $tag; |
249: | } |
250: | } |
251: | } |
252: | |
253: | |
254: | |
255: | |
256: | |
257: | |
258: | |
259: | |
260: | public static function forClass( |
261: | ReflectionClass $class, |
262: | array $aliases = [] |
263: | ): self { |
264: | $docBlocks = PHPDocUtil::getAllClassDocComments($class, true, true); |
265: | return self::fromDocBlocks($docBlocks, null, null, $class->getName(), $aliases); |
266: | } |
267: | |
268: | |
269: | |
270: | |
271: | |
272: | |
273: | |
274: | |
275: | public static function forMethod( |
276: | ReflectionMethod $method, |
277: | ?ReflectionClass $class = null, |
278: | array $aliases = [] |
279: | ): self { |
280: | $docBlocks = PHPDocUtil::getAllMethodDocComments($method, $class, true, $classDocBlocks); |
281: | $name = $method->getName(); |
282: | $static = ($class ?? $method->getDeclaringClass())->getName(); |
283: | return self::fromDocBlocks($docBlocks, $classDocBlocks, "{$name}()", $static, $aliases); |
284: | } |
285: | |
286: | |
287: | |
288: | |
289: | |
290: | |
291: | |
292: | |
293: | public static function forProperty( |
294: | ReflectionProperty $property, |
295: | ?ReflectionClass $class = null, |
296: | array $aliases = [] |
297: | ): self { |
298: | $docBlocks = PHPDocUtil::getAllPropertyDocComments($property, $class, true, $classDocBlocks); |
299: | $name = $property->getName(); |
300: | $static = ($class ?? $property->getDeclaringClass())->getName(); |
301: | return self::fromDocBlocks($docBlocks, $classDocBlocks, "\${$name}", $static, $aliases); |
302: | } |
303: | |
304: | |
305: | |
306: | |
307: | |
308: | |
309: | |
310: | |
311: | public static function forConstant( |
312: | ReflectionClassConstant $constant, |
313: | ?ReflectionClass $class = null, |
314: | array $aliases = [] |
315: | ): self { |
316: | $docBlocks = PHPDocUtil::getAllConstantDocComments($constant, $class, true, $classDocBlocks); |
317: | $name = $constant->getName(); |
318: | $static = ($class ?? $constant->getDeclaringClass())->getName(); |
319: | return self::fromDocBlocks($docBlocks, $classDocBlocks, $name, $static, $aliases); |
320: | } |
321: | |
322: | |
323: | |
324: | |
325: | |
326: | |
327: | |
328: | |
329: | public static function fromTags( |
330: | array $tags, |
331: | ?string $summary = null, |
332: | ?string $description = null, |
333: | ?string $class = null, |
334: | ?string $member = null |
335: | ): self { |
336: | if ($summary !== null) { |
337: | $summary = Str::coalesce(trim($summary), null); |
338: | } |
339: | if ($description !== null) { |
340: | $description = Str::coalesce(trim($description), null); |
341: | } |
342: | if ($summary === null && $description !== null) { |
343: | throw new InvalidArgumentException('$description must be empty if $summary is empty'); |
344: | } |
345: | |
346: | $phpDoc = new static(null, null, $class, $member); |
347: | $phpDoc->Summary = $summary; |
348: | $phpDoc->Description = $description; |
349: | |
350: | $count = 0; |
351: | foreach ($tags as $tag) { |
352: | if ($tag instanceof ParamTag) { |
353: | $phpDoc->Params[$tag->getName()] = $tag; |
354: | } elseif ($tag instanceof ReturnTag) { |
355: | $phpDoc->Return = $tag; |
356: | } elseif ($tag instanceof VarTag) { |
357: | $name = $tag->getName(); |
358: | if ($name !== null) { |
359: | $phpDoc->Vars[$name] = $tag; |
360: | } else { |
361: | $phpDoc->Vars[] = $tag; |
362: | } |
363: | } elseif ($tag instanceof MethodTag) { |
364: | $phpDoc->Methods[$tag->getName()] = $tag; |
365: | } elseif ($tag instanceof PropertyTag) { |
366: | $phpDoc->Properties[$tag->getName()] = $tag; |
367: | } elseif ($tag instanceof TemplateTag) { |
368: | $_class = $tag->getClass(); |
369: | if ($_class === $class || $_class === null || $class === null) { |
370: | $phpDoc->Templates[$tag->getName()] = $tag; |
371: | } else { |
372: | $phpDoc->InheritedTemplates[$_class][$tag->getName()] = $tag; |
373: | continue; |
374: | } |
375: | } elseif ($tag instanceof ErrorTag) { |
376: | $phpDoc->Errors[] = $tag; |
377: | continue; |
378: | } else { |
379: | $phpDoc->Tags[$tag->getTag()][] = $tag; |
380: | continue; |
381: | } |
382: | |
383: | $phpDoc->Tags[$tag->getTag()] ??= []; |
384: | $count++; |
385: | } |
386: | |
387: | if ($count) { |
388: | $phpDoc->updateTags(); |
389: | } |
390: | |
391: | return $phpDoc; |
392: | } |
393: | |
394: | |
395: | |
396: | |
397: | |
398: | |
399: | |
400: | public function inherit(self $parent) |
401: | { |
402: | |
403: | $byClass = $this->Class !== null |
404: | && $parent->Class !== null |
405: | && $this->Class !== $parent->Class |
406: | && $this->Member === null |
407: | && $parent->Member === null |
408: | && ((interface_exists($parent->Class) && !interface_exists($this->Class)) |
409: | || (trait_exists($parent->Class) && !trait_exists($this->Class))); |
410: | |
411: | $tags = $this->Tags; |
412: | foreach (Arr::flatten($tags) as $tag) { |
413: | $idx[(string) $tag] = true; |
414: | } |
415: | foreach (array_intersect_key( |
416: | $parent->Tags, |
417: | $byClass |
418: | ? self::getInheritableByClassTagIndex() |
419: | : self::getInheritableTagIndex(), |
420: | ) as $name => $theirs) { |
421: | foreach ($theirs as $tag) { |
422: | if (!isset($idx[(string) $tag])) { |
423: | $tags[$name][] = $tag; |
424: | } |
425: | } |
426: | } |
427: | |
428: | if ($this->Member !== null || $this->Class === null) { |
429: | $params = $this->Params; |
430: | foreach ($parent->Params as $name => $theirs) { |
431: | $params[$name] = $this->mergeTag($params[$name] ?? null, $theirs); |
432: | } |
433: | |
434: | $return = $this->mergeTag($this->Return, $parent->Return); |
435: | |
436: | $vars = $this->Vars; |
437: | if ( |
438: | count($parent->Vars) === 1 |
439: | && array_key_first($parent->Vars) === 0 |
440: | ) { |
441: | $vars[0] = $this->mergeTag($vars[0] ?? null, $parent->Vars[0]); |
442: | } |
443: | } |
444: | |
445: | if ($this->Member === null) { |
446: | $methods = $this->Methods; |
447: | foreach ($parent->Methods as $name => $theirs) { |
448: | $methods[$name] = $this->mergeTag($methods[$name] ?? null, $theirs); |
449: | } |
450: | |
451: | $properties = $this->Properties; |
452: | foreach ($parent->Properties as $name => $theirs) { |
453: | $properties[$name] = $this->mergeTag($properties[$name] ?? null, $theirs); |
454: | } |
455: | } |
456: | |
457: | $templates = $this->InheritedTemplates; |
458: | if ($parent->Class !== null && $parent->Class !== $this->Class) { |
459: | unset($templates[$parent->Class]); |
460: | if ($parent->Templates) { |
461: | $templates[$parent->Class] = $parent->Templates; |
462: | } |
463: | } |
464: | |
465: | if (!$byClass) { |
466: | $summary = $this->Summary; |
467: | $description = $this->Description; |
468: | if ($description !== null && $parent->Description !== null) { |
469: | $description = Regex::replace( |
470: | '/\{@inherit[Dd]oc\}/', |
471: | $parent->Description, |
472: | $description, |
473: | ); |
474: | } else { |
475: | $summary ??= $parent->Summary; |
476: | $description ??= $parent->Description; |
477: | } |
478: | } |
479: | |
480: | return $this |
481: | ->with('Summary', $summary ?? $this->Summary) |
482: | ->with('Description', $description ?? $this->Description) |
483: | ->with('Tags', $tags) |
484: | ->with('Params', $params ?? $this->Params) |
485: | ->with('Return', $return ?? $this->Return) |
486: | ->with('Vars', $vars ?? $this->Vars) |
487: | ->with('Methods', $methods ?? $this->Methods) |
488: | ->with('Properties', $properties ?? $this->Properties) |
489: | ->with('InheritedTemplates', $templates); |
490: | } |
491: | |
492: | |
493: | |
494: | |
495: | private static function getInheritableTagIndex(): array |
496: | { |
497: | return self::$InheritableTagIndex[static::class] ??= |
498: | self::doGetInheritableTagIndex(static::INHERITABLE_TAGS); |
499: | } |
500: | |
501: | |
502: | |
503: | |
504: | private static function getInheritableByClassTagIndex(): array |
505: | { |
506: | return self::$InheritableByClassTagIndex[static::class] ??= |
507: | self::doGetInheritableTagIndex(static::INHERITABLE_BY_CLASS_TAGS); |
508: | } |
509: | |
510: | |
511: | |
512: | |
513: | |
514: | private static function doGetInheritableTagIndex(array $inheritable): array |
515: | { |
516: | foreach ($inheritable as $tag => $prefixes) { |
517: | if ($prefixes === false) { |
518: | $idx[$tag] = true; |
519: | continue; |
520: | } elseif ($prefixes === true) { |
521: | $prefixes = static::DEFAULT_TAG_PREFIXES; |
522: | } |
523: | foreach ($prefixes as $prefix) { |
524: | $idx[$prefix . $tag] = true; |
525: | } |
526: | } |
527: | |
528: | return array_diff_key($idx, static::MERGED_TAGS); |
529: | } |
530: | |
531: | |
532: | |
533: | |
534: | |
535: | |
536: | |
537: | |
538: | private function mergeTag(?AbstractTag $ours, ?AbstractTag $theirs): ?AbstractTag |
539: | { |
540: | if ($theirs === null) { |
541: | return $ours; |
542: | } |
543: | |
544: | if ($ours === null) { |
545: | return $theirs; |
546: | } |
547: | |
548: | return $ours->inherit($theirs); |
549: | } |
550: | |
551: | |
552: | |
553: | |
554: | |
555: | |
556: | |
557: | |
558: | |
559: | |
560: | |
561: | |
562: | |
563: | |
564: | |
565: | public function normalise() |
566: | { |
567: | $tags = $this->Tags; |
568: | unset($tags['inheritDoc']); |
569: | |
570: | $vars = $this->Vars; |
571: | if (count($vars) === 1) { |
572: | $key = array_key_first($vars); |
573: | $var = $vars[$key]; |
574: | if ( |
575: | $this->Class !== null |
576: | && ($name = $var->getName()) !== null |
577: | && "\${$name}" === $this->Member |
578: | ) { |
579: | $vars = [$var = $var->withName(null)]; |
580: | $key = 0; |
581: | } |
582: | if ($key === 0 && ($varDesc = $var->getDescription()) !== null) { |
583: | $vars = [$var->withDescription(null)]; |
584: | if ($this->Summary === null) { |
585: | $summary = $varDesc; |
586: | } elseif ($this->Summary !== $varDesc) { |
587: | $description = Arr::implode("\n\n", [ |
588: | $this->Description, |
589: | $varDesc, |
590: | ], ''); |
591: | } |
592: | } |
593: | } |
594: | |
595: | return $this |
596: | ->with('Summary', $summary ?? $this->Summary) |
597: | ->with('Description', $description ?? $this->Description) |
598: | ->with('Tags', $tags) |
599: | ->with('Vars', $vars); |
600: | } |
601: | |
602: | |
603: | |
604: | |
605: | |
606: | |
607: | |
608: | public function flatten() |
609: | { |
610: | $phpDoc = clone $this; |
611: | $phpDoc->Original = $phpDoc; |
612: | $phpDoc->InheritedTemplates = []; |
613: | |
614: | return $phpDoc; |
615: | } |
616: | |
617: | |
618: | |
619: | |
620: | private function handlePropertyChanged(string $property): void |
621: | { |
622: | $tag = [ |
623: | 'Params' => 'param', |
624: | 'Return' => 'return', |
625: | 'Vars' => 'var', |
626: | 'Methods' => 'method', |
627: | 'Properties' => 'property', |
628: | 'Templates' => 'template', |
629: | ][$property] ?? null; |
630: | |
631: | if ($tag !== null) { |
632: | $this->updateTags($tag); |
633: | } |
634: | } |
635: | |
636: | private function updateTags(?string $tag = null): void |
637: | { |
638: | if ($tag === 'param' || $tag === null) { |
639: | if ($this->Params) { |
640: | $this->Tags['param'] = array_values($this->Params); |
641: | } else { |
642: | unset($this->Tags['param']); |
643: | } |
644: | } |
645: | |
646: | if ($tag === 'return' || $tag === null) { |
647: | if ($this->Return) { |
648: | $this->Tags['return'] = [$this->Return]; |
649: | } else { |
650: | unset($this->Tags['return']); |
651: | } |
652: | } |
653: | |
654: | if ($tag === 'var' || $tag === null) { |
655: | if ($this->Vars) { |
656: | $this->Tags['var'] = array_values($this->Vars); |
657: | } else { |
658: | unset($this->Tags['var']); |
659: | } |
660: | } |
661: | |
662: | if ($tag === 'method' || $tag === null) { |
663: | if ($this->Methods) { |
664: | $this->Tags['method'] = array_values($this->Methods); |
665: | } else { |
666: | unset($this->Tags['method']); |
667: | } |
668: | } |
669: | |
670: | if ($tag === 'property' || $tag === null) { |
671: | if ($this->Properties) { |
672: | $this->Tags['property'] = array_values($this->Properties); |
673: | } else { |
674: | unset($this->Tags['property']); |
675: | } |
676: | } |
677: | |
678: | if ($tag === 'template' || $tag === null) { |
679: | if ($templates = $this->getTemplates(false)) { |
680: | $this->Tags['template'] = array_values($templates); |
681: | } else { |
682: | unset($this->Tags['template']); |
683: | } |
684: | } |
685: | } |
686: | |
687: | |
688: | |
689: | |
690: | |
691: | |
692: | public function getOriginal() |
693: | { |
694: | return $this->Original; |
695: | } |
696: | |
697: | |
698: | |
699: | |
700: | public function getSummary(): ?string |
701: | { |
702: | return $this->Summary; |
703: | } |
704: | |
705: | |
706: | |
707: | |
708: | public function getDescription(): ?string |
709: | { |
710: | return $this->Description; |
711: | } |
712: | |
713: | |
714: | |
715: | |
716: | public function hasTag(string $name): bool |
717: | { |
718: | return isset($this->Tags[$name]); |
719: | } |
720: | |
721: | |
722: | |
723: | |
724: | |
725: | |
726: | public function getTags(): array |
727: | { |
728: | return $this->Tags; |
729: | } |
730: | |
731: | |
732: | |
733: | |
734: | |
735: | |
736: | public function getParams(): array |
737: | { |
738: | return $this->Params; |
739: | } |
740: | |
741: | |
742: | |
743: | |
744: | |
745: | |
746: | public function hasReturn(): bool |
747: | { |
748: | return $this->Return !== null; |
749: | } |
750: | |
751: | |
752: | |
753: | |
754: | public function getReturn(): ?ReturnTag |
755: | { |
756: | return $this->Return; |
757: | } |
758: | |
759: | |
760: | |
761: | |
762: | |
763: | |
764: | public function getVars(): array |
765: | { |
766: | return $this->Vars; |
767: | } |
768: | |
769: | |
770: | |
771: | |
772: | |
773: | |
774: | public function getMethods(): array |
775: | { |
776: | return $this->Methods; |
777: | } |
778: | |
779: | |
780: | |
781: | |
782: | |
783: | |
784: | public function getProperties(): array |
785: | { |
786: | return $this->Properties; |
787: | } |
788: | |
789: | |
790: | |
791: | |
792: | |
793: | |
794: | public function getTemplates(bool $includeClass = true): array |
795: | { |
796: | if ( |
797: | $includeClass |
798: | || $this->Class === null |
799: | || $this->Member === null |
800: | ) { |
801: | return $this->Templates; |
802: | } |
803: | |
804: | foreach ($this->Templates as $name => $template) { |
805: | if ($template->getMember() !== null) { |
806: | $templates[$name] = $template; |
807: | } |
808: | } |
809: | |
810: | return $templates ?? []; |
811: | } |
812: | |
813: | |
814: | |
815: | |
816: | |
817: | |
818: | public function getClassTemplates(): array |
819: | { |
820: | foreach ($this->Templates as $name => $template) { |
821: | if ($template->getClass() !== null && $template->getMember() === null) { |
822: | $templates[$name] = $template; |
823: | } |
824: | } |
825: | |
826: | return $templates ?? []; |
827: | } |
828: | |
829: | |
830: | |
831: | |
832: | |
833: | |
834: | public function getTagTemplates(AbstractTag $tag): array |
835: | { |
836: | if (!in_array($tag, $this->Tags[$tag->getTag()] ?? [], true)) { |
837: | throw new InvalidArgumentException('Tag does not belong to the PHPDoc'); |
838: | } |
839: | |
840: | $class = $tag->getClass(); |
841: | if ( |
842: | $class === $this->Class |
843: | || $class === null |
844: | || $this->Class === null |
845: | ) { |
846: | return $this->Templates; |
847: | } |
848: | |
849: | return $this->InheritedTemplates[$class] ?? []; |
850: | } |
851: | |
852: | |
853: | |
854: | |
855: | |
856: | |
857: | public function hasErrors(): bool |
858: | { |
859: | return (bool) $this->Errors; |
860: | } |
861: | |
862: | |
863: | |
864: | |
865: | |
866: | |
867: | public function getErrors(): array |
868: | { |
869: | return $this->Errors; |
870: | } |
871: | |
872: | |
873: | |
874: | |
875: | |
876: | |
877: | public function getClass(): ?string |
878: | { |
879: | return $this->Class; |
880: | } |
881: | |
882: | |
883: | |
884: | |
885: | public function getMember(): ?string |
886: | { |
887: | return $this->Member; |
888: | } |
889: | |
890: | |
891: | |
892: | |
893: | |
894: | |
895: | public function getStatic(): ?string |
896: | { |
897: | return $this->Static; |
898: | } |
899: | |
900: | |
901: | |
902: | |
903: | |
904: | |
905: | public function getSelf(): ?string |
906: | { |
907: | return $this->Self; |
908: | } |
909: | |
910: | |
911: | |
912: | |
913: | public function isEmpty(): bool |
914: | { |
915: | return $this->Original->Summary === null |
916: | && $this->Original->Description === null |
917: | && !$this->Original->Tags; |
918: | } |
919: | |
920: | |
921: | |
922: | |
923: | |
924: | public function hasDetail(): bool |
925: | { |
926: | if ($this->Description !== null) { |
927: | return true; |
928: | } |
929: | |
930: | foreach ([$this->Params, [$this->Return], $this->Vars] as $tags) { |
931: | foreach ($tags as $tag) { |
932: | if ( |
933: | $tag |
934: | && ($description = $tag->getDescription()) !== null |
935: | && $description !== $this->Summary |
936: | ) { |
937: | return true; |
938: | } |
939: | } |
940: | } |
941: | |
942: | foreach (array_diff( |
943: | array_keys($this->Tags), |
944: | static::STANDARD_TAGS, |
945: | ) as $key) { |
946: | if (!Regex::match('/^(?:phpstan|psalm)-|^disregard$/D', $key)) { |
947: | return true; |
948: | } |
949: | } |
950: | |
951: | return false; |
952: | } |
953: | |
954: | |
955: | |
956: | |
957: | public function __toString(): string |
958: | { |
959: | $tags = Arr::flatten(Arr::unset($this->Tags, 'template')); |
960: | |
961: | $text = Arr::implode("\n\n", [ |
962: | $this->Summary, |
963: | $this->Description, |
964: | Arr::implode("\n", $this->getTemplates(false), ''), |
965: | Arr::implode("\n", $tags, ''), |
966: | ], ''); |
967: | |
968: | if ($text === '') { |
969: | return '/** */'; |
970: | } |
971: | |
972: | return "/**\n * " . Regex::replace( |
973: | ["/\n(?!\n)/", "/\n(?=\n)/"], |
974: | ["\n * ", "\n *"], |
975: | $text, |
976: | ) . "\n */"; |
977: | } |
978: | |
979: | |
980: | |
981: | |
982: | |
983: | |
984: | |
985: | |
986: | |
987: | |
988: | |
989: | |
990: | public static function fromDocBlocks( |
991: | array $docBlocks, |
992: | ?array $classDocBlocks = null, |
993: | ?string $member = null, |
994: | ?string $static = null, |
995: | array $aliases = [] |
996: | ): self { |
997: | foreach ($docBlocks as $self => $group) { |
998: | if (!is_array($group)) { |
999: | $group = [$self => $group]; |
1000: | } |
1001: | foreach ($group as $class => $docBlock) { |
1002: | $_phpDoc = new static( |
1003: | $docBlock, |
1004: | $classDocBlocks[$self] ?? null, |
1005: | is_string($class) ? $class : null, |
1006: | $member, |
1007: | $static, |
1008: | is_string($self) ? $self : null, |
1009: | $aliases, |
1010: | ); |
1011: | |
1012: | $phpDoc ??= $_phpDoc; |
1013: | if ($_phpDoc !== $phpDoc) { |
1014: | $phpDoc = $phpDoc->inherit($_phpDoc); |
1015: | } |
1016: | } |
1017: | } |
1018: | |
1019: | return $phpDoc ?? new static(); |
1020: | } |
1021: | |
1022: | |
1023: | |
1024: | |
1025: | |
1026: | private function parse(string $content, array $aliases, ?int &$tags): void |
1027: | { |
1028: | |
1029: | |
1030: | |
1031: | $this->Lines = Regex::split( |
1032: | '/\h*+(?:\r\n|\n|\r)/', |
1033: | trim(Regex::replace('/(?<=\r\n|\n|\r)\h*+\* ?/', '', $content)), |
1034: | ); |
1035: | $this->NextLine = Arr::first($this->Lines); |
1036: | |
1037: | if (!Regex::match(self::PHPDOC_TAG, $this->NextLine)) { |
1038: | $this->Summary = Str::coalesce( |
1039: | $this->getLinesUntil(self::BLANK_OR_PHPDOC_TAG, true), |
1040: | null, |
1041: | ); |
1042: | |
1043: | if ( |
1044: | $this->NextLine !== null |
1045: | && !Regex::match(self::PHPDOC_TAG, $this->NextLine) |
1046: | ) { |
1047: | $this->Description = Str::coalesce( |
1048: | trim($this->getLinesUntil(self::PHPDOC_TAG)), |
1049: | null, |
1050: | ); |
1051: | } |
1052: | } |
1053: | |
1054: | $tags = 0; |
1055: | while ($this->Lines && Regex::match( |
1056: | self::PHPDOC_TAG, |
1057: | $text = $this->getLinesUntil(self::PHPDOC_TAG), |
1058: | $matches, |
1059: | )) { |
1060: | |
1061: | $text = trim(substr($text, strlen($matches[0]))); |
1062: | $tag = ltrim($matches['tag'], '\\'); |
1063: | |
1064: | try { |
1065: | switch ($tag) { |
1066: | |
1067: | case 'param': |
1068: | if (!Regex::match(self::PARAM_TAG, $text, $matches, \PREG_UNMATCHED_AS_NULL)) { |
1069: | $this->throw('Invalid syntax', $tag); |
1070: | } |
1071: | |
1072: | $name = $matches['param_name']; |
1073: | $this->Params[$name] = new ParamTag( |
1074: | $name, |
1075: | $matches['param_type'], |
1076: | $matches['param_reference'] !== null, |
1077: | $matches['param_variadic'] !== null, |
1078: | $matches['param_description'], |
1079: | $this->Class, |
1080: | $this->Member, |
1081: | $this->Static, |
1082: | $this->Self, |
1083: | $aliases, |
1084: | ); |
1085: | break; |
1086: | |
1087: | |
1088: | case 'return': |
1089: | if (!Regex::match(self::RETURN_TAG, $text, $matches, \PREG_UNMATCHED_AS_NULL)) { |
1090: | $this->throw('Invalid syntax', $tag); |
1091: | } |
1092: | |
1093: | $type = $matches['return_type']; |
1094: | $this->Return = new ReturnTag( |
1095: | $type, |
1096: | $matches['return_description'], |
1097: | $this->Class, |
1098: | $this->Member, |
1099: | $this->Static, |
1100: | $this->Self, |
1101: | $aliases, |
1102: | ); |
1103: | break; |
1104: | |
1105: | |
1106: | case 'var': |
1107: | if (!Regex::match(self::VAR_TAG, $text, $matches, \PREG_UNMATCHED_AS_NULL)) { |
1108: | $this->throw('Invalid syntax', $tag); |
1109: | } |
1110: | |
1111: | $type = $matches['var_type']; |
1112: | $name = $matches['var_name']; |
1113: | $var = new VarTag( |
1114: | $type, |
1115: | $name, |
1116: | $matches['var_description'], |
1117: | $this->Class, |
1118: | $this->Member, |
1119: | $this->Static, |
1120: | $this->Self, |
1121: | $aliases, |
1122: | ); |
1123: | if ($name !== null) { |
1124: | $this->Vars[$name] = $var; |
1125: | } else { |
1126: | $this->Vars[] = $var; |
1127: | } |
1128: | break; |
1129: | |
1130: | |
1131: | case 'method': |
1132: | if (!Regex::match(self::METHOD_TAG, $text, $matches, \PREG_UNMATCHED_AS_NULL)) { |
1133: | $this->throw('Invalid syntax', $tag); |
1134: | } |
1135: | |
1136: | $name = $matches['method_name']; |
1137: | $isStatic = $matches['method_static'] !== null; |
1138: | $type = $matches['method_type']; |
1139: | if ($isStatic && $type === null) { |
1140: | $isStatic = false; |
1141: | $type = 'static'; |
1142: | } |
1143: | $params = []; |
1144: | if ($matches['method_params'] !== null) { |
1145: | foreach (Str::splitDelimited( |
1146: | ',', |
1147: | $matches['method_params'], |
1148: | false, |
1149: | null, |
1150: | Str::PRESERVE_QUOTED, |
1151: | ) as $param) { |
1152: | if (!Regex::match(self::METHOD_PARAM, $param, $paramMatches, \PREG_UNMATCHED_AS_NULL)) { |
1153: | |
1154: | throw new ShouldNotHappenException(sprintf( |
1155: | '@method parameter parsing failed: %s', |
1156: | $text, |
1157: | )); |
1158: | |
1159: | } |
1160: | |
1161: | $_name = $paramMatches['param_name']; |
1162: | $params[$_name] = new MethodParam( |
1163: | $_name, |
1164: | $paramMatches['param_type'], |
1165: | $paramMatches['param_default'], |
1166: | $paramMatches['param_variadic'] !== null, |
1167: | ); |
1168: | } |
1169: | } |
1170: | $this->Methods[$name] = new MethodTag( |
1171: | $name, |
1172: | $type, |
1173: | $params, |
1174: | $isStatic, |
1175: | $matches['method_description'], |
1176: | $this->Class, |
1177: | $this->Member, |
1178: | $this->Static, |
1179: | $this->Self, |
1180: | $aliases, |
1181: | ); |
1182: | break; |
1183: | |
1184: | |
1185: | case 'property-read': |
1186: | case 'property-write': |
1187: | case 'property': |
1188: | if (!Regex::match(self::PROPERTY_TAG, $text, $matches, \PREG_UNMATCHED_AS_NULL)) { |
1189: | $this->throw('Invalid syntax', $tag); |
1190: | } |
1191: | |
1192: | $name = $matches['property_name']; |
1193: | $this->Properties[$name] = new PropertyTag( |
1194: | $name, |
1195: | $matches['property_type'], |
1196: | $tag === 'property-read', |
1197: | $tag === 'property-write', |
1198: | $matches['property_description'], |
1199: | $this->Class, |
1200: | $this->Member, |
1201: | $this->Static, |
1202: | $this->Self, |
1203: | $aliases, |
1204: | ); |
1205: | $tag = 'property'; |
1206: | break; |
1207: | |
1208: | |
1209: | case 'template-covariant': |
1210: | case 'template-contravariant': |
1211: | case 'template': |
1212: | if (!Regex::match(self::TEMPLATE_TAG, $text, $matches, \PREG_UNMATCHED_AS_NULL)) { |
1213: | $this->throw('Invalid syntax', $tag); |
1214: | } |
1215: | |
1216: | $name = $matches['template_name']; |
1217: | $this->Templates[$name] = new TemplateTag( |
1218: | $name, |
1219: | $matches['template_type'], |
1220: | $matches['template_default'], |
1221: | $tag === 'template-covariant', |
1222: | $tag === 'template-contravariant', |
1223: | $this->Class, |
1224: | $this->Member, |
1225: | $this->Static, |
1226: | $this->Self, |
1227: | $aliases, |
1228: | ); |
1229: | $tag = 'template'; |
1230: | break; |
1231: | |
1232: | default: |
1233: | $this->Tags[$tag][] = new GenericTag( |
1234: | $tag, |
1235: | Str::coalesce($text, null), |
1236: | $this->Class, |
1237: | $this->Member, |
1238: | ); |
1239: | continue 2; |
1240: | } |
1241: | } catch (InvalidArgumentException $ex) { |
1242: | $this->Errors[] = new ErrorTag( |
1243: | $tag, |
1244: | $ex->getMessage(), |
1245: | Str::coalesce($text, null), |
1246: | $this->Class, |
1247: | $this->Member, |
1248: | ); |
1249: | continue; |
1250: | } |
1251: | |
1252: | $this->Tags[$tag] ??= []; |
1253: | $tags++; |
1254: | } |
1255: | |
1256: | unset($this->Lines, $this->NextLine); |
1257: | } |
1258: | |
1259: | |
1260: | |
1261: | |
1262: | |
1263: | |
1264: | |
1265: | |
1266: | |
1267: | |
1268: | private function getLinesUntil(string $pattern, bool $unwrap = false): string |
1269: | { |
1270: | if (!$this->Lines) { |
1271: | |
1272: | throw new ShouldNotHappenException('No more lines'); |
1273: | |
1274: | } |
1275: | |
1276: | $lines = []; |
1277: | $inFence = false; |
1278: | |
1279: | do { |
1280: | $lines[] = $line = array_shift($this->Lines); |
1281: | $this->NextLine = Arr::first($this->Lines); |
1282: | |
1283: | if (!$unwrap) { |
1284: | if ( |
1285: | (!$inFence && Regex::match('/^(```+|~~~+)/', $line, $fence)) |
1286: | || ($inFence && isset($fence[0]) && $line === $fence[0]) |
1287: | ) { |
1288: | $inFence = !$inFence; |
1289: | } |
1290: | |
1291: | if ($inFence) { |
1292: | continue; |
1293: | } |
1294: | } |
1295: | |
1296: | if ($this->NextLine === null) { |
1297: | break; |
1298: | } |
1299: | |
1300: | if (Regex::match($pattern, $this->NextLine)) { |
1301: | break; |
1302: | } |
1303: | } while ($this->Lines); |
1304: | |
1305: | if ($inFence && isset($fence[0])) { |
1306: | $lines[] = $fence[0]; |
1307: | } |
1308: | |
1309: | return implode($unwrap ? ' ' : "\n", $lines); |
1310: | } |
1311: | |
1312: | |
1313: | |
1314: | |
1315: | |
1316: | private function throw(string $message, ?string $tag, ...$args): void |
1317: | { |
1318: | if ($tag !== null) { |
1319: | $message .= ' for @%s'; |
1320: | $args[] = $tag; |
1321: | } |
1322: | |
1323: | $message .= ' in DocBlock'; |
1324: | |
1325: | if (isset($this->Class)) { |
1326: | $message .= ' of %s'; |
1327: | $args[] = $this->Class; |
1328: | if (isset($this->Member)) { |
1329: | $message .= '::%s'; |
1330: | $args[] = $this->Member; |
1331: | } |
1332: | } |
1333: | |
1334: | throw new InvalidArgumentException(sprintf($message, ...$args)); |
1335: | } |
1336: | } |
1337: | |