Skip to content

Commit eae5f99

Browse files
Split hyperf/code-parser from hyperf/utils (#5670)
Co-authored-by: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Co-authored-by: 李铭昕 <715557344@qq.com>
0 parents  commit eae5f99

19 files changed

+1065
-0
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/tests export-ignore
2+
/.github export-ignore
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: Close Pull Request
2+
3+
on:
4+
pull_request_target:
5+
types: [opened]
6+
7+
jobs:
8+
run:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: superbrothers/close-pull-request@v3
12+
with:
13+
comment: "Hi, this is a READ-ONLY repository, please submit your PR on the https://github.com/hyperf/hyperf repository.<br><br> This Pull Request will close automatically.<br><br> Thanks! "

.github/workflows/release.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
on:
2+
push:
3+
# Sequence of patterns matched against refs/tags
4+
tags:
5+
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
6+
7+
name: Release
8+
9+
jobs:
10+
release:
11+
name: Release
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v2
16+
- name: Create Release
17+
id: create_release
18+
uses: actions/create-release@v1
19+
env:
20+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21+
with:
22+
tag_name: ${{ github.ref }}
23+
release_name: Release ${{ github.ref }}
24+
draft: false
25+
prerelease: false

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) Hyperf
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

composer.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "hyperf/code-parser",
3+
"description": "A code parser component for Hyperf.",
4+
"license": "MIT",
5+
"keywords": [
6+
"php",
7+
"swoole",
8+
"hyperf",
9+
"code-parser"
10+
],
11+
"homepage": "https://hyperf.io",
12+
"support": {
13+
"docs": "https://hyperf.wiki",
14+
"issues": "https://github.com/hyperf/hyperf/issues",
15+
"pull-request": "https://github.com/hyperf/hyperf/pulls",
16+
"source": "https://github.com/hyperf/hyperf"
17+
},
18+
"require": {
19+
"php": ">=8.0",
20+
"hyperf/collection": "~3.0.0",
21+
"hyperf/stringable": "~3.0.0",
22+
"hyperf/support": "~3.0.0"
23+
},
24+
"suggest": {
25+
"jean85/pretty-package-versions": "Required to use PrettyVersions. (^1.2|^2.0)",
26+
"nikic/php-parser": "Required to use PhpParser. (^4.0)"
27+
},
28+
"autoload": {
29+
"psr-4": {
30+
"Hyperf\\CodeParser\\": "src/"
31+
}
32+
},
33+
"autoload-dev": {
34+
"psr-4": {
35+
"HyperfTest\\CodeParser\\": "tests/"
36+
}
37+
},
38+
"config": {
39+
"sort-packages": true
40+
},
41+
"extra": {
42+
"branch-alias": {
43+
"dev-master": "3.0-dev"
44+
}
45+
}
46+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* This file is part of Hyperf.
6+
*
7+
* @link https://www.hyperf.io
8+
* @document https://hyperf.wiki
9+
* @contact group@hyperf.io
10+
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
11+
*/
12+
namespace Hyperf\CodeParser\Exception;
13+
14+
class InvalidArgumentException extends \InvalidArgumentException
15+
{
16+
}

src/Package.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* This file is part of Hyperf.
6+
*
7+
* @link https://www.hyperf.io
8+
* @document https://hyperf.wiki
9+
* @contact group@hyperf.io
10+
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
11+
*/
12+
namespace Hyperf\CodeParser;
13+
14+
use Jean85\PrettyVersions;
15+
use Throwable;
16+
17+
class Package
18+
{
19+
public static function getPrettyVersion(string $package): string
20+
{
21+
try {
22+
return (string) PrettyVersions::getVersion($package);
23+
} catch (Throwable $exception) {
24+
return 'unknown';
25+
}
26+
}
27+
}

