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\ParamClosureThisTag;
13: use PHPStan\PhpDoc\Tag\ParamOutTag;
14: use PHPStan\PhpDoc\Tag\ParamTag;
15: use PHPStan\PhpDoc\Tag\PropertyTag;
16: use PHPStan\PhpDoc\Tag\RequireExtendsTag;
17: use PHPStan\PhpDoc\Tag\RequireImplementsTag;
18: use PHPStan\PhpDoc\Tag\ReturnTag;
19: use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
20: use PHPStan\PhpDoc\Tag\TemplateTag;
21: use PHPStan\PhpDoc\Tag\ThrowsTag;
22: use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
23: use PHPStan\PhpDoc\Tag\TypeAliasTag;
24: use PHPStan\PhpDoc\Tag\TypedTag;
25: use PHPStan\PhpDoc\Tag\UsesTag;
26: use PHPStan\PhpDoc\Tag\VarTag;
27: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
28: use PHPStan\Reflection\ClassReflection;
29: use PHPStan\Reflection\ReflectionProvider;
30: use PHPStan\Type\ConditionalTypeForParameter;
31: use PHPStan\Type\Generic\TemplateTypeHelper;
32: use PHPStan\Type\Generic\TemplateTypeMap;
33: use PHPStan\Type\Generic\TemplateTypeVariance;
34: use PHPStan\Type\StaticType;
35: use PHPStan\Type\Type;
36: use PHPStan\Type\TypeTraverser;
37: use function array_key_exists;
38: use function array_map;
39: use function count;
40: use function is_bool;
41: use function substr;
42:
43: /**
44: * @api
45: */
46: final class ResolvedPhpDocBlock
47: {
48:
49: public const EMPTY_DOC_STRING = '/** */';
50:
51: private PhpDocNode $phpDocNode;
52:
53: /** @var PhpDocNode[] */
54: private array $phpDocNodes;
55:
56: private string $phpDocString;
57:
58: private ?string $filename;
59:
60: private ?NameScope $nameScope = null;
61:
62: private TemplateTypeMap $templateTypeMap;
63:
64: /** @var array<string, TemplateTag> */
65: private array $templateTags;
66:
67: private PhpDocNodeResolver $phpDocNodeResolver;
68:
69: private ReflectionProvider $reflectionProvider;
70:
71: /** @var array<(string|int), VarTag>|false */
72: private array|false $varTags = false;
73:
74: /** @var array<string, MethodTag>|false */
75: private array|false $methodTags = false;
76:
77: /** @var array<string, PropertyTag>|false */
78: private array|false $propertyTags = false;
79:
80: /** @var array<string, ExtendsTag>|false */
81: private array|false $extendsTags = false;
82:
83: /** @var array<string, ImplementsTag>|false */
84: private array|false $implementsTags = false;
85:
86: /** @var array<string, UsesTag>|false */
87: private array|false $usesTags = false;
88:
89: /** @var array<string, ParamTag>|false */
90: private array|false $paramTags = false;
91:
92: /** @var array<string, ParamOutTag>|false */
93: private array|false $paramOutTags = false;
94:
95: /** @var array<string, bool>|false */
96: private array|false $paramsImmediatelyInvokedCallable = false;
97:
98: /** @var array<string, ParamClosureThisTag>|false */
99: private array|false $paramClosureThisTags = false;
100:
101: private ReturnTag|false|null $returnTag = false;
102:
103: private ThrowsTag|false|null $throwsTag = false;
104:
105: /** @var array<MixinTag>|false */
106: private array|false $mixinTags = false;
107:
108: /** @var array<RequireExtendsTag>|false */
109: private array|false $requireExtendsTags = false;
110:
111: /** @var array<RequireImplementsTag>|false */
112: private array|false $requireImplementsTags = false;
113:
114: /** @var array<TypeAliasTag>|false */
115: private array|false $typeAliasTags = false;
116:
117: /** @var array<TypeAliasImportTag>|false */
118: private array|false $typeAliasImportTags = false;
119:
120: /** @var array<AssertTag>|false */
121: private array|false $assertTags = false;
122:
123: private SelfOutTypeTag|false|null $selfOutTypeTag = false;
124:
125: private DeprecatedTag|false|null $deprecatedTag = false;
126:
127: private ?bool $isDeprecated = null;
128:
129: private ?bool $isNotDeprecated = null;
130:
131: private ?bool $isInternal = null;
132:
133: private ?bool $isFinal = null;
134:
135: /** @var bool|'notLoaded'|null */
136: private bool|string|null $isPure = 'notLoaded';
137:
138: private ?bool $isReadOnly = null;
139:
140: private ?bool $isImmutable = null;
141:
142: private ?bool $isAllowedPrivateMutation = null;
143:
144: private ?bool $hasConsistentConstructor = null;
145:
146: private ?bool $acceptsNamedArguments = null;
147:
148: private function __construct()
149: {
150: }
151:
152: /**
153: * @param TemplateTag[] $templateTags
154: */
155: public static function create(
156: PhpDocNode $phpDocNode,
157: string $phpDocString,
158: ?string $filename,
159: NameScope $nameScope,
160: TemplateTypeMap $templateTypeMap,
161: array $templateTags,
162: PhpDocNodeResolver $phpDocNodeResolver,
163: ReflectionProvider $reflectionProvider,
164: ): self
165: {
166: // new property also needs to be added to withNameScope(), createEmpty() and merge()
167: $self = new self();
168: $self->phpDocNode = $phpDocNode;
169: $self->phpDocNodes = [$phpDocNode];
170: $self->phpDocString = $phpDocString;
171: $self->filename = $filename;
172: $self->nameScope = $nameScope;
173: $self->templateTypeMap = $templateTypeMap;
174: $self->templateTags = $templateTags;
175: $self->phpDocNodeResolver = $phpDocNodeResolver;
176: $self->reflectionProvider = $reflectionProvider;
177:
178: return $self;
179: }
180:
181: public function withNameScope(NameScope $nameScope): self
182: {
183: $self = new self();
184: $self->phpDocNode = $this->phpDocNode;
185: $self->phpDocNodes = $this->phpDocNodes;
186: $self->phpDocString = $this->phpDocString;
187: $self->filename = $this->filename;
188: $self->nameScope = $nameScope;
189: $self->templateTypeMap = $this->templateTypeMap;
190: $self->templateTags = $this->templateTags;
191: $self->phpDocNodeResolver = $this->phpDocNodeResolver;
192: $self->reflectionProvider = $this->reflectionProvider;
193:
194: return $self;
195: }
196:
197: public static function createEmpty(): self
198: {
199: // new property also needs to be added to merge()
200: $self = new self();
201: $self->phpDocString = self::EMPTY_DOC_STRING;
202: $self->phpDocNodes = [];
203: $self->filename = null;
204: $self->templateTypeMap = TemplateTypeMap::createEmpty();
205: $self->templateTags = [];
206: $self->varTags = [];
207: $self->methodTags = [];
208: $self->propertyTags = [];
209: $self->extendsTags = [];
210: $self->implementsTags = [];
211: $self->usesTags = [];
212: $self->paramTags = [];
213: $self->paramOutTags = [];
214: $self->paramsImmediatelyInvokedCallable = [];
215: $self->paramClosureThisTags = [];
216: $self->returnTag = null;
217: $self->throwsTag = null;
218: $self->mixinTags = [];
219: $self->requireExtendsTags = [];
220: $self->requireImplementsTags = [];
221: $self->typeAliasTags = [];
222: $self->typeAliasImportTags = [];
223: $self->assertTags = [];
224: $self->selfOutTypeTag = null;
225: $self->deprecatedTag = null;
226: $self->isDeprecated = false;
227: $self->isNotDeprecated = false;
228: $self->isInternal = false;
229: $self->isFinal = false;
230: $self->isPure = null;
231: $self->isReadOnly = false;
232: $self->isImmutable = false;
233: $self->isAllowedPrivateMutation = false;
234: $self->hasConsistentConstructor = false;
235: $self->acceptsNamedArguments = true;
236:
237: return $self;
238: }
239:
240: /**
241: * @param array<int, self> $parents
242: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
243: */
244: public function merge(array $parents, array $parentPhpDocBlocks): self
245: {
246: $className = $this->nameScope !== null ? $this->nameScope->getClassName() : null;
247: $classReflection = $className !== null && $this->reflectionProvider->hasClass($className)
248: ? $this->reflectionProvider->getClass($className)
249: : null;
250:
251: // new property also needs to be added to createEmpty()
252: $result = new self();
253: // we will resolve everything on $this here so these properties don't have to be populated
254: // skip $result->phpDocNode
255: $phpDocNodes = $this->phpDocNodes;
256: $acceptsNamedArguments = $this->acceptsNamedArguments();
257: foreach ($parents as $parent) {
258: foreach ($parent->phpDocNodes as $phpDocNode) {
259: $phpDocNodes[] = $phpDocNode;
260: $acceptsNamedArguments = $acceptsNamedArguments && $parent->acceptsNamedArguments();
261: }
262: }
263: $result->phpDocNodes = $phpDocNodes;
264: $result->phpDocString = $this->phpDocString;
265: $result->filename = $this->filename;
266: // skip $result->nameScope
267: $result->templateTypeMap = $this->templateTypeMap;
268: $result->templateTags = $this->templateTags;
269: // skip $result->phpDocNodeResolver
270: $result->varTags = self::mergeVarTags($this->getVarTags(), $parents, $parentPhpDocBlocks);
271: $result->methodTags = $this->getMethodTags();
272: $result->propertyTags = $this->getPropertyTags();
273: $result->extendsTags = $this->getExtendsTags();
274: $result->implementsTags = $this->getImplementsTags();
275: $result->usesTags = $this->getUsesTags();
276: $result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks);
277: $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks);
278: $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parents, $parentPhpDocBlocks);
279: $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parents, $parentPhpDocBlocks);
280: $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $classReflection, $parents, $parentPhpDocBlocks);
281: $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents);
282: $result->mixinTags = $this->getMixinTags();
283: $result->requireExtendsTags = $this->getRequireExtendsTags();
284: $result->requireImplementsTags = $this->getRequireImplementsTags();
285: $result->typeAliasTags = $this->getTypeAliasTags();
286: $result->typeAliasImportTags = $this->getTypeAliasImportTags();
287: $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks);
288: $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents);
289: $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $this->isNotDeprecated(), $parents);
290: $result->isDeprecated = $result->deprecatedTag !== null;
291: $result->isNotDeprecated = $this->isNotDeprecated();
292: $result->isInternal = $this->isInternal();
293: $result->isFinal = $this->isFinal();
294: $result->isPure = self::mergePureTags($this->isPure(), $parents);
295: $result->isReadOnly = $this->isReadOnly();
296: $result->isImmutable = $this->isImmutable();
297: $result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation();
298: $result->hasConsistentConstructor = $this->hasConsistentConstructor();
299: $result->acceptsNamedArguments = $acceptsNamedArguments;
300:
301: return $result;
302: }
303:
304: /**
305: * @param array<string, string> $parameterNameMapping
306: */
307: public function changeParameterNamesByMapping(array $parameterNameMapping): self
308: {
309: if (count($this->phpDocNodes) === 0) {
310: return $this;
311: }
312:
313: $mapParameterCb = static function (Type $type, callable $traverse) use ($parameterNameMapping): Type {
314: if ($type instanceof ConditionalTypeForParameter) {
315: $parameterName = substr($type->getParameterName(), 1);
316: if (array_key_exists($parameterName, $parameterNameMapping)) {
317: $type = $type->changeParameterName('$' . $parameterNameMapping[$parameterName]);
318: }
319: }
320:
321: return $traverse($type);
322: };
323:
324: $newParamTags = [];
325: foreach ($this->getParamTags() as $key => $paramTag) {
326: if (!array_key_exists($key, $parameterNameMapping)) {
327: continue;
328: }
329: $transformedType = TypeTraverser::map($paramTag->getType(), $mapParameterCb);
330: $newParamTags[$parameterNameMapping[$key]] = $paramTag->withType($transformedType);
331: }
332:
333: $newParamOutTags = [];
334: foreach ($this->getParamOutTags() as $key => $paramOutTag) {
335: if (!array_key_exists($key, $parameterNameMapping)) {
336: continue;
337: }
338:
339: $transformedType = TypeTraverser::map($paramOutTag->getType(), $mapParameterCb);
340: $newParamOutTags[$parameterNameMapping[$key]] = $paramOutTag->withType($transformedType);
341: }
342:
343: $newParamsImmediatelyInvokedCallable = [];
344: foreach ($this->getParamsImmediatelyInvokedCallable() as $key => $immediatelyInvokedCallable) {
345: if (!array_key_exists($key, $parameterNameMapping)) {
346: continue;
347: }
348:
349: $newParamsImmediatelyInvokedCallable[$parameterNameMapping[$key]] = $immediatelyInvokedCallable;
350: }
351:
352: $paramClosureThisTags = $this->getParamClosureThisTags();
353: $newParamClosureThisTags = [];
354: foreach ($paramClosureThisTags as $key => $paramClosureThisTag) {
355: if (!array_key_exists($key, $parameterNameMapping)) {
356: continue;
357: }
358:
359: $transformedType = TypeTraverser::map($paramClosureThisTag->getType(), $mapParameterCb);
360: $newParamClosureThisTags[$parameterNameMapping[$key]] = $paramClosureThisTag->withType($transformedType);
361: }
362:
363: $returnTag = $this->getReturnTag();
364: if ($returnTag !== null) {
365: $transformedType = TypeTraverser::map($returnTag->getType(), $mapParameterCb);
366: $returnTag = $returnTag->withType($transformedType);
367: }
368:
369: $assertTags = $this->getAssertTags();
370: if (count($assertTags) > 0) {
371: $assertTags = array_map(static function (AssertTag $tag) use ($parameterNameMapping): AssertTag {
372: $parameterName = substr($tag->getParameter()->getParameterName(), 1);
373: if (array_key_exists($parameterName, $parameterNameMapping)) {
374: $tag = $tag->withParameter($tag->getParameter()->changeParameterName('$' . $parameterNameMapping[$parameterName]));
375: }
376: return $tag;
377: }, $assertTags);
378: }
379:
380: $self = new self();
381: $self->phpDocNode = $this->phpDocNode;
382: $self->phpDocNodes = $this->phpDocNodes;
383: $self->phpDocString = $this->phpDocString;
384: $self->filename = $this->filename;
385: $self->nameScope = $this->nameScope;
386: $self->templateTypeMap = $this->templateTypeMap;
387: $self->templateTags = $this->templateTags;
388: $self->phpDocNodeResolver = $this->phpDocNodeResolver;
389: $self->reflectionProvider = $this->reflectionProvider;
390: $self->varTags = $this->varTags;
391: $self->methodTags = $this->methodTags;
392: $self->propertyTags = $this->propertyTags;
393: $self->extendsTags = $this->extendsTags;
394: $self->implementsTags = $this->implementsTags;
395: $self->usesTags = $this->usesTags;
396: $self->paramTags = $newParamTags;
397: $self->paramOutTags = $newParamOutTags;
398: $self->paramsImmediatelyInvokedCallable = $newParamsImmediatelyInvokedCallable;
399: $self->paramClosureThisTags = $newParamClosureThisTags;
400: $self->returnTag = $returnTag;
401: $self->throwsTag = $this->throwsTag;
402: $self->mixinTags = $this->mixinTags;
403: $self->requireImplementsTags = $this->requireImplementsTags;
404: $self->requireExtendsTags = $this->requireExtendsTags;
405: $self->typeAliasTags = $this->typeAliasTags;
406: $self->typeAliasImportTags = $this->typeAliasImportTags;
407: $self->assertTags = $assertTags;
408: $self->selfOutTypeTag = $this->selfOutTypeTag;
409: $self->deprecatedTag = $this->deprecatedTag;
410: $self->isDeprecated = $this->isDeprecated;
411: $self->isNotDeprecated = $this->isNotDeprecated;
412: $self->isInternal = $this->isInternal;
413: $self->isFinal = $this->isFinal;
414: $self->isPure = $this->isPure;
415:
416: return $self;
417: }
418:
419: public function hasPhpDocString(): bool
420: {
421: return $this->phpDocString !== self::EMPTY_DOC_STRING;
422: }
423:
424: public function getPhpDocString(): string
425: {
426: return $this->phpDocString;
427: }
428:
429: /**
430: * @return PhpDocNode[]
431: */
432: public function getPhpDocNodes(): array
433: {
434: return $this->phpDocNodes;
435: }
436:
437: public function getFilename(): ?string
438: {
439: return $this->filename;
440: }
441:
442: private function getNameScope(): NameScope
443: {
444: return $this->nameScope;
445: }
446:
447: public function getNullableNameScope(): ?NameScope
448: {
449: return $this->nameScope;
450: }
451:
452: /**
453: * @return array<(string|int), VarTag>
454: */
455: public function getVarTags(): array
456: {
457: if ($this->varTags === false) {
458: $this->varTags = $this->phpDocNodeResolver->resolveVarTags(
459: $this->phpDocNode,
460: $this->getNameScope(),
461: );
462: }
463: return $this->varTags;
464: }
465:
466: /**
467: * @return array<string, MethodTag>
468: */
469: public function getMethodTags(): array
470: {
471: if ($this->methodTags === false) {
472: $this->methodTags = $this->phpDocNodeResolver->resolveMethodTags(
473: $this->phpDocNode,
474: $this->getNameScope(),
475: );
476: }
477: return $this->methodTags;
478: }
479:
480: /**
481: * @return array<string, PropertyTag>
482: */
483: public function getPropertyTags(): array
484: {
485: if ($this->propertyTags === false) {
486: $this->propertyTags = $this->phpDocNodeResolver->resolvePropertyTags(
487: $this->phpDocNode,
488: $this->getNameScope(),
489: );
490: }
491: return $this->propertyTags;
492: }
493:
494: /**
495: * @return array<string, TemplateTag>
496: */
497: public function getTemplateTags(): array
498: {
499: return $this->templateTags;
500: }
501:
502: /**
503: * @return array<string, ExtendsTag>
504: */
505: public function getExtendsTags(): array
506: {
507: if ($this->extendsTags === false) {
508: $this->extendsTags = $this->phpDocNodeResolver->resolveExtendsTags(
509: $this->phpDocNode,
510: $this->getNameScope(),
511: );
512: }
513: return $this->extendsTags;
514: }
515:
516: /**
517: * @return array<string, ImplementsTag>
518: */
519: public function getImplementsTags(): array
520: {
521: if ($this->implementsTags === false) {
522: $this->implementsTags = $this->phpDocNodeResolver->resolveImplementsTags(
523: $this->phpDocNode,
524: $this->getNameScope(),
525: );
526: }
527: return $this->implementsTags;
528: }
529:
530: /**
531: * @return array<string, UsesTag>
532: */
533: public function getUsesTags(): array
534: {
535: if ($this->usesTags === false) {
536: $this->usesTags = $this->phpDocNodeResolver->resolveUsesTags(
537: $this->phpDocNode,
538: $this->getNameScope(),
539: );
540: }
541: return $this->usesTags;
542: }
543:
544: /**
545: * @return array<string, ParamTag>
546: */
547: public function getParamTags(): array
548: {
549: if ($this->paramTags === false) {
550: $this->paramTags = $this->phpDocNodeResolver->resolveParamTags(
551: $this->phpDocNode,
552: $this->getNameScope(),
553: );
554: }
555: return $this->paramTags;
556: }
557:
558: /**
559: * @return array<string, ParamOutTag>
560: */
561: public function getParamOutTags(): array
562: {
563: if ($this->paramOutTags === false) {
564: $this->paramOutTags = $this->phpDocNodeResolver->resolveParamOutTags(
565: $this->phpDocNode,
566: $this->getNameScope(),
567: );
568: }
569: return $this->paramOutTags;
570: }
571:
572: /**
573: * @return array<string, bool>
574: */
575: public function getParamsImmediatelyInvokedCallable(): array
576: {
577: if ($this->paramsImmediatelyInvokedCallable === false) {
578: $this->paramsImmediatelyInvokedCallable = $this->phpDocNodeResolver->resolveParamImmediatelyInvokedCallable($this->phpDocNode);
579: }
580:
581: return $this->paramsImmediatelyInvokedCallable;
582: }
583:
584: /**
585: * @return array<string, ParamClosureThisTag>
586: */
587: public function getParamClosureThisTags(): array
588: {
589: if ($this->paramClosureThisTags === false) {
590: $this->paramClosureThisTags = $this->phpDocNodeResolver->resolveParamClosureThisTags(
591: $this->phpDocNode,
592: $this->getNameScope(),
593: );
594: }
595:
596: return $this->paramClosureThisTags;
597: }
598:
599: public function getReturnTag(): ?ReturnTag
600: {
601: if (is_bool($this->returnTag)) {
602: $this->returnTag = $this->phpDocNodeResolver->resolveReturnTag(
603: $this->phpDocNode,
604: $this->getNameScope(),
605: );
606: }
607: return $this->returnTag;
608: }
609:
610: public function getThrowsTag(): ?ThrowsTag
611: {
612: if (is_bool($this->throwsTag)) {
613: $this->throwsTag = $this->phpDocNodeResolver->resolveThrowsTags(
614: $this->phpDocNode,
615: $this->getNameScope(),
616: );
617: }
618: return $this->throwsTag;
619: }
620:
621: /**
622: * @return array<MixinTag>
623: */
624: public function getMixinTags(): array
625: {
626: if ($this->mixinTags === false) {
627: $this->mixinTags = $this->phpDocNodeResolver->resolveMixinTags(
628: $this->phpDocNode,
629: $this->getNameScope(),
630: );
631: }
632:
633: return $this->mixinTags;
634: }
635:
636: /**
637: * @return array<RequireExtendsTag>
638: */
639: public function getRequireExtendsTags(): array
640: {
641: if ($this->requireExtendsTags === false) {
642: $this->requireExtendsTags = $this->phpDocNodeResolver->resolveRequireExtendsTags(
643: $this->phpDocNode,
644: $this->getNameScope(),
645: );
646: }
647:
648: return $this->requireExtendsTags;
649: }
650:
651: /**
652: * @return array<RequireImplementsTag>
653: */
654: public function getRequireImplementsTags(): array
655: {
656: if ($this->requireImplementsTags === false) {
657: $this->requireImplementsTags = $this->phpDocNodeResolver->resolveRequireImplementsTags(
658: $this->phpDocNode,
659: $this->getNameScope(),
660: );
661: }
662:
663: return $this->requireImplementsTags;
664: }
665:
666: /**
667: * @return array<TypeAliasTag>
668: */
669: public function getTypeAliasTags(): array
670: {
671: if ($this->typeAliasTags === false) {
672: $this->typeAliasTags = $this->phpDocNodeResolver->resolveTypeAliasTags(
673: $this->phpDocNode,
674: $this->getNameScope(),
675: );
676: }
677:
678: return $this->typeAliasTags;
679: }
680:
681: /**
682: * @return array<TypeAliasImportTag>
683: */
684: public function getTypeAliasImportTags(): array
685: {
686: if ($this->typeAliasImportTags === false) {
687: $this->typeAliasImportTags = $this->phpDocNodeResolver->resolveTypeAliasImportTags(
688: $this->phpDocNode,
689: $this->getNameScope(),
690: );
691: }
692:
693: return $this->typeAliasImportTags;
694: }
695:
696: /**
697: * @return array<AssertTag>
698: */
699: public function getAssertTags(): array
700: {
701: if ($this->assertTags === false) {
702: $this->assertTags = $this->phpDocNodeResolver->resolveAssertTags(
703: $this->phpDocNode,
704: $this->getNameScope(),
705: );
706: }
707:
708: return $this->assertTags;
709: }
710:
711: public function getSelfOutTag(): ?SelfOutTypeTag
712: {
713: if ($this->selfOutTypeTag === false) {
714: $this->selfOutTypeTag = $this->phpDocNodeResolver->resolveSelfOutTypeTag(
715: $this->phpDocNode,
716: $this->getNameScope(),
717: );
718: }
719:
720: return $this->selfOutTypeTag;
721: }
722:
723: public function getDeprecatedTag(): ?DeprecatedTag
724: {
725: if (is_bool($this->deprecatedTag)) {
726: $this->deprecatedTag = $this->phpDocNodeResolver->resolveDeprecatedTag(
727: $this->phpDocNode,
728: $this->getNameScope(),
729: );
730: }
731: return $this->deprecatedTag;
732: }
733:
734: public function isDeprecated(): bool
735: {
736: if ($this->isDeprecated === null) {
737: $this->isDeprecated = $this->phpDocNodeResolver->resolveIsDeprecated(
738: $this->phpDocNode,
739: );
740: }
741: return $this->isDeprecated;
742: }
743:
744: /**
745: * @internal
746: */
747: public function isNotDeprecated(): bool
748: {
749: if ($this->isNotDeprecated === null) {
750: $this->isNotDeprecated = $this->phpDocNodeResolver->resolveIsNotDeprecated(
751: $this->phpDocNode,
752: );
753: }
754: return $this->isNotDeprecated;
755: }
756:
757: public function isInternal(): bool
758: {
759: if ($this->isInternal === null) {
760: $this->isInternal = $this->phpDocNodeResolver->resolveIsInternal(
761: $this->phpDocNode,
762: );
763: }
764: return $this->isInternal;
765: }
766:
767: public function isFinal(): bool
768: {
769: if ($this->isFinal === null) {
770: $this->isFinal = $this->phpDocNodeResolver->resolveIsFinal(
771: $this->phpDocNode,
772: );
773: }
774: return $this->isFinal;
775: }
776:
777: public function hasConsistentConstructor(): bool
778: {
779: if ($this->hasConsistentConstructor === null) {
780: $this->hasConsistentConstructor = $this->phpDocNodeResolver->resolveHasConsistentConstructor(
781: $this->phpDocNode,
782: );
783: }
784: return $this->hasConsistentConstructor;
785: }
786:
787: public function acceptsNamedArguments(): bool
788: {
789: if ($this->acceptsNamedArguments === null) {
790: $this->acceptsNamedArguments = $this->phpDocNodeResolver->resolveAcceptsNamedArguments(
791: $this->phpDocNode,
792: );
793: }
794: return $this->acceptsNamedArguments;
795: }
796:
797: public function getTemplateTypeMap(): TemplateTypeMap
798: {
799: return $this->templateTypeMap;
800: }
801:
802: public function isPure(): ?bool
803: {
804: if ($this->isPure === 'notLoaded') {
805: $pure = $this->phpDocNodeResolver->resolveIsPure(
806: $this->phpDocNode,
807: );
808: if ($pure) {
809: $this->isPure = true;
810: return $this->isPure;
811: }
812:
813: $impure = $this->phpDocNodeResolver->resolveIsImpure(
814: $this->phpDocNode,
815: );
816: if ($impure) {
817: $this->isPure = false;
818: return $this->isPure;
819: }
820:
821: $this->isPure = null;
822: }
823:
824: return $this->isPure;
825: }
826:
827: public function isReadOnly(): bool
828: {
829: if ($this->isReadOnly === null) {
830: $this->isReadOnly = $this->phpDocNodeResolver->resolveIsReadOnly(
831: $this->phpDocNode,
832: );
833: }
834: return $this->isReadOnly;
835: }
836:
837: public function isImmutable(): bool
838: {
839: if ($this->isImmutable === null) {
840: $this->isImmutable = $this->phpDocNodeResolver->resolveIsImmutable(
841: $this->phpDocNode,
842: );
843: }
844: return $this->isImmutable;
845: }
846:
847: public function isAllowedPrivateMutation(): bool
848: {
849: if ($this->isAllowedPrivateMutation === null) {
850: $this->isAllowedPrivateMutation = $this->phpDocNodeResolver->resolveAllowPrivateMutation(
851: $this->phpDocNode,
852: );
853: }
854:
855: return $this->isAllowedPrivateMutation;
856: }
857:
858: /**
859: * @param array<string|int, VarTag> $varTags
860: * @param array<int, self> $parents
861: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
862: * @return array<string|int, VarTag>
863: */
864: private static function mergeVarTags(array $varTags, array $parents, array $parentPhpDocBlocks): array
865: {
866: // Only allow one var tag per comment. Check the parent if child does not have this tag.
867: if (count($varTags) > 0) {
868: return $varTags;
869: }
870:
871: foreach ($parents as $i => $parent) {
872: $result = self::mergeOneParentVarTags($parent, $parentPhpDocBlocks[$i]);
873: if ($result === null) {
874: continue;
875: }
876:
877: return $result;
878: }
879:
880: return [];
881: }
882:
883: /**
884: * @return array<string|int, VarTag>|null
885: */
886: private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array
887: {
888: foreach ($parent->getVarTags() as $key => $parentVarTag) {
889: return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock, TemplateTypeVariance::createInvariant())];
890: }
891:
892: return null;
893: }
894:
895: /**
896: * @param array<string, ParamTag> $paramTags
897: * @param array<int, self> $parents
898: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
899: * @return array<string, ParamTag>
900: */
901: private static function mergeParamTags(array $paramTags, array $parents, array $parentPhpDocBlocks): array
902: {
903: foreach ($parents as $i => $parent) {
904: $paramTags = self::mergeOneParentParamTags($paramTags, $parent, $parentPhpDocBlocks[$i]);
905: }
906:
907: return $paramTags;
908: }
909:
910: /**
911: * @param array<string, ParamTag> $paramTags
912: * @return array<string, ParamTag>
913: */
914: private static function mergeOneParentParamTags(array $paramTags, self $parent, PhpDocBlock $phpDocBlock): array
915: {
916: $parentParamTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamTags());
917:
918: foreach ($parentParamTags as $name => $parentParamTag) {
919: if (array_key_exists($name, $paramTags)) {
920: continue;
921: }
922:
923: $paramTags[$name] = self::resolveTemplateTypeInTag(
924: $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())),
925: $phpDocBlock,
926: TemplateTypeVariance::createContravariant(),
927: );
928: }
929:
930: return $paramTags;
931: }
932:
933: /**
934: * @param array<int, self> $parents
935: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
936: * @return ReturnTag|Null
937: */
938: private static function mergeReturnTags(?ReturnTag $returnTag, ?ClassReflection $classReflection, array $parents, array $parentPhpDocBlocks): ?ReturnTag
939: {
940: if ($returnTag !== null) {
941: return $returnTag;
942: }
943:
944: foreach ($parents as $i => $parent) {
945: $result = self::mergeOneParentReturnTag($returnTag, $classReflection, $parent, $parentPhpDocBlocks[$i]);
946: if ($result === null) {
947: continue;
948: }
949:
950: return $result;
951: }
952:
953: return null;
954: }
955:
956: private static function mergeOneParentReturnTag(?ReturnTag $returnTag, ?ClassReflection $classReflection, self $parent, PhpDocBlock $phpDocBlock): ?ReturnTag
957: {
958: $parentReturnTag = $parent->getReturnTag();
959: if ($parentReturnTag === null) {
960: return $returnTag;
961: }
962:
963: $parentType = $parentReturnTag->getType();
964:
965: if ($classReflection !== null) {
966: $parentType = TypeTraverser::map(
967: $parentType,
968: static function (Type $type, callable $traverse) use ($classReflection): Type {
969: if ($type instanceof StaticType) {
970: return $type->changeBaseClass($classReflection);
971: }
972:
973: return $traverse($type);
974: },
975: );
976:
977: $parentReturnTag = $parentReturnTag->withType($parentType);
978: }
979:
980: // Each parent would overwrite the previous one except if it returns a less specific type.
981: // Do not care for incompatible types as there is a separate rule for that.
982: if ($returnTag !== null && $parentType->isSuperTypeOf($returnTag->getType())->yes()) {
983: return null;
984: }
985:
986: return self::resolveTemplateTypeInTag(
987: $parentReturnTag->withType(
988: $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentReturnTag->getType()),
989: )->toImplicit(),
990: $phpDocBlock,
991: TemplateTypeVariance::createCovariant(),
992: );
993: }
994:
995: /**
996: * @param array<AssertTag> $assertTags
997: * @param array<int, self> $parents
998: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
999: * @return array<AssertTag>
1000: */
1001: private static function mergeAssertTags(array $assertTags, array $parents, array $parentPhpDocBlocks): array
1002: {
1003: if (count($assertTags) > 0) {
1004: return $assertTags;
1005: }
1006: foreach ($parents as $i => $parent) {
1007: $result = $parent->getAssertTags();
1008: if (count($result) === 0) {
1009: continue;
1010: }
1011:
1012: $phpDocBlock = $parentPhpDocBlocks[$i];
1013:
1014: return array_map(
1015: static fn (AssertTag $assertTag) => self::resolveTemplateTypeInTag(
1016: $assertTag->withParameter(
1017: $phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()),
1018: )->toImplicit(),
1019: $phpDocBlock,
1020: TemplateTypeVariance::createCovariant(),
1021: ),
1022: $result,
1023: );
1024: }
1025:
1026: return $assertTags;
1027: }
1028:
1029: /**
1030: * @param array<int, self> $parents
1031: */
1032: private static function mergeSelfOutTypeTags(?SelfOutTypeTag $selfOutTypeTag, array $parents): ?SelfOutTypeTag
1033: {
1034: if ($selfOutTypeTag !== null) {
1035: return $selfOutTypeTag;
1036: }
1037: foreach ($parents as $parent) {
1038: $result = $parent->getSelfOutTag();
1039: if ($result === null) {
1040: continue;
1041: }
1042: return $result;
1043: }
1044:
1045: return null;
1046: }
1047:
1048: /**
1049: * @param array<int, self> $parents
1050: */
1051: private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, bool $hasNotDeprecatedTag, array $parents): ?DeprecatedTag
1052: {
1053: if ($deprecatedTag !== null) {
1054: return $deprecatedTag;
1055: }
1056:
1057: if ($hasNotDeprecatedTag) {
1058: return null;
1059: }
1060:
1061: foreach ($parents as $parent) {
1062: $result = $parent->getDeprecatedTag();
1063: if ($result === null && !$parent->isNotDeprecated()) {
1064: continue;
1065: }
1066: return $result;
1067: }
1068:
1069: return null;
1070: }
1071:
1072: /**
1073: * @param array<int, self> $parents
1074: */
1075: private static function mergeThrowsTags(?ThrowsTag $throwsTag, array $parents): ?ThrowsTag
1076: {
1077: if ($throwsTag !== null) {
1078: return $throwsTag;
1079: }
1080: foreach ($parents as $parent) {
1081: $result = $parent->getThrowsTag();
1082: if ($result === null) {
1083: continue;
1084: }
1085:
1086: return $result;
1087: }
1088:
1089: return null;
1090: }
1091:
1092: /**
1093: * @param array<string, ParamOutTag> $paramOutTags
1094: * @param array<int, self> $parents
1095: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
1096: * @return array<string, ParamOutTag>
1097: */
1098: private static function mergeParamOutTags(array $paramOutTags, array $parents, array $parentPhpDocBlocks): array
1099: {
1100: foreach ($parents as $i => $parent) {
1101: $paramOutTags = self::mergeOneParentParamOutTags($paramOutTags, $parent, $parentPhpDocBlocks[$i]);
1102: }
1103:
1104: return $paramOutTags;
1105: }
1106:
1107: /**
1108: * @param array<string, ParamOutTag> $paramOutTags
1109: * @return array<string, ParamOutTag>
1110: */
1111: private static function mergeOneParentParamOutTags(array $paramOutTags, self $parent, PhpDocBlock $phpDocBlock): array
1112: {
1113: $parentParamOutTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamOutTags());
1114:
1115: foreach ($parentParamOutTags as $name => $parentParamTag) {
1116: if (array_key_exists($name, $paramOutTags)) {
1117: continue;
1118: }
1119:
1120: $paramOutTags[$name] = self::resolveTemplateTypeInTag(
1121: $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())),
1122: $phpDocBlock,
1123: TemplateTypeVariance::createCovariant(),
1124: );
1125: }
1126:
1127: return $paramOutTags;
1128: }
1129:
1130: /**
1131: * @param array<string, bool> $paramsImmediatelyInvokedCallable
1132: * @param array<int, self> $parents
1133: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
1134: * @return array<string, bool>
1135: */
1136: private static function mergeParamsImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, array $parents, array $parentPhpDocBlocks): array
1137: {
1138: foreach ($parents as $i => $parent) {
1139: $paramsImmediatelyInvokedCallable = self::mergeOneParentParamImmediatelyInvokedCallable($paramsImmediatelyInvokedCallable, $parent, $parentPhpDocBlocks[$i]);
1140: }
1141:
1142: return $paramsImmediatelyInvokedCallable;
1143: }
1144:
1145: /**
1146: * @param array<string, bool> $paramsImmediatelyInvokedCallable
1147: * @return array<string, bool>
1148: */
1149: private static function mergeOneParentParamImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, self $parent, PhpDocBlock $phpDocBlock): array
1150: {
1151: $parentImmediatelyInvokedCallable = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamsImmediatelyInvokedCallable());
1152:
1153: foreach ($parentImmediatelyInvokedCallable as $name => $parentIsImmediatelyInvokedCallable) {
1154: if (array_key_exists($name, $paramsImmediatelyInvokedCallable)) {
1155: continue;
1156: }
1157:
1158: $paramsImmediatelyInvokedCallable[$name] = $parentIsImmediatelyInvokedCallable;
1159: }
1160:
1161: return $paramsImmediatelyInvokedCallable;
1162: }
1163:
1164: /**
1165: * @param array<string, ParamClosureThisTag> $paramsClosureThisTags
1166: * @param array<int, self> $parents
1167: * @param array<int, PhpDocBlock> $parentPhpDocBlocks
1168: * @return array<string, ParamClosureThisTag>
1169: */
1170: private static function mergeParamClosureThisTags(array $paramsClosureThisTags, array $parents, array $parentPhpDocBlocks): array
1171: {
1172: foreach ($parents as $i => $parent) {
1173: $paramsClosureThisTags = self::mergeOneParentParamClosureThisTag($paramsClosureThisTags, $parent, $parentPhpDocBlocks[$i]);
1174: }
1175:
1176: return $paramsClosureThisTags;
1177: }
1178:
1179: /**
1180: * @param array<string, ParamClosureThisTag> $paramsClosureThisTags
1181: * @return array<string, ParamClosureThisTag>
1182: */
1183: private static function mergeOneParentParamClosureThisTag(array $paramsClosureThisTags, self $parent, PhpDocBlock $phpDocBlock): array
1184: {
1185: $parentClosureThisTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamClosureThisTags());
1186:
1187: foreach ($parentClosureThisTags as $name => $parentParamClosureThisTag) {
1188: if (array_key_exists($name, $paramsClosureThisTags)) {
1189: continue;
1190: }
1191:
1192: $paramsClosureThisTags[$name] = self::resolveTemplateTypeInTag(
1193: $parentParamClosureThisTag->withType(
1194: $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamClosureThisTag->getType()),
1195: ),
1196: $phpDocBlock,
1197: TemplateTypeVariance::createContravariant(),
1198: );
1199: }
1200:
1201: return $paramsClosureThisTags;
1202: }
1203:
1204: /**
1205: * @param array<int, self> $parents
1206: */
1207: private static function mergePureTags(?bool $isPure, array $parents): ?bool
1208: {
1209: if ($isPure !== null) {
1210: return $isPure;
1211: }
1212:
1213: foreach ($parents as $parent) {
1214: $parentIsPure = $parent->isPure();
1215: if ($parentIsPure === null) {
1216: continue;
1217: }
1218:
1219: return $parentIsPure;
1220: }
1221:
1222: return null;
1223: }
1224:
1225: /**
1226: * @template T of TypedTag
1227: * @param T $tag
1228: * @return T
1229: */
1230: private static function resolveTemplateTypeInTag(
1231: TypedTag $tag,
1232: PhpDocBlock $phpDocBlock,
1233: TemplateTypeVariance $positionVariance,
1234: ): TypedTag
1235: {
1236: $type = TemplateTypeHelper::resolveTemplateTypes(
1237: $tag->getType(),
1238: $phpDocBlock->getClassReflection()->getActiveTemplateTypeMap(),
1239: $phpDocBlock->getClassReflection()->getCallSiteVarianceMap(),
1240: $positionVariance,
1241: );
1242: return $tag->withType($type);
1243: }
1244:
1245: }
1246: