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