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