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