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