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