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, ResolvedPhpDocBlock> */
70: private array $resolvedPhpDocBlockCache = [];
71:
72: private int $resolvedPhpDocBlockCacheCount = 0;
73:
74: public function __construct(
75: private ReflectionProviderProvider $reflectionProviderProvider,
76: #[AutowiredParameter(ref: '@defaultAnalysisParser')]
77: private Parser $phpParser,
78: private PhpDocStringResolver $phpDocStringResolver,
79: private PhpDocNodeResolver $phpDocNodeResolver,
80: private AnonymousClassNameHelper $anonymousClassNameHelper,
81: private FileHelper $fileHelper,
82: private Cache $cache,
83: #[AutowiredParameter(ref: '%cache.resolvedPhpDocBlockCacheCountMax%')]
84: private int $resolvedPhpDocBlockCacheCountMax,
85: #[AutowiredParameter(ref: '%cache.nameScopeMapMemoryCacheCountMax%')]
86: private int $nameScopeMapMemoryCacheCountMax,
87: )
88: {
89: }
90:
91: /** @api */
92: public function getResolvedPhpDoc(
93: ?string $fileName,
94: ?string $className,
95: ?string $traitName,
96: ?string $functionName,
97: ?string $docComment,
98: ): ResolvedPhpDocBlock
99: {
100: if ($className === null && $traitName !== null) {
101: throw new ShouldNotHappenException();
102: }
103:
104: if (in_array($docComment, [null, ''], true)) {
105: return ResolvedPhpDocBlock::createEmpty();
106: }
107:
108: if ($fileName !== null) {
109: $fileName = $this->fileHelper->normalizePath($fileName);
110: }
111:
112: $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName);
113: $phpDocKey = $this->getPhpDocKey($nameScopeKey, $docComment);
114: if (isset($this->resolvedPhpDocBlockCache[$phpDocKey])) {
115: return $this->resolvedPhpDocBlockCache[$phpDocKey];
116: }
117:
118: if ($this->resolvedPhpDocBlockCacheCount >= $this->resolvedPhpDocBlockCacheCountMax) {
119: $this->resolvedPhpDocBlockCache = array_slice(
120: $this->resolvedPhpDocBlockCache,
121: 1,
122: preserve_keys: true,
123: );
124:
125: $this->resolvedPhpDocBlockCacheCount--;
126: }
127:
128: $this->resolvedPhpDocBlockCacheCount++;
129:
130: if ($fileName === null) {
131: return $this->resolvedPhpDocBlockCache[$phpDocKey] = $this->createResolvedPhpDocBlock($this->phpDocStringResolver->resolve($docComment), new NameScope(null, []), $docComment, null);
132: }
133:
134: try {
135: $nameScope = $this->getNameScope($fileName, $className, $traitName, $functionName);
136: } catch (NameScopeAlreadyBeingCreatedException) {
137: return $this->resolvedPhpDocBlockCache[$phpDocKey] = ResolvedPhpDocBlock::createEmpty();
138: }
139:
140: return $this->resolvedPhpDocBlockCache[$phpDocKey] = $this->createResolvedPhpDocBlock(
141: $this->phpDocStringResolver->resolve($docComment),
142: $nameScope,
143: $docComment,
144: $fileName,
145: );
146: }
147:
148: private function createResolvedPhpDocBlock(
149: PhpDocNode $phpDocNode,
150: NameScope $nameScope,
151: string $phpDocString,
152: ?string $fileName,
153: ): ResolvedPhpDocBlock
154: {
155: $docBlockTemplateTypes = [];
156: $templateTypeMap = $nameScope->getTemplateTypeMap();
157: $templateTags = [];
158: $phpDocNodeTemplateTagsByName = [];
159: foreach ($phpDocNode->getTags() as $tagNode) {
160: $valueNode = $tagNode->value;
161: if (!$valueNode instanceof TemplateTagValueNode) {
162: continue;
163: }
164:
165: $phpDocNodeTemplateTagsByName[$valueNode->name] = true;
166: }
167: foreach ($nameScope->getTemplateTags() as $templateTagName => $templateTag) {
168: if (!array_key_exists($templateTagName, $phpDocNodeTemplateTagsByName)) {
169: continue;
170: }
171: $templateTags[$templateTagName] = $templateTag;
172: $templateType = $templateTypeMap->getType($templateTagName);
173: if ($templateType === null) {
174: continue;
175: }
176: $docBlockTemplateTypes[$templateTagName] = $templateType;
177: }
178:
179: return ResolvedPhpDocBlock::create(
180: $phpDocNode,
181: $phpDocString,
182: $fileName,
183: $nameScope,
184: new TemplateTypeMap($docBlockTemplateTypes),
185: $templateTags,
186: $this->phpDocNodeResolver,
187: $this->reflectionProviderProvider->getReflectionProvider(),
188: );
189: }
190:
191: /**
192: * @throws NameScopeAlreadyBeingCreatedException
193: */
194: public function getNameScope(
195: string $fileName,
196: ?string $className,
197: ?string $traitName,
198: ?string $functionName,
199: ): NameScope
200: {
201: $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName);
202: if (isset($this->inProcess[$nameScopeKey])) {
203: throw new NameScopeAlreadyBeingCreatedException();
204: }
205:
206: [$nameScopeMap] = $this->getNameScopeMap($fileName);
207: if (!isset($nameScopeMap[$nameScopeKey])) {
208: throw new NameScopeAlreadyBeingCreatedException();
209: }
210:
211: $intermediaryNameScope = $nameScopeMap[$nameScopeKey];
212:
213: $this->inProcess[$nameScopeKey] = true;
214:
215: try {
216: $parents = [$intermediaryNameScope];
217: $i = $intermediaryNameScope;
218: while ($i->getParent() !== null) {
219: $parents[] = $i->getParent();
220: $i = $i->getParent();
221: }
222:
223: $phpDocTemplateTypes = [];
224: $templateTags = [];
225: $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
226: foreach (array_reverse($parents) as $parent) {
227: $nameScope = new NameScope(
228: $parent->getNamespace(),
229: $parent->getUses(),
230: $parent->getClassName(),
231: $parent->getFunctionName(),
232: new TemplateTypeMap($phpDocTemplateTypes),
233: $templateTags,
234: $parent->getTypeAliasesMap(),
235: $parent->shouldBypassTypeAliases(),
236: $parent->getConstUses(),
237: $parent->getClassNameForTypeAlias(),
238: );
239: if ($parent->getTraitData() !== null) {
240: [$traitFileName, $traitClassName, $traitName, $lookForTraitName, $traitDocComment] = $parent->getTraitData();
241: if (!$reflectionProvider->hasClass($traitName)) {
242: continue;
243: }
244: $traitReflection = $reflectionProvider->getClass($traitName);
245: $useTags = $this->getResolvedPhpDoc(
246: $traitFileName,
247: $traitClassName,
248: $lookForTraitName,
249: null,
250: $traitDocComment,
251: )->getUsesTags();
252: $useType = null;
253: foreach ($useTags as $useTag) {
254: $useTagType = $useTag->getType();
255: if (!$useTagType instanceof GenericObjectType) {
256: continue;
257: }
258:
259: if ($useTagType->getClassName() !== $traitReflection->getName()) {
260: continue;
261: }
262:
263: $useType = $useTagType;
264: break;
265: }
266: $traitTemplateTypeMap = $traitReflection->getTemplateTypeMap();
267: $namesToUnset = [];
268: if ($useType === null) {
269: foreach ($traitTemplateTypeMap->resolveToBounds()->getTypes() as $name => $templateType) {
270: $phpDocTemplateTypes[$name] = $templateType;
271: $namesToUnset[] = $name;
272: }
273: } else {
274: $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes());
275: $nameScopeTemplateTypeMap = $traitTemplateTypeMap->map(
276: static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap, TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createStatic()),
277: );
278: foreach ($nameScopeTemplateTypeMap->getTypes() as $name => $templateType) {
279: $phpDocTemplateTypes[$name] = $templateType;
280: $namesToUnset[] = $name;
281: }
282: }
283: $parent = $parent->unsetTemplatePhpDocNodes($namesToUnset);
284: }
285:
286: $templateTypeScope = $nameScope->getTemplateTypeScope();
287: if ($templateTypeScope === null) {
288: continue;
289: }
290:
291: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope);
292: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
293: $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap, $templateTags);
294: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope);
295: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
296: $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap, $templateTags);
297: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope);
298: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
299: foreach (array_keys($templateTags) as $name) {
300: $templateType = $templateTypeMap->getType($name);
301: if ($templateType === null) {
302: continue;
303: }
304: $phpDocTemplateTypes[$name] = $templateType;
305: }
306: }
307:
308: return new NameScope(
309: $intermediaryNameScope->getNamespace(),
310: $intermediaryNameScope->getUses(),
311: $intermediaryNameScope->getClassName(),
312: $intermediaryNameScope->getFunctionName(),
313: new TemplateTypeMap($phpDocTemplateTypes),
314: $templateTags,
315: $intermediaryNameScope->getTypeAliasesMap(),
316: $intermediaryNameScope->shouldBypassTypeAliases(),
317: $intermediaryNameScope->getConstUses(),
318: $intermediaryNameScope->getClassNameForTypeAlias(),
319: );
320: } finally {
321: unset($this->inProcess[$nameScopeKey]);
322: }
323: }
324:
325: /**
326: * @return array{array<string, IntermediaryNameScope>}
327: */
328: private function getNameScopeMap(string $fileName): array
329: {
330: if (!isset($this->memoryCache[$fileName])) {
331: $cacheKey = sprintf('ftm-%s', $fileName);
332: $variableCacheKey = sprintf('v5-%s', ComposerHelper::getPhpDocParserVersion());
333: $cached = $this->loadCachedPhpDocNodeMap($cacheKey, $variableCacheKey);
334: if ($cached === null) {
335: [$nameScopeMap, $files] = $this->createPhpDocNodeMap($fileName, null, null, [], $fileName);
336: $filesWithHashes = [];
337: foreach ($files as $file) {
338: $newHash = hash_file('sha256', $file);
339: $filesWithHashes[$file] = $newHash;
340: }
341: $this->cache->save($cacheKey, $variableCacheKey, [$nameScopeMap, $filesWithHashes]);
342: } else {
343: [$nameScopeMap, $files] = $cached;
344: }
345: if ($this->memoryCacheCount >= $this->nameScopeMapMemoryCacheCountMax) {
346: $this->memoryCache = array_slice(
347: $this->memoryCache,
348: 1,
349: preserve_keys: true,
350: );
351: $this->memoryCacheCount--;
352: }
353:
354: $this->memoryCache[$fileName] = [$nameScopeMap, $files];
355: $this->memoryCacheCount++;
356: }
357:
358: return $this->memoryCache[$fileName];
359: }
360:
361: /**
362: * @param non-empty-string $cacheKey
363: * @return array{array<string, IntermediaryNameScope>, list<string>}|null
364: */
365: private function loadCachedPhpDocNodeMap(string $cacheKey, string $variableCacheKey): ?array
366: {
367: $cached = $this->cache->load($cacheKey, $variableCacheKey);
368: if ($cached !== null) {
369: /**
370: * @var array<string, string> $filesWithHashes
371: */
372: [$nameScopeMap, $filesWithHashes] = $cached;
373: $useCache = true;
374: foreach ($filesWithHashes as $file => $hash) {
375: $newHash = @hash_file('sha256', $file);
376: if ($newHash === false) {
377: $useCache = false;
378: break;
379: }
380: if ($newHash === $hash) {
381: continue;
382: }
383: $useCache = false;
384: break;
385: }
386:
387: if ($useCache) {
388: return [$nameScopeMap, array_keys($filesWithHashes)];
389: }
390: }
391:
392: return null;
393: }
394:
395: /**
396: * @param array<string, string> $traitMethodAliases
397: * @param array<string, true> $activeTraitResolutions
398: * @return array{array<string, IntermediaryNameScope>, list<string>}
399: */
400: private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName, array $activeTraitResolutions = []): array
401: {
402: /** @var array<string, IntermediaryNameScope> $nameScopeMap */
403: $nameScopeMap = [];
404:
405: /** @var array<int, IntermediaryNameScope> $typeMapStack */
406: $typeMapStack = [];
407:
408: /** @var array<int, array<string, true>> $typeAliasStack */
409: $typeAliasStack = [];
410:
411: /** @var string[] $classStack */
412: $classStack = [];
413: if ($lookForTrait !== null && $traitUseClass !== null) {
414: $classStack[] = $traitUseClass;
415: $typeAliasStack[] = [];
416: }
417: $namespace = null;
418:
419: $traitFound = false;
420:
421: $files = [$fileName];
422:
423: /** @var array<string|null> $functionStack */
424: $functionStack = [];
425: $uses = [];
426: $constUses = [];
427: $this->processNodes(
428: $this->phpParser->parseFile($fileName),
429: function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, $activeTraitResolutions, &$nameScopeMap, &$typeMapStack, &$typeAliasStack, &$classStack, &$namespace, &$functionStack, &$uses, &$constUses, &$files): ?int {
430: if ($node instanceof Node\Stmt\ClassLike) {
431: if ($traitFound && $fileName === $originalClassFileName) {
432: return self::SKIP_NODE;
433: }
434:
435: if ($lookForTrait !== null && !$traitFound) {
436: if (!$node instanceof Node\Stmt\Trait_) {
437: return self::SKIP_NODE;
438: }
439: if ((string) $node->namespacedName !== $lookForTrait) {
440: return self::SKIP_NODE;
441: }
442:
443: $traitFound = true;
444: $functionStack[] = null;
445: } else {
446: if ($node->name === null) {
447: if (!$node instanceof Node\Stmt\Class_) {
448: throw new ShouldNotHappenException();
449: }
450:
451: $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName);
452: } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) {
453: $className = $node->name->name;
454: } else {
455: if ($traitFound) {
456: return self::SKIP_NODE;
457: }
458: $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
459: }
460: $classStack[] = $className;
461: $functionStack[] = null;
462: }
463: } elseif ($node instanceof Node\Stmt\ClassMethod) {
464: if (array_key_exists($node->name->name, $traitMethodAliases)) {
465: $functionStack[] = $traitMethodAliases[$node->name->name];
466: } else {
467: $functionStack[] = $node->name->name;
468: }
469: } elseif ($node instanceof Node\Stmt\Function_) {
470: $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
471: } elseif ($node instanceof Node\PropertyHook) {
472: $propertyName = $node->getAttribute('propertyName');
473: if ($propertyName !== null) {
474: $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString());
475: }
476: }
477:
478: $className = array_last($classStack) ?? null;
479: $functionName = array_last($functionStack) ?? null;
480: $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
481:
482: $phpDocNode = null;
483: $docComment = null;
484: if (
485: $node instanceof Node\Stmt
486: || ($node instanceof Node\PropertyHook && $node->getAttribute('propertyName') !== null)
487: ) {
488: $docComment = GetLastDocComment::forNode($node);
489: if ($docComment !== null) {
490: $phpDocNode = $this->phpDocStringResolver->resolve($docComment);
491: }
492: }
493:
494: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_ || $node instanceof Node\PropertyHook) {
495: if ($phpDocNode !== null) {
496: if ($node instanceof Node\Stmt\ClassLike) {
497: $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNode);
498: }
499:
500: $parentNameScope = array_last($typeMapStack) ?? null;
501:
502: $typeMapStack[] = new IntermediaryNameScope(
503: $namespace,
504: $uses,
505: $className,
506: $functionName,
507: $this->chooseTemplateTagValueNodesByPriority($phpDocNode->getTags()),
508: $parentNameScope,
509: array_last($typeAliasStack) ?? [],
510: constUses: $constUses,
511: typeAliasClassName: $lookForTrait,
512: );
513: } elseif ($node instanceof Node\Stmt\ClassLike) {
514: $typeAliasStack[] = [];
515: } else {
516: $parentNameScope = array_last($typeMapStack) ?? null;
517: $typeMapStack[] = new IntermediaryNameScope(
518: $namespace,
519: $uses,
520: $className,
521: $functionName,
522: [],
523: $parentNameScope,
524: array_last($typeAliasStack) ?? [],
525: constUses: $constUses,
526: typeAliasClassName: $lookForTrait,
527: );
528: }
529: }
530:
531: if (
532: (
533: $node instanceof Node\PropertyHook
534: || (
535: $node instanceof Node\Stmt
536: && !$node instanceof Node\Stmt\Namespace_
537: && !$node instanceof Node\Stmt\Declare_
538: && !$node instanceof Node\Stmt\Use_
539: && !$node instanceof Node\Stmt\GroupUse
540: && !$node instanceof Node\Stmt\TraitUse
541: && !$node instanceof Node\Stmt\TraitUseAdaptation
542: && !$node instanceof Node\Stmt\InlineHTML
543: && !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_)
544: )
545: ) && !array_key_exists($nameScopeKey, $nameScopeMap)
546: ) {
547: $parentNameScope = array_last($typeMapStack) ?? null;
548: $typeAliasesMap = array_last($typeAliasStack) ?? [];
549: $nameScopeMap[$nameScopeKey] = new IntermediaryNameScope(
550: $namespace,
551: $uses,
552: $className,
553: $functionName,
554: $parentNameScope !== null ? $parentNameScope->getTemplatePhpDocNodes() : [],
555: $parentNameScope !== null ? $parentNameScope->getParent() : null,
556: $typeAliasesMap,
557: constUses: $constUses,
558: typeAliasClassName: $lookForTrait,
559: );
560: }
561:
562: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_ || $node instanceof Node\PropertyHook) {
563: if ($phpDocNode !== null || !$node instanceof Node\Stmt\ClassLike) {
564: return self::POP_TYPE_MAP_STACK;
565: }
566:
567: return null;
568: }
569:
570: if ($node instanceof Node\Stmt\Namespace_) {
571: $namespace = $node->name !== null ? (string) $node->name : null;
572: } elseif ($node instanceof Node\Stmt\Use_) {
573: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL) {
574: foreach ($node->uses as $use) {
575: $uses[strtolower($use->getAlias()->name)] = (string) $use->name;
576: }
577: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT) {
578: foreach ($node->uses as $use) {
579: $constUses[strtolower($use->getAlias()->name)] = (string) $use->name;
580: }
581: }
582: } elseif ($node instanceof Node\Stmt\GroupUse) {
583: $prefix = (string) $node->prefix;
584: foreach ($node->uses as $use) {
585: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL || $use->type === Node\Stmt\Use_::TYPE_NORMAL) {
586: $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
587: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT || $use->type === Node\Stmt\Use_::TYPE_CONSTANT) {
588: $constUses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
589: }
590: }
591: } elseif ($node instanceof Node\Stmt\TraitUse) {
592: $traitMethodAliases = [];
593: foreach ($node->adaptations as $traitUseAdaptation) {
594: if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
595: continue;
596: }
597:
598: if ($traitUseAdaptation->newName === null) {
599: continue;
600: }
601:
602: $methodName = $traitUseAdaptation->method->toString();
603: $newTraitName = $traitUseAdaptation->newName->toString();
604:
605: if ($traitUseAdaptation->trait === null) {
606: foreach ($node->traits as $traitName) {
607: $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName;
608: }
609: continue;
610: }
611:
612: $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName;
613: }
614:
615: foreach ($node->traits as $traitName) {
616: /** @var class-string $traitName */
617: $traitName = (string) $traitName;
618: $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
619: if (!$reflectionProvider->hasClass($traitName)) {
620: continue;
621: }
622:
623: $traitReflection = $reflectionProvider->getClass($traitName);
624: if (!$traitReflection->isTrait()) {
625: continue;
626: }
627: if ($traitReflection->getFileName() === null) {
628: continue;
629: }
630: if (!is_file($traitReflection->getFileName())) {
631: continue;
632: }
633:
634: $className = array_last($classStack) ?? null;
635: if ($className === null) {
636: throw new ShouldNotHappenException();
637: }
638:
639: $traitResolutionKey = $this->getTraitResolutionKey($traitReflection->getFileName(), $traitName, $className, $originalClassFileName);
640: if (isset($activeTraitResolutions[$traitResolutionKey])) {
641: continue;
642: }
643:
644: $nestedActiveTraitResolutions = $activeTraitResolutions;
645: $nestedActiveTraitResolutions[$traitResolutionKey] = true;
646:
647: [$traitNameScopeMap, $traitFiles] = $this->createPhpDocNodeMap(
648: $traitReflection->getFileName(),
649: $traitName,
650: $className,
651: $traitMethodAliases[$traitName] ?? [],
652: $originalClassFileName,
653: $nestedActiveTraitResolutions,
654: );
655: $nameScopeMap = array_merge($nameScopeMap, array_map(static fn ($originalNameScope) => $originalNameScope->getTraitData() === null ? $originalNameScope->withTraitData($originalClassFileName, $className, $traitName, $lookForTrait, $docComment) : $originalNameScope, $traitNameScopeMap));
656: $files = array_merge($files, $traitFiles);
657: }
658: }
659:
660: return null;
661: },
662: static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void {
663: if ($node instanceof Node\Stmt\ClassLike) {
664: if (count($classStack) === 0) {
665: throw new ShouldNotHappenException();
666: }
667: array_pop($classStack);
668:
669: if (count($typeAliasStack) === 0) {
670: throw new ShouldNotHappenException();
671: }
672:
673: array_pop($typeAliasStack);
674:
675: if (count($functionStack) === 0) {
676: throw new ShouldNotHappenException();
677: }
678:
679: array_pop($functionStack);
680: } elseif ($node instanceof Node\Stmt\Namespace_) {
681: $namespace = null;
682: $uses = [];
683: $constUses = [];
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: