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: | |
96: | |
97: | class AnalyzeTaskHandler implements TaskHandler |
98: | { |
99: | |
100: | |
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: | |
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: | |
142: | |
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) { |
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) { |
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: | |
285: | |
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: | |
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: | |
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: | |
440: | |
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: | |
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: | |
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: | |
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: | |
532: | |
533: | |
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: | |
559: | |
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: | |
587: | |
588: | |
589: | |
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: | |
623: | |
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: | |
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: | |
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: | |
821: | |
822: | protected function extractSingleLineDescription(?PhpDocTagValueNode $tagValue): array |
823: | { |
824: | return $tagValue?->getAttribute('description') ?? []; |
825: | } |
826: | |
827: | |
828: | |
829: | |
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: | |
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: | |
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: | |
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: | |
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: | |