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