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