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