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