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