Completed
Push — master ( e6039a...13e566 )
by Peter
06:01
created

DocComment::getTokens()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
/**
4
 * This software package is licensed under AGPL, Commercial license.
5
 *
6
 * @package maslosoft/addendum
7
 * @licence AGPL, Commercial
8
 * @copyright Copyright (c) Piotr Masełkowski <[email protected]> (Meta container, further improvements, bugfixes)
9
 * @copyright Copyright (c) Maslosoft (Meta container, further improvements, bugfixes)
10
 * @copyright Copyright (c) Jan Suchal (Original version, builder, parser)
11
 * @link https://maslosoft.com/addendum/ - maslosoft addendum
12
 * @link https://code.google.com/p/addendum/ - original addendum project
13
 */
14
15
namespace Maslosoft\Addendum\Builder;
16
17
use function assert;
18
use Exception;
19
use Maslosoft\Addendum\Reflection\ReflectionAnnotatedClass;
20
use Maslosoft\Addendum\Utilities\ClassChecker;
21
use Maslosoft\Addendum\Utilities\NameNormalizer;
22
use ReflectionClass;
23
use ReflectionMethod;
24
use ReflectionProperty;
25
use Reflector;
26
27
class DocComment
28
{
29
30
	private static $use = [];
31
	private static $useAliases = [];
32
	private static $namespaces = [];
33
	private static $classNames = [];
34
	private static $classes = [];
35
	private static $methods = [];
36
	private static $fields = [];
37
	private static $parsedFiles = [];
38
39 1
	public static function clearCache()
40
	{
41 1
		self::$namespaces = [];
42 1
		self::$classNames = [];
43 1
		self::$classes = [];
44 1
		self::$methods = [];
45 1
		self::$fields = [];
46 1
		self::$parsedFiles = [];
47 1
	}
48
49 52
	public function get($reflection)
50
	{
51 52
		if ($reflection instanceof ReflectionClass)
52
		{
53 45
			return $this->forClass($reflection);
54
		}
55 27
		elseif ($reflection instanceof ReflectionMethod)
56
		{
57 9
			return $this->forMethod($reflection);
58
		}
59 23
		elseif ($reflection instanceof ReflectionProperty)
60
		{
61 23
			return $this->forProperty($reflection);
62
		}
63
		throw new Exception("This method can only be used on reflection classes");
64
	}
65
66
	/**
67
	 * Get doc comment for file
68
	 * If file contains several classes, $className will be returned
69
	 * If file name matches class name, this class will be returned
70
	 * @param string $name
71
	 * @param string $className
72
	 * @return array
73
	 */
74 2
	public function forFile($name, $className = null)
75
	{
76 2
		$fqn = $this->process($name, $className);
0 ignored issues
show
Documentation introduced by
$className is of type string|null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
77 2
		if (null !== $className)
78
		{
79
			$fqn = $className;
80
		}
81
		/**
82
		 * TODO Use some container here with ArrayAccess interface. Array-like access is *required* here.
83
		 */
84
		$result = [
85 2
			'namespace' => isset(self::$namespaces[$fqn]) ? self::$namespaces[$fqn] : [],
86 2
			'use' => isset(self::$use[$fqn]) ? self::$use[$fqn] : [],
87 2
			'useAliases' => isset(self::$useAliases[$fqn]) ? self::$useAliases[$fqn] : [],
88 2
			'className' => isset(self::$classNames[$fqn]) ? self::$classNames[$fqn] : [],
89 2
			'class' => isset(self::$classes[$fqn]) ? self::$classes[$fqn] : '',
90 2
			'methods' => isset(self::$methods[$fqn]) ? self::$methods[$fqn] : [],
91 2
			'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
92
		];
93
94 2
		return $result;
95
	}
96
97 54
	public function forClass(Reflector $reflection)
98
	{
99 54
		assert($reflection instanceof ReflectionClass);
100 54
		if (ClassChecker::isAnonymous($reflection->name))
101
		{
102 1
			echo '';
103
		}
104 54
		$fqn = $reflection->getName();
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
105 54
		$this->process($reflection->getFileName(), $fqn);
0 ignored issues
show
Documentation introduced by
$fqn is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
106 54
		if (ClassChecker::isAnonymous($reflection->name))
107
		{
108 1
			$info = new ReflectionClass($reflection->getName());
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
109 1
			$anonFqn = $reflection->getName();
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
110 1
			NameNormalizer::normalize($anonFqn);
111 1
			$this->processAnonymous($info, $anonFqn);
112
		}
113
		$result = [
114 54
			'namespace' => isset(self::$namespaces[$fqn]) ? self::$namespaces[$fqn] : [],
115 54
			'use' => isset(self::$use[$fqn]) ? self::$use[$fqn] : [],
116 54
			'useAliases' => isset(self::$useAliases[$fqn]) ? self::$useAliases[$fqn] : [],
117 54
			'className' => isset(self::$classNames[$fqn]) ? self::$classNames[$fqn] : [],
118 54
			'class' => isset(self::$classes[$fqn]) ? self::$classes[$fqn] : '',
119 54
			'methods' => isset(self::$methods[$fqn]) ? self::$methods[$fqn] : [],
120 54
			'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
121
		];
122 54
		if (ClassChecker::isAnonymous($reflection->name))
123
		{
124 1
			assert(!empty($anonFqn));
125 1
			$result['className'] = self::$classNames[$anonFqn];
0 ignored issues
show
Bug introduced by
The variable $anonFqn does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
126 1
			$result['class'] = self::$classes[$anonFqn];
127
		}
128 54
		return $result;
129
	}
130
131 9
	public function forMethod(Reflector $reflection)
132
	{
133 9
		assert($reflection instanceof ReflectionMethod);
134 9
		$this->process($reflection->getDeclaringClass()->getFileName());
135
136
137
138 9
		$class = $reflection->getDeclaringClass()->getName();
0 ignored issues
show
introduced by
Consider using $reflection->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
139 9
		$method = $reflection->getName();
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
140 9
		return isset(self::$methods[$class][$method]) ? self::$methods[$class][$method] : false;
141
	}
142
143 23
	public function forProperty(Reflector $reflection)
144
	{
145 23
		assert($reflection instanceof ReflectionProperty);
146 23
		$this->process($reflection->getDeclaringClass()->getFileName());
147
148
149 23
		$class = $reflection->getDeclaringClass()->getName();
0 ignored issues
show
introduced by
Consider using $reflection->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
150 23
		$field = $reflection->getName();
151 23
		return isset(self::$fields[$class][$field]) ? self::$fields[$class][$field] : false;
152
	}
153
154 1
	private function processAnonymous(Reflector $reflection, $fqn)
155
	{
156 1
		if (!isset(self::$parsedFiles[$fqn]))
157
		{
158
			/* @var $reflection ReflectionAnnotatedClass */
159 1
			self::$classNames[$fqn] = $fqn;
160 1
			self::$classes[$fqn] = $reflection->getDocComment();
161 1
			self::$methods[$fqn] = [];
162 1
			self::$fields[$fqn] = [];
163 1
			foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
164
			{
165 1
				self::$methods[$fqn][$method->name] = $method->getDocComment();
166
			}
167 1
			foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property)
168
			{
169 1
				self::$fields[$fqn][$property->name] = $property->getDocComment();
170
			}
171 1
			self::$parsedFiles[$fqn] = $fqn;
172
		}
173 1
		return self::$parsedFiles[$fqn];
174
	}
