Completed
Push — master ( 8a93d3...c82149 )
by Peter
04:18
created

DocComment::clearCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

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