src/PhpDocReader.php

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* This file is part of Hyperf.
6+
*
7+
* @link https://www.hyperf.io
8+
* @document https://hyperf.wiki
9+
* @contact group@hyperf.io
10+
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
11+
*/
12+
namespace Hyperf\CodeParser;
13+
14+
use PhpDocReader\AnnotationException;
15+
use PhpDocReader\PhpParser\UseStatementParser;
16+
use ReflectionClass;
17+
use ReflectionMethod;
18+
use ReflectionNamedType;
19+
use ReflectionParameter;
20+
use ReflectionProperty;
21+
use Reflector;
22+
23+
/**
24+
* @see https://github.com/PHP-DI/PhpDocReader
25+
*/
26+
class PhpDocReader
27+
{
28+
private const PRIMITIVE_TYPES = [
29+
'bool' => 'bool',
30+
'boolean' => 'bool',
31+
'string' => 'string',
32+
'int' => 'int',
33+
'integer' => 'int',
34+
'float' => 'float',
35+
'double' => 'float',
36+
'array' => 'array',
37+
'object' => 'object',
38+
'callable' => 'callable',
39+
'resource' => 'resource',
40+
'mixed' => 'mixed',
41+
'iterable' => 'iterable',
42+
];
43+
44+
protected static ?PhpDocReader $instance = null;
45+
46+
private UseStatementParser $parser;
47+
48+
/**
49+
* @param bool $ignorePhpDocErrors enable or disable throwing errors when PhpDoc errors occur (when parsing annotations)
50+
*/
51+
public function __construct(private bool $ignorePhpDocErrors = false)
52+
{
53+
$this->parser = new UseStatementParser();
54+
}
55+
56+
public static function getInstance(): PhpDocReader
57+
{
58+
if (static::$instance) {
59+
return static::$instance;
60+
}
61+
return static::$instance = new static();
62+
}
63+
64+
/**
65+
* Parse the docblock of the property to get the type (class or primitive type) of the param annotation.
66+
*
67+
* @throws AnnotationException
68+
*/
69+
public function getReturnType(ReflectionMethod $method, bool $withoutNamespace = false): array
70+
{
71+
return $this->readReturnClass($method, true, $withoutNamespace);
72+
}
73+
74+
/**
75+
* Parse the docblock of the property to get the class of the param annotation.
76+
*
77+
* @throws AnnotationException
78+
*/
79+
public function getReturnClass(ReflectionMethod $method, bool $withoutNamespace = false): array
80+
{
81+
return $this->readReturnClass($method, false, $withoutNamespace);
82+
}
83+
84+
protected function readReturnClass(ReflectionMethod $method, bool $allowPrimitiveTypes, bool $withoutNamespace = false): array
85+
{
86+
// Use reflection
87+
$returnType = $method->getReturnType();
88+
if ($returnType instanceof ReflectionNamedType) {
89+
if (! $returnType->isBuiltin() || $allowPrimitiveTypes) {
90+
return [($returnType->allowsNull() ? '?' : '') . $returnType->getName()];
91+
}
92+
}
93+
94+
$docComment = $method->getDocComment();
95+
if (! $docComment) {
96+
return ['mixed'];
97+
}
98+
if (preg_match('/@return\s+([^\s]+)\s+/', $docComment, $matches)) {
99+
[, $type] = $matches;
100+
} else {
101+
return ['mixed'];
102+
}
103+
104+
$result = [];
105+
$class = $method->getDeclaringClass();
106+
$types = explode('|', $type);
107+
foreach ($types as $type) {
108+
// Ignore primitive types
109+
if (isset(self::PRIMITIVE_TYPES[$type])) {
110+
if ($allowPrimitiveTypes) {
111+
$result[] = self::PRIMITIVE_TYPES[$type];
112+
}
113+
continue;
114+
}
115+
116+
// Ignore types containing special characters ([], <> ...)
117+
if (! preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) {
118+
continue;
119+
}
120+
121+
// If the class name is not fully qualified (i.e. doesn't start with a \)
122+
if ($type[0] !== '\\' && ! $withoutNamespace) {
123+
// Try to resolve the FQN using the class context
124+
$resolvedType = $this->tryResolveFqn($type, $class, $method);
125+
126+
if (! $resolvedType && ! $this->ignorePhpDocErrors) {
127+
throw new AnnotationException(sprintf(
128+
'The @return annotation for parameter "%s" of %s::%s contains a non existent class "%s". '
129+
. 'Did you maybe forget to add a "use" statement for this annotation?',
130+
$method,
131+
$class->name,
132+
$method->name,
133+
$type
134+
));
135+
}
136+
137+
$type = $resolvedType;
138+
}
139+
140+
if (! $this->ignorePhpDocErrors && ! $withoutNamespace && ! $this->classExists($type)) {
141+
throw new AnnotationException(sprintf(
142+
'The @return annotation for parameter "%s" of %s::%s contains a non existent class "%s"',
143+
$method,
144+
$class->name,
145+
$method->name,
146+
$type
147+
));
148+
}
149+
150+
// Remove the leading \ (FQN shouldn't contain it)
151+
$result[] = is_string($type) ? ltrim($type, '\\') : null;
152+
}
153+
154+
return $result;
155+
}
156+
157+
/**
158+
* Attempts to resolve the FQN of the provided $type based on the $class and $member context.
159+
*
160+
* @return null|string Fully qualified name of the type, or null if it could not be resolved
161+
*/
162+
protected function tryResolveFqn(string $type, ReflectionClass $class, Reflector $member): ?string
163+
{
164+
$alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos);
165+
$loweredAlias = strtolower($alias);
166+
167+
// Retrieve "use" statements
168+
$uses = $this->parser->parseUseStatements($class);
169+
170+
if (isset($uses[$loweredAlias])) {
171+
// Imported classes
172+
if ($pos !== false) {
173+
return $uses[$loweredAlias] . substr($type, $pos);
174+
}
175+
return $uses[$loweredAlias];
176+
}
177+
178+
if ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
179+
return $class->getNamespaceName() . '\\' . $type;
180+
}
181+
182+
if (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
183+
// Class namespace
184+
return $uses['__NAMESPACE__'] . '\\' . $type;
185+
}
186+
187+
if ($this->classExists($type)) {
188+
// No namespace
189+
return $type;
190+
}
191+
192+
// If all fail, try resolving through related traits
193+
return $this->tryResolveFqnInTraits($type, $class, $member);
194+
}
195+
196+
/**
197+
* Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching
198+
* through the traits that are used by the provided $class.
199+
*
200+
* @return null|string Fully qualified name of the type, or null if it could not be resolved
201+
*/
202+
protected function tryResolveFqnInTraits(string $type, ReflectionClass $class, Reflector $member): ?string
203+
{
204+
/** @var ReflectionClass[] $traits */
205+
$traits = [];
206+
207+
// Get traits for the class and its parents
208+
while ($class) {
209+
$traits = array_merge($traits, $class->getTraits());
210+
$class = $class->getParentClass();
211+
}
212+
213+
foreach ($traits as $trait) {
214+
// Eliminate traits that don't have the property/method/parameter
215+
if ($member instanceof ReflectionProperty && ! $trait->hasProperty($member->name)) {
216+
continue;
217+
}
218+
if ($member instanceof ReflectionMethod && ! $trait->hasMethod($member->name)) {
219+
continue;
220+
}
221+
if ($member instanceof ReflectionParameter && ! $trait->hasMethod($member->getDeclaringFunction()->name)) {
222+
continue;
223+
}
224+
225+
// Run the resolver again with the ReflectionClass instance for the trait
226+
$resolvedType = $this->tryResolveFqn($type, $trait, $member);
227+
228+
if ($resolvedType) {
229+
return $resolvedType;
230+
}
231+
}
232+
233+
return null;
234+
}
235+
236+
protected function classExists(string $class): bool
237+
{
238+
return class_exists($class) || interface_exists($class);
239+
}
240+
}

0 commit comments

Comments
 (0)