PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / vendor / prefixed / php-di / phpdoc-reader / src / PhpDocReader / PhpDocReader.php
matomo / app / vendor / prefixed / php-di / phpdoc-reader / src / PhpDocReader Last commit date
PhpParser 1 year ago AnnotationException.php 2 years ago PhpDocReader.php 1 year ago
PhpDocReader.php
228 lines
1 <?php
2
3 declare (strict_types=1);
4 namespace Matomo\Dependencies\PhpDocReader;
5
6 use Matomo\Dependencies\PhpDocReader\PhpParser\UseStatementParser;
7 use ReflectionClass;
8 use ReflectionMethod;
9 use ReflectionParameter;
10 use ReflectionProperty;
11 use Reflector;
12 /**
13 * PhpDoc reader
14 */
15 class PhpDocReader
16 {
17 /** @var UseStatementParser */
18 private $parser;
19 private const PRIMITIVE_TYPES = ['bool' => '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'];
20 /** @var bool */
21 private $ignorePhpDocErrors;
22 /**
23 * @param bool $ignorePhpDocErrors Enable or disable throwing errors when PhpDoc errors occur (when parsing annotations).
24 */
25 public function __construct(bool $ignorePhpDocErrors = \false)
26 {
27 $this->parser = new UseStatementParser();
28 $this->ignorePhpDocErrors = $ignorePhpDocErrors;
29 }
30 /**
31 * Parse the docblock of the property to get the type (class or primitive type) of the var annotation.
32 *
33 * @return string|null Type of the property (content of var annotation)
34 * @throws AnnotationException
35 */
36 public function getPropertyType(ReflectionProperty $property) : ?string
37 {
38 return $this->readPropertyType($property, \true);
39 }
40 /**
41 * Parse the docblock of the property to get the class of the var annotation.
42 *
43 * @return string|null Type of the property (content of var annotation)
44 * @throws AnnotationException
45 */
46 public function getPropertyClass(ReflectionProperty $property) : ?string
47 {
48 return $this->readPropertyType($property, \false);
49 }
50 private function readPropertyType(ReflectionProperty $property, bool $allowPrimitiveTypes) : ?string
51 {
52 // Get the content of the @var annotation
53 $docComment = $property->getDocComment();
54 if (!$docComment) {
55 return null;
56 }
57 if (preg_match('/@var\\s+([^\\s]+)/', $docComment, $matches)) {
58 [, $type] = $matches;
59 } else {
60 return null;
61 }
62 // Ignore primitive types
63 if (isset(self::PRIMITIVE_TYPES[$type])) {
64 if ($allowPrimitiveTypes) {
65 return self::PRIMITIVE_TYPES[$type];
66 }
67 return null;
68 }
69 // Ignore types containing special characters ([], <> ...)
70 if (!preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) {
71 return null;
72 }
73 $class = $property->getDeclaringClass();
74 // If the class name is not fully qualified (i.e. doesn't start with a \)
75 if ($type[0] !== '\\') {
76 // Try to resolve the FQN using the class context
77 $resolvedType = $this->tryResolveFqn($type, $class, $property);
78 if (!$resolvedType && !$this->ignorePhpDocErrors) {
79 throw new AnnotationException(sprintf('The @var annotation on %s::%s contains a non existent class "%s". ' . 'Did you maybe forget to add a "use" statement for this annotation?', $class->name, $property->getName(), $type));
80 }
81 $type = $resolvedType;
82 }
83 if (!$this->ignorePhpDocErrors && !$this->classExists($type)) {
84 throw new AnnotationException(sprintf('The @var annotation on %s::%s contains a non existent class "%s"', $class->name, $property->getName(), $type));
85 }
86 // Remove the leading \ (FQN shouldn't contain it)
87 $type = is_string($type) ? ltrim($type, '\\') : null;
88 return $type;
89 }
90 /**
91 * Parse the docblock of the property to get the type (class or primitive type) of the param annotation.
92 *
93 * @return string|null Type of the property (content of var annotation)
94 * @throws AnnotationException
95 */
96 public function getParameterType(ReflectionParameter $parameter) : ?string
97 {
98 return $this->readParameterClass($parameter, \true);
99 }
100 /**
101 * Parse the docblock of the property to get the class of the param annotation.
102 *
103 * @return string|null Type of the property (content of var annotation)
104 * @throws AnnotationException
105 */
106 public function getParameterClass(ReflectionParameter $parameter) : ?string
107 {
108 return $this->readParameterClass($parameter, \false);
109 }
110 private function readParameterClass(ReflectionParameter $parameter, bool $allowPrimitiveTypes) : ?string
111 {
112 // Use reflection
113 $parameterType = $parameter->getType();
114 if ($parameterType && $parameterType instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) {
115 return $parameterType->getName();
116 }
117 $parameterName = $parameter->name;
118 // Get the content of the @param annotation
119 $method = $parameter->getDeclaringFunction();
120 $docComment = $method->getDocComment();
121 if (!$docComment) {
122 return null;
123 }
124 if (preg_match('/@param\\s+([^\\s]+)\\s+\\$' . $parameterName . '/', $docComment, $matches)) {
125 [, $type] = $matches;
126 } else {
127 return null;
128 }
129 // Ignore primitive types
130 if (isset(self::PRIMITIVE_TYPES[$type])) {
131 if ($allowPrimitiveTypes) {
132 return self::PRIMITIVE_TYPES[$type];
133 }
134 return null;
135 }
136 // Ignore types containing special characters ([], <> ...)
137 if (!preg_match('/^[a-zA-Z0-9\\\\_]+$/', $type)) {
138 return null;
139 }
140 $class = $parameter->getDeclaringClass();
141 // If the class name is not fully qualified (i.e. doesn't start with a \)
142 if ($type[0] !== '\\') {
143 // Try to resolve the FQN using the class context
144 $resolvedType = $this->tryResolveFqn($type, $class, $parameter);
145 if (!$resolvedType && !$this->ignorePhpDocErrors) {
146 throw new AnnotationException(sprintf('The @param 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?', $parameterName, $class->name, $method->name, $type));
147 }
148 $type = $resolvedType;
149 }
150 if (!$this->ignorePhpDocErrors && !$this->classExists($type)) {
151 throw new AnnotationException(sprintf('The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s"', $parameterName, $class->name, $method->name, $type));
152 }
153 // Remove the leading \ (FQN shouldn't contain it)
154 $type = is_string($type) ? ltrim($type, '\\') : null;
155 return $type;
156 }
157 /**
158 * Attempts to resolve the FQN of the provided $type based on the $class and $member context.
159 *
160 * @return string|null Fully qualified name of the type, or null if it could not be resolved
161 */
162 private 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 // Retrieve "use" statements
167 $uses = $this->parser->parseUseStatements($class);
168 if (isset($uses[$loweredAlias])) {
169 // Imported classes
170 if ($pos !== \false) {
171 return $uses[$loweredAlias] . substr($type, $pos);
172 }
173 return $uses[$loweredAlias];
174 }
175 if ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
176 return $class->getNamespaceName() . '\\' . $type;
177 }
178 if (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
179 // Class namespace
180 return $uses['__NAMESPACE__'] . '\\' . $type;
181 }
182 if ($this->classExists($type)) {
183 // No namespace
184 return $type;
185 }
186 // If all fail, try resolving through related traits
187 return $this->tryResolveFqnInTraits($type, $class, $member);
188 }
189 /**
190 * Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching
191 * through the traits that are used by the provided $class.
192 *
193 * @return string|null Fully qualified name of the type, or null if it could not be resolved
194 */
195 private function tryResolveFqnInTraits(string $type, ReflectionClass $class, Reflector $member) : ?string
196 {
197 /** @var ReflectionClass[] $traits */
198 $traits = [];
199 // Get traits for the class and its parents
200 while ($class) {
201 $traits = array_merge($traits, $class->getTraits());
202 $class = $class->getParentClass();
203 }
204 foreach ($traits as $trait) {
205 // Eliminate traits that don't have the property/method/parameter
206 if ($member instanceof ReflectionProperty && !$trait->hasProperty($member->name)) {
207 continue;
208 }
209 if ($member instanceof ReflectionMethod && !$trait->hasMethod($member->name)) {
210 continue;
211 }
212 if ($member instanceof ReflectionParameter && !$trait->hasMethod($member->getDeclaringFunction()->name)) {
213 continue;
214 }
215 // Run the resolver again with the ReflectionClass instance for the trait
216 $resolvedType = $this->tryResolveFqn($type, $trait, $member);
217 if ($resolvedType) {
218 return $resolvedType;
219 }
220 }
221 return null;
222 }
223 private function classExists(string $class) : bool
224 {
225 return class_exists($class) || interface_exists($class);
226 }
227 }
228