Completed
Push — master ( 3f65d6...dd9585 )
by Peter
05:11
created

DocComment::forFile()   D

Complexity

Conditions 10
Paths 512

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 10.0454

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 12
cts 13
cp 0.9231
rs 4.1777
c 0
b 0
f 0
cc 10
nc 512
nop 2
crap 10.0454

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 function get_class;
20
use Maslosoft\Addendum\Reflection\ReflectionAnnotatedClass;
21
use Maslosoft\Addendum\Reflection\ReflectionFile;
22
use Maslosoft\Addendum\Utilities\ClassChecker;
23
use Maslosoft\Addendum\Utilities\NameNormalizer;
24
use ReflectionClass;
25
use ReflectionMethod;
26
use ReflectionProperty;
27
use Reflector;
28
use const T_CLASS;
29
use const T_INTERFACE;
30
use const T_TRAIT;
31
32
class DocComment
33
{
34
	const TypeTrait = 'trait';
35
	const TypeClass = 'class';
36
	const TypeInterface = 'interface';
37
	private static $use = [];
38
	private static $useAliases = [];
39
	private static $namespaces = [];
40
	private static $types = [];
41
	private static $classNames = [];
42
	private static $classes = [];
43
	private static $methods = [];
44
	private static $fields = [];
45
	private static $parsedFiles = [];
46
47 1
	public static function clearCache()
48
	{
49 1
		self::$namespaces = [];
50 1
		self::$types = [];
51 1
		self::$classNames = [];
52 1
		self::$classes = [];
53 1
		self::$methods = [];
54 1
		self::$fields = [];
55 1
		self::$parsedFiles = [];
56 1
	}
57
58 51
	public function get($reflection)
59
	{
60 51
		if ($reflection instanceof ReflectionClass)
61
		{
62 44
			return $this->forClass($reflection);
63
		}
64 26
		elseif ($reflection instanceof ReflectionMethod)
65
		{
66 9
			return $this->forMethod($reflection);
67
		}
68 22
		elseif ($reflection instanceof ReflectionProperty)
69
		{
70 22
			return $this->forProperty($reflection);
71
		}
72
		throw new Exception("This method can only be used on reflection classes");
73
	}
74
75
	/**
76
	 * Get doc comment for file
77
	 * If file contains several classes, $className will be returned
78
	 * If file name matches class name, this class will be returned
79
	 * @param string $name
80
	 * @param string $className
81
	 * @return array
82
	 */
83 6
	public function forFile($name, $className = null)
84
	{
85 6
		$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...
86 6
		if (null !== $className)
87
		{
88
			$fqn = $className;
89
		}
90
		/**
91
		 * TODO Use some container here with ArrayAccess interface. Array-like access is *required* here.
92
		 */
93
		$result = [
94 6
			'namespace' => isset(self::$namespaces[$fqn]) ? self::$namespaces[$fqn] : [],
95 6
			'type' => isset(self::$types[$fqn]) ? self::$types[$fqn] : '',
96 6
			'use' => isset(self::$use[$fqn]) ? self::$use[$fqn] : [],
97 6
			'useAliases' => isset(self::$useAliases[$fqn]) ? self::$useAliases[$fqn] : [],
98 6
			'className' => isset(self::$classNames[$fqn]) ? self::$classNames[$fqn] : [],
99 6
			'class' => isset(self::$classes[$fqn]) ? self::$classes[$fqn] : '',
100 6
			'methods' => isset(self::$methods[$fqn]) ? self::$methods[$fqn] : [],
101 6
			'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
102
		];
103
104 6
		return $result;
105
	}
106
107 53
	public function forClass(Reflector $reflection)
108
	{
109 53
		assert($reflection instanceof ReflectionClass || $reflection instanceof ReflectionFile, sprintf("Expected `%s` or `%s`, got `%s`", ReflectionClass::class, ReflectionFile::class, get_class($reflection)));
110 53
		if (ClassChecker::isAnonymous($reflection->name))
111
		{
112 1
			echo '';
113
		}
114 53
		$fqn = $reflection->getName();
115 53
		$this->process($reflection->getFileName(), $fqn);
116 53
		if (ClassChecker::isAnonymous($reflection->name))
117
		{
118 1
			$info = new ReflectionClass($reflection->getName());
119 1
			$anonFqn = $reflection->getName();
120 1
			NameNormalizer::normalize($anonFqn);
121 1
			$this->processAnonymous($info, $anonFqn);
122
		}
123
		$result = [
124 53
			'namespace' => isset(self::$namespaces[$fqn]) ? self::$namespaces[$fqn] : [],
125 53
			'type' => isset(self::$types[$fqn]) ? self::$types[$fqn] : '',
126 53
			'use' => isset(self::$use[$fqn]) ? self::$use[$fqn] : [],
127 53
			'useAliases' => isset(self::$useAliases[$fqn]) ? self::$useAliases[$fqn] : [],
128 53
			'className' => isset(self::$classNames[$fqn]) ? self::$classNames[$fqn] : [],
129 53
			'class' => isset(self::$classes[$fqn]) ? self::$classes[$fqn] : '',
130 53
			'methods' => isset(self::$methods[$fqn]) ? self::$methods[$fqn] : [],
131 53
			'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
132
		];
133 53
		if (ClassChecker::isAnonymous($reflection->name))
134
		{
135 1
			assert(!empty($anonFqn));
136 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...
137 1
			$result['class'] = self::$classes[$anonFqn];
138
		}
139 53
		return $result;
140
	}
141
142 9
	public function forMethod(Reflector $reflection)
143
	{
144 9
		assert($reflection instanceof ReflectionMethod);
145 9
		$this->process($reflection->getDeclaringClass()->getFileName());
146
147
148
149 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...
150 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...
151 9
		return isset(self::$methods[$class][$method]) ? self::$methods[$class][$method] : false;
152
	}
153
154 22
	public function forProperty(Reflector $reflection)
155
	{
156 22
		assert($reflection instanceof ReflectionProperty);
157 22
		$this->process($reflection->getDeclaringClass()->getFileName());
158
159
160 22
		$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...
161 22
		$field = $reflection->getName();
162 22
		return isset(self::$fields[$class][$field]) ? self::$fields[$class][$field] : false;
163
	}
164
165 1
	private function processAnonymous(Reflector $reflection, $fqn)
166
	{
167 1
		if (!isset(self::$parsedFiles[$fqn]))
168
		{
169
			/* @var $reflection ReflectionAnnotatedClass */
170 1
			self::$classNames[$fqn] = $fqn;
171 1
			self::$classes[$fqn] = $reflection->getDocComment();
172 1
			self::$methods[$fqn] = [];
173 1
			self::$fields[$fqn] = [];
174 1
			foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
175
			{
176 1
				self::$methods[$fqn][$method->name] = $method->getDocComment();
177
			}
178 1
			foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property)
179
			{
180 1
				self::$fields[$fqn][$property->name] = $property->getDocComment();
181
			}
182 1
			self::$parsedFiles[$fqn] = $fqn;
183
		}
184 1
		return self::$parsedFiles[$fqn];
185
	}
