Completed
Push — master ( 7d8097...8a93d3 )
by Peter
06:00 queued 01: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 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 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 47
	public function get($reflection)
47
	{
48 47
		if ($reflection instanceof ReflectionClass)
49 47
		{
50 40
			return $this->forClass($reflection);
51
		}
52 26
		elseif ($reflection instanceof ReflectionMethod)
53
		{
54 8
			return $this->forMethod($reflection);
55
		}
56 22
		elseif ($reflection instanceof ReflectionProperty)
57
		{
58 22
			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 49
	public function forClass(Reflector $reflection)
93
	{
94 49
		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 49
		{
96
			echo '';
97
		}
98 49
		$fqn = $reflection->getName();
99 49
		$this->process($reflection->getFileName(), $fqn);
100 49
		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 49
		{
102
			$info = new \ReflectionClass($reflection->getName());
103
			$anonFqn = $reflection->getName();
104
			NameNormalizer::normalize($anonFqn);
105
			$this->processAnonymous($info, $anonFqn);
106
		}
107
		$result = [
108 49
			'namespace' => isset(self::$namespaces[$fqn]) ? self::$namespaces[$fqn] : [],
109 49
			'use' => isset(self::$use[$fqn]) ? self::$use[$fqn] : [],
110 49
			'useAliases' => isset(self::$useAliases[$fqn]) ? self::$useAliases[$fqn] : [],
111 49
			'className' => isset(self::$classNames[$fqn]) ? self::$classNames[$fqn] : [],
112 49
			'class' => isset(self::$classes[$fqn]) ? self::$classes[$fqn] : '',
113 49
			'methods' => isset(self::$methods[$fqn]) ? self::$methods[$fqn] : [],
114 49
			'fields' => isset(self::$fields[$fqn]) ? self::$fields[$fqn] : []
115 49
		];
116 49
		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 49
		{
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 49
		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 22
	public function forProperty(Reflector $reflection)
136
	{
137 22
		$this->process($reflection->getDeclaringClass()->getFileName());
138
139
140 22
		$class = $reflection->getDeclaringClass()->getName();
141 22
		$field = $reflection->getName();
142 22
		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 56
	private function process($file, $fqn = false)
168
	{
169 56
		if (!isset(self::$parsedFiles[$file]))
170 56
		{
171 47
			$fqn = $this->parse($file, $fqn);
172 47
			self::$parsedFiles[$file] = $fqn;
173 47
		}
174 56
		return self::$parsedFiles[$file];
175
	}
176
177 47
	protected function parse($file, $fqn = false)
178
	{
179 47
		$use = [];
180 47
		$aliases = [];
181 47
		$namespace = '\\';
182 47
		$tokens = $this->getTokens($file);
183 47
		$class = false;
184 47
		$comment = null;
185 47
		$max = count($tokens);
186 47
		$i = 0;
187 47
		while ($i < $max)
188
		{
189 47
			$token = $tokens[$i];
190 47
			if (is_array($token))
191 47
			{
192 47
				list($code, $value) = $token;
193
194
				switch ($code)
195
				{
196 47
					case T_DOC_COMMENT:
197 47
						$comment = $value;
198 47
						break;
199
200 47
					case T_NAMESPACE:
201 47
						$comment = false;
202 47
						$tokensCount = count($tokens);
203 47
						for ($j = $i + 1; $j < $tokensCount; $j++)
204
						{
205 47
							if ($tokens[$j][0] === T_STRING)
206 47
							{
207 47
								$namespace .= '\\' . $tokens[$j][1];
208 47
							}
209 47
							elseif ($tokens[$j] === '{' || $tokens[$j] === ';')
210
							{
211 47
								break;
212
							}
213 47
						}
214
215 47
						$namespace = preg_replace('~^\\\\+~', '', $namespace);
216 47
						break;
217 47
					case T_USE:
218
						// After class declaration, this should ignore `use` trait
219
						if ($class)
220 44
						{
221 4
							break;
222
						}
223 44
						$comment = false;
224 44
						$useNs = '';
225 44
						$tokensCount = count($tokens);
226 44
						$as = false;
227 44
						for ($j = $i + 1; $j < $tokensCount; $j++)
228
						{
229 44
							$tokenName = $tokens[$j][0];
230 44
							if (isset($tokens[$j][1]) && $tokens[$j][1] == 'IMatcher')
231 44
							{
232 1
								echo 's';
233 1
							}
234 44
							if ($tokenName === T_STRING && !$as)
235 44
							{
236 44
								$useNs .= '\\' . $tokens[$j][1];
237 44
							}
238 44
							if ($tokenName === T_STRING && $as)
239 44
							{
240 2
								$alias = $tokens[$j][1];
241 2
								break;
242
							}
243 44
							if ($tokenName === T_AS)
244 44
							{
245 2
								$as = true;
246 2
							}
247 44
							if ($tokens[$j] === '{' || $tokens[$j] === ';')
248 44
							{
249 44
								break;
250
							}
251 44
						}
252 44
						$use[] = preg_replace('~^\\\\+~', '', $useNs);
253
						if ($as)
254 44
						{
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 44
						break;
258 47
					case T_TRAIT:
259 47
					case T_CLASS:
260 47
					case T_INTERFACE:
261
						// Ignore magic constant `::class`
262 47
						if ($tokens[$i - 1][0] == T_DOUBLE_COLON)
263 47
						{
264 4
							break;
265
						}
266 47
						$class = $this->getString($tokens, $i, $max);
267 47
						if (!$fqn)
268 47
						{
269 1
							$fqn = sprintf('%s\%s', $namespace, $class);
270 1
						}
271 47
						if ($comment !== false)
272 47
						{
273 39
							self::$classes[$fqn] = $comment;
274 39
							$comment = false;
275 39
						}
276 47
						self::$namespaces[$fqn] = $namespace;
277 47
						self::$classNames[$fqn] = $class;
278 47
						self::$use[$fqn] = $use;
279 47
						self::$useAliases[$fqn] = $aliases;
280 47
						break;
281
282 47
					case T_VARIABLE:
283 38
						if ($comment !== false && $class)
284 38
						{
285 32
							$field = substr($token[1], 1);
286 32
							self::$fields[$fqn][$field] = $comment;
287 32
							$comment = false;
288 32
						}
289 38
						break;
290
291 47
					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 47
					case T_WHITESPACE:
303 47
					case T_PUBLIC:
304 47
					case T_PROTECTED:
305 47
					case T_PRIVATE:
306 47
					case T_ABSTRACT:
307 47
					case T_FINAL:
308 47
					case T_VAR:
309 47
						break;
310
311 47
					default:
312 47
						$comment = false;
313 47
						break;
314 47
				}
315 47
			}
316
			else
317
			{
318 47
				$comment = false;
319
			}
320 47
			$i++;
321 47
		}
322 47
		return $fqn;
323
	}
324
325 47
	private function getString($tokens, &$i, $max)
326
	{
327
		do
328
		{
329
			/**
330
			 * TODO Workaround for problem desribed near T_CLASS token
331
			 */
332 47
			if (!isset($tokens[$i]))
333 47
			{
334
				$i++;
335
				continue;
336
			}
337 47
			$token = $tokens[$i];
338 47
			$i++;
339 47
			if (is_array($token))
340 47
			{
341 47
				if ($token[0] == T_STRING)
342 47
				{
343 47
					return $token[1];
344
				}
345 47
			}
346
		}
347 47
		while ($i <= $max);
348
		return false;
349
	}
350
351 47
	private function getTokens($file)
352
	{
353 47
		return token_get_all(file_get_contents($file));
354
	}
355
356
}
357