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