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