175
176 61
	private function process($file, $fqn = false)
177
	{
178 61
		if (!isset(self::$parsedFiles[$file]))
179
		{
180 52
			$fqn = $this->parse($file, $fqn);
181 52
			self::$parsedFiles[$file] = $fqn;
182
		}
183 61
		return self::$parsedFiles[$file];
184
	}
185
186 52
	protected function parse($file, $fqn = false)
187
	{
188 52
		$use = [];
189 52
		$aliases = [];
190 52
		$namespace = '\\';
191 52
		$tokens = $this->getTokens($file);
192 52
		$class = false;
193 52
		$comment = null;
194 52
		$max = count($tokens);
195 52
		$i = 0;
196 52
		while ($i < $max)
197
		{
198 52
			$token = $tokens[$i];
199 52
			if (is_array($token))
200
			{
201 52
				list($code, $value) = $token;
202
203
				switch ($code)
204
				{
205 52
					case T_DOC_COMMENT:
206 52
						$comment = $value;
207 52
						break;
208
209 52
					case T_NAMESPACE:
210 52
						$comment = false;
211 52
						$tokensCount = count($tokens);
212 52
						for ($j = $i + 1; $j < $tokensCount; $j++)
213
						{
214 52
							if ($tokens[$j][0] === T_STRING)
215
							{
216 52
								$namespace .= '\\' . $tokens[$j][1];
217
							}
218 52
							elseif ($tokens[$j] === '{' || $tokens[$j] === ';')
219
							{
220 52
								break;
221
							}
222
						}
223
224 52
						$namespace = preg_replace('~^\\\\+~', '', $namespace);
225 52
						break;
226 52
					case T_USE:
227
						// After class declaration, this should ignore `use` trait
228 49
						if ($class)
229
						{
230 4
							break;
231
						}
232 49
						$comment = false;
233 49
						$useNs = '';
234 49
						$tokensCount = count($tokens);
235 49
						$as = false;
236 49
						for ($j = $i + 1; $j < $tokensCount; $j++)
237
						{
238 49
							$tokenName = $tokens[$j][0];
239 49
							if (isset($tokens[$j][1]) && $tokens[$j][1] == 'IMatcher')
240
							{
241 1
								echo 's';
242
							}
243 49
							if ($tokenName === T_STRING && !$as)
244
							{
245 49
								$useNs .= '\\' . $tokens[$j][1];
246
							}
247 49
							if ($tokenName === T_STRING && $as)
248
							{
249 2
								$alias = $tokens[$j][1];
250 2
								break;
251
							}
252 49
							if ($tokenName === T_AS)
253
							{
254 2
								$as = true;
255
							}
256 49
							if ($tokens[$j] === '{' || $tokens[$j] === ';')
257
							{
258 49
								break;
259
							}
260
						}
261 49
						$use[] = preg_replace('~^\\\\+~', '', $useNs);
262 49
						if ($as)
263
						{
264 2
							assert(!empty($alias));
265 2
							$aliases[$useNs] = $alias;
0 ignored issues
show
Bug introduced by
The variable $alias does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
266
						}
267 49
						break;
268 52
					case T_TRAIT:
269 52
					case T_CLASS:
270 52
					case T_INTERFACE:
271
						// Ignore magic constant `::class`
272 52
						if ($tokens[$i - 1][0] == T_DOUBLE_COLON)
273
						{
274 4
							break;
275
						}
276 52
						$class = $this->getString($tokens, $i, $max);
277 52
						if (!$fqn)
278
						{
279 1
							$fqn = sprintf('%s\%s', $namespace, $class);
280
						}
281 52
						if ($comment !== false)
282
						{
283 44
							self::$classes[$fqn] = $comment;
284 44
							$comment = false;
285
						}
286 52
						self::$namespaces[$fqn] = $namespace;
287 52
						self::$classNames[$fqn] = $class;
288 52
						self::$use[$fqn] = $use;
289 52
						self::$useAliases[$fqn] = $aliases;
290 52
						break;
291
292 52
					case T_VARIABLE:
293 39
						if ($comment !== false && $class)
294
						{
295 33
							$field = substr($token[1], 1);
296 33
							self::$fields[$fqn][$field] = $comment;
297 33
							$comment = false;
298
						}
299 39
						break;
300
301 52
					case T_FUNCTION:
302 20
						if ($comment !== false && $class)
303
						{
304 12
							$function = $this->getString($tokens, $i, $max);
305 12
							self::$methods[$fqn][$function] = $comment;
306 12
							$comment = false;
307
						}
308
309 20
						break;
310
311
					// ignore
312 52
					case T_WHITESPACE:
313 52
					case T_PUBLIC:
314 52
					case T_PROTECTED:
315 52
					case T_PRIVATE:
316 52
					case T_ABSTRACT:
317 52
					case T_FINAL:
318 52
					case T_VAR:
319 52
						break;
320
321
					default:
322 52
						$comment = false;
323 52
						break;
324
				}
325
			}
326
			else
327
			{
328 52
				$comment = false;
329
			}
330 52
			$i++;
331
		}
332 52
		return $fqn;
333
	}
334
335 52
	private function getString($tokens, &$i, $max)
336
	{
337
		do
338
		{
339
			/**
340
			 * TODO Workaround for problem described near T_CLASS token
341
			 */
342 52
			if (!isset($tokens[$i]))
343
			{
344
				$i++;
345
				continue;
346
			}
347 52
			$token = $tokens[$i];
348 52
			$i++;
349 52
			if (is_array($token))
350
			{
351 52
				if ($token[0] == T_STRING)
352
				{
353 52
					return $token[1];
354
				}
355
			}
356
		}
357 52
		while ($i <= $max);
358
		return false;
359
	}
360
361 52
	private function getTokens($file)
362
	{
363 52
		return token_get_all(file_get_contents($file));
364
	}
365
366
}
367