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