1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use Closure;
6: use PhpParser\Comment\Doc;
7: use PhpParser\Node;
8: use PHPStan\Analyser\NameScope;
9: use PHPStan\BetterReflection\Util\GetLastDocComment;
10: use PHPStan\Broker\AnonymousClassNameHelper;
11: use PHPStan\File\FileHelper;
12: use PHPStan\Parser\Parser;
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 function array_key_exists;
25: use function array_keys;
26: use function array_map;
27: use function array_merge;
28: use function array_pop;
29: use function array_slice;
30: use function count;
31: use function is_array;
32: use function is_callable;
33: use function is_file;
34: use function ltrim;
35: use function md5;
36: use function sprintf;
37: use function strpos;
38: use function strtolower;
39:
40: class FileTypeMapper
41: {
42:
43: private const SKIP_NODE = 1;
44: private const POP_TYPE_MAP_STACK = 2;
45:
46: /** @var NameScope[][] */
47: private array $memoryCache = [];
48:
49: private int $memoryCacheCount = 0;
50:
51: /** @var (false|callable(): NameScope|NameScope)[][] */
52: private array $inProcess = [];
53:
54: /** @var array<string, ResolvedPhpDocBlock> */
55: private array $resolvedPhpDocBlockCache = [];
56:
57: private int $resolvedPhpDocBlockCacheCount = 0;
58:
59: public function __construct(
60: private ReflectionProviderProvider $reflectionProviderProvider,
61: private Parser $phpParser,
62: private PhpDocStringResolver $phpDocStringResolver,
63: private PhpDocNodeResolver $phpDocNodeResolver,
64: private AnonymousClassNameHelper $anonymousClassNameHelper,
65: private FileHelper $fileHelper,
66: )
67: {
68: }
69:
70: /** @api */
71: public function getResolvedPhpDoc(
72: ?string $fileName,
73: ?string $className,
74: ?string $traitName,
75: ?string $functionName,
76: string $docComment,
77: ): ResolvedPhpDocBlock
78: {
79: if ($className === null && $traitName !== null) {
80: throw new ShouldNotHappenException();
81: }
82:
83: if ($docComment === '') {
84: return ResolvedPhpDocBlock::createEmpty();
85: }
86:
87: if ($fileName !== null) {
88: $fileName = $this->fileHelper->normalizePath($fileName);
89: }
90:
91: $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName);
92: $phpDocKey = md5(sprintf('%s-%s', $nameScopeKey, $docComment));
93: if (isset($this->resolvedPhpDocBlockCache[$phpDocKey])) {
94: return $this->resolvedPhpDocBlockCache[$phpDocKey];
95: }
96:
97: if ($fileName === null) {
98: return $this->createResolvedPhpDocBlock($phpDocKey, new NameScope(null, []), $docComment, null);
99: }
100:
101: $nameScopeMap = [];
102:
103: if (!isset($this->inProcess[$fileName])) {
104: $nameScopeMap = $this->getNameScopeMap($fileName);
105: }
106:
107: if (isset($nameScopeMap[$nameScopeKey])) {
108: return $this->createResolvedPhpDocBlock($phpDocKey, $nameScopeMap[$nameScopeKey], $docComment, $fileName);
109: }
110:
111: if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits
112: return ResolvedPhpDocBlock::createEmpty();
113: }
114:
115: if ($this->inProcess[$fileName][$nameScopeKey] === false) { // PHPDoc has cyclic dependency
116: return ResolvedPhpDocBlock::createEmpty();
117: }
118:
119: if (is_callable($this->inProcess[$fileName][$nameScopeKey])) {
120: $resolveCallback = $this->inProcess[$fileName][$nameScopeKey];
121: $this->inProcess[$fileName][$nameScopeKey] = false;
122: $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback();
123: }
124:
125: return $this->createResolvedPhpDocBlock($phpDocKey, $this->inProcess[$fileName][$nameScopeKey], $docComment, $fileName);
126: }
127:
128: private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameScope, string $phpDocString, ?string $fileName): ResolvedPhpDocBlock
129: {
130: $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString);
131: if ($this->resolvedPhpDocBlockCacheCount >= 2048) {
132: $this->resolvedPhpDocBlockCache = array_slice(
133: $this->resolvedPhpDocBlockCache,
134: 1,
135: null,
136: true,
137: );
138:
139: $this->resolvedPhpDocBlockCacheCount--;
140: }
141:
142: $templateTypeMap = $nameScope->getTemplateTypeMap();
143: $phpDocTemplateTypes = [];
144: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope);
145: foreach (array_keys($templateTags) as $name) {
146: $templateType = $templateTypeMap->getType($name);
147: if ($templateType === null) {
148: continue;
149: }
150: $phpDocTemplateTypes[$name] = $templateType;
151: }
152:
153: $this->resolvedPhpDocBlockCache[$phpDocKey] = ResolvedPhpDocBlock::create(
154: $phpDocNode,
155: $phpDocString,
156: $fileName,
157: $nameScope,
158: new TemplateTypeMap($phpDocTemplateTypes),
159: $templateTags,
160: $this->phpDocNodeResolver,
161: );
162: $this->resolvedPhpDocBlockCacheCount++;
163:
164: return $this->resolvedPhpDocBlockCache[$phpDocKey];
165: }
166:
167: private function resolvePhpDocStringToDocNode(string $phpDocString): PhpDocNode
168: {
169: return $this->phpDocStringResolver->resolve($phpDocString);
170: }
171:
172: /**
173: * @return NameScope[]
174: */
175: private function getNameScopeMap(string $fileName): array
176: {
177: if (!isset($this->memoryCache[$fileName])) {
178: $map = $this->createResolvedPhpDocMap($fileName);
179: if ($this->memoryCacheCount >= 2048) {
180: $this->memoryCache = array_slice(
181: $this->memoryCache,
182: 1,
183: null,
184: true,
185: );
186: $this->memoryCacheCount--;
187: }
188:
189: $this->memoryCache[$fileName] = $map;
190: $this->memoryCacheCount++;
191: }
192:
193: return $this->memoryCache[$fileName];
194: }
195:
196: /**
197: * @return NameScope[]
198: */
199: private function createResolvedPhpDocMap(string $fileName): array
200: {
201: $nameScopeMap = $this->createNameScopeMap($fileName, null, null, [], $fileName);
202: $resolvedNameScopeMap = [];
203:
204: try {
205: $this->inProcess[$fileName] = $nameScopeMap;
206:
207: foreach ($nameScopeMap as $nameScopeKey => $resolveCallback) {
208: $this->inProcess[$fileName][$nameScopeKey] = false;
209: $this->inProcess[$fileName][$nameScopeKey] = $data = $resolveCallback();
210: $resolvedNameScopeMap[$nameScopeKey] = $data;
211: }
212:
213: } finally {
214: unset($this->inProcess[$fileName]);
215: }
216:
217: return $resolvedNameScopeMap;
218: }
219:
220: /**
221: * @param array<string, string> $traitMethodAliases
222: * @return (callable(): NameScope)[]
223: */
224: private function createNameScopeMap(
225: string $fileName,
226: ?string $lookForTrait,
227: ?string $traitUseClass,
228: array $traitMethodAliases,
229: string $originalClassFileName,
230: ): array
231: {
232: /** @var (callable(): NameScope)[] $nameScopeMap */
233: $nameScopeMap = [];
234:
235: /** @var (callable(): TemplateTypeMap)[] $typeMapStack */
236: $typeMapStack = [];
237:
238: /** @var array<int, array<string, true>> $typeAliasStack */
239: $typeAliasStack = [];
240:
241: /** @var string[] $classStack */
242: $classStack = [];
243: if ($lookForTrait !== null && $traitUseClass !== null) {
244: $classStack[] = $traitUseClass;
245: $typeAliasStack[] = [];
246: }
247: $namespace = null;
248:
249: $traitFound = false;
250:
251: /** @var array<string|null> $functionStack */
252: $functionStack = [];
253: $uses = [];
254: $constUses = [];
255: $this->processNodes(
256: $this->phpParser->parseFile($fileName),
257: function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack, &$constUses): ?int {
258: if ($node instanceof Node\Stmt\ClassLike) {
259: if ($traitFound && $fileName === $originalClassFileName) {
260: return self::SKIP_NODE;
261: }
262:
263: if ($lookForTrait !== null && !$traitFound) {
264: if (!$node instanceof Node\Stmt\Trait_) {
265: return self::SKIP_NODE;
266: }
267: if ((string) $node->namespacedName !== $lookForTrait) {
268: return self::SKIP_NODE;
269: }
270:
271: $traitFound = true;
272: $typeAliasStack[] = $this->getTypeAliasesMap($node->getDocComment());
273: $functionStack[] = null;
274: } else {
275: if ($node->name === null) {
276: if (!$node instanceof Node\Stmt\Class_) {
277: throw new ShouldNotHappenException();
278: }
279:
280: $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName);
281: } elseif ((bool) $node->getAttribute('anonymousClass', false)) {
282: $className = $node->name->name;
283: } else {
284: if ($traitFound) {
285: return self::SKIP_NODE;
286: }
287: $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
288: }
289: $classStack[] = $className;
290: $typeAliasStack[] = $this->getTypeAliasesMap($node->getDocComment());
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: }
302:
303: $className = $classStack[count($classStack) - 1] ?? null;
304: $functionName = $functionStack[count($functionStack) - 1] ?? null;
305:
306: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
307: $phpDocString = GetLastDocComment::forNode($node);
308: if ($phpDocString !== null) {
309: $typeMapStack[] = function () use ($namespace, $uses, $className, $lookForTrait, $functionName, $phpDocString, $typeMapStack, $typeAliasStack, $constUses): TemplateTypeMap {
310: $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString);
311: $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null;
312: $currentTypeMap = $typeMapCb !== null ? $typeMapCb() : null;
313: $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? [];
314: $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, $typeAliasesMap, false, $constUses, $lookForTrait);
315: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope);
316: $templateTypeScope = $nameScope->getTemplateTypeScope();
317: if ($templateTypeScope === null) {
318: throw new ShouldNotHappenException();
319: }
320: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
321: $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap);
322: $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope);
323: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
324:
325: return new TemplateTypeMap(array_merge(
326: $currentTypeMap !== null ? $currentTypeMap->getTypes() : [],
327: $templateTypeMap->getTypes(),
328: ));
329: };
330: }
331: }
332:
333: $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null;
334: $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? [];
335:
336: $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
337: if (
338: $node instanceof Node\Stmt
339: && !$node instanceof Node\Stmt\Namespace_
340: && !$node instanceof Node\Stmt\Declare_
341: && !$node instanceof Node\Stmt\DeclareDeclare
342: && !$node instanceof Node\Stmt\Use_
343: && !$node instanceof Node\Stmt\UseUse
344: && !$node instanceof Node\Stmt\GroupUse
345: && !$node instanceof Node\Stmt\TraitUse
346: && !$node instanceof Node\Stmt\TraitUseAdaptation
347: && !$node instanceof Node\Stmt\InlineHTML
348: && !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_)
349: && !array_key_exists($nameScopeKey, $nameScopeMap)
350: ) {
351: $nameScopeMap[$nameScopeKey] = static fn (): NameScope => new NameScope(
352: $namespace,
353: $uses,
354: $className,
355: $functionName,
356: ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty()),
357: $typeAliasesMap,
358: false,
359: $constUses,
360: $lookForTrait,
361: );
362: }
363:
364: if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
365: $phpDocString = GetLastDocComment::forNode($node);
366: if ($phpDocString !== null) {
367: return self::POP_TYPE_MAP_STACK;
368: }
369:
370: return null;
371: }
372:
373: if ($node instanceof Node\Stmt\Namespace_) {
374: $namespace = $node->name !== null ? (string) $node->name : null;
375: } elseif ($node instanceof Node\Stmt\Use_) {
376: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL) {
377: foreach ($node->uses as $use) {
378: $uses[strtolower($use->getAlias()->name)] = (string) $use->name;
379: }
380: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT) {
381: foreach ($node->uses as $use) {
382: $constUses[strtolower($use->getAlias()->name)] = (string) $use->name;
383: }
384: }
385: } elseif ($node instanceof Node\Stmt\GroupUse) {
386: $prefix = (string) $node->prefix;
387: foreach ($node->uses as $use) {
388: if ($node->type === Node\Stmt\Use_::TYPE_NORMAL || $use->type === Node\Stmt\Use_::TYPE_NORMAL) {
389: $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
390: } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT || $use->type === Node\Stmt\Use_::TYPE_CONSTANT) {
391: $constUses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
392: }
393: }
394: } elseif ($node instanceof Node\Stmt\TraitUse) {
395: $traitMethodAliases = [];
396: foreach ($node->adaptations as $traitUseAdaptation) {
397: if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
398: continue;
399: }
400:
401: if ($traitUseAdaptation->trait === null) {
402: continue;
403: }
404:
405: if ($traitUseAdaptation->newName === null) {
406: continue;
407: }
408:
409: $traitMethodAliases[$traitUseAdaptation->trait->toString()][$traitUseAdaptation->method->toString()] = $traitUseAdaptation->newName->toString();
410: }
411:
412: $useDocComment = null;
413: if ($node->getDocComment() !== null) {
414: $useDocComment = $node->getDocComment()->getText();
415: }
416:
417: foreach ($node->traits as $traitName) {
418: /** @var class-string $traitName */
419: $traitName = (string) $traitName;
420: $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider();
421: if (!$reflectionProvider->hasClass($traitName)) {
422: continue;
423: }
424:
425: $traitReflection = $reflectionProvider->getClass($traitName);
426: if (!$traitReflection->isTrait()) {
427: continue;
428: }
429: if ($traitReflection->getFileName() === null) {
430: continue;
431: }
432: if (!is_file($traitReflection->getFileName())) {
433: continue;
434: }
435:
436: $className = $classStack[count($classStack) - 1] ?? null;
437: if ($className === null) {
438: throw new ShouldNotHappenException();
439: }
440:
441: $traitPhpDocMap = $this->createNameScopeMap(
442: $traitReflection->getFileName(),
443: $traitName,
444: $className,
445: $traitMethodAliases[$traitName] ?? [],
446: $originalClassFileName,
447: );
448: $finalTraitPhpDocMap = [];
449: foreach ($traitPhpDocMap as $nameScopeTraitKey => $callback) {
450: $finalTraitPhpDocMap[$nameScopeTraitKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScope {
451: /** @var NameScope $original */
452: $original = $callback();
453: if (!$traitReflection->isGeneric()) {
454: return $original;
455: }
456:
457: $traitTemplateTypeMap = $traitReflection->getTemplateTypeMap();
458:
459: $useType = null;
460: if ($useDocComment !== null) {
461: $useTags = $this->getResolvedPhpDoc(
462: $fileName,
463: $className,
464: $lookForTrait,
465: null,
466: $useDocComment,
467: )->getUsesTags();
468: foreach ($useTags as $useTag) {
469: $useTagType = $useTag->getType();
470: if (!$useTagType instanceof GenericObjectType) {
471: continue;
472: }
473:
474: if ($useTagType->getClassName() !== $traitReflection->getName()) {
475: continue;
476: }
477:
478: $useType = $useTagType;
479: break;
480: }
481: }
482:
483: if ($useType === null) {
484: return $original->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds());
485: }
486:
487: $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes());
488:
489: return $original->withTemplateTypeMap($traitTemplateTypeMap->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap)));
490: };
491: }
492: $nameScopeMap = array_merge($nameScopeMap, $finalTraitPhpDocMap);
493: }
494: }
495:
496: return null;
497: },
498: static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void {
499: if ($node instanceof Node\Stmt\ClassLike) {
500: if (count($classStack) === 0) {
501: throw new ShouldNotHappenException();
502: }
503: array_pop($classStack);
504:
505: if (count($typeAliasStack) === 0) {
506: throw new ShouldNotHappenException();
507: }
508:
509: array_pop($typeAliasStack);
510:
511: if (count($functionStack) === 0) {
512: throw new ShouldNotHappenException();
513: }
514:
515: array_pop($functionStack);
516: } elseif ($node instanceof Node\Stmt\Namespace_) {
517: $namespace = null;
518: $uses = [];
519: $constUses = [];
520: } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
521: if (count($functionStack) === 0) {
522: throw new ShouldNotHappenException();
523: }
524:
525: array_pop($functionStack);
526: }
527: if ($callbackResult !== self::POP_TYPE_MAP_STACK) {
528: return;
529: }
530:
531: if (count($typeMapStack) === 0) {
532: throw new ShouldNotHappenException();
533: }
534: array_pop($typeMapStack);
535: },
536: );
537:
538: if (count($typeMapStack) > 0) {
539: throw new ShouldNotHappenException();
540: }
541:
542: return $nameScopeMap;
543: }
544:
545: /**
546: * @return array<string, true>
547: */
548: private function getTypeAliasesMap(?Doc $docComment): array
549: {
550: if ($docComment === null) {
551: return [];
552: }
553:
554: $phpDocNode = $this->phpDocStringResolver->resolve($docComment->getText());
555: $nameScope = new NameScope(null, []);
556:
557: $aliasesMap = [];
558: foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasImportTags($phpDocNode, $nameScope)) as $key) {
559: $aliasesMap[$key] = true;
560: }
561:
562: foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasTags($phpDocNode, $nameScope)) as $key) {
563: $aliasesMap[$key] = true;
564: }
565:
566: return $aliasesMap;
567: }
568:
569: /**
570: * @param Node[]|Node|scalar|null $node
571: * @param Closure(Node $node): mixed $nodeCallback
572: * @param Closure(Node $node, mixed $callbackResult): void $endNodeCallback
573: */
574: private function processNodes($node, Closure $nodeCallback, Closure $endNodeCallback): void
575: {
576: if ($node instanceof Node) {
577: $callbackResult = $nodeCallback($node);
578: if ($callbackResult === self::SKIP_NODE) {
579: return;
580: }
581: foreach ($node->getSubNodeNames() as $subNodeName) {
582: $subNode = $node->{$subNodeName};
583: $this->processNodes($subNode, $nodeCallback, $endNodeCallback);
584: }
585: $endNodeCallback($node, $callbackResult);
586: } elseif (is_array($node)) {
587: foreach ($node as $subNode) {
588: $this->processNodes($subNode, $nodeCallback, $endNodeCallback);
589: }
590: }
591: }
592:
593: private function getNameScopeKey(
594: ?string $file,
595: ?string $class,
596: ?string $trait,
597: ?string $function,
598: ): string
599: {
600: if ($class === null && $trait === null && $function === null) {
601: return md5(sprintf('%s', $file ?? 'no-file'));
602: }
603:
604: if ($class !== null && strpos($class, 'class@anonymous') !== false) {
605: throw new ShouldNotHappenException('Wrong anonymous class name, FilTypeMapper should be called with ClassReflection::getName().');
606: }
607:
608: return md5(sprintf('%s-%s-%s-%s', $file ?? 'no-file', $class, $trait, $function));
609: }
610:
611: }
612: