1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\PhpDoc;
4:
5: use PHPStan\Analyser\NameScope;
6: use PHPStan\PhpDoc\Tag\AssertTag;
7: use PHPStan\PhpDoc\Tag\DeprecatedTag;
8: use PHPStan\PhpDoc\Tag\ExtendsTag;
9: use PHPStan\PhpDoc\Tag\ImplementsTag;
10: use PHPStan\PhpDoc\Tag\MethodTag;
11: use PHPStan\PhpDoc\Tag\MixinTag;
12: use PHPStan\PhpDoc\Tag\ParamOutTag;
13: use PHPStan\PhpDoc\Tag\ParamTag;
14: use PHPStan\PhpDoc\Tag\PropertyTag;
15: use PHPStan\PhpDoc\Tag\ReturnTag;
16: use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
17: use PHPStan\PhpDoc\Tag\TemplateTag;
18: use PHPStan\PhpDoc\Tag\ThrowsTag;
19: use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
20: use PHPStan\PhpDoc\Tag\TypeAliasTag;
21: use PHPStan\PhpDoc\Tag\TypedTag;
22: use PHPStan\PhpDoc\Tag\UsesTag;
23: use PHPStan\PhpDoc\Tag\VarTag;
24: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
25: use PHPStan\Type\ConditionalTypeForParameter;
26: use PHPStan\Type\Generic\TemplateTypeHelper;
27: use PHPStan\Type\Generic\TemplateTypeMap;
28: use PHPStan\Type\Type;
29: use PHPStan\Type\TypeTraverser;
30: use function array_key_exists;
31: use function array_map;
32: use function count;
33: use function is_bool;
34: use function substr;
35:
36: /** @api */
37: class ResolvedPhpDocBlock
38: {
39:
40: public const EMPTY_DOC_STRING = '/** */';
41:
42: private PhpDocNode $phpDocNode;
43:
44: /** @var PhpDocNode[] */
45: private array $phpDocNodes;
46:
47: private string $phpDocString;
48:
49: private ?string $filename;
50:
51: private ?NameScope $nameScope = null;
52:
53: private TemplateTypeMap $templateTypeMap;
54:
55: /** @var array<string, TemplateTag> */
56: private array $templateTags;
57:
58: private PhpDocNodeResolver $phpDocNodeResolver;
59:
60: /** @var array<(string|int), VarTag>|false */
61: private array|false $varTags = false;
62:
63: /** @var array<string, MethodTag>|false */
64: private array|false $methodTags = false;
65:
66: /** @var array<string, PropertyTag>|false */
67: private array|false $propertyTags = false;
68:
69: /** @var array<string, ExtendsTag>|false */
70: private array|false $extendsTags = false;
71:
72: /** @var array<string, ImplementsTag>|false */
73: private array|false $implementsTags = false;
74:
75: /** @var array<string, UsesTag>|false */
76: private array|false $usesTags = false;
77:
78: /** @var array<string, ParamTag>|false */
79: private array|false $paramTags = false;
80:
81: /** @var array<string, ParamOutTag>|false */
82: private array|false $paramOutTags = false;
83:
84: private ReturnTag|false|null $returnTag = false;
85:
86: private ThrowsTag|false|null $throwsTag = false;
87:
88: /** @var array<MixinTag>|false */
89: private array|false $mixinTags = false;
90:
91: /** @var array<TypeAliasTag>|false */
92: private array|false $typeAliasTags = false;
93:
94: /** @var array<TypeAliasImportTag>|false */
95: private array|false $typeAliasImportTags = false;
96:
97: /** @var array<AssertTag>|false */
98: private array|false $assertTags = false;
99:
100: private SelfOutTypeTag|false|null $selfOutTypeTag = false;
101:
102: private DeprecatedTag|false|null $deprecatedTag = false;
103:
104: private ?bool $isDeprecated = null;
105:
106: private ?bool $isInternal = null;
107:
108: private ?bool $isFinal = null;
109:
110: /** @var bool|'notLoaded'|null */
111: private bool|string|null $isPure = 'notLoaded';
112:
113: private ?bool $isReadOnly = null;
114:
115: private ?bool $isImmutable = null;
116:
117: private ?bool $isAllowedPrivateMutation = null;
118:
119: private ?bool $hasConsistentConstructor = null;
120:
121: private ?bool $acceptsNamedArguments = null;
122:
123: private function __construct()
124: {
125: }
126:
127: /**
128: * @param TemplateTag[] $templateTags
129: */
130: public static function create(
131: PhpDocNode $phpDocNode,
132: string $phpDocString,
133: ?string $filename,
134: NameScope $nameScope,
135: TemplateTypeMap $templateTypeMap,
136: array $templateTags,
137: PhpDocNodeResolver $phpDocNodeResolver,
138: ): self
139: {
140: // new property also needs to be added to createEmpty() and merge()
141: $self = new self();
142: $self->phpDocNode = $phpDocNode;
143: $self->phpDocNodes = [$phpDocNode];
144: $self->phpDocString = $phpDocString;
145: $self->filename = $filename;
146: $self->nameScope = $nameScope;
147: $self->templateTypeMap = $templateTypeMap;
148: $self->templateTags = $templateTags;
149: $self->phpDocNodeResolver = $phpDocNodeResolver;
150:
151: return $self;
152: }
153:
154: public static function createEmpty(): self
155: {
156: // new property also needs to be added to merge()
157: $self = new self();
158: $self->phpDocString = self::EMPTY_DOC_STRING;
159: $self->phpDocNodes = [];
160: $self->filename = null;
161: $self->templateTypeMap = TemplateTypeMap::createEmpty();
162: $self->templateTags = [];
163: $self->varTags = [];
164: $self->methodTags = [];
165: $self->propertyTags = [];
166: $self->extendsTags = [];
167: $self->implementsTags = [];
168: $self->usesTags = [];
169: $self->paramTags = [];
170: $self->paramOutTags = [];
171: $self->returnTag = null;
172: $self->throwsTag = null;
173: $self->mixinTags = [];
174: $self->typeAliasTags = [];
175: $self->typeAliasImportTags = [];
176: $self->assertTags = [];
177: $self->selfOutTypeTag = null;
178: $self->deprecatedTag = null;
179: $self->isDeprecated = false;
180: $self->isInternal = false;
181: $self->isFinal = false;
182: $self->isPure = null;
183: $self->isReadOnly = false;
184: $self->isImmutable = false;
185: $self->isAllowedPrivateMutation = false;
186: $self->hasConsistentConstructor = false;
187: $self->acceptsNamedArguments = true;
188:
189: return $self;
190: }
191:
192: /**
193: * @param array<int, self> $parents
194: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
195: */
196: public function merge(array $parents, array $parentPhpDocBlocks): self
197: {
198: // new property also needs to be added to createEmpty()
199: $result = new self();
200: // we will resolve everything on $this here so these properties don't have to be populated
201: // skip $result->phpDocNode
202: $phpDocNodes = $this->phpDocNodes;
203: $acceptsNamedArguments = $this->acceptsNamedArguments();
204: foreach ($parents as $parent) {
205: foreach ($parent->phpDocNodes as $phpDocNode) {
206: $phpDocNodes[] = $phpDocNode;
207: $acceptsNamedArguments = $acceptsNamedArguments && $parent->acceptsNamedArguments();
208: }
209: }
210: $result->phpDocNodes = $phpDocNodes;
211: $result->phpDocString = $this->phpDocString;
212: $result->filename = $this->filename;
213: // skip $result->nameScope
214: $result->templateTypeMap = $this->templateTypeMap;
215: $result->templateTags = $this->templateTags;
216: // skip $result->phpDocNodeResolver
217: $result->varTags = self::mergeVarTags($this->getVarTags(), $parents, $parentPhpDocBlocks);
218: $result->methodTags = $this->getMethodTags();
219: $result->propertyTags = $this->getPropertyTags();
220: $result->extendsTags = $this->getExtendsTags();
221: $result->implementsTags = $this->getImplementsTags();
222: $result->usesTags = $this->getUsesTags();
223: $result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks);
224: $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks);
225: $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $parents, $parentPhpDocBlocks);
226: $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents);
227: $result->mixinTags = $this->getMixinTags();
228: $result->typeAliasTags = $this->getTypeAliasTags();
229: $result->typeAliasImportTags = $this->getTypeAliasImportTags();
230: $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks);
231: $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents);
232: $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $parents);
233: $result->isDeprecated = $result->deprecatedTag !== null;
234: $result->isInternal = $this->isInternal();
235: $result->isFinal = $this->isFinal();
236: $result->isPure = $this->isPure();
237: $result->isReadOnly = $this->isReadOnly();
238: $result->isImmutable = $this->isImmutable();
239: $result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation();
240: $result->hasConsistentConstructor = $this->hasConsistentConstructor();
241: $result->acceptsNamedArguments = $acceptsNamedArguments;
242:
243: return $result;
244: }
245:
246: /**
247: * @param array<string, string> $parameterNameMapping
248: */
249: public function changeParameterNamesByMapping(array $parameterNameMapping): self
250: {
251: if (count($this->phpDocNodes) === 0) {
252: return $this;
253: }
254:
255: $paramTags = $this->getParamTags();
256:
257: $newParamTags = [];
258: foreach ($paramTags as $key => $paramTag) {
259: if (!array_key_exists($key, $parameterNameMapping)) {
260: continue;
261: }
262: $newParamTags[$parameterNameMapping[$key]] = $paramTag;
263: }
264:
265: $paramOutTags = $this->getParamOutTags();
266:
267: $newParamOutTags = [];
268: foreach ($paramOutTags as $key => $paramOutTag) {
269: if (!array_key_exists($key, $parameterNameMapping)) {
270: continue;
271: }
272: $newParamOutTags[$parameterNameMapping[$key]] = $paramOutTag;
273: }
274:
275: $returnTag = $this->getReturnTag();
276: if ($returnTag !== null) {
277: $transformedType = TypeTraverser::map($returnTag->getType(), static function (Type $type, callable $traverse) use ($parameterNameMapping): Type {
278: if ($type instanceof ConditionalTypeForParameter) {
279: $parameterName = substr($type->getParameterName(), 1);
280: if (array_key_exists($parameterName, $parameterNameMapping)) {
281: $type = $type->changeParameterName('$' . $parameterNameMapping[$parameterName]);
282: }
283: }
284:
285: return $traverse($type);
286: });
287: $returnTag = $returnTag->withType($transformedType);
288: }
289:
290: $assertTags = $this->getAssertTags();
291: if (count($assertTags) > 0) {
292: $assertTags = array_map(static function (AssertTag $tag) use ($parameterNameMapping): AssertTag {
293: $parameterName = substr($tag->getParameter()->getParameterName(), 1);
294: if (array_key_exists($parameterName, $parameterNameMapping)) {
295: $tag = $tag->withParameter($tag->getParameter()->changeParameterName('$' . $parameterNameMapping[$parameterName]));
296: }
297: return $tag;
298: }, $assertTags);
299: }
300:
301: $self = new self();
302: $self->phpDocNode = $this->phpDocNode;
303: $self->phpDocNodes = $this->phpDocNodes;
304: $self->phpDocString = $this->phpDocString;
305: $self->filename = $this->filename;
306: $self->nameScope = $this->nameScope;
307: $self->templateTypeMap = $this->templateTypeMap;
308: $self->templateTags = $this->templateTags;
309: $self->phpDocNodeResolver = $this->phpDocNodeResolver;
310: $self->varTags = $this->varTags;
311: $self->methodTags = $this->methodTags;
312: $self->propertyTags = $this->propertyTags;
313: $self->extendsTags = $this->extendsTags;
314: $self->implementsTags = $this->implementsTags;
315: $self->usesTags = $this->usesTags;
316: $self->paramTags = $newParamTags;
317: $self->paramOutTags = $newParamOutTags;
318: $self->returnTag = $returnTag;
319: $self->throwsTag = $this->throwsTag;
320: $self->mixinTags = $this->mixinTags;
321: $self->typeAliasTags = $this->typeAliasTags;
322: $self->typeAliasImportTags = $this->typeAliasImportTags;
323: $self->assertTags = $assertTags;
324: $self->selfOutTypeTag = $this->selfOutTypeTag;
325: $self->deprecatedTag = $this->deprecatedTag;
326: $self->isDeprecated = $this->isDeprecated;
327: $self->isInternal = $this->isInternal;
328: $self->isFinal = $this->isFinal;
329: $self->isPure = $this->isPure;
330:
331: return $self;
332: }
333:
334: public function hasPhpDocString(): bool
335: {
336: return $this->phpDocString !== self::EMPTY_DOC_STRING;
337: }
338:
339: public function getPhpDocString(): string
340: {
341: return $this->phpDocString;
342: }
343:
344: /**
345: * @return PhpDocNode[]
346: */
347: public function getPhpDocNodes(): array
348: {
349: return $this->phpDocNodes;
350: }
351:
352: public function getFilename(): ?string
353: {
354: return $this->filename;
355: }
356:
357: private function getNameScope(): NameScope
358: {
359: return $this->nameScope;
360: }
361:
362: public function getNullableNameScope(): ?NameScope
363: {
364: return $this->nameScope;
365: }
366:
367: /**
368: * @return array<(string|int), VarTag>
369: */
370: public function getVarTags(): array
371: {
372: if ($this->varTags === false) {
373: $this->varTags = $this->phpDocNodeResolver->resolveVarTags(
374: $this->phpDocNode,
375: $this->getNameScope(),
376: );
377: }
378: return $this->varTags;
379: }
380:
381: /**
382: * @return array<string, MethodTag>
383: */
384: public function getMethodTags(): array
385: {
386: if ($this->methodTags === false) {
387: $this->methodTags = $this->phpDocNodeResolver->resolveMethodTags(
388: $this->phpDocNode,
389: $this->getNameScope(),
390: );
391: }
392: return $this->methodTags;
393: }
394:
395: /**
396: * @return array<string, PropertyTag>
397: */
398: public function getPropertyTags(): array
399: {
400: if ($this->propertyTags === false) {
401: $this->propertyTags = $this->phpDocNodeResolver->resolvePropertyTags(
402: $this->phpDocNode,
403: $this->getNameScope(),
404: );
405: }
406: return $this->propertyTags;
407: }
408:
409: /**
410: * @return array<string, TemplateTag>
411: */
412: public function getTemplateTags(): array
413: {
414: return $this->templateTags;
415: }
416:
417: /**
418: * @return array<string, ExtendsTag>
419: */
420: public function getExtendsTags(): array
421: {
422: if ($this->extendsTags === false) {
423: $this->extendsTags = $this->phpDocNodeResolver->resolveExtendsTags(
424: $this->phpDocNode,
425: $this->getNameScope(),
426: );
427: }
428: return $this->extendsTags;
429: }
430:
431: /**
432: * @return array<string, ImplementsTag>
433: */
434: public function getImplementsTags(): array
435: {
436: if ($this->implementsTags === false) {
437: $this->implementsTags = $this->phpDocNodeResolver->resolveImplementsTags(
438: $this->phpDocNode,
439: $this->getNameScope(),
440: );
441: }
442: return $this->implementsTags;
443: }
444:
445: /**
446: * @return array<string, UsesTag>
447: */
448: public function getUsesTags(): array
449: {
450: if ($this->usesTags === false) {
451: $this->usesTags = $this->phpDocNodeResolver->resolveUsesTags(
452: $this->phpDocNode,
453: $this->getNameScope(),
454: );
455: }
456: return $this->usesTags;
457: }
458:
459: /**
460: * @return array<string, ParamTag>
461: */
462: public function getParamTags(): array
463: {
464: if ($this->paramTags === false) {
465: $this->paramTags = $this->phpDocNodeResolver->resolveParamTags(
466: $this->phpDocNode,
467: $this->getNameScope(),
468: );
469: }
470: return $this->paramTags;
471: }
472:
473: /**
474: * @return array<string, ParamOutTag>
475: */
476: public function getParamOutTags(): array
477: {
478: if ($this->paramOutTags === false) {
479: $this->paramOutTags = $this->phpDocNodeResolver->resolveParamOutTags(
480: $this->phpDocNode,
481: $this->getNameScope(),
482: );
483: }
484: return $this->paramOutTags;
485: }
486:
487: public function getReturnTag(): ?ReturnTag
488: {
489: if (is_bool($this->returnTag)) {
490: $this->returnTag = $this->phpDocNodeResolver->resolveReturnTag(
491: $this->phpDocNode,
492: $this->getNameScope(),
493: );
494: }
495: return $this->returnTag;
496: }
497:
498: public function getThrowsTag(): ?ThrowsTag
499: {
500: if (is_bool($this->throwsTag)) {
501: $this->throwsTag = $this->phpDocNodeResolver->resolveThrowsTags(
502: $this->phpDocNode,
503: $this->getNameScope(),
504: );
505: }
506: return $this->throwsTag;
507: }
508:
509: /**
510: * @return array<MixinTag>
511: */
512: public function getMixinTags(): array
513: {
514: if ($this->mixinTags === false) {
515: $this->mixinTags = $this->phpDocNodeResolver->resolveMixinTags(
516: $this->phpDocNode,
517: $this->getNameScope(),
518: );
519: }
520:
521: return $this->mixinTags;
522: }
523:
524: /**
525: * @return array<TypeAliasTag>
526: */
527: public function getTypeAliasTags(): array
528: {
529: if ($this->typeAliasTags === false) {
530: $this->typeAliasTags = $this->phpDocNodeResolver->resolveTypeAliasTags(
531: $this->phpDocNode,
532: $this->getNameScope(),
533: );
534: }
535:
536: return $this->typeAliasTags;
537: }
538:
539: /**
540: * @return array<TypeAliasImportTag>
541: */
542: public function getTypeAliasImportTags(): array
543: {
544: if ($this->typeAliasImportTags === false) {
545: $this->typeAliasImportTags = $this->phpDocNodeResolver->resolveTypeAliasImportTags(
546: $this->phpDocNode,
547: $this->getNameScope(),
548: );
549: }
550:
551: return $this->typeAliasImportTags;
552: }
553:
554: /**
555: * @return array<AssertTag>
556: */
557: public function getAssertTags(): array
558: {
559: if ($this->assertTags === false) {
560: $this->assertTags = $this->phpDocNodeResolver->resolveAssertTags(
561: $this->phpDocNode,
562: $this->getNameScope(),
563: );
564: }
565:
566: return $this->assertTags;
567: }
568:
569: public function getSelfOutTag(): ?SelfOutTypeTag
570: {
571: if ($this->selfOutTypeTag === false) {
572: $this->selfOutTypeTag = $this->phpDocNodeResolver->resolveSelfOutTypeTag(
573: $this->phpDocNode,
574: $this->getNameScope(),
575: );
576: }
577:
578: return $this->selfOutTypeTag;
579: }
580:
581: public function getDeprecatedTag(): ?DeprecatedTag
582: {
583: if (is_bool($this->deprecatedTag)) {
584: $this->deprecatedTag = $this->phpDocNodeResolver->resolveDeprecatedTag(
585: $this->phpDocNode,
586: $this->getNameScope(),
587: );
588: }
589: return $this->deprecatedTag;
590: }
591:
592: public function isDeprecated(): bool
593: {
594: if ($this->isDeprecated === null) {
595: $this->isDeprecated = $this->phpDocNodeResolver->resolveIsDeprecated(
596: $this->phpDocNode,
597: );
598: }
599: return $this->isDeprecated;
600: }
601:
602: public function isInternal(): bool
603: {
604: if ($this->isInternal === null) {
605: $this->isInternal = $this->phpDocNodeResolver->resolveIsInternal(
606: $this->phpDocNode,
607: );
608: }
609: return $this->isInternal;
610: }
611:
612: public function isFinal(): bool
613: {
614: if ($this->isFinal === null) {
615: $this->isFinal = $this->phpDocNodeResolver->resolveIsFinal(
616: $this->phpDocNode,
617: );
618: }
619: return $this->isFinal;
620: }
621:
622: public function hasConsistentConstructor(): bool
623: {
624: if ($this->hasConsistentConstructor === null) {
625: $this->hasConsistentConstructor = $this->phpDocNodeResolver->resolveHasConsistentConstructor(
626: $this->phpDocNode,
627: );
628: }
629: return $this->hasConsistentConstructor;
630: }
631:
632: public function acceptsNamedArguments(): bool
633: {
634: if ($this->acceptsNamedArguments === null) {
635: $this->acceptsNamedArguments = $this->phpDocNodeResolver->resolveAcceptsNamedArguments(
636: $this->phpDocNode,
637: );
638: }
639: return $this->acceptsNamedArguments;
640: }
641:
642: public function getTemplateTypeMap(): TemplateTypeMap
643: {
644: return $this->templateTypeMap;
645: }
646:
647: public function isPure(): ?bool
648: {
649: if ($this->isPure === 'notLoaded') {
650: $pure = $this->phpDocNodeResolver->resolveIsPure(
651: $this->phpDocNode,
652: );
653: if ($pure) {
654: $this->isPure = true;
655: return $this->isPure;
656: } else {
657: $impure = $this->phpDocNodeResolver->resolveIsImpure(
658: $this->phpDocNode,
659: );
660: if ($impure) {
661: $this->isPure = false;
662: return $this->isPure;
663: }
664: }
665:
666: $this->isPure = null;
667: }
668:
669: return $this->isPure;
670: }
671:
672: public function isReadOnly(): bool
673: {
674: if ($this->isReadOnly === null) {
675: $this->isReadOnly = $this->phpDocNodeResolver->resolveIsReadOnly(
676: $this->phpDocNode,
677: );
678: }
679: return $this->isReadOnly;
680: }
681:
682: public function isImmutable(): bool
683: {
684: if ($this->isImmutable === null) {
685: $this->isImmutable = $this->phpDocNodeResolver->resolveIsImmutable(
686: $this->phpDocNode,
687: );
688: }
689: return $this->isImmutable;
690: }
691:
692: public function isAllowedPrivateMutation(): bool
693: {
694: if ($this->isAllowedPrivateMutation === null) {
695: $this->isAllowedPrivateMutation = $this->phpDocNodeResolver->resolveAllowPrivateMutation(
696: $this->phpDocNode,
697: );
698: }
699:
700: return $this->isAllowedPrivateMutation;
701: }
702:
703: /**
704: * @param array<string|int, VarTag> $varTags
705: * @param array<int, self> $parents
706: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
707: * @return array<string|int, VarTag>
708: */
709: private static function mergeVarTags(array $varTags, array $parents, array $parentPhpDocBlocks): array
710: {
711: // Only allow one var tag per comment. Check the parent if child does not have this tag.
712: if (count($varTags) > 0) {
713: return $varTags;
714: }
715:
716: foreach ($parents as $i => $parent) {
717: $result = self::mergeOneParentVarTags($parent, $parentPhpDocBlocks[$i]);
718: if ($result === null) {
719: continue;
720: }
721:
722: return $result;
723: }
724:
725: return [];
726: }
727:
728: /**
729: * @param ResolvedPhpDocBlock $parent
730: * @return array<string|int, VarTag>|null
731: */
732: private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array
733: {
734: foreach ($parent->getVarTags() as $key => $parentVarTag) {
735: return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock)];
736: }
737:
738: return null;
739: }
740:
741: /**
742: * @param array<string, ParamTag> $paramTags
743: * @param array<int, self> $parents
744: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
745: * @return array<string, ParamTag>
746: */
747: private static function mergeParamTags(array $paramTags, array $parents, array $parentPhpDocBlocks): array
748: {
749: foreach ($parents as $i => $parent) {
750: $paramTags = self::mergeOneParentParamTags($paramTags, $parent, $parentPhpDocBlocks[$i]);
751: }
752:
753: return $paramTags;
754: }
755:
756: /**
757: * @param array<string, ParamTag> $paramTags
758: * @param ResolvedPhpDocBlock $parent
759: * @return array<string, ParamTag>
760: */
761: private static function mergeOneParentParamTags(array $paramTags, self $parent, PhpDocBlock $phpDocBlock): array
762: {
763: $parentParamTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamTags());
764:
765: foreach ($parentParamTags as $name => $parentParamTag) {
766: if (array_key_exists($name, $paramTags)) {
767: continue;
768: }
769:
770: $paramTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock);
771: }
772:
773: return $paramTags;
774: }
775:
776: /**
777: * @param array<int, self> $parents
778: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
779: * @return ReturnTag|Null
780: */
781: private static function mergeReturnTags(?ReturnTag $returnTag, array $parents, array $parentPhpDocBlocks): ?ReturnTag
782: {
783: if ($returnTag !== null) {
784: return $returnTag;
785: }
786:
787: foreach ($parents as $i => $parent) {
788: $result = self::mergeOneParentReturnTag($returnTag, $parent, $parentPhpDocBlocks[$i]);
789: if ($result === null) {
790: continue;
791: }
792:
793: return $result;
794: }
795:
796: return null;
797: }
798:
799: private static function mergeOneParentReturnTag(?ReturnTag $returnTag, self $parent, PhpDocBlock $phpDocBlock): ?ReturnTag
800: {
801: $parentReturnTag = $parent->getReturnTag();
802: if ($parentReturnTag === null) {
803: return $returnTag;
804: }
805:
806: $parentType = $parentReturnTag->getType();
807:
808: // Each parent would overwrite the previous one except if it returns a less specific type.
809: // Do not care for incompatible types as there is a separate rule for that.
810: if ($returnTag !== null && $parentType->isSuperTypeOf($returnTag->getType())->yes()) {
811: return null;
812: }
813:
814: return self::resolveTemplateTypeInTag(
815: $parentReturnTag->withType(
816: $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentReturnTag->getType()),
817: )->toImplicit(),
818: $phpDocBlock,
819: );
820: }
821:
822: /**
823: * @param array<AssertTag> $assertTags
824: * @param array<int, self> $parents
825: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
826: * @return array<AssertTag>
827: */
828: private static function mergeAssertTags(array $assertTags, array $parents, array $parentPhpDocBlocks): array
829: {
830: if (count($assertTags) > 0) {
831: return $assertTags;
832: }
833: foreach ($parents as $i => $parent) {
834: $result = $parent->getAssertTags();
835: if (count($result) === 0) {
836: continue;
837: }
838:
839: $phpDocBlock = $parentPhpDocBlocks[$i];
840:
841: return array_map(
842: static fn (AssertTag $assertTag) => $assertTag->withParameter(
843: $phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()),
844: ),
845: $result,
846: );
847: }
848:
849: return $assertTags;
850: }
851:
852: /**
853: * @param array<int, self> $parents
854: */
855: private static function mergeSelfOutTypeTags(?SelfOutTypeTag $selfOutTypeTag, array $parents): ?SelfOutTypeTag
856: {
857: if ($selfOutTypeTag !== null) {
858: return $selfOutTypeTag;
859: }
860: foreach ($parents as $parent) {
861: $result = $parent->getSelfOutTag();
862: if ($result === null) {
863: continue;
864: }
865: return $result;
866: }
867:
868: return null;
869: }
870:
871: /**
872: * @param array<int, self> $parents
873: */
874: private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, array $parents): ?DeprecatedTag
875: {
876: if ($deprecatedTag !== null) {
877: return $deprecatedTag;
878: }
879: foreach ($parents as $parent) {
880: $result = $parent->getDeprecatedTag();
881: if ($result === null) {
882: continue;
883: }
884: return $result;
885: }
886:
887: return null;
888: }
889:
890: /**
891: * @param array<int, self> $parents
892: */
893: private static function mergeThrowsTags(?ThrowsTag $throwsTag, array $parents): ?ThrowsTag
894: {
895: if ($throwsTag !== null) {
896: return $throwsTag;
897: }
898: foreach ($parents as $parent) {
899: $result = $parent->getThrowsTag();
900: if ($result === null) {
901: continue;
902: }
903:
904: return $result;
905: }
906:
907: return null;
908: }
909:
910: /**
911: * @param array<string, ParamOutTag> $paramOutTags
912: * @param array<int, self> $parents
913: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
914: * @return array<string, ParamOutTag>
915: */
916: private static function mergeParamOutTags(array $paramOutTags, array $parents, array $parentPhpDocBlocks): array
917: {
918: foreach ($parents as $i => $parent) {
919: $paramOutTags = self::mergeOneParentParamOutTags($paramOutTags, $parent, $parentPhpDocBlocks[$i]);
920: }
921:
922: return $paramOutTags;
923: }
924:
925: /**
926: * @param array<string, ParamOutTag> $paramOutTags
927: * @param ResolvedPhpDocBlock $parent
928: * @return array<string, ParamOutTag>
929: */
930: private static function mergeOneParentParamOutTags(array $paramOutTags, self $parent, PhpDocBlock $phpDocBlock): array
931: {
932: $parentParamOutTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamOutTags());
933:
934: foreach ($parentParamOutTags as $name => $parentParamTag) {
935: if (array_key_exists($name, $paramOutTags)) {
936: continue;
937: }
938:
939: $paramOutTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock);
940: }
941:
942: return $paramOutTags;
943: }
944:
945: /**
946: * @template T of TypedTag
947: * @param T $tag
948: * @return T
949: */
950: private static function resolveTemplateTypeInTag(TypedTag $tag, PhpDocBlock $phpDocBlock): TypedTag
951: {
952: $type = TemplateTypeHelper::resolveTemplateTypes(
953: $tag->getType(),
954: $phpDocBlock->getClassReflection()->getActiveTemplateTypeMap(),
955: );
956: return $tag->withType($type);
957: }
958:
959: }
960: