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