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