1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use Closure;
6: use PhpParser\Comment\Doc;
7: use PhpParser\Node;
8: use PHPStan\Analyser\IntermediaryNameScope;
9: use PHPStan\Analyser\NameScope;
10: use PHPStan\BetterReflection\Util\GetLastDocComment;
11: use PHPStan\Broker\AnonymousClassNameHelper;
12: use PHPStan\Cache\Cache;
13: use PHPStan\DependencyInjection\AutowiredParameter;
14: use PHPStan\DependencyInjection\AutowiredService;
15: use PHPStan\File\FileHelper;
16: use PHPStan\Internal\ComposerHelper;
17: use PHPStan\Parser\Parser;
18: use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException;
19: use PHPStan\PhpDoc\PhpDocNodeResolver;
20: use PHPStan\PhpDoc\PhpDocStringResolver;
21: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
22: use PHPStan\PhpDoc\Tag\TemplateTag;
23: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
24: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
25: use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
26: use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
27: use PHPStan\ShouldNotHappenException;
28: use PHPStan\Type\Generic\GenericObjectType;
29: use PHPStan\Type\Generic\TemplateTypeFactory;
30: use PHPStan\Type\Generic\TemplateTypeHelper;
31: use PHPStan\Type\Generic\TemplateTypeMap;
32: use PHPStan\Type\Generic\TemplateTypeVariance;
33: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
34: use function array_key_exists;
35: use function array_keys;
36: use function array_last;
37: use function array_map;
38: use function array_merge;
39: use function array_pop;
40: use function array_reverse;
41: use function array_slice;
42: use function count;
43: use function hash_file;
44: use function in_array;
45: use function is_array;
46: use function is_file;
47: use function ltrim;
48: use function md5;
49: use function sprintf;
50: use function str_contains;
51: use function str_starts_with;
52: use function strtolower;
53:
54: #[AutowiredService]
55: final class FileTypeMapper
56: {
57:
58: private const SKIP_NODE = 1;
59: private const POP_TYPE_MAP_STACK = 2;
60:
61: /** @var array<string, array{array<string, IntermediaryNameScope>}> */
62: private array $memoryCache = [];
63:
64: private int $memoryCacheCount = 0;
65:
66: /** @var array<string, true> */
67: private array $inProcess = [];
68:
69: /** @var array<string, ResolvedPhpDocBlock> */
70: private array $resolvedPhpDocBlockCache = [];
71:
72: private int $resolvedPhpDocBlockCacheCount = 0;
73:
74: public function __construct(
75: private ReflectionProviderProvider $reflectionProviderProvider,
76: #[AutowiredParameter(ref: '@defaultAnalysisParser')]
77: private Parser $phpParser,
78: private PhpDocStringResolver $phpDocStringResolver,
79: private PhpDocNodeResolver $phpDocNodeResolver,
80: private AnonymousClassNameHelper $anonymousClassNameHelper,
81: private FileHelper $fileHelper,
82: private Cache $cache,
83: )
84: {
85: }
86:
87: /** @api */
88: public function getResolvedPhpDoc(
89: ?string $fileName,
90: ?string $className,
91: ?string $traitName,
92: ?string $functionName,
93: ?string $docComment,
94: ): ResolvedPhpDocBlock
95: {
96: if ($className === null && $traitName !== null) {
97: throw new ShouldNotHappenException();
98: }
99:
100: if (in_array($docComment, [null, ''], true)) {
101: return ResolvedPhpDocBlock::createEmpty();
102: }
103:
104: if ($fileName !== null) {
105: $fileName = $this->fileHelper->normalizePath($fileName);
106: }
107:
108: $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName);
109: $phpDocKey = $this->getPhpDocKey($nameScopeKey, $docComment);
110: if (isset($this->resolvedPhpDocBlockCache[$phpDocKey])) {
111: return $this->resolvedPhpDocBlockCache[$phpDocKey];
112: }
113:
114: if ($this->resolvedPhpDocBlockCacheCount >= 2048) {
115: $this->resolvedPhpDocBlockCache = array_slice(
116: $this->resolvedPhpDocBlockCache,
117: 1,
118: preserve_keys: true,
119: );
120:
121: $this->resolvedPhpDocBlockCacheCount--;
122: }
123:
124: $this->resolvedPhpDocBlockCacheCount++;
125:
126: if ($fileName === null) {
127: return $this->resolvedPhpDocBlockCache[$phpDocKey] = $this->createResolvedPhpDocBlock($this->phpDocStringResolver->resolve($docComment), new NameScope(null, []), $docComment, null);
128: }
129:
130: try {
131: $nameScope = $this->getNameScope($fileName, $className, $traitName, $functionName);
132: } catch (NameScopeAlreadyBeingCreatedException) {
133: return $this->resolvedPhpDocBlockCache[$phpDocKey] = ResolvedPhpDocBlock::createEmpty();
134: }
135:
136: return $this->resolvedPhpDocBlockCache[$phpDocKey] = $this->createResolvedPhpDocBlock(
137: $this->phpDocStringResolver->resolve($docComment),
138: $nameScope,
139: $docComment,
140: $fileName,
141: );
142: }
143:
144: private function createResolvedPhpDocBlock(
145: PhpDocNode $phpDocNode,
146: NameScope $nameScope,
147: string $phpDocString,
148: ?string $fileName,
149: ): ResolvedPhpDocBlock
150: {
151: $docBlockTemplateTypes = [];
152: $templateTypeMap = $nameScope->getTemplateTypeMap();
153: $templateTags = [];
154: $phpDocNodeTemplateTagsByName = [];
155: foreach ($phpDocNode->getTags() as $tagNode) {
156: $valueNode = $tagNode->value;
157: if (!$valueNode instanceof TemplateTagValueNode) {
158: continue;
159: }
160:
161: $phpDocNodeTemplateTagsByName[$valueNode->name] = true;
162: }
163: foreach ($nameScope->getTemplateTags() as $templateTagName => $templateTag) {
164: if (!array_key_exists($templateTagName, $phpDocNodeTemplateTagsByName)) {
165: continue;
166: }
167: $templateTags[$templateTagName] = $templateTag;
168: $templateType = $templateTypeMap->getType($templateTagName);
169: if ($templateType === null) {
170: continue;
171: }
172: $docBlockTemplateTypes[$templateTagName] = $templateType;
173: }
174:
175: return ResolvedPhpDocBlock::create(
176: $phpDocNode,
177: $phpDocString,
178: $fileName,
179: $nameScope,
180: new TemplateTypeMap($docBlockTemplateTypes),
181: $templateTags,
182: $this->phpDocNodeResolver,
183: $this->reflectionProviderProvider->getReflectionProvider(),
184: );
185: }
186:
187: /**
188: * @throws NameScopeAlreadyBeingCreatedException
189: */
190: public function getNameScope(
191: string $fileName,
192: ?string $className,
193: ?string $traitName,
194: ?string $functionName,
195: ): NameScope
196: {
197: $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName);
198: if (isset($this->inProcess[$nameScopeKey])) {
199: throw new NameScopeAlreadyBeingCreatedException();
200: }
201:
202: [$nameScopeMap] = $this->getNameScopeMap($fileName);
203: if (!isset($nameScopeMap[$nameScopeKey])) {
204: throw new NameScopeAlreadyBeingCreatedException();
205: }
206:
207: $intermediaryNameScope = $nameScopeMap[$nameScopeKey];
208:
209: $this->inProcess[$nameScopeKey] = true;
210:
211: try {
212: $parents = [$intermediaryNameScope];
213: $i = $intermediaryNameScope;
214: while ($i->getParent() !== null) {
215: $parents[] = $i->getParent();
216: $i = $i->getParent();
217: }
218:
219: $phpDocTemplateTypes = [];
220: $templateTags = [];
221: $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
222: foreach (array_reverse($parents) as $parent) {
223: $nameScope = new NameScope(
224: $parent->getNamespace(),
225: $parent->getUses(),
226: $parent->getClassName(),
227: $parent->getFunctionName(),
228: new TemplateTypeMap($phpDocTemplateTypes),
229: $templateTags,
230: $parent->getTypeAliasesMap(),
231: $parent->shouldBypassTypeAliases(),
232: $parent->getConstUses(),
233: $parent->getClassNameForTypeAlias(),
234: );
235: if ($parent->getTraitData() !== null) {
236: [$traitFileName, $traitClassName, $traitName, $lookForTraitName, $traitDocComment] = $parent->getTraitData();
237: if (!$reflectionProvider->hasClass($traitName)) {
238: continue;
239: }
240: $traitReflection = $reflectionProvider->getClass($traitName);
241: $useTags = $this->getResolvedPhpDoc(
242: $traitFileName,
243: $traitClassName,
244: $lookForTraitName,
245: null,
246: $traitDocComment,
247: )->getUsesTags();
248: $useType = null;
249: foreach ($useTags as $useTag) {
250: $useTagType = $useTag->getType();
251: if (!$useTagType instanceof GenericObjectType) {
252: continue;
253: }
254:
255: if ($useTagType->getClassName() !== $traitReflection->getName()) {
256: continue;
257: }
258:
259: $useType = $useTagType;
260: break;
261: }
262: $traitTemplateTypeMap = $traitReflection->getTemplateTypeMap();
263: $namesToUnset = [];
264: if ($useType === null) {
265: foreach ($traitTemplateTypeMap->resolveToBounds()->getTypes() as $name => $templateType) {
266: $phpDocTemplateTypes[$name] = $templateType;
267: $namesToUnset[] = $name;
268: }
269: } else {
270: $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes());
271: $nameScopeTemplateTypeMap = $traitTemplateTypeMap->map(
272: static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap, TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createStatic()),
273: );
274: foreach ($nameScopeTemplateTypeMap->getTypes() as $name => $templateType) {
275: $phpDocTemplateTypes[$name] = $templateType;
276: $namesToUnset[] = $name;
277: }
278: }
279: $parent = $parent->unsetTemplatePhpDocNodes($namesToUnset);
280: }
281:
282: $templateTypeScope = $nameScope->getTemplateTypeScope();
283: if ($templateTypeScope === null) {
284: continue;
285: }
286:
287: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope);
288: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
289: $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap, $templateTags);
290: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope);
291: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
292: $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap, $templateTags);
293: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope);
294: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
295: foreach (array_keys($templateTags) as $name) {
296: $templateType = $templateTypeMap->getType($name);
297: if ($templateType === null) {
298: continue;
299: }
300: $phpDocTemplateTypes[$name] = $templateType;
301: }
302: }
303:
304: return new NameScope(
305: $intermediaryNameScope->getNamespace(),
306: $intermediaryNameScope->getUses(),
307: $intermediaryNameScope->getClassName(),
308: $intermediaryNameScope->getFunctionName(),
309: new TemplateTypeMap($phpDocTemplateTypes),
310: $templateTags,
311: $intermediaryNameScope->getTypeAliasesMap(),
312: $intermediaryNameScope->shouldBypassTypeAliases(),
313: $intermediaryNameScope->getConstUses(),
314: $intermediaryNameScope->getClassNameForTypeAlias(),
315: );
316: } finally {
317: unset($this->inProcess[$nameScopeKey]);
318: }
319: }
320:
321: /**
322: * @return array{array<string, IntermediaryNameScope>}
323: */
324: private function getNameScopeMap(string $fileName): array
325: {
326: if (!isset($this->memoryCache[$fileName])) {
327: $cacheKey = sprintf('ftm-%s', $fileName);
328: $variableCacheKey = sprintf('v3-%s', ComposerHelper::getPhpDocParserVersion());
329: $cached = $this->loadCachedPhpDocNodeMap($cacheKey, $variableCacheKey);
330: if ($cached === null) {
331: [$nameScopeMap, $files] = $this->createPhpDocNodeMap($fileName, null, null, [], $fileName);
332: $filesWithHashes = [];
333: foreach ($files as $file) {
334: $newHash = hash_file('sha256', $file);
335: $filesWithHashes[$file] = $newHash;
336: }
337: $this->cache->save($cacheKey, $variableCacheKey, [$nameScopeMap, $filesWithHashes]);
338: } else {
339: [$nameScopeMap, $files] = $cached;
340: }
341: if ($this->memoryCacheCount >= 2048) {
342: $this->memoryCache = array_slice(
343: $this->memoryCache,
344: 1,
345: preserve_keys: true,
346: );
347: $this->memoryCacheCount--;
348: }
349:
350: $this->memoryCache[$fileName] = [$nameScopeMap, $files];
351: $this->memoryCacheCount++;
352: }
353:
354: return $this->memoryCache[$fileName];
355: }
356:
357: /**
358: * @param non-empty-string $cacheKey
359: * @return array{array<string, IntermediaryNameScope>, list<string>}|null
360: */
361: private function loadCachedPhpDocNodeMap(string $cacheKey, string $variableCacheKey): ?array
362: {
363: $cached = $this->cache->load($cacheKey, $variableCacheKey);
364: if ($cached !== null) {
365: /**
366: * @var array<string, string> $filesWithHashes
367: */
368: [$nameScopeMap, $filesWithHashes] = $cached;
369: $useCache = true;
370: foreach ($filesWithHashes as $file => $hash) {
371: $newHash = @hash_file('sha256', $file);
372: if ($newHash === false) {
373: $useCache = false;
374: break;
375: }
376: if ($newHash === $hash) {
377: continue;
378: }
379: $useCache = false;
380: break;
381: }
382:
383: if ($useCache) {
384: return [$nameScopeMap, array_keys($filesWithHashes)];
385: }
386: }
387:
388: return null;
389: }
390:
391: /**
392: * @param array<string, string> $traitMethodAliases
393: * @return array{array<string, IntermediaryNameScope>, list<string>}
394: */
395: private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName): array
396: {
397: /** @var array<string, IntermediaryNameScope> $nameScopeMap */
398: $nameScopeMap = [];
399:
400: /** @var array<int, IntermediaryNameScope> $typeMapStack */
401: $typeMapStack = [];
402:
403: /** @var array<int, array<string, true>> $typeAliasStack */
404: $typeAliasStack = [];
405:
406: /** @var string[] $classStack */
407: $classStack = [];
408: if ($lookForTrait !== null && $traitUseClass !== null) {
409: $classStack[] = $traitUseClass;
410: $typeAliasStack[] = [];
411: }
412: $namespace = null;
413:
414: $traitFound = false;
415:
416: $files = [$fileName];
417:
418: /** @var array<string|null> $functionStack */
419: $functionStack = [];
420: $uses = [];
421: $constUses = [];
422: $this->processNodes(
423: $this->phpParser->parseFile($fileName),
424: function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$typeMapStack, &$typeAliasStack, &$classStack, &$namespace, &$functionStack, &$uses, &$constUses, &$files): ?int {
425: if ($node instanceof Node\Stmt\ClassLike) {
426: if ($traitFound && $fileName === $originalClassFileName) {
427: return self::SKIP_NODE;
428: }
429:
430: if ($lookForTrait !== null && !$traitFound) {
431: if (!$node instanceof Node\Stmt\Trait_) {
432: return self::SKIP_NODE;
433: }
434: if ((string) $node->namespacedName !== $lookForTrait) {
435: return self::SKIP_NODE;
436: }
437:
438: $traitFound = true;
439: $functionStack[] = null;
440: } else {
441: if ($node->name === null) {
442: if (!$node instanceof Node\Stmt\Class_) {
443: throw new ShouldNotHappenException();
444: }
445:
446: $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName);
447: } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) {
448: $className = $node->name->name;
449: } else {
450: if ($traitFound) {
451: return self::SKIP_NODE;
452: }
453: $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
454: }
455: $classStack[] = $className;
456: $functionStack[] = null;
457: }
458: } elseif ($node instanceof Node\Stmt\ClassMethod) {
459: if (array_key_exists($node->name->name, $traitMethodAliases)) {
460: $functionStack[] = $traitMethodAliases[$node->name->name];
461: } else {
462: $functionStack[] = $node->name->name;
463: }
464: } elseif ($node instanceof Node\Stmt\Function_) {
465: $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
466: } elseif ($node instanceof Node\PropertyHook) {
467: $propertyName = $node->getAttribute('propertyName');
468: if ($propertyName !== null) {
469: $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString());
470: }
471: }
472:
473: $className = array_last($classStack) ?? null;
474: $functionName = array_last($functionStack) ?? null;
475: $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
476:
477: $phpDocNode = null;
478: $docComment = null;
479: if (
480: $node instanceof Node\Stmt
481: || ($node instanceof Node\PropertyHook && $node->getAttribute('propertyName') !== null)
482: ) {
483: $docComment = GetLastDocComment::forNode($node);
484: if ($docComment !== null) {
485: $phpDocNode = $this->phpDocStringResolver->resolve($docComment);
486: }
487: }
488:
489: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_ || $node instanceof Node\PropertyHook) {
490: if ($phpDocNode !== null) {
491: if ($node instanceof Node\Stmt\ClassLike) {
492: $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNode);
493: }
494:
495: $parentNameScope = array_last($typeMapStack) ?? null;
496:
497: $typeMapStack[] = new IntermediaryNameScope(
498: $namespace,
499: $uses,
500: $className,
501: $functionName,
502: $this->chooseTemplateTagValueNodesByPriority($phpDocNode->getTags()),
503: $parentNameScope,
504: array_last($typeAliasStack) ?? [],
505: constUses: $constUses,
506: typeAliasClassName: $lookForTrait,
507: );
508: } elseif ($node instanceof Node\Stmt\ClassLike) {
509: $typeAliasStack[] = [];
510: } else {
511: $parentNameScope = array_last($typeMapStack) ?? null;
512: $typeMapStack[] = new IntermediaryNameScope(
513: $namespace,
514: $uses,
515: $className,
516: $functionName,
517: [],
518: $parentNameScope,
519: array_last($typeAliasStack) ?? [],
520: constUses: $constUses,
521: typeAliasClassName: $lookForTrait,
522: );
523: }
524: }
525:
526: if (
527: (
528: $node instanceof Node\PropertyHook
529: || (
530: $node instanceof Node\Stmt
531: && !$node instanceof Node\Stmt\Namespace_
532: && !$node instanceof Node\Stmt\Declare_
533: && !$node instanceof Node\Stmt\Use_
534: && !$node instanceof Node\Stmt\GroupUse
535: && !$node instanceof Node\Stmt\TraitUse
536: && !$node instanceof Node\Stmt\TraitUseAdaptation
537: && !$node instanceof Node\Stmt\InlineHTML
538: && !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_)
539: )
540: ) && !array_key_exists($nameScopeKey, $nameScopeMap)
541: ) {
542: $parentNameScope = array_last($typeMapStack) ?? null;
543: $typeAliasesMap = array_last($typeAliasStack) ?? [];
544: $nameScopeMap[$nameScopeKey] = new IntermediaryNameScope(
545: $namespace,
546: $uses,
547: $className,
548: $functionName,
549: $parentNameScope !== null ? $parentNameScope->getTemplatePhpDocNodes() : [],
550: $parentNameScope !== null ? $parentNameScope->getParent() : null,
551: $typeAliasesMap,
552: constUses: $constUses,
553: typeAliasClassName: $lookForTrait,
554: );
555: }
556:
557: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_ || $node instanceof Node\PropertyHook) {
558: if ($phpDocNode !== null || !$node instanceof Node\Stmt\ClassLike) {
559: return self::POP_TYPE_MAP_STACK;
560: }
561:
562: return null;
563: }
564:
565: if ($node instanceof Node\Stmt\Namespace_) {
566: $namespace = $node->name !== null ? (string) $node->name : null;
567: } elseif ($node instanceof Node\Stmt\Use_) {
568: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL) {
569: foreach ($node->uses as $use) {
570: $uses[strtolower($use->getAlias()->name)] = (string) $use->name;
571: }
572: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT) {
573: foreach ($node->uses as $use) {
574: $constUses[strtolower($use->getAlias()->name)] = (string) $use->name;
575: }
576: }
577: } elseif ($node instanceof Node\Stmt\GroupUse) {
578: $prefix = (string) $node->prefix;
579: foreach ($node->uses as $use) {
580: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL || $use->type === Node\Stmt\Use_::TYPE_NORMAL) {
581: $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
582: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT || $use->type === Node\Stmt\Use_::TYPE_CONSTANT) {
583: $constUses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
584: }
585: }
586: } elseif ($node instanceof Node\Stmt\TraitUse) {
587: $traitMethodAliases = [];
588: foreach ($node->adaptations as $traitUseAdaptation) {
589: if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
590: continue;
591: }
592:
593: if ($traitUseAdaptation->newName === null) {
594: continue;
595: }
596:
597: $methodName = $traitUseAdaptation->method->toString();
598: $newTraitName = $traitUseAdaptation->newName->toString();
599:
600: if ($traitUseAdaptation->trait === null) {
601: foreach ($node->traits as $traitName) {
602: $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName;
603: }
604: continue;
605: }
606:
607: $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName;
608: }
609:
610: foreach ($node->traits as $traitName) {
611: /** @var class-string $traitName */
612: $traitName = (string) $traitName;
613: $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
614: if (!$reflectionProvider->hasClass($traitName)) {
615: continue;
616: }
617:
618: $traitReflection = $reflectionProvider->getClass($traitName);
619: if (!$traitReflection->isTrait()) {
620: continue;
621: }
622: if ($traitReflection->getFileName() === null) {
623: continue;
624: }
625: if (!is_file($traitReflection->getFileName())) {
626: continue;
627: }
628:
629: $className = array_last($classStack) ?? null;
630: if ($className === null) {
631: throw new ShouldNotHappenException();
632: }
633:
634: [$traitNameScopeMap, $traitFiles] = $this->createPhpDocNodeMap(
635: $traitReflection->getFileName(),
636: $traitName,
637: $className,
638: $traitMethodAliases[$traitName] ?? [],
639: $originalClassFileName,
640: );
641: $nameScopeMap = array_merge($nameScopeMap, array_map(static fn ($originalNameScope) => $originalNameScope->getTraitData() === null ? $originalNameScope->withTraitData($fileName, $className, $traitName, $lookForTrait, $docComment) : $originalNameScope, $traitNameScopeMap));
642: $files = array_merge($files, $traitFiles);
643: }
644: }
645:
646: return null;
647: },
648: static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void {
649: if ($node instanceof Node\Stmt\ClassLike) {
650: if (count($classStack) === 0) {
651: throw new ShouldNotHappenException();
652: }
653: array_pop($classStack);
654:
655: if (count($typeAliasStack) === 0) {
656: throw new ShouldNotHappenException();
657: }
658:
659: array_pop($typeAliasStack);
660:
661: if (count($functionStack) === 0) {
662: throw new ShouldNotHappenException();
663: }
664:
665: array_pop($functionStack);
666: } elseif ($node instanceof Node\Stmt\Namespace_) {
667: $namespace = null;
668: $uses = [];
669: $constUses = [];
670: } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
671: if (count($functionStack) === 0) {
672: throw new ShouldNotHappenException();
673: }
674:
675: array_pop($functionStack);
676: } elseif ($node instanceof Node\PropertyHook) {
677: $propertyName = $node->getAttribute('propertyName');
678: if ($propertyName !== null) {
679: if (count($functionStack) === 0) {
680: throw new ShouldNotHappenException();
681: }
682:
683: array_pop($functionStack);
684: }
685: }
686: if ($callbackResult !== self::POP_TYPE_MAP_STACK) {
687: return;
688: }
689:
690: if (count($typeMapStack) === 0) {
691: throw new ShouldNotHappenException();
692: }
693: array_pop($typeMapStack);
694: },
695: );
696:
697: if (count($typeMapStack) > 0) {
698: throw new ShouldNotHappenException();
699: }
700:
701: return [$nameScopeMap, $files];
702: }
703:
704: /**
705: * @param PhpDocTagNode[] $tags
706: * @return array<string, array{string, TemplateTagValueNode}>
707: */
708: private function chooseTemplateTagValueNodesByPriority(array $tags): array
709: {
710: $resolved = [];
711: $resolvedPrefix = [];
712:
713: $prefixPriority = [
714: '' => 0,
715: 'phan' => 1,
716: 'psalm' => 2,
717: 'phpstan' => 3,
718: ];
719: foreach ($tags as $phpDocTagNode) {
720: $valueNode = $phpDocTagNode->value;
721: if (!$valueNode instanceof TemplateTagValueNode) {
722: continue;
723: }
724:
725: $tagName = $phpDocTagNode->name;
726: if (str_starts_with($tagName, '@phan-')) {
727: $prefix = 'phan';
728: } elseif (str_starts_with($tagName, '@psalm-')) {
729: $prefix = 'psalm';
730: } elseif (str_starts_with($tagName, '@phpstan-')) {
731: $prefix = 'phpstan';
732: } else {
733: $prefix = '';
734: }
735:
736: if (isset($resolved[$valueNode->name])) {
737: $setPrefix = $resolvedPrefix[$valueNode->name];
738: if ($prefixPriority[$prefix] <= $prefixPriority[$setPrefix]) {
739: continue;
740: }
741: }
742:
743: $resolved[$valueNode->name] = [$phpDocTagNode->name, $valueNode];
744: $resolvedPrefix[$valueNode->name] = $prefix;
745: }
746:
747: return $resolved;
748: }
749:
750: /**
751: * @return array<string, true>
752: */
753: private function getTypeAliasesMap(PhpDocNode $phpDocNode): array
754: {
755: $nameScope = new NameScope(null, []);
756:
757: $aliasesMap = [];
758: foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasImportTags($phpDocNode, $nameScope)) as $key) {
759: $aliasesMap[$key] = true;
760: }
761:
762: foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasTags($phpDocNode, $nameScope)) as $key) {
763: $aliasesMap[$key] = true;
764: }
765:
766: return $aliasesMap;
767: }
768:
769: /**
770: * @param Node[]|Node|scalar|null $node
771: * @param Closure(Node $node): mixed $nodeCallback
772: * @param Closure(Node $node, mixed $callbackResult): void $endNodeCallback
773: */
774: private function processNodes($node, Closure $nodeCallback, Closure $endNodeCallback): void
775: {
776: if ($node instanceof Node) {
777: $callbackResult = $nodeCallback($node);
778: if ($callbackResult === self::SKIP_NODE) {
779: return;
780: }
781: foreach ($node->getSubNodeNames() as $subNodeName) {
782: $subNode = $node->{$subNodeName};
783: $this->processNodes($subNode, $nodeCallback, $endNodeCallback);
784: }
785: $endNodeCallback($node, $callbackResult);
786: } elseif (is_array($node)) {
787: foreach ($node as $subNode) {
788: $this->processNodes($subNode, $nodeCallback, $endNodeCallback);
789: }
790: }
791: }
792:
793: private function getNameScopeKey(
794: ?string $file,
795: ?string $class,
796: ?string $trait,
797: ?string $function,
798: ): string
799: {
800: if ($class === null && $trait === null && $function === null) {
801: return md5(sprintf('%s', $file ?? 'no-file'));
802: }
803:
804: if ($class !== null && str_contains($class, 'class@anonymous')) {
805: throw new ShouldNotHappenException('Wrong anonymous class name, FilTypeMapper should be called with ClassReflection::getName().');
806: }
807:
808: return md5(sprintf('%s-%s-%s-%s', $file ?? 'no-file', $class, $trait, $function));
809: }
810:
811: private function getPhpDocKey(string $nameScopeKey, string $docComment): string
812: {
813: $doc = new Doc($docComment);
814: return md5(sprintf('%s-%s', $nameScopeKey, $doc->getReformattedText()));
815: }
816:
817: }
818: