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