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