Completed
Push — master ( 7d4d47...4dc2a1 )
by Peter
08:01
created

DocComment   D

Complexity

Total Complexity 82

Size/Duplication

Total Lines 333
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 85.96%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 82
lcom 1
cbo 3
dl 0
loc 333
ccs 196
cts 228
cp 0.8596
rs 4.8717
c 2
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A clearCache() 0 9 1
A getTokens() 0 4 1
A forMethod() 0 10 2
A forProperty() 0 9 2
A get() 0 15 4
D forFile() 0 22 9
F forClass() 0 31 11
A processAnonymous() 0 21 4
A process() 0 9 2
D parse() 0 147 41
B getString() 0 25 5

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 http://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 Maslosoft\Addendum\Utilities\ClassChecker;
18
use Maslosoft\Addendum\Utilities\NameNormalizer;
19
use ReflectionClass;
20
use ReflectionMethod;
21
use ReflectionProperty;
22
use Reflector;
23
24
class DocComment
25
{
26
27
	private static $use = [];
28
	private static $useAliases = [];
29
	private static $namespaces = [];
30
	private static $classNames = [];
31
	private static $classes = [];
32
	private static $methods = [];
33
	private static $fields = [];
34
	private static $parsedFiles = [];
35
36 1
	public static function clearCache()
37
	{
38 1
		self::$namespaces = [];
39 1
		self::$classNames = [];
40 1
		self::$classes = [];
41 1
		self::$methods = [];
42 1
		self::$fields = [];
43 1
		self::$parsedFiles = [];
44 1
	}
45
46 46
	public function get($reflection)
47
	{
48 46
		if ($reflection instanceof ReflectionClass)
49 46
		{
50 39
			return $this->forClass($reflection);
51
		}
52 25
		elseif ($reflection instanceof ReflectionMethod)
53
		{
54 8
			return $this->forMethod($reflection);
55
		}
56 21
		elseif ($reflection instanceof ReflectionProperty)
57
		{
58 21
			return $this->forProperty($reflection);
59
		}
60
	}
61
62
	/**
63
	 * Get doc comment for file
64
	 * If file contains several classes, $className will be returned
65
	 * If file name matches class name, this class will be returned
66
	 * @param string $name
67
	 * @param string $className
68
	 */
69 2
	public function forFile($name, $className = null)
70
	{
71 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...
72 2
		if (null !== $className)
73 2
		{
74
			$fqn = $className;
75
		}
76
		/**
77
		 * TODO Use some container here with ArrayAccess interface. Array-like access is *required* here.
78
		 */
79
		$result = [
80 2
			'namespace' => isset(self::$namespaces[$fqn]) ? self::$namespaces[$fqn] : [],
81 2
			'use' => isset(self::$use[$fqn]) ? self::$use[$fqn] : [],
82 2
			'useAliases' => isset(self::$useAliases[$fqn]) ? self::$useAliases[$fqn] : [],
83 2
			'className' => isset(self::$classNames[$fqn]) ? self::$classNames[$fqn] : [],
84 2
			'class' => isset(self::$classes[$fqn]) ? self::$classes[$fqn] : '',
85 2
			'methods' => isset(self::$methods[$fqn]) ? self::$methods[$fqn] : [],
86 2
			'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
87 2
		];
88
89 2
		return $result;
90
	}
91
92 48
	public function forClass(Reflector $reflection)
93
	{
94 48
		if (ClassChecker::isAnonymous($reflection->name))
0 ignored issues
show
Bug introduced by
Accessing name on the interface Reflector suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
95 48
		{
96
			echo '';
97
		}
98 48
		$fqn = $reflection->getName();
99 48
		$this->process($reflection->getFileName(), $fqn);
100 48
		if (ClassChecker::isAnonymous($reflection->name))
0 ignored issues
show
Bug introduced by
Accessing name on the interface Reflector suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
101 48
		{
102
			$info = new \ReflectionClass($reflection->getName());
103
			$anonFqn = $reflection->getName();
104
			NameNormalizer::normalize($anonFqn);
105
			$this->processAnonymous($info, $anonFqn);
106
		}
107
		$result = [
108 48
			'namespace' => isset(self::$namespaces[$fqn]) ? self::$namespaces[$fqn] : [],
109 48
			'use' => isset(self::$use[$fqn]) ? self::$use[$fqn] : [],
110 48
			'useAliases' => isset(self::$useAliases[$fqn]) ? self::$useAliases[$fqn] : [],
111 48
			'className' => isset(self::$classNames[$fqn]) ? self::$classNames[$fqn] : [],
112 48
			'class' => isset(self::$classes[$fqn]) ? self::$classes[$fqn] : '',
113 48
			'methods' => isset(self::$methods[$fqn]) ? self::$methods[$fqn] : [],
114 48
			'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
115 48
		];
116 48
		if (ClassChecker::isAnonymous($reflection->name))
0 ignored issues
show
Bug introduced by
Accessing name on the interface Reflector suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
117 48
		{
118
			$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...
119
			$result['class'] = self::$classes[$anonFqn];
120
		}
121 48
		return $result;
122
	}
123
124 8
	public function forMethod(Reflector $reflection)
125
	{
126 8
		$this->process($reflection->getDeclaringClass()->getFileName());
127
128
129
130 8
		$class = $reflection->getDeclaringClass()->getName();
131 8
		$method = $reflection->getName();
132 8
		return isset(self::$methods[$class][$method]) ? self::$methods[$class][$method] : false;
133
	}
134
135 21
	public function forProperty(Reflector $reflection)
136
	{
137 21
		$this->process($reflection->getDeclaringClass()->getFileName());
138
139
140 21
		$class = $reflection->getDeclaringClass()->getName();
141 21
		$field = $reflection->getName();
142 21
		return isset(self::$fields[$class][$field]) ? self::$fields[$class][$field] : false;
143
	}
144
145
	private function processAnonymous(Reflector $reflection, $fqn)
146
	{
147
		if (!isset(self::$parsedFiles[$fqn]))
148
		{
149
			/* @var $reflection \Maslosoft\Addendum\Reflection\ReflectionAnnotatedClass */
150
			self::$classNames[$fqn] = $fqn;
151
			self::$classes[$fqn] = $reflection->getDocComment();
152
			self::$methods[$fqn] = [];
153
			self::$fields[$fqn] = [];
154
			foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method)
155
			{
156
				self::$methods[$fqn][$method->name] = $method->getDocComment();
157
			}
158
			foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property)
159
			{
160
				self::$fields[$fqn][$property->name] = $property->getDocComment();
161
			}
162
			self::$parsedFiles[$fqn] = $fqn;
163
		}
