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, 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: if ($this->resolvedPhpDocBlockCacheCount >= $this->resolvedPhpDocBlockCacheCountMax) {
122: $this->resolvedPhpDocBlockCache = array_slice(
123: $this->resolvedPhpDocBlockCache,
124: 1,
125: preserve_keys: true,
126: );
127:
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: $cacheKey = sprintf('ftm-%s', $fileName);
341: $variableCacheKey = sprintf('v5-%s', ComposerHelper::getPhpDocParserVersion());
342: $cached = $this->loadCachedPhpDocNodeMap($cacheKey, $variableCacheKey);
343: if ($cached === null) {
344: [$nameScopeMap, $files] = $this->createPhpDocNodeMap($fileName, null, null, [], $fileName);
345: $filesWithHashes = [];
346: foreach ($files as $file) {
347: $newHash = hash_file('sha256', $file);
348: $filesWithHashes[$file] = $newHash;
349: }
350: $this->cache->save($cacheKey, $variableCacheKey, [$nameScopeMap, $filesWithHashes]);
351: } else {
352: [$nameScopeMap, $files] = $cached;
353: }
354: if ($this->memoryCacheCount >= $this->nameScopeMapMemoryCacheCountMax) {
355: $this->memoryCache = array_slice(
356: $this->memoryCache,
357: 1,
358: preserve_keys: true,
359: );
360: $this->memoryCacheCount--;
361: }
362:
363: $this->memoryCache[$fileName] = [$nameScopeMap, $files];
364: $this->memoryCacheCount++;
365: }
366:
367: return $this->memoryCache[$fileName];
368: }
369:
370: /**
371: * @param non-empty-string $cacheKey
372: * @return array{array<string, IntermediaryNameScope>, list<string>}|null
373: */
374: private function loadCachedPhpDocNodeMap(string $cacheKey, string $variableCacheKey): ?array
375: {
376: $cached = $this->cache->load($cacheKey, $variableCacheKey);
377: if ($cached !== null) {
378: /**
379: * @var array<string, string> $filesWithHashes
380: */
381: [$nameScopeMap, $filesWithHashes] = $cached;
382: $useCache = true;
383: foreach ($filesWithHashes as $file => $hash) {
384: $newHash = @hash_file('sha256', $file);
385: if ($newHash === false) {
386: $useCache = false;
387: break;
388: }
389: if ($newHash === $hash) {
390: continue;
391: }
392: $useCache = false;
393: break;
394: }
395:
396: if ($useCache) {
397: return [$nameScopeMap, array_keys($filesWithHashes)];
398: }
399: }
400:
401: return null;
402: }
403:
404: /**
405: * @param array<string, string> $traitMethodAliases
406: * @param array<string, true> $activeTraitResolutions
407: * @return array{array<string, IntermediaryNameScope>, list<string>}
408: */
409: private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName, array $activeTraitResolutions = []): array
410: {
411: /** @var array<string, IntermediaryNameScope> $nameScopeMap */
412: $nameScopeMap = [];
413:
414: /** @var array<int, IntermediaryNameScope> $typeMapStack */
415: $typeMapStack = [];
416:
417: /** @var array<int, array<string, true>> $typeAliasStack */
418: $typeAliasStack = [];
419:
420: /** @var string[] $classStack */
421: $classStack = [];
422: if ($lookForTrait !== null && $traitUseClass !== null) {
423: $classStack[] = $traitUseClass;
424: $typeAliasStack[] = [];
425: }
426: $namespace = null;
427:
428: $traitFound = false;
429:
430: $files = [$fileName];
431:
432: /** @var array<string|null> $functionStack */
433: $functionStack = [];
434: $uses = [];
435: $constUses = [];
436: $this->processNodes(
437: $this->phpParser->parseFile($fileName),
438: function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, $activeTraitResolutions, &$nameScopeMap, &$typeMapStack, &$typeAliasStack, &$classStack, &$namespace, &$functionStack, &$uses, &$constUses, &$files): ?int {
439: if ($node instanceof Node\Stmt\ClassLike) {
440: if ($traitFound && $fileName === $originalClassFileName) {
441: return self::SKIP_NODE;
442: }
443:
444: if ($lookForTrait !== null && !$traitFound) {
445: if (!$node instanceof Node\Stmt\Trait_) {
446: return self::SKIP_NODE;
447: }
448: if ((string) $node->namespacedName !== $lookForTrait) {
449: return self::SKIP_NODE;
450: }
451:
452: $traitFound = true;
453: $functionStack[] = null;
454: } else {
455: if ($node->name === null) {
456: if (!$node instanceof Node\Stmt\Class_) {
457: throw new ShouldNotHappenException();
458: }
459:
460: $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName);
461: } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) {
462: $className = $node->name->name;
463: } else {
464: if ($traitFound) {
465: return self::SKIP_NODE;
466: }
467: $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
468: }
469: $classStack[] = $className;
470: $functionStack[] = null;
471: }
472: } elseif ($node instanceof Node\Stmt\ClassMethod) {
473: if (array_key_exists($node->name->name, $traitMethodAliases)) {
474: $functionStack[] = $traitMethodAliases[$node->name->name];
475: } else {
476: $functionStack[] = $node->name->name;
477: }
478: } elseif ($node instanceof Node\Stmt\Function_) {
479: $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
480: } elseif ($node instanceof Node\PropertyHook) {
481: $propertyName = $node->getAttribute('propertyName');
482: if ($propertyName !== null) {
483: $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString());
484: }
485: }
486:
487: $className = array_last($classStack) ?? null;
488: $functionName = array_last($functionStack) ?? null;
489: $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
490:
491: $phpDocNode = null;
492: $docComment = null;
493: if (
494: $node instanceof Node\Stmt
495: || ($node instanceof Node\PropertyHook && $node->getAttribute('propertyName') !== null)
496: ) {
497: $docComment = GetLastDocComment::forNode($node);
498: if ($docComment !== null) {
499: $phpDocNode = $this->phpDocStringResolver->resolve($docComment);
500: }
501: }
502:
503: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_ || $node instanceof Node\PropertyHook) {
504: if ($phpDocNode !== null) {
505: if ($node instanceof Node\Stmt\ClassLike) {
506: $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNode);
507: }
508:
509: $parentNameScope = array_last($typeMapStack) ?? null;
510:
511: $typeMapStack[] = new IntermediaryNameScope(
512: $namespace,
513: $uses,
514: $className,
515: $functionName,
516: $this->chooseTemplateTagValueNodesByPriority($phpDocNode->getTags()),
517: $parentNameScope,
518: array_last($typeAliasStack) ?? [],
519: constUses: $constUses,
520: typeAliasClassName: $lookForTrait,
521: );
522: } elseif ($node instanceof Node\Stmt\ClassLike) {
523: $typeAliasStack[] = [];
524: } else {
525: $parentNameScope = array_last($typeMapStack) ?? null;
526: $typeMapStack[] = new IntermediaryNameScope(
527: $namespace,
528: $uses,
529: $className,
530: $functionName,
531: [],
532: $parentNameScope,
533: array_last($typeAliasStack) ?? [],
534: constUses: $constUses,
535: typeAliasClassName: $lookForTrait,
536: );
537: }
538: }
539:
540: if (
541: (
542: $node instanceof Node\PropertyHook
543: || (
544: $node instanceof Node\Stmt
545: && !$node instanceof Node\Stmt\Namespace_
546: && !$node instanceof Node\Stmt\Declare_
547: && !$node instanceof Node\Stmt\Use_
548: && !$node instanceof Node\Stmt\GroupUse
549: && !$node instanceof Node\Stmt\TraitUse
550: && !$node instanceof Node\Stmt\TraitUseAdaptation
551: && !$node instanceof Node\Stmt\InlineHTML
552: && !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_)
553: )
554: ) && !array_key_exists($nameScopeKey, $nameScopeMap)
555: ) {
556: $parentNameScope = array_last($typeMapStack) ?? null;
557: $typeAliasesMap = array_last($typeAliasStack) ?? [];
558: $nameScopeMap[$nameScopeKey] = new IntermediaryNameScope(
559: $namespace,
560: $uses,
561: $className,
562: $functionName,
563: $parentNameScope !== null ? $parentNameScope->getTemplatePhpDocNodes() : [],
564: $parentNameScope !== null ? $parentNameScope->getParent() : null,
565: $typeAliasesMap,
566: constUses: $constUses,
567: typeAliasClassName: $lookForTrait,
568: );
569: }
570:
571: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_ || $node instanceof Node\PropertyHook) {
572: if ($phpDocNode !== null || !$node instanceof Node\Stmt\ClassLike) {
573: return self::POP_TYPE_MAP_STACK;
574: }
575:
576: return null;
577: }
578:
579: if ($node instanceof Node\Stmt\Namespace_) {
580: $namespace = $node->name !== null ? (string) $node->name : null;
581: } elseif ($node instanceof Node\Stmt\Use_) {
582: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL) {
583: foreach ($node->uses as $use) {
584: $uses[strtolower($use->getAlias()->name)] = (string) $use->name;
585: }
586: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT) {
587: foreach ($node->uses as $use) {
588: $constUses[strtolower($use->getAlias()->name)] = (string) $use->name;
589: }
590: }
591: } elseif ($node instanceof Node\Stmt\GroupUse) {
592: $prefix = (string) $node->prefix;
593: foreach ($node->uses as $use) {
594: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL || $use->type === Node\Stmt\Use_::TYPE_NORMAL) {
595: $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
596: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT || $use->type === Node\Stmt\Use_::TYPE_CONSTANT) {
597: $constUses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
598: }
599: }
600: } elseif ($node instanceof Node\Stmt\TraitUse) {
601: $traitMethodAliases = [];
602: foreach ($node->adaptations as $traitUseAdaptation) {
603: if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
604: continue;
605: }
606:
607: if ($traitUseAdaptation->newName === null) {
608: continue;
609: }
610:
611: $methodName = $traitUseAdaptation->method->toString();
612: $newTraitName = $traitUseAdaptation->newName->toString();
613:
614: if ($traitUseAdaptation->trait === null) {
615: foreach ($node->traits as $traitName) {
616: $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName;
617: }
618: continue;
619: }
620:
621: $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName;
622: }
623:
624: foreach ($node->traits as $traitName) {
625: /** @var class-string $traitName */
626: $traitName = (string) $traitName;
627: $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
628: if (!$reflectionProvider->hasClass($traitName)) {
629: continue;
630: }
631:
632: $traitReflection = $reflectionProvider->getClass($traitName);
633: if (!$traitReflection->isTrait()) {
634: continue;
635: }
636: if ($traitReflection->getFileName() === null) {
637: continue;
638: }
639: if (!is_file($traitReflection->getFileName())) {
640: continue;
641: }
642:
643: $className = array_last($classStack) ?? null;
644: if ($className === null) {
645: throw new ShouldNotHappenException();
646: }
647:
648: $traitResolutionKey = $this->getTraitResolutionKey($traitReflection->getFileName(), $traitName, $className, $originalClassFileName);
649: if (isset($activeTraitResolutions[$traitResolutionKey])) {
650: continue;
651: }
652:
653: $nestedActiveTraitResolutions = $activeTraitResolutions;
654: $nestedActiveTraitResolutions[$traitResolutionKey] = true;
655:
656: [$traitNameScopeMap, $traitFiles] = $this->createPhpDocNodeMap(
657: $traitReflection->getFileName(),
658: $traitName,
659: $className,
660: $traitMethodAliases[$traitName] ?? [],
661: $originalClassFileName,
662: $nestedActiveTraitResolutions,
663: );
664: $nameScopeMap = array_merge($nameScopeMap, array_map(static fn ($originalNameScope) => $originalNameScope->getTraitData() === null ? $originalNameScope->withTraitData($originalClassFileName, $className, $traitName, $lookForTrait, $docComment) : $originalNameScope, $traitNameScopeMap));
665: $files = array_merge($files, $traitFiles);
666: }
667: }
668:
669: return null;
670: },
671: static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void {
672: if ($node instanceof Node\Stmt\ClassLike) {
673: if (count($classStack) === 0) {
674: throw new ShouldNotHappenException();
675: }
676: array_pop($classStack);
677:
678: if (count($typeAliasStack) === 0) {
679: throw new ShouldNotHappenException();
680: }
681:
682: array_pop($typeAliasStack);
683:
684: if (count($functionStack) === 0) {
685: throw new ShouldNotHappenException();
686: }
687:
688: array_pop($functionStack);
689: } elseif ($node instanceof Node\Stmt\Namespace_) {
690: $namespace = null;
691: $uses = [];
692: $constUses = [];
693: } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
694: if (count($functionStack) === 0) {
695: throw new ShouldNotHappenException();
696: }
697:
698: array_pop($functionStack);
699: } elseif ($node instanceof Node\PropertyHook) {
700: $propertyName = $node->getAttribute('propertyName');
701: if ($propertyName !== null) {
702: if (count($functionStack) === 0) {
703: throw new ShouldNotHappenException();
704: }
705:
706: array_pop($functionStack);
707: }
708: }
709: if ($callbackResult !== self::POP_TYPE_MAP_STACK) {
710: return;
711: }
712:
713: if (count($typeMapStack) === 0) {
714: throw new ShouldNotHappenException();
715: }
716: array_pop($typeMapStack);
717: },
718: );
719:
720: if (count($typeMapStack) > 0) {
721: throw new ShouldNotHappenException();
722: }
723:
724: return [$nameScopeMap, $files];
725: }
726:
727: /**
728: * @param PhpDocTagNode[] $tags
729: * @return array<string, array{string, TemplateTagValueNode}>
730: */
731: private function chooseTemplateTagValueNodesByPriority(array $tags): array
732: {
733: $resolved = [];
734: $resolvedPrefix = [];
735:
736: $prefixPriority = [
737: '' => 0,
738: 'phan' => 1,
739: 'psalm' => 2,
740: 'phpstan' => 3,
741: ];
742: foreach ($tags as $phpDocTagNode) {
743: $valueNode = $phpDocTagNode->value;
744: if (!$valueNode instanceof TemplateTagValueNode) {
745: continue;
746: }
747:
748: $tagName = $phpDocTagNode->name;
749: if (str_starts_with($tagName, '@phan-')) {
750: $prefix = 'phan';
751: } elseif (str_starts_with($tagName, '@psalm-')) {
752: $prefix = 'psalm';
753: } elseif (str_starts_with($tagName, '@phpstan-')) {
754: $prefix = 'phpstan';
755: } else {
756: $prefix = '';
757: }
758:
759: if (isset($resolved[$valueNode->name])) {
760: $setPrefix = $resolvedPrefix[$valueNode->name];
761: if ($prefixPriority[$prefix] <= $prefixPriority[$setPrefix]) {
762: continue;
763: }
764: }
765:
766: $resolved[$valueNode->name] = [$phpDocTagNode->name, $valueNode];
767: $resolvedPrefix[$valueNode->name] = $prefix;
768: }
769:
770: return $resolved;
771: }
772:
773: /**
774: * @return array<string, true>
775: */
776: private function getTypeAliasesMap(PhpDocNode $phpDocNode): array
777: {
778: $nameScope = new NameScope(null, []);
779:
780: $aliasesMap = [];
781: foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasImportTags($phpDocNode, $nameScope)) as $key) {
782: $aliasesMap[$key] = true;
783: }
784:
785: foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasTags($phpDocNode, $nameScope)) as $key) {
786: $aliasesMap[$key] = true;
787: }
788:
789: return $aliasesMap;
790: }
791:
792: /**
793: * @param Node[]|Node|scalar|null $node
794: * @param Closure(Node $node): mixed $nodeCallback
795: * @param Closure(Node $node, mixed $callbackResult): void $endNodeCallback
796: */
797: private function processNodes($node, Closure $nodeCallback, Closure $endNodeCallback): void
798: {
799: if ($node instanceof Node) {
800: $callbackResult = $nodeCallback($node);
801: if ($callbackResult === self::SKIP_NODE) {
802: return;
803: }
804: foreach ($node->getSubNodeNames() as $subNodeName) {
805: $subNode = $node->{$subNodeName};
806: $this->processNodes($subNode, $nodeCallback, $endNodeCallback);
807: }
808: $endNodeCallback($node, $callbackResult);
809: } elseif (is_array($node)) {
810: foreach ($node as $subNode) {
811: $this->processNodes($subNode, $nodeCallback, $endNodeCallback);
812: }
813: }
814: }
815:
816: private function getNameScopeKey(
817: ?string $file,
818: ?string $class,
819: ?string $trait,
820: ?string $function,
821: ): string
822: {
823: if ($class === null && $trait === null && $function === null) {
824: return md5(sprintf('%s', $file ?? 'no-file'));
825: }
826:
827: if ($class !== null && str_contains($class, 'class@anonymous')) {
828: throw new ShouldNotHappenException('Wrong anonymous class name, FilTypeMapper should be called with ClassReflection::getName().');
829: }
830:
831: return md5(sprintf('%s-%s-%s-%s', $file ?? 'no-file', $class, $trait, $function));
832: }
833:
834: private function getPhpDocKey(string $nameScopeKey, string $docComment): string
835: {
836: $doc = new Doc($docComment);
837: return md5(sprintf('%s-%s', $nameScopeKey, $doc->getReformattedText()));
838: }
839:
840: private function getTraitResolutionKey(string $fileName, string $traitName, string $className, string $originalClassFileName): string
841: {
842: return md5(sprintf('%s-%s-%s-%s', $fileName, $traitName, $className, $originalClassFileName));
843: }
844:
845: }
846: