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