164
		return self::$parsedFiles[$fqn];
165
	}
166
167 55
	private function process($file, $fqn = false)
168
	{
169 55
		if (!isset(self::$parsedFiles[$file]))
170 55
		{
171 46
			$fqn = $this->parse($file, $fqn);
172 46
			self::$parsedFiles[$file] = $fqn;
173 46
		}
174 55
		return self::$parsedFiles[$file];
175
	}
176
177 46
	protected function parse($file, $fqn = false)
178
	{
179 46
		$use = [];
180 46
		$aliases = [];
181 46
		$namespace = '\\';
182 46
		$tokens = $this->getTokens($file);
183 46
		$class = false;
184 46
		$comment = null;
185 46
		$max = count($tokens);
186 46
		$i = 0;
187 46
		while ($i < $max)
188
		{
189 46
			$token = $tokens[$i];
190 46
			if (is_array($token))
191 46
			{
192 46
				list($code, $value) = $token;
193
194
				switch ($code)
195
				{
196 46
					case T_DOC_COMMENT:
197 46
						$comment = $value;
198 46
						break;
199
200 46
					case T_NAMESPACE:
201 46
						$comment = false;
202 46
						$tokensCount = count($tokens);
203 46
						for ($j = $i + 1; $j < $tokensCount; $j++)
204
						{
205 46
							if ($tokens[$j][0] === T_STRING)
206 46
							{
207 46
								$namespace .= '\\' . $tokens[$j][1];
208 46
							}
209 46
							elseif ($tokens[$j] === '{' || $tokens[$j] === ';')
210
							{
211 46
								break;
212
							}
213 46
						}
214
215 46
						$namespace = preg_replace('~^\\\\+~', '', $namespace);
216 46
						break;
217 46
					case T_USE:
218
						// After class declaration, this should ignore `use` trait
219
						if ($class)
220 43
						{
221 4
							break;
222
						}
223 43
						$comment = false;
224 43
						$useNs = '';
225 43
						$tokensCount = count($tokens);
226 43
						$as = false;
227 43
						for ($j = $i + 1; $j < $tokensCount; $j++)
228
						{
229 43
							$tokenName = $tokens[$j][0];
230 43
							if (isset($tokens[$j][1]) && $tokens[$j][1] == 'IMatcher')
231 43
							{
232 1
								echo 's';
233 1
							}
234 43
							if ($tokenName === T_STRING && !$as)
235 43
							{
236 43
								$useNs .= '\\' . $tokens[$j][1];
237 43
							}
238 43
							if ($tokenName === T_STRING && $as)
239 43
							{
240 2
								$alias = $tokens[$j][1];
241 2
								break;
242
							}
243 43
							if ($tokenName === T_AS)
244 43
							{
245 2
								$as = true;
246 2
							}
247 43
							if ($tokens[$j] === '{' || $tokens[$j] === ';')
248 43
							{
249 43
								break;
250
							}
251 43
						}
252 43
						$use[] = preg_replace('~^\\\\+~', '', $useNs);
253
						if ($as)
254 43
						{
255 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...
256 2
						}
257 43
						break;
258 46
					case T_TRAIT:
259 46
					case T_CLASS:
260 46
					case T_INTERFACE:
261
						// Ignore magic constant `::class`
262 46
						if ($tokens[$i - 1][0] == T_DOUBLE_COLON)
263 46
						{
264 4
							break;
265
						}
266 46
						$class = $this->getString($tokens, $i, $max);
267 46
						if (!$fqn)
268 46
						{
269 1
							$fqn = sprintf('%s\%s', $namespace, $class);
270 1
						}
271 46
						if ($comment !== false)
272 46
						{
273 38
							self::$classes[$fqn] = $comment;
274 38
							$comment = false;
275 38
						}
276 46
						self::$namespaces[$fqn] = $namespace;
277 46
						self::$classNames[$fqn] = $class;
278 46
						self::$use[$fqn] = $use;
279 46
						self::$useAliases[$fqn] = $aliases;
280 46
						break;
281
282 46
					case T_VARIABLE:
283 37
						if ($comment !== false && $class)
284 37
						{
285 31
							$field = substr($token[1], 1);
286 31
							self::$fields[$fqn][$field] = $comment;
287 31
							$comment = false;
288 31
						}
289 37
						break;
290
291 46
					case T_FUNCTION:
292 19
						if ($comment !== false && $class)
293 19
						{
294 11
							$function = $this->getString($tokens, $i, $max);
295 11
							self::$methods[$fqn][$function] = $comment;
296 11
							$comment = false;
297 11
						}
298
299 19
						break;
300
301
					// ignore
302 46
					case T_WHITESPACE:
303 46
					case T_PUBLIC:
304 46
					case T_PROTECTED:
305 46
					case T_PRIVATE:
306 46
					case T_ABSTRACT:
307 46
					case T_FINAL:
308 46
					case T_VAR:
309 46
						break;
310
311 46
					default:
312 46
						$comment = false;
313 46
						break;
314 46
				}
315 46
			}
316
			else
317
			{
318 46
				$comment = false;
319
			}
320 46
			$i++;
321 46
		}
322 46
		return $fqn;
323
	}
324
325 46
	private function getString($tokens, &$i, $max)
326
	{
327
		do
328
		{
329
			/**
330
			 * TODO Workaround for problem desribed near T_CLASS token
331
			 */
332 46
			if (!isset($tokens[$i]))
333 46
			{
334
				$i++;
335
				continue;
336
			}
337 46
			$token = $tokens[$i];
338 46
			$i++;
339 46
			if (is_array($token))
340 46
			{
341 46
				if ($token[0] == T_STRING)
342 46
				{
343 46
					return $token[1];
344
				}
345 46
			}
346
		}
347 46
		while ($i <= $max);
348
		return false;
349
	}
350
351 46
	private function getTokens($file)
352
	{
353 46
		return token_get_all(file_get_contents($file));
354
	}
355
356
}
357