.gitattributes000064400000000054151360553310007440 0ustar00/tests export-ignore /.github export-ignore LICENSE000064400000002054151360553310005554 0ustar00The MIT License (MIT) Copyright (c) Hyperf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. composer.json000064400000002240151360553310007266 0ustar00{ "name": "hyperf/code-parser", "description": "A code parser component for Hyperf.", "license": "MIT", "keywords": [ "php", "swoole", "hyperf", "code-parser" ], "homepage": "https://hyperf.io", "support": { "docs": "https://hyperf.wiki", "issues": "https://github.com/hyperf/hyperf/issues", "pull-request": "https://github.com/hyperf/hyperf/pulls", "source": "https://github.com/hyperf/hyperf" }, "require": { "php": ">=8.0", "hyperf/collection": "~3.0.0", "hyperf/stringable": "~3.0.0", "hyperf/support": "~3.0.0" }, "suggest": { "jean85/pretty-package-versions": "Required to use PrettyVersions. (^1.2|^2.0)", "nikic/php-parser": "Required to use PhpParser. (^4.0)" }, "autoload": { "psr-4": { "Hyperf\\CodeParser\\": "src/" } }, "autoload-dev": { "psr-4": { "HyperfTest\\CodeParser\\": "tests/" } }, "config": { "sort-packages": true }, "extra": { "branch-alias": { "dev-master": "3.0-dev" } } } src/Exception/InvalidArgumentException.php000064400000000533151360553310014755 0ustar00 'bool', 'boolean' => 'bool', 'string' => 'string', 'int' => 'int', 'integer' => 'int', 'float' => 'float', 'double' => 'float', 'array' => 'array', 'object' => 'object', 'callable' => 'callable', 'resource' => 'resource', 'mixed' => 'mixed', 'iterable' => 'iterable', ]; protected static ?PhpDocReader $instance = null; private UseStatementParser $parser; /** * @param bool $ignorePhpDocErrors enable or disable throwing errors when PhpDoc errors occur (when parsing annotations) */ public function __construct(private bool $ignorePhpDocErrors = false) { $this->parser = new UseStatementParser(); } public static function getInstance(): PhpDocReader { if (static::$instance) { return static::$instance; } return static::$instance = new static(); } /** * Parse the docblock of the property to get the type (class or primitive type) of the param annotation. * * @throws AnnotationException */ public function getReturnType(ReflectionMethod $method, bool $withoutNamespace = false): array { return $this->readReturnClass($method, true, $withoutNamespace); } /** * Parse the docblock of the property to get the class of the param annotation. * * @throws AnnotationException */ public function getReturnClass(ReflectionMethod $method, bool $withoutNamespace = false): array { return $this->readReturnClass($method, false, $withoutNamespace); } protected function readReturnClass(ReflectionMethod $method, bool $allowPrimitiveTypes, bool $withoutNamespace = false): array { // Use reflection $returnType = $method->getReturnType(); if ($returnType instanceof ReflectionNamedType) { if (! $returnType->isBuiltin() || $allowPrimitiveTypes) { return [($returnType->allowsNull() ? '?' : '') . $returnType->getName()]; } } $docComment = $method->getDocComment(); if (! $docComment) { return ['mixed']; } if (preg_match('/@return\s+([^\s]+)\s+/', $docComment, $matches)) { [, $type] = $matches; } else { return ['mixed']; } $result = []; $class = $method->getDeclaringClass(); $types = explode('|', $type); foreach ($types as $type) { // Ignore primitive types if (isset(self::PRIMITIVE_TYPES[$type])) { if ($allowPrimitiveTypes) { $result[] = self::PRIMITIVE_TYPES[$type]; } continue; } // Ignore types containing special characters ([], <> ...) if (! preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) { continue; } // If the class name is not fully qualified (i.e. doesn't start with a \) if ($type[0] !== '\\' && ! $withoutNamespace) { // Try to resolve the FQN using the class context $resolvedType = $this->tryResolveFqn($type, $class, $method); if (! $resolvedType && ! $this->ignorePhpDocErrors) { throw new AnnotationException(sprintf( 'The @return annotation for parameter "%s" of %s::%s contains a non existent class "%s". ' . 'Did you maybe forget to add a "use" statement for this annotation?', $method, $class->name, $method->name, $type )); } $type = $resolvedType; } if (! $this->ignorePhpDocErrors && ! $withoutNamespace && ! $this->classExists($type)) { throw new AnnotationException(sprintf( 'The @return annotation for parameter "%s" of %s::%s contains a non existent class "%s"', $method, $class->name, $method->name, $type )); } // Remove the leading \ (FQN shouldn't contain it) $result[] = is_string($type) ? ltrim($type, '\\') : null; } return $result; } /** * Attempts to resolve the FQN of the provided $type based on the $class and $member context. * * @return null|string Fully qualified name of the type, or null if it could not be resolved */ protected function tryResolveFqn(string $type, ReflectionClass $class, Reflector $member): ?string { $alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos); $loweredAlias = strtolower($alias); // Retrieve "use" statements $uses = $this->parser->parseUseStatements($class); if (isset($uses[$loweredAlias])) { // Imported classes if ($pos !== false) { return $uses[$loweredAlias] . substr($type, $pos); } return $uses[$loweredAlias]; } if ($this->classExists($class->getNamespaceName() . '\\' . $type)) { return $class->getNamespaceName() . '\\' . $type; } if (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) { // Class namespace return $uses['__NAMESPACE__'] . '\\' . $type; } if ($this->classExists($type)) { // No namespace return $type; } // If all fail, try resolving through related traits return $this->tryResolveFqnInTraits($type, $class, $member); } /** * Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching * through the traits that are used by the provided $class. * * @return null|string Fully qualified name of the type, or null if it could not be resolved */ protected function tryResolveFqnInTraits(string $type, ReflectionClass $class, Reflector $member): ?string { /** @var ReflectionClass[] $traits */ $traits = []; // Get traits for the class and its parents while ($class) { $traits = array_merge($traits, $class->getTraits()); $class = $class->getParentClass(); } foreach ($traits as $trait) { // Eliminate traits that don't have the property/method/parameter if ($member instanceof ReflectionProperty && ! $trait->hasProperty($member->name)) { continue; } if ($member instanceof ReflectionMethod && ! $trait->hasMethod($member->name)) { continue; } if ($member instanceof ReflectionParameter && ! $trait->hasMethod($member->getDeclaringFunction()->name)) { continue; } // Run the resolver again with the ReflectionClass instance for the trait $resolvedType = $this->tryResolveFqn($type, $trait, $member); if ($resolvedType) { return $resolvedType; } } return null; } protected function classExists(string $class): bool { return class_exists($class) || interface_exists($class); } } src/PhpDocReaderManager.php000064400000001113151360553310011635 0ustar00parser = $parserFactory->create(ParserFactory::ONLY_PHP7); } public static function getInstance(): PhpParser { if (static::$instance) { return static::$instance; } return static::$instance = new static(); } /** * @return null|Node\Stmt[] */ public function getNodesFromReflectionClass(ReflectionClass $reflectionClass): ?array { $code = file_get_contents($reflectionClass->getFileName()); return $this->parser->parse($code); } public function getNodeFromReflectionType(ReflectionType $reflection): Node\ComplexType|Node\Identifier|Node\Name { if ($reflection instanceof ReflectionUnionType) { $unionType = []; foreach ($reflection->getTypes() as $objType) { $type = $objType->getName(); if (! in_array($type, static::TYPES)) { $unionType[] = new Node\Name('\\' . $type); } else { $unionType[] = new Node\Identifier($type); } } return new Node\UnionType($unionType); } return $this->getTypeWithNullableOrNot($reflection); } public function getNodeFromReflectionParameter(ReflectionParameter $parameter): Node\Param { $result = new Node\Param( new Node\Expr\Variable($parameter->getName()) ); if ($parameter->isDefaultValueAvailable()) { $result->default = $this->getExprFromValue($parameter->getDefaultValue()); } if ($parameter->hasType()) { $result->type = $this->getNodeFromReflectionType($parameter->getType()); } if ($parameter->isPassedByReference()) { $result->byRef = true; } if ($parameter->isVariadic()) { $result->variadic = true; } return $result; } public function getExprFromValue($value): Node\Expr { return match (gettype($value)) { 'array' => value(function ($value) { $isList = ! Arr::isAssoc($value); $result = []; foreach ($value as $i => $item) { $key = null; if (! $isList) { $key = is_int($i) ? new Node\Scalar\LNumber($i) : new Node\Scalar\String_($i); } $result[] = new Node\Expr\ArrayItem($this->getExprFromValue($item), $key); } return new Node\Expr\Array_($result, [ 'kind' => Node\Expr\Array_::KIND_SHORT, ]); }, $value), 'string' => new Node\Scalar\String_($value), 'integer' => new Node\Scalar\LNumber($value), 'double' => new Node\Scalar\DNumber($value), 'NULL' => new Node\Expr\ConstFetch(new Node\Name('null')), 'boolean' => new Node\Expr\ConstFetch(new Node\Name($value ? 'true' : 'false')), 'object' => $this->getExprFromObject($value), default => throw new InvalidArgumentException($value . ' is invalid'), }; } /** * @return Node\Stmt\ClassMethod[] */ public function getAllMethodsFromStmts(array $stmts): array { $methods = []; foreach ($stmts as $namespace) { if (! $namespace instanceof Node\Stmt\Namespace_) { continue; } foreach ($namespace->stmts as $class) { if (! $class instanceof Node\Stmt\Class_ && ! $class instanceof Node\Stmt\Interface_) { continue; } foreach ($class->getMethods() as $method) { $methods[] = $method; } } } return $methods; } private function getExprFromObject(object $value) { $ref = new ReflectionClass($value); if (method_exists($ref, 'isEnum') && $ref->isEnum()) { return new Node\Expr\ClassConstFetch( new Node\Name('\\' . $value::class), $value->name ); } return new Node\Expr\New_( new Node\Name\FullyQualified($value::class) ); } private function getTypeWithNullableOrNot(ReflectionType $reflection): Node\ComplexType|Node\Identifier|Node\Name { if (! $reflection instanceof ReflectionNamedType) { throw new ReflectionException('ReflectionType must be ReflectionNamedType.'); } $name = $reflection->getName(); if ($reflection->allowsNull() && $name !== 'mixed') { return new Node\NullableType($this->getTypeFromString($name)); } if (! in_array($name, static::TYPES)) { return new Node\Name('\\' . $name); } return new Node\Identifier($name); } private function getTypeFromString(string $name) { if (! in_array($name, static::TYPES)) { return '\\' . $name; } return $name; } } src/Project.php000064400000003706151360553310007462 0ustar00getAutoloadRules() as $prefix => $prefixPath) { if ($this->isRootNamespace($prefix) || str_starts_with($path, $prefixPath)) { return $prefix . str_replace('/', '\\', substr($path, strlen($prefixPath))); } } throw new RuntimeException("Invalid project path: {$path}"); } public function className(string $path): string { return $this->namespace($path); } public function path(string $name, $extension = '.php'): string { if (Str::endsWith($name, '\\')) { $extension = ''; } foreach ($this->getAutoloadRules() as $prefix => $prefixPath) { if ($this->isRootNamespace($prefix) || str_starts_with($name, $prefix)) { return $prefixPath . str_replace('\\', '/', substr($name, strlen($prefix))) . $extension; } } throw new RuntimeException("Invalid class name: {$name}"); } protected function isRootNamespace(string $namespace): bool { return $namespace === ''; } protected function getAutoloadRules(): array { return data_get(Composer::getJsonContent(), 'autoload.psr-4', []); } }