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