1: <?php declare(strict_types = 1);
2:
3: namespace ApiGen\Analyzer;
4:
5: use ApiGen\Info\AliasInfo;
6: use ApiGen\Info\ClassInfo;
7: use ApiGen\Info\ClassLikeInfo;
8: use ApiGen\Info\ClassLikeReferenceInfo;
9: use ApiGen\Info\ConstantInfo;
10: use ApiGen\Info\EnumCaseInfo;
11: use ApiGen\Info\EnumInfo;
12: use ApiGen\Info\ErrorInfo;
13: use ApiGen\Info\ErrorKind;
14: use ApiGen\Info\Expr\ArgExprInfo;
15: use ApiGen\Info\Expr\ArrayExprInfo;
16: use ApiGen\Info\Expr\ArrayItemExprInfo;
17: use ApiGen\Info\Expr\BinaryOpExprInfo;
18: use ApiGen\Info\Expr\BooleanExprInfo;
19: use ApiGen\Info\Expr\ClassConstantFetchExprInfo;
20: use ApiGen\Info\Expr\ConstantFetchExprInfo;
21: use ApiGen\Info\Expr\DimFetchExprInfo;
22: use ApiGen\Info\Expr\FloatExprInfo;
23: use ApiGen\Info\Expr\IntegerExprInfo;
24: use ApiGen\Info\Expr\NewExprInfo;
25: use ApiGen\Info\Expr\NullExprInfo;
26: use ApiGen\Info\Expr\NullSafePropertyFetchExprInfo;
27: use ApiGen\Info\Expr\PropertyFetchExprInfo;
28: use ApiGen\Info\Expr\StringExprInfo;
29: use ApiGen\Info\Expr\TernaryExprInfo;
30: use ApiGen\Info\Expr\UnaryOpExprInfo;
31: use ApiGen\Info\ExprInfo;
32: use ApiGen\Info\FunctionInfo;
33: use ApiGen\Info\GenericParameterInfo;
34: use ApiGen\Info\GenericParameterVariance;
35: use ApiGen\Info\InterfaceInfo;
36: use ApiGen\Info\MemberInfo;
37: use ApiGen\Info\MethodInfo;
38: use ApiGen\Info\NameInfo;
39: use ApiGen\Info\ParameterInfo;
40: use ApiGen\Info\PropertyInfo;
41: use ApiGen\Info\TraitInfo;
42: use ApiGen\Task\Task;
43: use ApiGen\Task\TaskHandler;
44: use BackedEnum;
45: use Iterator;
46: use Nette\Utils\FileSystem;
47: use PhpParser\Node;
48: use PhpParser\Node\ComplexType;
49: use PhpParser\Node\Identifier;
50: use PhpParser\Node\IntersectionType;
51: use PhpParser\Node\Name;
52: use PhpParser\Node\NullableType;
53: use PhpParser\Node\UnionType;
54: use PhpParser\NodeTraverserInterface;
55: use PhpParser\Parser;
56: use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
57: use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode;
58: use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
59: use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode;
60: use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
61: use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
62: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
63: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
64: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
65: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
66: use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
67: use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
68: use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
69: use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
70: use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode;
71: use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
72: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
73: use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
74: use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
75: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
76: use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
77: use UnitEnum;
78:
79: use function array_map;
80: use function assert;
81: use function get_debug_type;
82: use function is_array;
83: use function is_object;
84: use function is_scalar;
85: use function is_string;
86: use function iterator_to_array;
87: use function mb_check_encoding;
88: use function sprintf;
89: use function str_ends_with;
90: use function strtolower;
91: use function substr;
92:
93:
94: /**
95: * @implements TaskHandler<AnalyzeTask, array<ClassLikeInfo | FunctionInfo | ClassLikeReferenceInfo | ErrorInfo>>
96: */
97: class AnalyzeTaskHandler implements TaskHandler
98: {
99: /**
100: * @param null $context
101: */
102: public function __construct(
103: protected Parser $parser,
104: protected NodeTraverserInterface $traverser,
105: protected Filter $filter,
106: protected mixed $context = null,
107: ) {
108: }
109:
110:
111: /**
112: * @param AnalyzeTask $task
113: */
114: public function handle(Task $task): array
115: {
116: try {
117: $content = FileSystem::read($task->sourceFile);
118:
119: if (!mb_check_encoding($content, 'UTF-8')) {
120: $error = new ErrorInfo(ErrorKind::InvalidEncoding, "File {$task->sourceFile} is not UTF-8 encoded");
121: return [$error];
122: }
123:
124: $ast = $this->parser->parse($content) ?? throw new \LogicException();
125: $ast = $this->traverser->traverse($ast);
126: return iterator_to_array($this->processNodes($task, $ast), preserve_keys: false);
127:
128: } catch (\PhpParser\Error $e) {
129: $error = new ErrorInfo(ErrorKind::SyntaxError, "Parse error in file {$task->sourceFile}:\n{$e->getMessage()}");
130: return [$error];
131:
132: } catch (\Throwable $e) {
133: $ex = new \LogicException("Failed to analyze file $task->sourceFile", 0, $e);
134: $error = new ErrorInfo(ErrorKind::InternalError, (string) $ex);
135: return [$error];
136: }
137: }
138:
139:
140: /**
141: * @param Node[] $nodes indexed by []
142: * @return Iterator<ClassLikeInfo | FunctionInfo | ClassLikeReferenceInfo>
143: */
144: protected function processNodes(AnalyzeTask $task, array $nodes): Iterator
145: {
146: foreach ($nodes as $node) {
147: if ($node instanceof Node\Stmt\ClassLike && $node->name !== null) {
148: try {
149: $task->primary = $task->primary && $this->filter->filterClassLikeNode($node);
150: $classLike = $this->processClassLike($task, $node);
151: yield $classLike;
152: yield from $this->extractDependencies($classLike);
153:
154: } catch (\Throwable $e) {
155: throw new \LogicException("Failed to analyze class-like $node->namespacedName", 0, $e);
156: }
157:
158: } elseif ($node instanceof Node\Stmt\Function_) {
159: try {
160: $functionInfo = $this->processFunction($task, $node);
161:
162: if ($functionInfo !== null) {
163: yield $functionInfo;
164: yield from $this->extractDependencies($functionInfo);
165: }
166:
167: } catch (\Throwable $e) {
168: throw new \LogicException("Failed to analyze function $node->namespacedName", 0, $e);
169: }
170:
171: } elseif ($node instanceof Node\Stmt) { // TODO: constants, class aliases
172: foreach ($node->getSubNodeNames() as $name) {
173: $subNode = $node->$name;
174:
175: if (is_array($subNode)) {
176: yield from $this->processNodes($task, $subNode);
177:
178: } elseif ($subNode instanceof Node) {
179: yield from $this->processNodes($task, [$subNode]);
180: }
181: }
182: }
183: }
184: }
185:
186:
187: protected function processClassLike(AnalyzeTask $task, Node\Stmt\ClassLike $node): ClassLikeInfo
188: {
189: $extendsTagNames = ['extends', 'template-extends', 'phpstan-extends'];
190: $implementsTagNames = ['implements', 'template-implements', 'phpstan-implements'];
191: $useTagNames = ['use', 'template-use', 'phpstan-use'];
192:
193: assert($node->namespacedName !== null);
194: $name = new NameInfo($node->namespacedName->toString());
195:
196: $classDoc = $this->extractPhpDoc($node);
197: $tags = $this->extractTags($classDoc);
198:
199: if ($task->primary && !$this->filter->filterClassLikeTags($tags)) {
200: $task->primary = false;
201: }
202:
203: if ($node instanceof Node\Stmt\Class_) {
204: $info = new ClassInfo($name, $task->primary);
205: $info->abstract = $node->isAbstract();
206: $info->final = $node->isFinal();
207: $info->readOnly = $node->isReadonly();
208: $info->extends = $node->extends ? $this->processName($node->extends, $tags, $extendsTagNames) : null;
209: $info->implements = $this->processNameList($node->implements, $tags, $implementsTagNames);
210:
211: $info->aliases = $this->extractAliases($classDoc, $node->getDocComment()?->getStartLine(), $node->getDocComment()?->getEndLine());
212: unset($tags['phpstan-type'], $tags['phpstan-import-type']);
213:
214: foreach ($node->getTraitUses() as $traitUse) { // TODO: trait adaptations
215: $info->uses += $this->processNameList($traitUse->traits, $tags, $useTagNames);
216: }
217:
218: } elseif ($node instanceof Node\Stmt\Interface_) {
219: $info = new InterfaceInfo($name, $task->primary);
220: $info->extends = $this->processNameList($node->extends, $tags, $extendsTagNames);
221:
222: } elseif ($node instanceof Node\Stmt\Trait_) {
223: $info = new TraitInfo($name, $task->primary);
224:
225: } elseif ($node instanceof Node\Stmt\Enum_) {
226: $autoImplement = new ClassLikeReferenceInfo($node->scalarType ? BackedEnum::class : UnitEnum::class);
227:
228: $info = new EnumInfo($name, $task->primary);
229: $info->scalarType = $node->scalarType?->name;
230: $info->implements = $this->processNameList($node->implements, $tags, $implementsTagNames) + [$autoImplement->fullLower => $autoImplement];
231:
232: foreach ($node->getTraitUses() as $traitUse) {
233: $info->uses += $this->processNameList($traitUse->traits, $tags, $useTagNames);
234: }
235:
236: } else {
237: throw new \LogicException(sprintf('Unsupported ClassLike node %s', get_debug_type($node)));
238: }
239:
240: $info->genericParameters = $this->extractGenericParameters($classDoc);
241: $info->description = $this->extractMultiLineDescription($classDoc);
242: $info->tags = $tags;
243: $info->file = $task->sourceFile;
244: $info->startLine = $node->getStartLine();
245: $info->endLine = $node->getEndLine();
246:
247: $info->mixins = $this->processMixinTags($tags['mixin'] ?? []);
248:
249: foreach ($this->extractMembers($info->tags, $node) as $member) {
250: if (!$this->filter->filterMemberInfo($info, $member)) {
251: continue;
252:
253: } elseif ($member instanceof ConstantInfo) {
254: $info->constants[$member->name] = $member;
255:
256: } elseif ($member instanceof PropertyInfo) {
257: $info->properties[$member->name] = $member;
258:
259: } elseif ($member instanceof MethodInfo) {
260: $info->methods[$member->nameLower] = $member;
261:
262: } elseif ($member instanceof EnumCaseInfo) {
263: assert($info instanceof EnumInfo);
264: $info->cases[$member->name] = $member;
265:
266: } else {
267: throw new \LogicException(sprintf('Unexpected member type %s', get_debug_type($member)));
268: }
269: }
270:
271: unset($info->tags['mixin']);
272: unset($info->tags['property'], $info->tags['property-read'], $info->tags['property-write']);
273: unset($info->tags['method']);
274:
275: if ($info->primary && !$this->filter->filterClassLikeInfo($info)) {
276: $info->primary = false;
277: }
278:
279: return $info;
280: }
281:
282:
283: /**
284: * @param PhpDocTagValueNode[][] $tags indexed by [tagName][]
285: * @return iterable<MemberInfo>
286: */
287: protected function extractMembers(array $tags, Node\Stmt\ClassLike $node): iterable
288: {
289: yield from $this->extractMembersFromBody($node);
290: yield from $this->extractMembersFromTags($tags);
291: }
292:
293:
294: /**
295: * @return iterable<MemberInfo>
296: */
297: protected function extractMembersFromBody(Node\Stmt\ClassLike $node): iterable
298: {
299: foreach ($node->stmts as $member) {
300: $memberDoc = $this->extractPhpDoc($member);
301: $description = $this->extractMultiLineDescription($memberDoc);
302: $tags = $this->extractTags($memberDoc);
303:
304: if (!$this->filter->filterMemberTags($tags)) {
305: continue;
306: }
307:
308: if ($member instanceof Node\Stmt\ClassConst) {
309: if (!$this->filter->filterConstantNode($member)) {
310: continue;
311: }
312:
313: foreach ($member->consts as $constant) {
314: $memberInfo = new ConstantInfo($constant->name->name, $this->processExpr($constant->value));
315: $memberInfo->type = $this->processTypeOrNull($member->type);
316:
317: $memberInfo->description = $description;
318: $memberInfo->tags = $tags;
319:
320: $memberInfo->startLine = $member->getComments() ? $member->getComments()[0]->getStartLine() : $member->getStartLine();
321: $memberInfo->endLine = $member->getEndLine();
322:
323: $memberInfo->public = $member->isPublic();
324: $memberInfo->protected = $member->isProtected();
325: $memberInfo->private = $member->isPrivate();
326: $memberInfo->final = $member->isFinal();
327:
328: yield $memberInfo;
329: }
330:
331: } elseif ($member instanceof Node\Stmt\Property) {
332: if (!$this->filter->filterPropertyNode($member)) {
333: continue;
334: }
335:
336: $varTag = isset($tags['var'][0]) && $tags['var'][0] instanceof VarTagValueNode ? $tags['var'][0] : null;
337: unset($tags['var']);
338:
339: foreach ($member->props as $property) {
340: $memberInfo = new PropertyInfo($property->name->name);
341:
342: $memberInfo->description = $this->extractSingleLineDescription($varTag);
343: $memberInfo->tags = $tags;
344:
345: $memberInfo->startLine = $member->getComments() ? $member->getComments()[0]->getStartLine() : $member->getStartLine();
346: $memberInfo->endLine = $member->getEndLine();
347:
348: $memberInfo->public = $member->isPublic();
349: $memberInfo->protected = $member->isProtected();
350: $memberInfo->private = $member->isPrivate();
351: $memberInfo->static = $member->isStatic();
352: $memberInfo->readOnly = $member->isReadonly();
353:
354: $memberInfo->type = $varTag ? $varTag->type : $this->processTypeOrNull($member->type);
355: $memberInfo->default = $this->processExprOrNull($property->default);
356:
357: yield $memberInfo;
358: }
359:
360: } elseif ($member instanceof Node\Stmt\ClassMethod) {
361: if (!$this->filter->filterMethodNode($member)) {
362: continue;
363: }
364:
365: /** @var ?ReturnTagValueNode $returnTag */
366: $returnTag = isset($tags['return'][0]) && $tags['return'][0] instanceof ReturnTagValueNode ? $tags['return'][0] : null;
367: unset($tags['param'], $tags['return']);
368:
369: $memberInfo = new MethodInfo($member->name->name);
370:
371: $memberInfo->description = $description;
372: $memberInfo->tags = $tags;
373:
374: $memberInfo->genericParameters = $this->extractGenericParameters($memberDoc);
375: $memberInfo->parameters = $this->processParameters($this->extractParamTagValues($memberDoc), $member->params);
376: $memberInfo->returnType = $returnTag ? $returnTag->type : $this->processTypeOrNull($member->returnType);
377: $memberInfo->returnDescription = $this->extractSingleLineDescription($returnTag);
378: $memberInfo->byRef = $member->byRef;
379:
380: $memberInfo->startLine = $member->getComments() ? $member->getComments()[0]->getStartLine() : $member->getStartLine();
381: $memberInfo->endLine = $member->getEndLine();
382:
383: $memberInfo->public = $member->isPublic();
384: $memberInfo->protected = $member->isProtected();
385: $memberInfo->private = $member->isPrivate();
386:
387: $memberInfo->static = $member->isStatic();
388: $memberInfo->abstract = $member->isAbstract();
389: $memberInfo->final = $member->isFinal();
390:
391: yield $memberInfo;
392:
393: if ($member->name->toLowerString() === '__construct') {
394: foreach ($member->params as $param) {
395: if ($param->flags === 0 || !$this->filter->filterPromotedPropertyNode($param)) {
396: continue;
397: }
398:
399: assert($param->var instanceof Node\Expr\Variable);
400: assert(is_string($param->var->name));
401: $propertyInfo = new PropertyInfo($param->var->name);
402:
403: $propertyInfo->description = $memberInfo->parameters[$propertyInfo->name]->description;
404:
405: $propertyInfo->startLine = $param->getStartLine();
406: $propertyInfo->endLine = $param->getEndLine();
407:
408: $propertyInfo->public = (bool) ($param->flags & Node\Stmt\Class_::MODIFIER_PUBLIC);
409: $propertyInfo->protected = (bool) ($param->flags & Node\Stmt\Class_::MODIFIER_PROTECTED);
410: $propertyInfo->private = (bool) ($param->flags & Node\Stmt\Class_::MODIFIER_PRIVATE);
411:
412: $propertyInfo->readOnly = (bool) ($param->flags & Node\Stmt\Class_::MODIFIER_READONLY);
413: $propertyInfo->type = $memberInfo->parameters[$propertyInfo->name]->type;
414:
415: yield $propertyInfo;
416: }
417: }
418:
419: } elseif ($member instanceof Node\Stmt\EnumCase) {
420: if (!$this->filter->filterEnumCaseNode($member)) {
421: continue;
422: }
423:
424: $memberInfo = new EnumCaseInfo($member->name->name, $this->processExprOrNull($member->expr));
425:
426: $memberInfo->description = $description;
427: $memberInfo->tags = $tags;
428:
429: $memberInfo->startLine = $member->getComments() ? $member->getComments()[0]->getStartLine() : $member->getStartLine();
430: $memberInfo->endLine = $member->getEndLine();
431:
432: yield $memberInfo;
433: }
434: }
435: }
436:
437:
438: /**
439: * @param PhpDocTagValueNode[][] $tags indexed by [tagName][]
440: * @return iterable<MemberInfo>
441: */
442: protected function extractMembersFromTags(array $tags): iterable
443: {
444: $propertyTags = [
445: 'property' => [false, false],
446: 'property-read' => [true, false],
447: 'property-write' => [false, true],
448: ];
449:
450: foreach ($propertyTags as $tag => [$readOnly, $writeOnly]) {
451: /** @var PropertyTagValueNode $value */
452: foreach ($tags[$tag] ?? [] as $value) {
453: $propertyInfo = new PropertyInfo(substr($value->propertyName, 1));
454: $propertyInfo->magic = true;
455: $propertyInfo->public = true;
456: $propertyInfo->type = $value->type;
457: $propertyInfo->description = $this->extractSingleLineDescription($value);
458: $propertyInfo->readOnly = $readOnly;
459: $propertyInfo->writeOnly = $writeOnly;
460:
461: yield $propertyInfo;
462: }
463: }
464:
465: /** @var MethodTagValueNode $value */
466: foreach ($tags['method'] ?? [] as $value) {
467: $methodInfo = new MethodInfo($value->methodName);
468: $methodInfo->magic = true;
469: $methodInfo->public = true;
470: $methodInfo->static = $value->isStatic;
471: $methodInfo->returnType = $value->returnType;
472: $methodInfo->description = $this->extractSingleLineDescription($value);
473:
474: foreach ($value->parameters as $position => $parameter) {
475: $parameterInfo = new ParameterInfo(substr($parameter->parameterName, 1), $position);
476: $parameterInfo->type = $parameter->type;
477: $parameterInfo->byRef = $parameter->isReference;
478: $parameterInfo->variadic = $parameter->isVariadic;
479: $parameterInfo->default = $parameter->defaultValue?->getAttribute('info');
480:
481: $methodInfo->parameters[$parameterInfo->name] = $parameterInfo;
482: }
483:
484: yield $methodInfo;
485: }
486: }
487:
488:
489: protected function processFunction(AnalyzeTask $task, Node\Stmt\Function_ $node): ?FunctionInfo
490: {
491: if (!$this->filter->filterFunctionNode($node)) {
492: return null;
493: }
494:
495: $phpDoc = $this->extractPhpDoc($node);
496: $tags = $this->extractTags($phpDoc);
497:
498: if (!$this->filter->filterFunctionTags($tags)) {
499: return null;
500: }
501:
502: assert($node->namespacedName !== null);
503: $name = new NameInfo($node->namespacedName->toString());
504: $info = new FunctionInfo($name, $task->primary);
505:
506: $info->description = $this->extractMultiLineDescription($phpDoc);
507: $info->tags = $tags;
508: $info->file = $task->sourceFile;
509: $info->startLine = $node->getStartLine();
510: $info->endLine = $node->getEndLine();
511:
512: /** @var ?ReturnTagValueNode $returnTag */
513: $returnTag = isset($tags['return'][0]) && $tags['return'][0] instanceof ReturnTagValueNode ? $tags['return'][0] : null;
514: unset($tags['param'], $tags['return']);
515:
516: $info->genericParameters = $this->extractGenericParameters($phpDoc);
517: $info->parameters = $this->processParameters($this->extractParamTagValues($phpDoc), $node->params);
518: $info->returnType = $returnTag ? $returnTag->type : $this->processTypeOrNull($node->returnType);
519: $info->returnDescription = $this->extractSingleLineDescription($returnTag);
520: $info->byRef = $node->byRef;
521:
522: if (!$this->filter->filterFunctionInfo($info)) {
523: return null;
524: }
525:
526: return $info;
527: }
528:
529:
530: /**
531: * @param ParamTagValueNode[] $paramTags indexed by [parameterName]
532: * @param Node\Param[] $parameters indexed by []
533: * @return ParameterInfo[]
534: */
535: protected function processParameters(array $paramTags, array $parameters): array
536: {
537: $parameterInfos = [];
538: foreach ($parameters as $position => $parameter) {
539: assert($parameter->var instanceof Node\Expr\Variable);
540: assert(is_scalar($parameter->var->name));
541:
542: $paramTag = $paramTags["\${$parameter->var->name}"] ?? null;
543: $parameterInfo = new ParameterInfo($parameter->var->name, $position);
544: $parameterInfo->description = $this->extractSingleLineDescription($paramTag);
545: $parameterInfo->type = $paramTag ? $paramTag->type : $this->processTypeOrNull($parameter->type);
546: $parameterInfo->byRef = $parameter->byRef;
547: $parameterInfo->variadic = $parameter->variadic || ($paramTag && $paramTag->isVariadic);
548: $parameterInfo->default = $this->processExprOrNull($parameter->default);
549:
550: $parameterInfos[$parameter->var->name] = $parameterInfo;
551: }
552:
553: return $parameterInfos;
554: }
555:
556:
557: /**
558: * @param PhpDocTagValueNode[][] $tagValues indexed by [tagName][]
559: * @param string[] $tagNames indexed by []
560: */
561: protected function processName(Node\Name $name, array $tagValues = [], array $tagNames = []): ClassLikeReferenceInfo
562: {
563: $refInfo = new ClassLikeReferenceInfo($name->toString());
564:
565: foreach ($tagNames as $tagName) {
566: foreach ($tagValues[$tagName] ?? [] as $tagValue) {
567: assert($tagValue instanceof ExtendsTagValueNode || $tagValue instanceof ImplementsTagValueNode || $tagValue instanceof UsesTagValueNode);
568:
569: $kind = $tagValue->type->type->getAttribute('kind');
570: assert($kind instanceof IdentifierKind);
571:
572: if ($kind === IdentifierKind::ClassLike) {
573: $refInfo = $tagValue->type->type->getAttribute('classLikeReference');
574: assert($refInfo instanceof ClassLikeReferenceInfo);
575:
576: $refInfo->genericArgs = $tagValue->type->genericTypes;
577: }
578: }
579: }
580:
581: return $refInfo;
582: }
583:
584:
585: /**
586: * @param Node\Name[] $names indexed by []
587: * @param PhpDocTagValueNode[][] $tagValues indexed by [tagName][]
588: * @param string[] $tagNames indexed by []
589: * @return ClassLikeReferenceInfo[] indexed by [classLikeName]
590: */
591: protected function processNameList(array $names, array $tagValues = [], array $tagNames = []): array
592: {
593: $nameMap = [];
594:
595: foreach ($names as $name) {
596: $nameInfo = new ClassLikeReferenceInfo($name->toString());
597: $nameMap[$nameInfo->fullLower] = $nameInfo;
598: }
599:
600: foreach ($tagNames as $tagName) {
601: foreach ($tagValues[$tagName] ?? [] as $tagValue) {
602: assert($tagValue instanceof ExtendsTagValueNode || $tagValue instanceof ImplementsTagValueNode || $tagValue instanceof UsesTagValueNode);
603:
604: $kind = $tagValue->type->type->getAttribute('kind');
605: assert($kind instanceof IdentifierKind);
606:
607: if ($kind === IdentifierKind::ClassLike) {
608: $refInfo = $tagValue->type->type->getAttribute('classLikeReference');
609: assert($refInfo instanceof ClassLikeReferenceInfo);
610:
611: $refInfo->genericArgs = $tagValue->type->genericTypes;
612: $nameMap[$refInfo->fullLower] = $refInfo;
613: }
614: }
615: }
616:
617: return $nameMap;
618: }
619:
620:
621: /**
622: * @param PhpDocTagValueNode[] $values indexed by []
623: * @return ClassLikeReferenceInfo[] indexed by [classLikeName]
624: */
625: protected function processMixinTags(array $values): array
626: {
627: $nameMap = [];
628:
629: foreach ($values as $value) {
630: if ($value instanceof MixinTagValueNode && $value->type instanceof IdentifierTypeNode) {
631: $kind = $value->type->getAttribute('kind');
632: assert($kind instanceof IdentifierKind);
633:
634: if ($kind === IdentifierKind::ClassLike) {
635: $refInfo = $value->type->getAttribute('classLikeReference');
636: assert($refInfo instanceof ClassLikeReferenceInfo);
637:
638: $nameMap[$refInfo->fullLower] = $refInfo;
639: }
640: }
641: }
642:
643: return $nameMap;
644: }
645:
646:
647: protected function processTypeOrNull(Identifier|Name|ComplexType|null $node): ?TypeNode
648: {
649: return $node ? $this->processType($node) : null;
650: }
651:
652:
653: protected function processType(Identifier|Name|ComplexType $node): TypeNode
654: {
655: if ($node instanceof ComplexType) {
656: if ($node instanceof NullableType) {
657: return new NullableTypeNode($this->processType($node->type));
658:
659: } elseif ($node instanceof UnionType) {
660: return new UnionTypeNode(array_map($this->processType(...), $node->types));
661:
662: } elseif ($node instanceof IntersectionType) {
663: return new IntersectionTypeNode(array_map($this->processType(...), $node->types));
664:
665: } else {
666: throw new \LogicException(sprintf('Unsupported complex type %s', get_debug_type($node)));
667: }
668:
669: } elseif ($node instanceof Name && !$node->isSpecialClassName()) {
670: $identifier = new IdentifierTypeNode($node->toString());
671: $identifier->setAttribute('kind', IdentifierKind::ClassLike);
672: $identifier->setAttribute('classLikeReference', new ClassLikeReferenceInfo($identifier->name));
673:
674: } else {
675: $identifier = new IdentifierTypeNode($node->toString());
676: $identifier->setAttribute('kind', IdentifierKind::Keyword);
677: }
678:
679: return $identifier;
680: }
681:
682:
683: protected function processExprOrNull(?Node\Expr $expr): ?ExprInfo
684: {
685: return $expr ? $this->processExpr($expr) : null;
686: }
687:
688:
689: protected function processExpr(Node\Expr $expr): ExprInfo
690: {
691: if ($expr instanceof Node\Scalar\LNumber) {
692: return new IntegerExprInfo($expr->value, $expr->getAttribute('kind'), $expr->getAttribute('rawValue'));
693:
694: } elseif ($expr instanceof Node\Scalar\DNumber) {
695: return new FloatExprInfo($expr->value, $expr->getAttribute('rawValue'));
696:
697: } elseif ($expr instanceof Node\Scalar\String_) {
698: return new StringExprInfo($expr->value, $expr->getAttribute('rawValue'));
699:
700: } elseif ($expr instanceof Node\Expr\Array_) {
701: $items = [];
702:
703: foreach ($expr->items as $item) {
704: $key = $this->processExprOrNull($item->key);
705: $value = $this->processExpr($item->value);
706: $items[] = new ArrayItemExprInfo($key, $value);
707: }
708:
709: return new ArrayExprInfo($items);
710:
711: } elseif ($expr instanceof Node\Expr\ClassConstFetch) {
712: assert($expr->class instanceof Node\Name);
713: assert($expr->name instanceof Node\Identifier);
714:
715: // TODO: handle 'self' & 'parent' differently?
716: return new ClassConstantFetchExprInfo($this->processName($expr->class), $expr->name->toString());
717:
718: } elseif ($expr instanceof Node\Expr\ConstFetch) {
719: $lower = $expr->name->toLowerString();
720:
721: if ($lower === 'true') {
722: return new BooleanExprInfo(true);
723:
724: } elseif ($lower === 'false') {
725: return new BooleanExprInfo(false);
726:
727: } elseif ($lower === 'null') {
728: return new NullExprInfo();
729:
730: } else {
731: return new ConstantFetchExprInfo($expr->name->toString());
732: }
733:
734: } elseif ($expr instanceof Node\Scalar\MagicConst) {
735: return new ConstantFetchExprInfo($expr->getName());
736:
737: } elseif ($expr instanceof Node\Expr\UnaryMinus) {
738: return new UnaryOpExprInfo('-', $this->processExpr($expr->expr));
739:
740: } elseif ($expr instanceof Node\Expr\UnaryPlus) {
741: return new UnaryOpExprInfo('+', $this->processExpr($expr->expr));
742:
743: } elseif ($expr instanceof Node\Expr\BinaryOp) {
744: return new BinaryOpExprInfo(
745: $expr->getOperatorSigil(),
746: $this->processExpr($expr->left),
747: $this->processExpr($expr->right),
748: );
749:
750: } elseif ($expr instanceof Node\Expr\Ternary) {
751: return new TernaryExprInfo(
752: $this->processExpr($expr->cond),
753: $this->processExprOrNull($expr->if),
754: $this->processExpr($expr->else),
755: );
756:
757: } elseif ($expr instanceof Node\Expr\ArrayDimFetch) {
758: assert($expr->dim !== null);
759: return new DimFetchExprInfo(
760: $this->processExpr($expr->var),
761: $this->processExpr($expr->dim),
762: );
763:
764: } elseif ($expr instanceof Node\Expr\PropertyFetch) {
765: return new PropertyFetchExprInfo(
766: $this->processExpr($expr->var),
767: $expr->name instanceof Node\Expr ? $this->processExpr($expr->name) : $expr->name->name,
768: );
769:
770: } elseif ($expr instanceof Node\Expr\NullsafePropertyFetch) {
771: return new NullSafePropertyFetchExprInfo(
772: $this->processExpr($expr->var),
773: $expr->name instanceof Node\Expr ? $this->processExpr($expr->name) : $expr->name->name,
774: );
775:
776: } elseif ($expr instanceof Node\Expr\New_) {
777: assert($expr->class instanceof Name);
778:
779: $args = [];
780: foreach ($expr->args as $arg) {
781: assert($arg instanceof Node\Arg);
782: $args[] = new ArgExprInfo($arg->name?->name, $this->processExpr($arg->value));
783: }
784:
785: return new NewExprInfo($this->processName($expr->class), $args);
786:
787: } else {
788: throw new \LogicException(sprintf('Unsupported expr node %s used in constant expression', get_debug_type($expr)));
789: }
790: }
791:
792:
793: protected function extractPhpDoc(Node $node): PhpDocNode
794: {
795: return $node->getAttribute('phpDoc') ?? new PhpDocNode([]);
796: }
797:
798:
799: /**
800: * @return PhpDocTextNode[] indexed by []
801: */
802: protected function extractMultiLineDescription(PhpDocNode $node): array
803: {
804: $textNodes = [];
805:
806: foreach ($node->children as $child) {
807: if ($child instanceof PhpDocTextNode) {
808: $textNodes[] = $child;
809:
810: } else {
811: break;
812: }
813: }
814:
815: return $textNodes;
816: }
817:
818:
819: /**
820: * @return PhpDocTextNode[] indexed by []
821: */
822: protected function extractSingleLineDescription(?PhpDocTagValueNode $tagValue): array
823: {
824: return $tagValue?->getAttribute('description') ?? [];
825: }
826:
827:
828: /**
829: * @return GenericParameterInfo[] indexed by [name]
830: */
831: protected function extractGenericParameters(PhpDocNode $node): array
832: {
833: $genericParameters = [];
834:
835: foreach ($node->children as $child) {
836: if ($child instanceof PhpDocTagNode && $child->value instanceof TemplateTagValueNode) {
837: $lower = strtolower($child->value->name);
838:
839: $variance = match (true) {
840: str_ends_with($child->name, '-covariant') => GenericParameterVariance::Covariant,
841: str_ends_with($child->name, '-contravariant') => GenericParameterVariance::Contravariant,
842: default => GenericParameterVariance::Invariant,
843: };
844:
845: $genericParameters[$lower] = new GenericParameterInfo(
846: name: $child->value->name,
847: variance: $variance,
848: bound: $child->value->bound,
849: default: $child->value->default,
850: description: $child->value->description,
851: );
852: }
853: }
854:
855: return $genericParameters;
856: }
857:
858:
859: /**
860: * @return AliasInfo[] indexed by [name]
861: */
862: protected function extractAliases(PhpDocNode $node, ?int $startLine, ?int $endLine): array
863: {
864: $aliases = [];
865:
866: foreach ($node->children as $child) {
867: if ($child instanceof PhpDocTagNode && $child->value instanceof TypeAliasTagValueNode) {
868: $lower = strtolower($child->value->alias);
869: $aliases[$lower] = new AliasInfo($child->value->alias, $child->value->type);
870: $aliases[$lower]->startLine = $startLine;
871: $aliases[$lower]->endLine = $endLine;
872: }
873: }
874:
875: return $aliases;
876: }
877:
878:
879: /**
880: * @return PhpDocTagValueNode[][] indexed by [tagName][]
881: */
882: protected function extractTags(PhpDocNode $node): array
883: {
884: $tags = [];
885:
886: foreach ($node->getTags() as $tag) {
887: if (!$tag->value instanceof InvalidTagValueNode) {
888: $tags[substr($tag->name, 1)][] = $tag->value;
889: }
890: }
891:
892: return $tags;
893: }
894:
895:
896: /**
897: * @return ParamTagValueNode[] indexed by [parameterName]
898: */
899: protected function extractParamTagValues(PhpDocNode $node): array
900: {
901: $values = [];
902:
903: foreach ($node->children as $child) {
904: if ($child instanceof PhpDocTagNode && $child->value instanceof ParamTagValueNode) {
905: $values[$child->value->parameterName] = $child->value;
906: }
907: }
908:
909: return $values;
910: }
911:
912:
913: /**
914: * @return ClassLikeReferenceInfo[] indexed by [classLikeName]
915: */
916: protected function extractDependencies(ClassLikeInfo | FunctionInfo $referencedBy): array
917: {
918: $dependencies = [];
919: $stack = [$referencedBy];
920: $index = 1;
921:
922: while ($index > 0) {
923: $value = $stack[--$index];
924:
925: if ($value instanceof ClassLikeReferenceInfo && $value->fullLower !== 'self' && $value->fullLower !== 'parent') {
926: $dependencies[$value->fullLower] ??= $value;
927: }
928:
929: foreach ((array) $value as $item) {
930: if (is_array($item) || is_object($item)) {
931: $stack[$index++] = $item;
932: }
933: }
934: }
935:
936: return $dependencies;
937: }
938: }
939: