DocComment   F
last analyzed

Complexity

Total Complexity 100

Size/Duplication

Total Lines 412
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 96.23%

Importance

Changes 0
Metric Value
wmc 100
lcom 1
cbo 3
dl 0
loc 412
ccs 204
cts 212
cp 0.9623
rs 2
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A clearCache() 0 10 1
A get() 0 18 4
D forFile() 0 23 10
F forClass() 0 31 12
A forMethod() 0 11 2
A forProperty() 0 10 2
A processAnonymous() 0 21 4
A process() 0 9 2
A getString() 0 25 5
A getTokens() 0 4 1
F parse() 0 215 57

How to fix   Complexity   

Complex Class

Complex classes like DocComment often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocComment, and based on these observations, apply Extract Interface, too.

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 function is_array;
29
use function str_replace;
30
use function strpos;
31
use function token_name;
32
use function var_dump;
33
use const T_ABSTRACT;
34
use const T_ARRAY;
35
use const T_CLASS;
36
use const T_INTERFACE;
37
use const T_PRIVATE;
38
use const T_PROTECTED;
39
use const T_PUBLIC;
40
use const T_STRING;
41
use const T_TRAIT;
42
43
class DocComment
44
{
45
	const TypeTrait = 'trait';
46
	const TypeClass = 'class';
47
	const TypeInterface = 'interface';
48
	private static $use = [];
49
	private static $useAliases = [];
50
	private static $namespaces = [];
51
	private static $types = [];
52
	private static $classNames = [];
53
	private static $classes = [];
54
	private static $methods = [];
55
	private static $fields = [];
56
	private static $parsedFiles = [];
57
58 1
	public static function clearCache()
59
	{
60 1
		self::$namespaces = [];
61 1
		self::$types = [];
62 1
		self::$classNames = [];
63 1
		self::$classes = [];
64 1
		self::$methods = [];
65 1
		self::$fields = [];
66 1
		self::$parsedFiles = [];
67 1
	}
68
69 51
	public function get($reflection)
70
	{
71 51
		if ($reflection instanceof ReflectionClass)
72
		{
73 44
			return $this->forClass($reflection);
74
		}
75
76 26
		if ($reflection instanceof ReflectionMethod)
77
		{
78 9
			return $this->forMethod($reflection);
79
		}
80
81 22
		if ($reflection instanceof ReflectionProperty)
82
		{
83 22
			return $this->forProperty($reflection);
84
		}
85
		throw new Exception("This method can only be used on reflection classes");
86
	}
87
88
	/**
89
	 * Get doc comment for file
90
	 * If file contains several classes, $className will be returned
91
	 * If file name matches class name, this class will be returned
92
	 * @param string $name
93
	 * @param string $className
94
	 * @return array
95
	 */
96 6
	public function forFile($name, $className = null)
97
	{
98 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...
99 6
		if (null !== $className)
100
		{
101
			$fqn = $className;
102
		}
103
		/**
104
		 * TODO Use some container here with ArrayAccess interface. Array-like access is *required* here.
105
		 */
106
		$result = [
107 6
			'namespace' => isset(self::$namespaces[$fqn]) ? self::$namespaces[$fqn] : [],
108 6
			'type' => isset(self::$types[$fqn]) ? self::$types[$fqn] : '',
109 6
			'use' => isset(self::$use[$fqn]) ? self::$use[$fqn] : [],
110 6
			'useAliases' => isset(self::$useAliases[$fqn]) ? self::$useAliases[$fqn] : [],
111 6
			'className' => isset(self::$classNames[$fqn]) ? self::$classNames[$fqn] : [],
112 6
			'class' => isset(self::$classes[$fqn]) ? self::$classes[$fqn] : '',
113 6
			'methods' => isset(self::$methods[$fqn]) ? self::$methods[$fqn] : [],
114 6
			'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
115
		];
116
117 6
		return $result;
118
	}
119
120 53
	public function forClass(Reflector $reflection)
121
	{
122 53
		assert($reflection instanceof ReflectionClass || $reflection instanceof ReflectionFile, sprintf("Expected `%s` or `%s`, got `%s`", ReflectionClass::class, ReflectionFile::class, get_class($reflection)));
123
124 53
		$fqn = $reflection->getName();
125 53
		$this->process($reflection->getFileName(), $fqn);
126 53
		if (ClassChecker::isAnonymous($reflection->name))
127
		{
128 1
			$info = new ReflectionClass($reflection->getName());
129 1
			$anonFqn = $reflection->getName();
130 1
			NameNormalizer::normalize($anonFqn);
131 1
			$this->processAnonymous($info, $anonFqn);
132
		}
133
		$result = [
134 53
			'namespace' => isset(self::$namespaces[$fqn]) ? self::$namespaces[$fqn] : [],
135 53
			'type' => isset(self::$types[$fqn]) ? self::$types[$fqn] : '',
136 53
			'use' => isset(self::$use[$fqn]) ? self::$use[$fqn] : [],
137 53
			'useAliases' => isset(self::$useAliases[$fqn]) ? self::$useAliases[$fqn] : [],
138 53
			'className' => isset(self::$classNames[$fqn]) ? self::$classNames[$fqn] : [],
139 53
			'class' => isset(self::$classes[$fqn]) ? self::$classes[$fqn] : '',
140 53
			'methods' => isset(self::$methods[$fqn]) ? self::$methods[$fqn] : [],
141 53
			'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
142
		];
143 53
		if (ClassChecker::isAnonymous($reflection->name))
144
		{
145 1
			assert(!empty($anonFqn));
146 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...
147 1
			$result['class'] = self::$classes[$anonFqn];
148
		}
149 53
		return $result;
150
	}
151
152 9
	public function forMethod(Reflector $reflection)
153
	{
154 9
		assert($reflection instanceof ReflectionMethod);
155 9
		$this->process($reflection->getDeclaringClass()->getFileName());
156
157
158
159 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...
160 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...
161 9
		return isset(self::$methods[$class][$method]) ? self::$methods[$class][$method] : false;
162
	}
163
164 22
	public function forProperty(Reflector $reflection)
165
	{
166 22
		assert($reflection instanceof ReflectionProperty);
167 22
		$this->process($reflection->getDeclaringClass()->getFileName());
168
169
170 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...
171 22
		$field = $reflection->getName();
172 22
		return isset(self::$fields[$class][$field]) ? self::$fields[$class][$field] : false;
173
	}
174
175 1
	private function processAnonymous(Reflector $reflection, $fqn)
176
	{
177 1
		if (!isset(self::$parsedFiles[$fqn]))
178
		{
179
			/* @var $reflection ReflectionAnnotatedClass */
180 1
			self::$classNames[$fqn] = $fqn;
181 1
			self::$classes[$fqn] = $reflection->getDocComment();
182 1
			self::$methods[$fqn] = [];
183 1
			self::$fields[$fqn] = [];
184 1
			foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
185
			{
186 1
				self::$methods[$fqn][$method->name] = $method->getDocComment();
187
			}
188 1
			foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property)
189
			{
190 1
				self::$fields[$fqn][$property->name] = $property->getDocComment();
191
			}
192 1
			self::$parsedFiles[$fqn] = $fqn;
193
		}
194 1
		return self::$parsedFiles[$fqn];
195
	}
196
197 64
	private function process($file, $fqn = false)
198
	{
199 64
		if (!isset(self::$parsedFiles[$file]))
200
		{
201 53
			$fqn = $this->parse($file, $fqn);
202 53
			self::$parsedFiles[$file] = $fqn;
203
		}
204 64
		return self::$parsedFiles[$file];
205
	}
206
207 53
	protected function parse($file, $fqn = false)
208
	{
209 53
		$use = [];
210 53
		$aliases = [];
211 53
		$namespace = '\\';
212 53
		$tokens = $this->getTokens($file);
213 53
		$class = false;
214 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...
215 53
		$comment = null;
216 53
		$max = count($tokens);
217 53
		$i = 0;
218 53
		while ($i < $max)
219
		{
220 53
			$token = $tokens[$i];
221 53
			if (is_array($token))
222
			{
223 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...
224
225
//				$tokenType = token_name($code);
226
//
227
//				// Seems doc comment before some entity
228
//				if(strpos($value, '@') !== false)
229
//				{
230
////					echo $value;
231
//				}
232
//
233
//				codecept_debug("$tokenType: " . str_replace("\n", '\n', str_replace("\t", '\t', $value)));
234
235
				switch ($code)
236
				{
237 53
					case T_DOC_COMMENT:
238 53
						$comment = $value;
239 53
						break;
240
241 53
					case T_NAMESPACE:
242 53
						$comment = false;
243 53
						$tokensCount = count($tokens);
244 53
						for ($j = $i + 1; $j < $tokensCount; $j++)
245
						{
246 53
							if ($tokens[$j][0] === T_STRING)
247
							{
248 53
								$namespace .= '\\' . $tokens[$j][1];
249
							}
250 53
							elseif ($tokens[$j] === '{' || $tokens[$j] === ';')
251
							{
252 53
								break;
253
							}
254
						}
255
256 53
						$namespace = preg_replace('~^\\\\+~', '', $namespace);
257 53
						break;
258 53
					case T_USE:
259
						// After class declaration, this should ignore `use` trait
260 49
						if ($class)
261
						{
262 4
							break;
263
						}
264 49
						$comment = false;
265 49
						$useNs = '';
266 49
						$tokensCount = count($tokens);
267 49
						$as = false;
268 49
						for ($j = $i + 1; $j < $tokensCount; $j++)
269
						{
270 49
							$tokenName = $tokens[$j][0];
271 49
							if ($tokenName === T_STRING && !$as)
272
							{
273 49
								$useNs .= '\\' . $tokens[$j][1];
274
							}
275 49
							if ($tokenName === T_STRING && $as)
276
							{
277 2
								$alias = $tokens[$j][1];
278 2
								break;
279
							}
280 49
							if ($tokenName === T_AS)
281
							{
282 2
								$as = true;
283
							}
284 49
							if ($tokens[$j] === '{' || $tokens[$j] === ';')
285
							{
286 49
								break;
287
							}
288
						}
289 49
						$use[] = preg_replace('~^\\\\+~', '', $useNs);
290 49
						if ($as)
291
						{
292 2
							assert(!empty($alias));
293 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...
294
						}
295 49
						break;
296 53
					case T_TRAIT:
297 53
					case T_CLASS:
298 53
					case T_INTERFACE:
299
						// Ignore magic constant `::class`
300 53
						if ($tokens[$i - 1][0] == T_DOUBLE_COLON)
301
						{
302 4
							break;
303
						}
304 53
						$class = $this->getString($tokens, $i, $max);
305 53
						if (!$fqn)
306
						{
307 4
							$fqn = sprintf('%s\%s', $namespace, $class);
308
						}
309 53
						if ($comment !== false)
310
						{
311 45
							self::$classes[$fqn] = $comment;
312 45
							$comment = false;
313
						}
314 53
						self::$namespaces[$fqn] = $namespace;
315 53
						self::$classNames[$fqn] = $class;
316 53
						self::$use[$fqn] = $use;
317 53
						self::$useAliases[$fqn] = $aliases;
318
319 53
						if($code === T_TRAIT)
320
						{
321 7
							self::$types[$fqn] = self::TypeTrait;
322
						}
323 49
						elseif($code === T_CLASS)
324
						{
325 47
							self::$types[$fqn] = self::TypeClass;
326
						}
327 2
						elseif($code === T_INTERFACE)
328
						{
329 2
							self::$types[$fqn] = self::TypeInterface;
330
						}
331
332 53
						break;
333
334 53
					case T_VARIABLE:
335 39
						if ($comment !== false && $class)
336
						{
337 33
							$field = substr($token[1], 1);
338 33
							self::$fields[$fqn][$field] = $comment;
339 33
							$comment = false;
340
						}
341 39
						break;
342
343 53
					case T_FUNCTION:
344 20
						if ($comment !== false && $class)
345
						{
346 12
							$function = $this->getString($tokens, $i, $max);
347 12
							self::$methods[$fqn][$function] = $comment;
348 12
							$comment = false;
349
						}
350
351 20
						break;
352
353
					// ignore
354 53
					case T_WHITESPACE:
355 53
					case T_PUBLIC:
356 53
					case T_PROTECTED:
357 53
					case T_PRIVATE:
358 53
					case T_ABSTRACT:
359 53
					case T_FINAL:
360 53
					case T_VAR:
361 53
						break;
362
363
					// Might be scalar typed property, eg:
364
					// `public string $title;`
365 53
					case T_STRING:
366 53
					case T_ARRAY:
367
						// NOTE: Maybe it will better to check ahead, ie if T_STRING, then check it next value is variable
368
369
						// Skip whitespace and check token before T_STRING and T_WHITESPACE
370 53
						if(isset($tokens[$i - 2]) && is_array($tokens[$i - 2]))
371
						{
372 53
							$prevType = $tokens[$i - 2][0];
373 53
							$prevVal = $tokens[$i - 2][1];
0 ignored issues
show
Unused Code introduced by
$prevVal 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...
374
							switch ($prevType)
375
							{
376 53
								case T_PUBLIC:
377 53
								case T_PROTECTED:
378 53
								case T_PRIVATE:
379 53
								case T_ABSTRACT:
380
									break 2;
381
							}
382
383
						}
384
						// Optional typed parameter, the `?` is present as a *string* token,
385
						// ie contains only `?` string, not an array
386
						// Skip whitespace and check token before T_STRING and T_WHITESPACE
387 53
						if(isset($tokens[$i - 3]) && is_array($tokens[$i - 3]))
388
						{
389 53
							$prevType = $tokens[$i - 3][0];
390 53
							$prevVal = $tokens[$i - 3][1];
0 ignored issues
show
Unused Code introduced by
$prevVal 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...
391
							switch ($prevType)
392
							{
393 53
								case T_PUBLIC:
394 53
								case T_PROTECTED:
395 53
								case T_PRIVATE:
396 53
								case T_ABSTRACT:
397
									break 2;
398
							}
399
400
						}
401 53
						break;
402
					default:
403 53
						$comment = false;
404 53
						break;
405
				}
406
			}
407
			else
408
			{
409
				switch ($token)
410
				{
411
					// Don't reset comment tag on `?` token, as it *probably* means optional val
412 53
					case '?':
413
						break;
414
					default:
415 53
						$comment = false;
416
				}
417
			}
418 53
			$i++;
419
		}
420 53
		return $fqn;
421
	}
422
423 53
	private function getString($tokens, &$i, $max)
424
	{
425
		do
426
		{
427
			/**
428
			 * TODO Workaround for problem described near T_CLASS token
429
			 */
430 53
			if (!isset($tokens[$i]))
431
			{
432
				$i++;
433
				continue;
434
			}
435 53
			$token = $tokens[$i];
436 53
			$i++;
437 53
			if (is_array($token))
438
			{
439 53
				if ($token[0] == T_STRING)
440
				{
441 53
					return $token[1];
442
				}
443
			}
444
		}
445 53
		while ($i <= $max);
446
		return false;
447
	}
448
449 53
	private function getTokens($file)
450
	{
451 53
		return token_get_all(file_get_contents($file));
452
	}
453
454
}
455