| 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: | |