Completed
Branch master (486a28)
by Paweł
02:07
created

PhpFileParser::computeNestingParentTokens()   C

Complexity

Conditions 11
Paths 14

Size

Total Lines 45
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 11.6653

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 11
eloc 27
c 2
b 0
f 1
nc 14
nop 1
dl 0
loc 45
rs 5.2653
ccs 28
cts 34
cp 0.8235
crap 11.6653

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
 * Splits file source to tokens, provides ways to manipulate tokens list and output modified source.
4
 * Intended to help in code replacements on language syntax level.
5
 */
6
class PhpFileParser implements Iterator, ArrayAccess {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
7
8
	/**
9
	 * @var string original file name
10
	 */
11
	private $fileName = null;
12
13
	/**
14
	 * @var array
15
	 */
16
	private $tokens = null;
17
18
	/**
19
	 * @var string
20
	 */
21
	private $sha1hash = null;
22
23
	/**
24
	 * @param $fileName
25
	 * @throws CodeReview_IOException
26
	 * @throws Exception
27
	 */
28 12
	public function __construct($fileName) {
29 12
		$this->validateFilePath($fileName);
30 10
		$this->fileName = $fileName;
31
32 10
		$contents = file_get_contents($fileName);
33 10
		if ($contents === false) {
34
			throw new CodeReview_IOException("Error while fetching contents of file $fileName");
35
		}
36
37 10
		$this->sha1hash = sha1_file($fileName);
38 10
		if ($this->sha1hash === false) {
39
			throw new CodeReview_IOException("Error while computing SHA1 hash of file $fileName");
40
		}
41
42 10
		$this->tokens = token_get_all($contents);
43 10
		if (!is_array($this->tokens)) {
44
			throw new Exception("Failed to parse PHP contents of $fileName");
45
		}
46 10
		$this->computeNestingParentTokens();
47 10
	}
48
49
	/**
50
	 * Return fileds to serialize.
51
	 *
52
	 * @return array
53
	 */
54 6
	public function __sleep() {
55 6
		return array('fileName', 'sha1hash', 'tokens');
56
	}
57
58
	/**
59
	 * Verify class contents against original file to detect changes.
60
	 */
61 6
	public function __wakeup() {
62 6
		$this->validateFileContents();
63 2
	}
64
65
	/**
66
	 * Uses SHA1 hash to determine if file contents has changed since analysis.
67
	 *
68
	 * @return bool
69
	 * @throws CodeReview_IOException
70
	 * @throws LogicException
71
	 */
72 6
	protected function validateFileContents() {
73 6
		if (!$this->fileName) {
74 1
			throw new LogicException("Missing file's path. Looks like severe internal error.");
75
		}
76 5
		$this->validateFilePath($this->fileName);
77 4
		if (!$this->sha1hash) {
78 1
			throw new LogicException("Missing file's SHA1 hash. Looks like severe internal error.");
79
		}
80 3
		if ($this->sha1hash !== sha1_file($this->fileName)) {
81 1
			throw new CodeReview_IOException("The file on disk has changed and this " . get_class($this) . " class instance is no longer valid for use. Please create fresh instance.");
82
		}
83 2
		return true;
84
	}
85
86
	/**
87
	 * Checks if file exists and is readable.
88
	 *
89
	 * @param $fileName
90
	 * @return bool
91
	 * @throws CodeReview_IOException
92
	 */
93 12
	protected function validateFilePath($fileName) {
94 12
		if (!file_exists($fileName)) {
95 2
			throw new CodeReview_IOException("File $fileName does not exists");
96
		}
97 11
		if (!is_file($fileName)) {
98 1
			throw new CodeReview_IOException("$fileName must be a file");
99
		}
100 10
		if (!is_readable($fileName)) {
101
			throw new CodeReview_IOException("File $fileName is not readable");
102
		}
103 10
		return true;
104
	}
105
106
	/**
107
	 * Compute parents of the tokens to easily determine containing methods and classes.
108
	 *
109
	 * @param bool $debug
110
	 */
111 10
	private function computeNestingParentTokens($debug = false) {
112 10
		$nesting = 0;
113 10
		$parents = array();
114 10
		$lastParent = null;
115 10
		foreach ($this->tokens as $key => $token) {
116 10
			if (is_array($token)) {
117
				//add info about parent to array
118 10
				$parent = $parents ? $parents[count($parents)-1] : null;
119 10
				$this->tokens[$key][3] = $parent;
120 10
				$this->tokens[$key][4] = $nesting;
121
122
				//is current token possible parent in current level?
123 10
				if ($this->isEqualToAnyToken(array(T_CLASS, T_INTERFACE, T_FUNCTION), $key)) {
124 10
					$lastParent = $key + 2;
125 10
				} elseif ($this->isEqualToAnyToken(array(T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES), $key)) {
126 7
					$nesting++;
127 7
					array_push($parents, '');//just a placeholder
128 7
					if ($debug) {
129
						echo "$nesting\{\$\n";
130
					}
131 7
				}
132
//				elseif ($this->isEqualToToken(T_DO, $key)) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
133
//					$lastParent = $key;
134
//				} elseif ($this->isEqualToToken(T_WHILE, $key)) {
135
//					$lastParent = $key;
136
//				} elseif ($this->isEqualToToken(T_FOR, $key)) {
137
//					$lastParent = $key;
138
//				}
139 10
			} else {
140 10
				if ($token == '{') {
141 10
					$nesting++;
142 10
					if ($debug) {
143
						echo "$nesting{\n";
144
					}
145 10
					array_push($parents, $lastParent);
146 10
				} elseif ($token == '}') {
147 10
					if ($debug) {
148
						echo "$nesting}\n";
149
					}
150 10
					$nesting--;
151 10
					array_pop($parents);
152 10
				}
153
			}
154 10
		}
155 10
	}
156
157
	/**
158
	 * @param array $tokens
159
	 * @param int   $offset
160
	 * @return bool
161
	 */
162 10
	public function isEqualToAnyToken($tokens, $offset = null) {
163 10
		foreach ($tokens as $token) {
164 10
			if ($this->isEqualToToken($token, $offset)) {
165 10
				return true;
166
			}
167 10
		}
168 10
		return false;
169
	}
170
171
	/**
172
	 * @param $token string|int individual token identifier or predefined T_* constant value for complex tokens
173
	 * @param int $offset optional offset when checking other than current
174
	 * @return bool
175
	 */
176 10
	public function isEqualToToken($token, $offset = null) {
177 10
		if ($offset === null) {
178
			$offset = $this->key();
179
		}
180 10
		if (!isset($this[$offset])) {
181
			return false;
182
		}
183 10
		$val = $this[$offset];
184 10
		if (is_string($token)) {
185
			//assume one char token that gets passed directly as string
186
			return $val == $token;
187
		}
188 10
		return is_array($val) && $val[0] == $token;
189
	}
190
191
	/**
192
	 * @param int $offset optional offset when checking other than current
193
	 * @return mixed
194
	 */
195
	public function getDefiningFunctionName($offset = null) {
196
		if ($offset === null) {
197
			$offset = $this->key();
198
		}
199
		$parentKey = $this->tokens[$offset][3];
200
		while ($parentKey !== null && !$this->isEqualToToken(T_FUNCTION, $parentKey - 2)) {
201
			$parentKey = $this->tokens[$parentKey][3];
202
		}
203
		if ($parentKey !== null) {
204
			$class = $this->getDefiningClassName($parentKey);
205
			if ($class) {
206
				return $class . '::' . $this->tokens[$parentKey][1];
207
			} else {
208
				return $this->tokens[$parentKey][1];
209
			}
210
		}
211
		return null;
212
	}
213
214
	/**
215
	 * @param int $offset optional offset when checking other than current
216
	 * @return mixed
217
	 */
218
	public function getDefiningClassName($offset = null) {
219
		if ($offset === null) {
220
			$offset = $this->key();
221
		}
222
		$parentKey = $this->tokens[$offset][3];
223
		while ($parentKey !== null && !$this->isEqualToToken(T_CLASS, $parentKey - 2)) {
224
			$parentKey = $this->tokens[$parentKey][3];
225
		}
226
		if ($parentKey !== null) {
227
			return $this->tokens[$parentKey][1];
228
		}
229
		return null;
230
	}
231
232
	/**
233
	 * @param string $fileName
234
	 * @return bool|string
235
	 * @throws CodeReview_IOException
236
	 */
237 1
	public function exportPhp($fileName = null) {
238 1
		$source = '';
239 1
		$data = $this->tokens;
240 1
		reset($data);
241 1
		foreach ($data as $val) {
242 1
			if (is_array($val)) {
243 1
				$source .= $val[1];
244 1
			} else {
245 1
				$source .= $val;
246
			}
247 1
		}
248
249 1
		if ($fileName !== null) {
250
			if (!is_writable($fileName)) {
251
				throw new CodeReview_IOException("$fileName must be writable");
252
			}
253
			return file_put_contents($fileName, $source) !== false;
254
		} else {
255 1
			return $source;
256
		}
257
	}
258
259
	/**
260
	 * (PHP 5 &gt;= 5.0.0)<br/>
261
	 * Return the current element
262
	 *
263
	 * @link http://php.net/manual/en/iterator.current.php
264
	 * @return mixed Can return any type.
265
	 */
266 3
	public function current() {
267 3
		return current($this->tokens);
268
	}
269
270
	/**
271
	 * (PHP 5 &gt;= 5.0.0)<br/>
272
	 * Move forward to next element
273
	 * @link http://php.net/manual/en/iterator.next.php
274
	 * @return void Any returned value is ignored.
275
	 */
276 3
	public function next() {
277 3
		next($this->tokens);
278 3
	}
279
280
	/**
281
	 * (PHP 5 &gt;= 5.0.0)<br/>
282
	 * Return the key of the current element
283
	 * @link http://php.net/manual/en/iterator.key.php
284
	 * @return mixed scalar on success, or null on failure.
285
	 */
286 3
	public function key() {
287 3
		return key($this->tokens);
288
	}
289
290
	/**
291
	 * (PHP 5 &gt;= 5.0.0)<br/>
292
	 * Checks if current position is valid
293
	 * @link http://php.net/manual/en/iterator.valid.php
294
	 * @return boolean The return value will be casted to boolean and then evaluated.
295
	 *       Returns true on success or false on failure.
296
	 */
297 3
	public function valid() {
298 3
		$key = key($this->tokens);
299 3
		$var = ($key !== null && $key !== false);
300 3
		return $var;
301
	}
302
303
	/**
304
	 * (PHP 5 &gt;= 5.0.0)<br/>
305
	 * Rewind the Iterator to the first element
306
	 * @link http://php.net/manual/en/iterator.rewind.php
307
	 * @return void Any returned value is ignored.
308
	 */
309 3
	public function rewind() {
310 3
		reset($this->tokens);
311 3
	}
312
313
	/**
314
	 * (PHP 5 &gt;= 5.0.0)<br/>
315
	 * Whether a offset exists
316
	 * @link http://php.net/manual/en/arrayaccess.offsetexists.php
317
	 * @param mixed $offset <p>
318
	 *                      An offset to check for.
319
	 * </p>
320
	 * @return boolean true on success or false on failure.
321
	 * </p>
322
	 * <p>
323
	 *       The return value will be casted to boolean if non-boolean was returned.
324
	 */
325 10
	public function offsetExists($offset) {
326 10
		return isset($this->tokens[$offset]);
327
	}
328
329
	/**
330
	 * (PHP 5 &gt;= 5.0.0)<br/>
331
	 * Offset to retrieve
332
	 * @link http://php.net/manual/en/arrayaccess.offsetget.php
333
	 * @param mixed $offset <p>
334
	 *                      The offset to retrieve.
335
	 * </p>
336
	 * @return mixed Can return all value types.
337
	 */
338 10
	public function offsetGet($offset) {
339 10
		return $this->tokens[$offset];
340
	}
341
342
	/**
343
	 * (PHP 5 &gt;= 5.0.0)<br/>
344
	 * Offset to set
345
	 * @link http://php.net/manual/en/arrayaccess.offsetset.php
346
	 * @param mixed $offset <p>
347
	 *                      The offset to assign the value to.
348
	 * </p>
349
	 * @param mixed $value  <p>
350
	 *                      The value to set.
351
	 * </p>
352
	 * @return void
353
	 */
354
	public function offsetSet($offset, $value) {
355
		$this->tokens[$offset] = $value;
356
	}
357
358
	/**
359
	 * (PHP 5 &gt;= 5.0.0)<br/>
360
	 * Offset to unset
361
	 * @link http://php.net/manual/en/arrayaccess.offsetunset.php
362
	 * @param mixed $offset <p>
363
	 *                      The offset to unset.
364
	 * </p>
365
	 * @return void
366
	 */
367
	public function offsetUnset($offset) {
368
		unset($this->tokens[$offset]);
369
	}
370
}