186
187 64
	private function process($file, $fqn = false)
188
	{
189 64
		if (!isset(self::$parsedFiles[$file]))
190
		{
191 53
			$fqn = $this->parse($file, $fqn);
192 53
			self::$parsedFiles[$file] = $fqn;
193
		}
194 64
		return self::$parsedFiles[$file];
195
	}
196
197 53
	protected function parse($file, $fqn = false)
198
	{
199 53
		$use = [];
200 53
		$aliases = [];
201 53
		$namespace = '\\';
202 53
		$tokens = $this->getTokens($file);
203 53
		$class = false;
204 53
		$isTrait = $isClass = $isInterface = false;
0 ignored issues
show
Unused Code introduced by
$isInterface is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$isClass is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$isTrait is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
205 53
		$comment = null;
206 53
		$max = count($tokens);
207 53
		$i = 0;
208 53
		while ($i < $max)
209
		{
210 53
			$token = $tokens[$i];
211 53
			if (is_array($token))
212
			{
213 53
				[$code, $value] = $token;
0 ignored issues
show
Bug introduced by
The variable $code does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
214
215
				switch ($code)
216
				{
217 53
					case T_DOC_COMMENT:
218 53
						$comment = $value;
219 53
						break;
220
221 53
					case T_NAMESPACE:
222 53
						$comment = false;
223 53
						$tokensCount = count($tokens);
224 53
						for ($j = $i + 1; $j < $tokensCount; $j++)
225
						{
226 53
							if ($tokens[$j][0] === T_STRING)
227
							{
228 53
								$namespace .= '\\' . $tokens[$j][1];
229
							}
230 53
							elseif ($tokens[$j] === '{' || $tokens[$j] === ';')
231
							{
232 53
								break;
233
							}
234
						}
235
236 53
						$namespace = preg_replace('~^\\\\+~', '', $namespace);
237 53
						break;
238 53
					case T_USE:
239
						// After class declaration, this should ignore `use` trait
240 49
						if ($class)
241
						{
242 4
							break;
243
						}
244 49
						$comment = false;
245 49
						$useNs = '';
246 49
						$tokensCount = count($tokens);
247 49
						$as = false;
248 49
						for ($j = $i + 1; $j < $tokensCount; $j++)
249
						{
250 49
							$tokenName = $tokens[$j][0];
251 49
							if (isset($tokens[$j][1]) && $tokens[$j][1] == 'IMatcher')
252
							{
253 1
								echo 's';
254
							}
255 49
							if ($tokenName === T_STRING && !$as)
256
							{
257 49
								$useNs .= '\\' . $tokens[$j][1];
258
							}
259 49
							if ($tokenName === T_STRING && $as)
260
							{
261 2
								$alias = $tokens[$j][1];
262 2
								break;
263
							}
264 49
							if ($tokenName === T_AS)
265
							{
266 2
								$as = true;
267
							}
268 49
							if ($tokens[$j] === '{' || $tokens[$j] === ';')
269
							{
270 49
								break;
271
							}
272
						}
273 49
						$use[] = preg_replace('~^\\\\+~', '', $useNs);
274 49
						if ($as)
275
						{
276 2
							assert(!empty($alias));
277 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...
278
						}
279 49
						break;
280 53
					case T_TRAIT:
281 53
					case T_CLASS:
282 53
					case T_INTERFACE:
283
						// Ignore magic constant `::class`
284 53
						if ($tokens[$i - 1][0] == T_DOUBLE_COLON)
285
						{
286 4
							break;
287
						}
288 53
						$class = $this->getString($tokens, $i, $max);
289 53
						if (!$fqn)
290
						{
291 4
							$fqn = sprintf('%s\%s', $namespace, $class);
292
						}
293 53
						if ($comment !== false)
294
						{
295 45
							self::$classes[$fqn] = $comment;
296 45
							$comment = false;
297
						}
298 53
						self::$namespaces[$fqn] = $namespace;
299 53
						self::$classNames[$fqn] = $class;
300 53
						self::$use[$fqn] = $use;
301 53
						self::$useAliases[$fqn] = $aliases;
302
303 53
						if($code === T_TRAIT)
304
						{
305 7
							self::$types[$fqn] = self::TypeTrait;
306
						}
307 49
						elseif($code === T_CLASS)
308
						{
309 47
							self::$types[$fqn] = self::TypeClass;
310
						}
311 2
						elseif($code === T_INTERFACE)
312
						{
313 2
							self::$types[$fqn] = self::TypeInterface;
314
						}
315
316 53
						break;
317
318 53
					case T_VARIABLE:
319 39
						if ($comment !== false && $class)
320
						{
321 33
							$field = substr($token[1], 1);
322 33
							self::$fields[$fqn][$field] = $comment;
323 33
							$comment = false;
324
						}
325 39
						break;
326
327 53
					case T_FUNCTION:
328 20
						if ($comment !== false && $class)
329
						{
330 12
							$function = $this->getString($tokens, $i, $max);
331 12
							self::$methods[$fqn][$function] = $comment;
332 12
							$comment = false;
333
						}
334
335 20
						break;
336
337
					// ignore
338 53
					case T_WHITESPACE:
339 53
					case T_PUBLIC:
340 53
					case T_PROTECTED:
341 53
					case T_PRIVATE:
342 53
					case T_ABSTRACT:
343 53
					case T_FINAL:
344 53
					case T_VAR:
345 53
						break;
346
347
					default:
348 53
						$comment = false;
349 53
						break;
350
				}
351
			}
352
			else
353
			{
354 53
				$comment = false;
355
			}
356 53
			$i++;
357
		}
358 53
		return $fqn;
359
	}
360
361 53
	private function getString($tokens, &$i, $max)
362
	{
363
		do
364
		{
365
			/**
366
			 * TODO Workaround for problem described near T_CLASS token
367
			 */
368 53
			if (!isset($tokens[$i]))
369
			{
370
				$i++;
371
				continue;
372
			}
373 53
			$token = $tokens[$i];
374 53
			$i++;
375 53
			if (is_array($token))
376
			{
377 53
				if ($token[0] == T_STRING)
378
				{
379 53
					return $token[1];
380
				}
381
			}
382
		}
383 53
		while ($i <= $max);
384
		return false;
385
	}
386
387 53
	private function getTokens($file)
388
	{
389 53
		return token_get_all(file_get_contents($file));
390
	}
391
392
}
393