Completed
Push — master ( 3c32f9...42fff5 )
by Paweł
03:24
created

PhpFileParser::computeNestingParentTokens()   C

Complexity

Conditions 11
Paths 14

Size

Total Lines 38
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 11.6653

Importance

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

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
namespace CodeReview;
3
4
/**
5
 * Splits file source to tokens, provides ways to manipulate tokens list and output modified source.
6
 * Intended to help in code replacements on language syntax level.
7
 */
8
class PhpFileParser implements \Iterator, \ArrayAccess {
9
10
	/**
11
	 * @var string original file name
12
	 */
13
	private $fileName = null;
14
15
	/**
16
	 * @var array
17
	 */
18
	private $tokens = null;
19
20
	/**
21
	 * @var string
22
	 */
23
	private $sha1hash = null;
24
25
	/**
26
	 * @param $fileName
27
	 * @throws \CodeReview\IOException
28
	 * @throws Exception
29
	 */
30 13
	public function __construct($fileName) {
31 13
		$this->validateFilePath($fileName);
32 11
		$this->fileName = $fileName;
33
34 11
		$contents = file_get_contents($fileName);
35 11
		if ($contents === false) {
36
			throw new IOException("Error while fetching contents of file $fileName");
37
		}
38
39 11
		$this->sha1hash = sha1_file($fileName);
40 11
		if ($this->sha1hash === false) {
41
			throw new IOException("Error while computing SHA1 hash of file $fileName");
42
		}
43
44 11
		$this->tokens = token_get_all($contents);
45 11
		if (!is_array($this->tokens)) {
46
			throw new Exception("Failed to parse PHP contents of $fileName");
47
		}
48 11
		$this->computeNestingParentTokens();
49 11
	}
50
51
	/**
52
	 * Return fileds to serialize.
53
	 *
54
	 * @return array
55
	 */
56 6
	public function __sleep() {
57 6
		return array('fileName', 'sha1hash', 'tokens');
58
	}
59
60
	/**
61
	 * Verify class contents against original file to detect changes.
62
	 */
63 6
	public function __wakeup() {
64 6
		$this->validateFileContents();
65 2
	}
66
67
	/**
68
	 * Uses SHA1 hash to determine if file contents has changed since analysis.
69
	 *
70
	 * @return bool
71
	 * @throws \CodeReview\IOException
72
	 * @throws LogicException
73
	 */
74 6
	protected function validateFileContents() {
75 6
		if (!$this->fileName) {
76 1
			throw new \LogicException("Missing file's path. Looks like severe internal error.");
77
		}
78 5
		$this->validateFilePath($this->fileName);
79 4
		if (!$this->sha1hash) {
80 1
			throw new \LogicException("Missing file's SHA1 hash. Looks like severe internal error.");
81
		}
82 3
		if ($this->sha1hash !== sha1_file($this->fileName)) {
83 1
			throw new IOException("The file on disk has changed and this " . get_class($this) . " class instance is no longer valid for use. Please create fresh instance.");
84
		}
85 2
		return true;
86
	}
87
88
	/**
89
	 * Checks if file exists and is readable.
90
	 *
91
	 * @param $fileName
92
	 * @return bool
93
	 * @throws \CodeReview\IOException
94
	 */
95 13
	protected function validateFilePath($fileName) {
96 13
		if (!file_exists($fileName)) {
97 2
			throw new IOException("File $fileName does not exists");
98
		}
99 12
		if (!is_file($fileName)) {
100 1
			throw new IOException("$fileName must be a file");
101
		}
102 11
		if (!is_readable($fileName)) {
103
			throw new IOException("File $fileName is not readable");
104
		}
105 11
		return true;
106
	}
107
108
	/**
109
	 * Compute parents of the tokens to easily determine containing methods and classes.
110
	 *
111
	 * @param bool $debug
112
	 */
113 11
	private function computeNestingParentTokens($debug = false) {
114 11
		$nesting = 0;
115 11
		$parents = array();
116 11
		$lastParent = null;
117 11
		foreach ($this->tokens as $key => $token) {
118 11
			if (is_array($token)) {
119
				//add info about parent to array
120 11
				$parent = $parents ? $parents[count($parents)-1] : null;
121 11
				$this->tokens[$key][3] = $parent;
122 11
				$this->tokens[$key][4] = $nesting;
123
124
				//is current token possible parent in current level?
125 11
				if ($this->isEqualToAnyToken(array(T_CLASS, T_INTERFACE, T_FUNCTION), $key)) {
126 11
					$lastParent = $key + 2;
127 11
				} elseif ($this->isEqualToAnyToken(array(T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES), $key)) {
128 7
					$nesting++;
129 7
					array_push($parents, '');//just a placeholder
130 7
					if ($debug) {
131
						echo "$nesting\{\$\n";
132
					}
133 7
				}
134 11
			} else {
135 11
				if ($token == '{') {
136 11
					$nesting++;
137 11
					if ($debug) {
138
						echo "$nesting{\n";
139
					}
140 11
					array_push($parents, $lastParent);
141 11
				} elseif ($token == '}') {
142 11
					if ($debug) {
143
						echo "$nesting}\n";
144
					}
145 11
					$nesting--;
146 11
					array_pop($parents);
147 11
				}
148
			}
149 11
		}
150 11
	}
151
152
	/**
153
	 * @param array $tokens
154
	 * @param int   $offset
155
	 * @return bool
156
	 */
157 11
	public function isEqualToAnyToken($tokens, $offset = null) {
158 11
		foreach ($tokens as $token) {
159 11
			if ($this->isEqualToToken($token, $offset)) {
160 11
				return true;
161
			}
162 11
		}
163 11
		return false;
164
	}
165
166
	/**
167
	 * @param $token string|int individual token identifier or predefined T_* constant value for complex tokens
168
	 * @param int $offset optional offset when checking other than current
169
	 * @return bool
170
	 */
171 11
	public function isEqualToToken($token, $offset = null) {
172 11
		if ($offset === null) {
173
			$offset = $this->key();
174
		}
175 11
		if (!isset($this[$offset])) {
176
			return false;
177
		}
178 11
		$val = $this[$offset];
179 11
		if (is_string($token)) {
180
			//assume one char token that gets passed directly as string
181
			return $val == $token;
182
		}
183 11
		return is_array($val) && $val[0] == $token;
184
	}
185
186
	/**
187
	 * @param int $offset optional offset when checking other than current
188
	 * @return mixed
189
	 */
190
	public function getDefiningFunctionName($offset = null) {
191
		if ($offset === null) {
192
			$offset = $this->key();
193
		}
194
		$parentKey = $this->tokens[$offset][3];
195
		while ($parentKey !== null && !$this->isEqualToToken(T_FUNCTION, $parentKey - 2)) {
196
			$parentKey = $this->tokens[$parentKey][3];
197
		}
198
		if ($parentKey !== null) {
199
			$class = $this->getDefiningClassName($parentKey);
200
			if ($class) {
201
				return $class . '::' . $this->tokens[$parentKey][1];
202
			} else {
203
				return $this->tokens[$parentKey][1];
204
			}
205
		}
206
		return null;
207
	}
208
209
	/**
210
	 * @param int $offset optional offset when checking other than current
211
	 * @return mixed
212
	 */
213
	public function getDefiningClassName($offset = null) {
214
		if ($offset === null) {
215
			$offset = $this->key();
216
		}
217
		$parentKey = $this->tokens[$offset][3];
218
		while ($parentKey !== null && !$this->isEqualToToken(T_CLASS, $parentKey - 2)) {
219
			$parentKey = $this->tokens[$parentKey][3];
220
		}
221
		if ($parentKey !== null) {
222
			return $this->tokens[$parentKey][1];
223
		}
224
		return null;
225
	}
226
227
	/**
228
	 * @param string $fileName
229
	 * @return bool|string
230
	 * @throws \CodeReview\IOException
231
	 */
232 1
	public function exportPhp($fileName = null) {
233 1
		$source = '';
234 1
		$data = $this->tokens;
235 1
		reset($data);
236 1
		foreach ($data as $val) {
237 1
			if (is_array($val)) {
238 1
				$source .= $val[1];
239 1
			} else {
240 1
				$source .= $val;
241
			}
242 1
		}
243
244 1
		if ($fileName !== null) {
245
			if (!is_writable($fileName)) {
246
				throw new IOException("$fileName must be writable");
247
			}
248
			return file_put_contents($fileName, $source) !== false;
249
		} else {
250 1
			return $source;
251
		}
252
	}
253
254
	/**
255
	 * (PHP 5 &gt;= 5.0.0)<br/>
256
	 * Return the current element
257
	 *
258
	 * @link http://php.net/manual/en/iterator.current.php
259
	 * @return mixed Can return any type.
260
	 */
261 4
	public function current() {
262 4
		return current($this->tokens);
263
	}
264
265
	/**
266
	 * (PHP 5 &gt;= 5.0.0)<br/>
267
	 * Move forward to next element
268
	 * @link http://php.net/manual/en/iterator.next.php
269
	 * @return void Any returned value is ignored.
270
	 */
271 4
	public function next() {
272 4
		next($this->tokens);
273 4
	}
274
275
	/**
276
	 * (PHP 5 &gt;= 5.0.0)<br/>
277
	 * Return the key of the current element
278
	 * @link http://php.net/manual/en/iterator.key.php
279
	 * @return mixed scalar on success, or null on failure.
280
	 */
281 4
	public function key() {
282 4
		return key($this->tokens);
283
	}
284
285
	/**
286
	 * (PHP 5 &gt;= 5.0.0)<br/>
287
	 * Checks if current position is valid
288
	 * @link http://php.net/manual/en/iterator.valid.php
289
	 * @return boolean The return value will be casted to boolean and then evaluated.
290
	 *       Returns true on success or false on failure.
291
	 */
292 4
	public function valid() {
293 4
		$key = key($this->tokens);
294 4
		$var = ($key !== null && $key !== false);
295 4
		return $var;
296
	}
297
298
	/**
299
	 * (PHP 5 &gt;= 5.0.0)<br/>
300
	 * Rewind the Iterator to the first element
301
	 * @link http://php.net/manual/en/iterator.rewind.php
302
	 * @return void Any returned value is ignored.
303
	 */
304 4
	public function rewind() {
305 4
		reset($this->tokens);
306 4
	}
307
308
	/**
309
	 * (PHP 5 &gt;= 5.0.0)<br/>
310
	 * Whether a offset exists
311
	 * @link http://php.net/manual/en/arrayaccess.offsetexists.php
312
	 * @param mixed $offset <p>
313
	 *                      An offset to check for.
314
	 * </p>
315
	 * @return boolean true on success or false on failure.
316
	 * </p>
317
	 * <p>
318
	 *       The return value will be casted to boolean if non-boolean was returned.
319
	 */
320 11
	public function offsetExists($offset) {
321 11
		return isset($this->tokens[$offset]);
322
	}
323
324
	/**
325
	 * (PHP 5 &gt;= 5.0.0)<br/>
326
	 * Offset to retrieve
327
	 * @link http://php.net/manual/en/arrayaccess.offsetget.php
328
	 * @param mixed $offset <p>
329
	 *                      The offset to retrieve.
330
	 * </p>
331
	 * @return mixed Can return all value types.
332
	 */
333 11
	public function offsetGet($offset) {
334 11
		return $this->tokens[$offset];
335
	}
336
337
	/**
338
	 * (PHP 5 &gt;= 5.0.0)<br/>
339
	 * Offset to set
340
	 * @link http://php.net/manual/en/arrayaccess.offsetset.php
341
	 * @param mixed $offset <p>
342
	 *                      The offset to assign the value to.
343
	 * </p>
344
	 * @param mixed $value  <p>
345
	 *                      The value to set.
346
	 * </p>
347
	 * @return void
348
	 */
349
	public function offsetSet($offset, $value) {
350
		$this->tokens[$offset] = $value;
351
	}
352
353
	/**
354
	 * (PHP 5 &gt;= 5.0.0)<br/>
355
	 * Offset to unset
356
	 * @link http://php.net/manual/en/arrayaccess.offsetunset.php
357
	 * @param mixed $offset <p>
358
	 *                      The offset to unset.
359
	 * </p>
360
	 * @return void
361
	 */
362
	public function offsetUnset($offset) {
363
		unset($this->tokens[$offset]);
364
	}
365
}