1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use Closure;
6: use PhpParser\Node;
7: use PHPStan\Analyser\NameScope;
8: use PHPStan\BetterReflection\Util\GetLastDocComment;
9: use PHPStan\Broker\AnonymousClassNameHelper;
10: use PHPStan\File\FileHelper;
11: use PHPStan\Parser\Parser;
12: use PHPStan\PhpDoc\PhpDocNodeResolver;
13: use PHPStan\PhpDoc\PhpDocStringResolver;
14: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
15: use PHPStan\PhpDoc\Tag\TemplateTag;
16: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
17: use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
18: use PHPStan\ShouldNotHappenException;
19: use PHPStan\Type\Generic\GenericObjectType;
20: use PHPStan\Type\Generic\TemplateTypeFactory;
21: use PHPStan\Type\Generic\TemplateTypeHelper;
22: use PHPStan\Type\Generic\TemplateTypeMap;
23: use PHPStan\Type\Generic\TemplateTypeVariance;
24: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
25: use function array_key_exists;
26: use function array_keys;
27: use function array_map;
28: use function array_merge;
29: use function array_pop;
30: use function array_slice;
31: use function count;
32: use function is_array;
33: use function is_callable;
34: use function is_file;
35: use function ltrim;
36: use function md5;
37: use function sprintf;
38: use function str_contains;
39: use function strtolower;
40:
41: final class FileTypeMapper
42: {
43:
44: private const SKIP_NODE = 1;
45: private const POP_TYPE_MAP_STACK = 2;
46:
47: /** @var NameScope[][] */
48: private array $memoryCache = [];
49:
50: private int $memoryCacheCount = 0;
51:
52: /** @var (true|callable(): NameScope|NameScope)[][] */
53: private array $inProcess = [];
54:
55: /** @var array<string, ResolvedPhpDocBlock> */
56: private array $resolvedPhpDocBlockCache = [];
57:
58: private int $resolvedPhpDocBlockCacheCount = 0;
59:
60: public function __construct(
61: private ReflectionProviderProvider $reflectionProviderProvider,
62: private Parser $phpParser,
63: private PhpDocStringResolver $phpDocStringResolver,
64: private PhpDocNodeResolver $phpDocNodeResolver,
65: private AnonymousClassNameHelper $anonymousClassNameHelper,
66: private FileHelper $fileHelper,
67: )
68: {
69: }
70:
71: /** @api */
72: public function getResolvedPhpDoc(
73: ?string $fileName,
74: ?string $className,
75: ?string $traitName,
76: ?string $functionName,
77: string $docComment,
78: ): ResolvedPhpDocBlock
79: {
80: if ($className === null && $traitName !== null) {
81: throw new ShouldNotHappenException();
82: }
83:
84: if ($docComment === '') {
85: return ResolvedPhpDocBlock::createEmpty();
86: }
87:
88: if ($fileName !== null) {
89: $fileName = $this->fileHelper->normalizePath($fileName);
90: }
91:
92: $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName);
93: $phpDocKey = md5(sprintf('%s-%s', $nameScopeKey, $docComment));
94: if (isset($this->resolvedPhpDocBlockCache[$phpDocKey])) {
95: return $this->resolvedPhpDocBlockCache[$phpDocKey];
96: }
97:
98: if ($fileName === null) {
99: return $this->createResolvedPhpDocBlock($phpDocKey, new NameScope(null, []), $docComment, null);
100: }
101:
102: $nameScopeMap = [];
103:
104: if (!isset($this->inProcess[$fileName])) {
105: $nameScopeMap = $this->getNameScopeMap($fileName);
106: }
107:
108: if (isset($nameScopeMap[$nameScopeKey])) {
109: return $this->createResolvedPhpDocBlock($phpDocKey, $nameScopeMap[$nameScopeKey], $docComment, $fileName);
110: }
111:
112: if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits
113: return ResolvedPhpDocBlock::createEmpty();
114: }
115:
116: if ($this->inProcess[$fileName][$nameScopeKey] === true) { // PHPDoc has cyclic dependency
117: return ResolvedPhpDocBlock::createEmpty();
118: }
119:
120: if (is_callable($this->inProcess[$fileName][$nameScopeKey])) {
121: $resolveCallback = $this->inProcess[$fileName][$nameScopeKey];
122: $this->inProcess[$fileName][$nameScopeKey] = true;
123: $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback();
124: }
125:
126: return $this->createResolvedPhpDocBlock($phpDocKey, $this->inProcess[$fileName][$nameScopeKey], $docComment, $fileName);
127: }
128:
129: private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameScope, string $phpDocString, ?string $fileName): ResolvedPhpDocBlock
130: {
131: $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString);
132: if ($this->resolvedPhpDocBlockCacheCount >= 2048) {
133: $this->resolvedPhpDocBlockCache = array_slice(
134: $this->resolvedPhpDocBlockCache,
135: 1,
136: null,
137: true,
138: );
139:
140: $this->resolvedPhpDocBlockCacheCount--;
141: }
142:
143: $templateTypeMap = $nameScope->getTemplateTypeMap();
144: $phpDocTemplateTypes = [];
145: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope);
146: foreach (array_keys($templateTags) as $name) {
147: $templateType = $templateTypeMap->getType($name);
148: if ($templateType === null) {
149: continue;
150: }
151: $phpDocTemplateTypes[$name] = $templateType;
152: }
153:
154: $this->resolvedPhpDocBlockCache[$phpDocKey] = ResolvedPhpDocBlock::create(
155: $phpDocNode,
156: $phpDocString,
157: $fileName,
158: $nameScope,
159: new TemplateTypeMap($phpDocTemplateTypes),
160: $templateTags,
161: $this->phpDocNodeResolver,
162: $this->reflectionProviderProvider->getReflectionProvider(),
163: );
164: $this->resolvedPhpDocBlockCacheCount++;
165:
166: return $this->resolvedPhpDocBlockCache[$phpDocKey];
167: }
168:
169: /**
170: * @return NameScope[]
171: */
172: private function getNameScopeMap(string $fileName): array
173: {
174: if (!isset($this->memoryCache[$fileName])) {
175: $map = $this->createResolvedPhpDocMap($fileName);
176: if ($this->memoryCacheCount >= 2048) {
177: $this->memoryCache = array_slice(
178: $this->memoryCache,
179: 1,
180: null,
181: true,
182: );
183: $this->memoryCacheCount--;
184: }
185:
186: $this->memoryCache[$fileName] = $map;
187: $this->memoryCacheCount++;
188: }
189:
190: return $this->memoryCache[$fileName];
191: }
192:
193: /**
194: * @return NameScope[]
195: */
196: private function createResolvedPhpDocMap(string $fileName): array
197: {
198: $phpDocNodeMap = $this->createPhpDocNodeMap($fileName, null, $fileName, [], $fileName);
199: $nameScopeMap = $this->createNameScopeMap($fileName, null, null, [], $fileName, $phpDocNodeMap);
200: $resolvedNameScopeMap = [];
201:
202: try {
203: $this->inProcess[$fileName] = $nameScopeMap;
204:
205: foreach ($nameScopeMap as $nameScopeKey => $resolveCallback) {
206: $this->inProcess[$fileName][$nameScopeKey] = true;
207: $this->inProcess[$fileName][$nameScopeKey] = $data = $resolveCallback();
208: $resolvedNameScopeMap[$nameScopeKey] = $data;
209: }
210:
211: } finally {
212: unset($this->inProcess[$fileName]);
213: }
214:
215: return $resolvedNameScopeMap;
216: }
217:
218: /**
219: * @param array<string, string> $traitMethodAliases
220: * @return array<string, PhpDocNode>
221: */
222: private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName): array
223: {
224: /** @var array<string, PhpDocNode> $phpDocNodeMap */
225: $phpDocNodeMap = [];
226:
227: /** @var string[] $classStack */
228: $classStack = [];
229: if ($lookForTrait !== null && $traitUseClass !== null) {
230: $classStack[] = $traitUseClass;
231: }
232: $namespace = null;
233:
234: $traitFound = false;
235:
236: /** @var array<string|null> $functionStack */
237: $functionStack = [];
238: $this->processNodes(
239: $this->phpParser->parseFile($fileName),
240: function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$phpDocNodeMap, &$classStack, &$namespace, &$functionStack): ?int {
241: if ($node instanceof Node\Stmt\ClassLike) {
242: if ($traitFound && $fileName === $originalClassFileName) {
243: return self::SKIP_NODE;
244: }
245:
246: if ($lookForTrait !== null && !$traitFound) {
247: if (!$node instanceof Node\Stmt\Trait_) {
248: return self::SKIP_NODE;
249: }
250: if ((string) $node->namespacedName !== $lookForTrait) {
251: return self::SKIP_NODE;
252: }
253:
254: $traitFound = true;
255: $functionStack[] = null;
256: } else {
257: if ($node->name === null) {
258: if (!$node instanceof Node\Stmt\Class_) {
259: throw new ShouldNotHappenException();
260: }
261:
262: $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName);
263: } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) {
264: $className = $node->name->name;
265: } else {
266: if ($traitFound) {
267: return self::SKIP_NODE;
268: }
269: $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
270: }
271: $classStack[] = $className;
272: $functionStack[] = null;
273: }
274: } elseif ($node instanceof Node\Stmt\ClassMethod) {
275: if (array_key_exists($node->name->name, $traitMethodAliases)) {
276: $functionStack[] = $traitMethodAliases[$node->name->name];
277: } else {
278: $functionStack[] = $node->name->name;
279: }
280: } elseif ($node instanceof Node\Stmt\Function_) {
281: $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
282: } elseif ($node instanceof Node\PropertyHook) {
283: $propertyName = $node->getAttribute('propertyName');
284: if ($propertyName !== null) {
285: $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString());
286: }
287: }
288:
289: $className = $classStack[count($classStack) - 1] ?? null;
290: $functionName = $functionStack[count($functionStack) - 1] ?? null;
291:
292: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
293: $docComment = GetLastDocComment::forNode($node);
294: if ($docComment !== null) {
295: $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
296: $phpDocNodeMap[$nameScopeKey] = $this->phpDocStringResolver->resolve($docComment);
297: }
298:
299: return null;
300: } elseif ($node instanceof Node\PropertyHook) {
301: $propertyName = $node->getAttribute('propertyName');
302: if ($propertyName !== null) {
303: $docComment = GetLastDocComment::forNode($node);
304: if ($docComment !== null) {
305: $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
306: $phpDocNodeMap[$nameScopeKey] = $this->phpDocStringResolver->resolve($docComment);
307: }
308: }
309:
310: return null;
311: }
312:
313: if ($node instanceof Node\Stmt\Namespace_) {
314: $namespace = $node->name !== null ? (string) $node->name : null;
315: } elseif ($node instanceof Node\Stmt\TraitUse) {
316: $traitMethodAliases = [];
317: foreach ($node->adaptations as $traitUseAdaptation) {
318: if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
319: continue;
320: }
321:
322: if ($traitUseAdaptation->newName === null) {
323: continue;
324: }
325:
326: $methodName = $traitUseAdaptation->method->toString();
327: $newTraitName = $traitUseAdaptation->newName->toString();
328:
329: if ($traitUseAdaptation->trait === null) {
330: foreach ($node->traits as $traitName) {
331: $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName;
332: }
333: continue;
334: }
335:
336: $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName;
337: }
338:
339: foreach ($node->traits as $traitName) {
340: /** @var class-string $traitName */
341: $traitName = (string) $traitName;
342: $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
343: if (!$reflectionProvider->hasClass($traitName)) {
344: continue;
345: }
346:
347: $traitReflection = $reflectionProvider->getClass($traitName);
348: if (!$traitReflection->isTrait()) {
349: continue;
350: }
351: if ($traitReflection->getFileName() === null) {
352: continue;
353: }
354: if (!is_file($traitReflection->getFileName())) {
355: continue;
356: }
357:
358: $className = $classStack[count($classStack) - 1] ?? null;
359: if ($className === null) {
360: throw new ShouldNotHappenException();
361: }
362:
363: $phpDocNodeMap = array_merge($phpDocNodeMap, $this->createPhpDocNodeMap(
364: $traitReflection->getFileName(),
365: $traitName,
366: $className,
367: $traitMethodAliases[$traitName] ?? [],
368: $originalClassFileName,
369: ));
370: }
371: }
372:
373: return null;
374: },
375: static function (Node $node) use (&$namespace, &$functionStack, &$classStack): void {
376: if ($node instanceof Node\Stmt\ClassLike) {
377: if (count($classStack) === 0) {
378: throw new ShouldNotHappenException();
379: }
380: array_pop($classStack);
381:
382: if (count($functionStack) === 0) {
383: throw new ShouldNotHappenException();
384: }
385:
386: array_pop($functionStack);
387: } elseif ($node instanceof Node\Stmt\Namespace_) {
388: $namespace = null;
389: } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
390: if (count($functionStack) === 0) {
391: throw new ShouldNotHappenException();
392: }
393:
394: array_pop($functionStack);
395: } elseif ($node instanceof Node\PropertyHook) {
396: $propertyName = $node->getAttribute('propertyName');
397: if ($propertyName !== null) {
398: if (count($functionStack) === 0) {
399: throw new ShouldNotHappenException();
400: }
401:
402: array_pop($functionStack);
403: }
404: }
405: },
406: );
407:
408: return $phpDocNodeMap;
409: }
410:
411: /**
412: * @param array<string, string> $traitMethodAliases
413: * @param array<string, PhpDocNode> $phpDocNodeMap
414: * @return (callable(): NameScope)[]
415: */
416: private function createNameScopeMap(
417: string $fileName,
418: ?string $lookForTrait,
419: ?string $traitUseClass,
420: array $traitMethodAliases,
421: string $originalClassFileName,
422: array $phpDocNodeMap,
423: ): array
424: {
425: /** @var (callable(): NameScope)[] $nameScopeMap */
426: $nameScopeMap = [];
427:
428: /** @var (callable(): TemplateTypeMap)[] $typeMapStack */
429: $typeMapStack = [];
430:
431: /** @var array<int, array<string, true>> $typeAliasStack */
432: $typeAliasStack = [];
433:
434: /** @var string[] $classStack */
435: $classStack = [];
436: if ($lookForTrait !== null && $traitUseClass !== null) {
437: $classStack[] = $traitUseClass;
438: $typeAliasStack[] = [];
439: }
440: $namespace = null;
441:
442: $traitFound = false;
443:
444: /** @var array<string|null> $functionStack */
445: $functionStack = [];
446: $uses = [];
447: $constUses = [];
448: $this->processNodes(
449: $this->phpParser->parseFile($fileName),
450: function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack, &$constUses): ?int {
451: if ($node instanceof Node\Stmt\ClassLike) {
452: if ($traitFound && $fileName === $originalClassFileName) {
453: return self::SKIP_NODE;
454: }
455:
456: if ($lookForTrait !== null && !$traitFound) {
457: if (!$node instanceof Node\Stmt\Trait_) {
458: return self::SKIP_NODE;
459: }
460: if ((string) $node->namespacedName !== $lookForTrait) {
461: return self::SKIP_NODE;
462: }
463:
464: $traitFound = true;
465: $traitNameScopeKey = $this->getNameScopeKey($originalClassFileName, $classStack[count($classStack) - 1] ?? null, $lookForTrait, null);
466: if (array_key_exists($traitNameScopeKey, $phpDocNodeMap)) {
467: $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNodeMap[$traitNameScopeKey]);
468: } else {
469: $typeAliasStack[] = [];
470: }
471: $functionStack[] = null;
472: } else {
473: if ($node->name === null) {
474: if (!$node instanceof Node\Stmt\Class_) {
475: throw new ShouldNotHappenException();
476: }
477:
478: $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName);
479: } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) {
480: $className = $node->name->name;
481: } else {
482: if ($traitFound) {
483: return self::SKIP_NODE;
484: }
485: $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
486: }
487: $classStack[] = $className;
488: $classNameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, null);
489: if (array_key_exists($classNameScopeKey, $phpDocNodeMap)) {
490: $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNodeMap[$classNameScopeKey]);
491: } else {
492: $typeAliasStack[] = [];
493: }
494: $functionStack[] = null;
495: }
496: } elseif ($node instanceof Node\Stmt\ClassMethod) {
497: if (array_key_exists($node->name->name, $traitMethodAliases)) {
498: $functionStack[] = $traitMethodAliases[$node->name->name];
499: } else {
500: $functionStack[] = $node->name->name;
501: }
502: } elseif ($node instanceof Node\Stmt\Function_) {
503: $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
504: } elseif ($node instanceof Node\PropertyHook) {
505: $propertyName = $node->getAttribute('propertyName');
506: if ($propertyName !== null) {
507: $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString());
508: }
509: }
510:
511: $className = $classStack[count($classStack) - 1] ?? null;
512: $functionName = $functionStack[count($functionStack) - 1] ?? null;
513: $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
514:
515: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
516: // property hook skipped on purpose, it does not support @template
517: if (array_key_exists($nameScopeKey, $phpDocNodeMap)) {
518: $phpDocNode = $phpDocNodeMap[$nameScopeKey];
519: $typeMapStack[] = function () use ($namespace, $uses, $className, $lookForTrait, $functionName, $phpDocNode, $typeMapStack, $typeAliasStack, $constUses): TemplateTypeMap {
520: $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null;
521: $currentTypeMap = $typeMapCb !== null ? $typeMapCb() : null;
522: $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? [];
523: $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, $typeAliasesMap, false, $constUses, $lookForTrait);
524: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope);
525: $templateTypeScope = $nameScope->getTemplateTypeScope();
526: if ($templateTypeScope === null) {
527: throw new ShouldNotHappenException();
528: }
529: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
530: $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap);
531: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope);
532: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
533:
534: return new TemplateTypeMap(array_merge(
535: $currentTypeMap !== null ? $currentTypeMap->getTypes() : [],
536: $templateTypeMap->getTypes(),
537: ));
538: };
539: }
540: }
541:
542: $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null;
543: $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? [];
544:
545: if (
546: (
547: $node instanceof Node\PropertyHook
548: || (
549: $node instanceof Node\Stmt
550: && !$node instanceof Node\Stmt\Namespace_
551: && !$node instanceof Node\Stmt\Declare_
552: && !$node instanceof Node\Stmt\Use_
553: && !$node instanceof Node\Stmt\GroupUse
554: && !$node instanceof Node\Stmt\TraitUse
555: && !$node instanceof Node\Stmt\TraitUseAdaptation
556: && !$node instanceof Node\Stmt\InlineHTML
557: && !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_)
558: )
559: ) && !array_key_exists($nameScopeKey, $nameScopeMap)
560: ) {
561: $nameScopeMap[$nameScopeKey] = static fn (): NameScope => new NameScope(
562: $namespace,
563: $uses,
564: $className,
565: $functionName,
566: ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty()),
567: $typeAliasesMap,
568: false,
569: $constUses,
570: $lookForTrait,
571: );
572: }
573:
574: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
575: // property hook skipped on purpose, it does not support @template
576: if (array_key_exists($nameScopeKey, $phpDocNodeMap)) {
577: return self::POP_TYPE_MAP_STACK;
578: }
579:
580: return null;
581: }
582:
583: if ($node instanceof Node\Stmt\Namespace_) {
584: $namespace = $node->name !== null ? (string) $node->name : null;
585: } elseif ($node instanceof Node\Stmt\Use_) {
586: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL) {
587: foreach ($node->uses as $use) {
588: $uses[strtolower($use->getAlias()->name)] = (string) $use->name;
589: }
590: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT) {
591: foreach ($node->uses as $use) {
592: $constUses[strtolower($use->getAlias()->name)] = (string) $use->name;
593: }
594: }
595: } elseif ($node instanceof Node\Stmt\GroupUse) {
596: $prefix = (string) $node->prefix;
597: foreach ($node->uses as $use) {
598: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL || $use->type === Node\Stmt\Use_::TYPE_NORMAL) {
599: $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
600: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT || $use->type === Node\Stmt\Use_::TYPE_CONSTANT) {
601: $constUses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
602: }
603: }
604: } elseif ($node instanceof Node\Stmt\TraitUse) {
605: $traitMethodAliases = [];
606: foreach ($node->adaptations as $traitUseAdaptation) {
607: if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
608: continue;
609: }
610:
611: if ($traitUseAdaptation->newName === null) {
612: continue;
613: }
614:
615: $methodName = $traitUseAdaptation->method->toString();
616: $newTraitName = $traitUseAdaptation->newName->toString();
617:
618: if ($traitUseAdaptation->trait === null) {
619: foreach ($node->traits as $traitName) {
620: $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName;
621: }
622: continue;
623: }
624:
625: $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName;
626: }
627:
628: $useDocComment = null;
629: if ($node->getDocComment() !== null) {
630: $useDocComment = $node->getDocComment()->getText();
631: }
632:
633: foreach ($node->traits as $traitName) {
634: /** @var class-string $traitName */
635: $traitName = (string) $traitName;
636: $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
637: if (!$reflectionProvider->hasClass($traitName)) {
638: continue;
639: }
640:
641: $traitReflection = $reflectionProvider->getClass($traitName);
642: if (!$traitReflection->isTrait()) {
643: continue;
644: }
645: if ($traitReflection->getFileName() === null) {
646: continue;
647: }
648: if (!is_file($traitReflection->getFileName())) {
649: continue;
650: }
651:
652: $className = $classStack[count($classStack) - 1] ?? null;
653: if ($className === null) {
654: throw new ShouldNotHappenException();
655: }
656:
657: $traitPhpDocMap = $this->createNameScopeMap(
658: $traitReflection->getFileName(),
659: $traitName,
660: $className,
661: $traitMethodAliases[$traitName] ?? [],
662: $originalClassFileName,
663: $phpDocNodeMap,
664: );
665: $finalTraitPhpDocMap = [];
666: foreach ($traitPhpDocMap as $nameScopeTraitKey => $callback) {
667: $finalTraitPhpDocMap[$nameScopeTraitKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScope {
668: /** @var NameScope $original */
669: $original = $callback();
670: if (!$traitReflection->isGeneric()) {
671: return $original;
672: }
673:
674: $traitTemplateTypeMap = $traitReflection->getTemplateTypeMap();
675:
676: $useType = null;
677: if ($useDocComment !== null) {
678: $useTags = $this->getResolvedPhpDoc(
679: $fileName,
680: $className,
681: $lookForTrait,
682: null,
683: $useDocComment,
684: )->getUsesTags();
685: foreach ($useTags as $useTag) {
686: $useTagType = $useTag->getType();
687: if (!$useTagType instanceof GenericObjectType) {
688: continue;
689: }
690:
691: if ($useTagType->getClassName() !== $traitReflection->getName()) {
692: continue;
693: }
694:
695: $useType = $useTagType;
696: break;
697: }
698: }
699:
700: if ($useType === null) {
701: return $original->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds());
702: }
703:
704: $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes());
705:
706: return $original->withTemplateTypeMap($traitTemplateTypeMap->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap, TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createStatic())));
707: };
708: }
709: $nameScopeMap = array_merge($nameScopeMap, $finalTraitPhpDocMap);
710: }
711: }
712:
713: return null;
714: },
715: static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void {
716: if ($node instanceof Node\Stmt\ClassLike) {
717: if (count($classStack) === 0) {
718: throw new ShouldNotHappenException();
719: }
720: array_pop($classStack);
721:
722: if (count($typeAliasStack) === 0) {
723: throw new ShouldNotHappenException();
724: }
725:
726: array_pop($typeAliasStack);
727:
728: if (count($functionStack) === 0) {
729: throw new ShouldNotHappenException();
730: }
731:
732: array_pop($functionStack);
733: } elseif ($node instanceof Node\Stmt\Namespace_) {
734: $namespace = null;
735: $uses = [];
736: $constUses = [];
737: } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
738: if (count($functionStack) === 0) {
739: throw new ShouldNotHappenException();
740: }
741:
742: array_pop($functionStack);
743: } elseif ($node instanceof Node\PropertyHook) {
744: $propertyName = $node->getAttribute('propertyName');
745: if ($propertyName !== null) {
746: if (count($functionStack) === 0) {
747: throw new ShouldNotHappenException();
748: }
749:
750: array_pop($functionStack);
751: }
752: }
753: if ($callbackResult !== self::POP_TYPE_MAP_STACK) {
754: return;
755: }
756:
757: if (count($typeMapStack) === 0) {
758: throw new ShouldNotHappenException();
759: }
760: array_pop($typeMapStack);
761: },
762: );
763:
764: if (count($typeMapStack) > 0) {
765: throw new ShouldNotHappenException();
766: }
767:
768: return $nameScopeMap;
769: }
770:
771: /**
772: * @return array<string, true>
773: */
774: private function getTypeAliasesMap(PhpDocNode $phpDocNode): array
775: {
776: $nameScope = new NameScope(null, []);
777:
778: $aliasesMap = [];
779: foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasImportTags($phpDocNode, $nameScope)) as $key) {
780: $aliasesMap[$key] = true;
781: }
782:
783: foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasTags($phpDocNode, $nameScope)) as $key) {
784: $aliasesMap[$key] = true;
785: }
786:
787: return $aliasesMap;
788: }
789:
790: /**
791: * @param Node[]|Node|scalar|null $node
792: * @param Closure(Node $node): mixed $nodeCallback
793: * @param Closure(Node $node, mixed $callbackResult): void $endNodeCallback
794: */
795: private function processNodes($node, Closure $nodeCallback, Closure $endNodeCallback): void
796: {
797: if ($node instanceof Node) {
798: $callbackResult = $nodeCallback($node);
799: if ($callbackResult === self::SKIP_NODE) {
800: return;
801: }
802: foreach ($node->getSubNodeNames() as $subNodeName) {
803: $subNode = $node->{$subNodeName};
804: $this->processNodes($subNode, $nodeCallback, $endNodeCallback);
805: }
806: $endNodeCallback($node, $callbackResult);
807: } elseif (is_array($node)) {
808: foreach ($node as $subNode) {
809: $this->processNodes($subNode, $nodeCallback, $endNodeCallback);
810: }
811: }
812: }
813:
814: private function getNameScopeKey(
815: ?string $file,
816: ?string $class,
817: ?string $trait,
818: ?string $function,
819: ): string
820: {
821: if ($class === null && $trait === null && $function === null) {
822: return md5(sprintf('%s', $file ?? 'no-file'));
823: }
824:
825: if ($class !== null && str_contains($class, 'class@anonymous')) {
826: throw new ShouldNotHappenException('Wrong anonymous class name, FilTypeMapper should be called with ClassReflection::getName().');
827: }
828:
829: return md5(sprintf('%s-%s-%s-%s', $file ?? 'no-file', $class, $trait, $function));
830: }
831:
832: }
833: