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