Completed
Push — master ( 75b133...6283a1 )
by Martijn
02:31
created

Parser   C

Complexity

Total Complexity 73

Size/Duplication

Total Lines 367
Duplicated Lines 1.36 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 73
lcom 1
cbo 8
dl 5
loc 367
rs 5.5447
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A addDirs() 0 6 2
B extractStatements() 0 23 6
B parse() 0 23 5
C tokenToStatements() 0 52 13
B queueClass() 0 21 5
B queueClassesFromComments() 0 13 6
C parseTokens() 5 59 14
B parseFiles() 0 31 4
B inherit() 0 16 5
C expand() 0 42 11

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Parser 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 Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SwaggerGen\Parser\Php;
4
5
/**
6
 * Parses comments in PHP into a structure of functions, classes and methods,
7
 * resolving inheritance, references and namespaces.
8
 *
9
 * @package    SwaggerGen
10
 * @author     Martijn van der Lee <[email protected]>
11
 * @copyright  2014-2015 Martijn van der Lee
12
 * @license    https://opensource.org/licenses/MIT MIT
13
 */
14
class Parser extends Entity\AbstractEntity implements \SwaggerGen\Parser\IParser
15
{
16
17
	const COMMENT_TAG = 'rest';
18
19
// transient
20
21
	private $current_file = null;
22
	private $files_queued = array();
23
	private $files_done = array();
24
	private $dirs = array();
25
// States
26
27
	public $Statements = array();
28
	
29
	/**
30
	 * @var \SwaggerGen\Statement[]|null
31
	 */
32
	private $lastStatements = array();
33
34
	/**
35
	 * @var Entity\ParserClass[]
36
	 */
37
	public $Classes = array();
38
39
	/**
40
	 * @var Entity\ParserFunction[]
41
	 */
42
	public $Functions = array();
43
44
	/**
45
	 * @var \SwaggerGen\Parser\AbstractPreprocessor
46
	 */
47
	private $Preprocessor;
48
49
	/**
50
	 * Directories available to all parse calls
51
	 *
52
	 * @var string[]
53
	 */
54
	protected $common_dirs = array();
55
56
	public function __construct(Array $dirs = array())
57
	{
58
		foreach ($dirs as $dir) {
59
			$this->common_dirs[] = realpath($dir);
60
		}
61
62
		$this->Preprocessor = new Preprocessor(self::COMMENT_TAG);
63
	}
64
65
	public function addDirs(Array $dirs)
66
	{
67
		foreach ($dirs as $dir) {
68
			$this->common_dirs[] = realpath($dir);
69
		}
70
	}
71
	
72
	private function extractStatements() {
73
		// Core comments
74
		$Statements = $this->Statements;
75
76
		// Functions
77
		foreach ($this->Functions as $Function) {
78
			if ($Function->hasCommand('method')) {
79
				$Statements = array_merge($Statements, $Function->Statements);
80
			}
81
		}
82
83
		// Classes
84
		foreach ($this->Classes as $Class) {
85
			$Statements = array_merge($Statements, $Class->Statements);
86
			foreach ($Class->Methods as $Method) {
87
				if ($Method->hasCommand('method')) {
88
					$Statements = array_merge($Statements, $Method->Statements);
89
				}
90
			}
91
		}
92
93
		return $Statements;
94
	}
95
96
	public function parse($file, Array $dirs = array(), Array $defines = array())
97
	{
98
		$this->dirs = $this->common_dirs;
99
		foreach ($dirs as $dir) {
100
			$this->dirs[] = realpath($dir);
101
		}
102
103
		$this->parseFiles(array($file), $defines);
104
105
		// Inherit classes
106
		foreach ($this->Classes as $Class) {
107
			$this->inherit($Class);
108
		}
109
110
		// Expand functions with used and seen functions/methods.
111
		foreach ($this->Classes as $Class) {
112
			foreach ($Class->Methods as $Method) {
113
				$Method->Statements = $this->expand($Method->Statements, $Class);
114
			}
115
		}
116
117
		return $this->extractStatements();
118
	}
119
120
	/**
121
	 * Convert a T_*_COMMENT string to an array of Statements
122
	 * @param array $token
123
	 * @return \SwaggerGen\Statement[]
124
	 */
125
	public function tokenToStatements($token)
126
	{
127
		$comment = $token[1];
128
		$commentLineNumber = $token[2];
129
		$commentLines = array();
130
131
		$match = array();
132
		if (preg_match('~^/\*\*?\s*(.*)\s*\*\/$~sm', $comment, $match) === 1) {
133
			$lines = preg_split('~\n~', $match[0]);
134
			foreach ($lines as $line) {
135
				if (preg_match('~^\s*\*?\s*(.*?)\s*$~', $line, $match) === 1) {
136
					if (!empty($match[1])) {
137
						$commentLines[] = trim($match[1]);
138
					}
139
				}
140
			}
141
		} elseif (preg_match('~^//\s*(.*)$~', $comment, $match) === 1) {
142
			$commentLines[] = trim($match[1]);
143
		}
144
		// to commands
145
		$match = array();
146
		$command = null;
147
		$data = '';
148
		$commandLineNumber = 0;
149
		$Statements = array();
150
		foreach ($commentLines as $lineNumber => $line) {
151
			// If new @-command, store any old and start new
152
			if ($command !== null && chr(ord($line)) === '@') {
153
				$Statements[] = new Statement($command, $data, $this->current_file, $commentLineNumber + $commandLineNumber);
154
				$command = null;
155
				$data = '';
156
			}
157
158
			if (preg_match('~^@' . preg_quote(self::COMMENT_TAG) . '\\\\([a-z][-a-z]*\\??)\\s*(.*)$~', $line, $match) === 1) {
159
				$command = $match[1];
160
				$data = $match[2];
161
				$commandLineNumber = $lineNumber;
162
			} elseif ($command !== null) {
163
				if ($lineNumber < count($commentLines) - 1) {
164
					$data.= ' ' . $line;
165
				} else {
166
					$data.= preg_replace('~\s*\**\/\s*$~', '', $line);
167
				}
168
			}
169
		}
170
171
		if ($command !== null) {
172
			$Statements[] = new Statement($command, $data, $this->current_file, $commentLineNumber + $commandLineNumber);
173
		}
174
175
		return $Statements;
176
	}
177
178
	public function queueClass($classname)
179
	{
180
		foreach ($this->dirs as $dir) {
181
			$paths = array(
182
				$dir . DIRECTORY_SEPARATOR . $classname . '.php',
183
				$dir . DIRECTORY_SEPARATOR . $classname . '.class.php',
184
			);
185
186
			foreach ($paths as $path) {
187
				$realpath = realpath($path);
188
				if (in_array($realpath, $this->files_done)) {
189
					return;
190
				} elseif (is_file($realpath)) {
191
					$this->files_queued[] = $realpath;
192
					return;
193
				}
194
			}
195
		}
196
197
		// assume it's a class;
198
	}
199
200
	/**
201
	 * Add to the queue any classes based on the commands.
202
	 * @param \SwaggerGen\Statement[] $Statements
203
	 */
204
	public function queueClassesFromComments(Array $Statements)
205
	{
206
		foreach ($Statements as $Statement) {
207
			if ($Statement->command === 'uses' || $Statement->command === 'see') {
208
				$match = array();
209
				if (preg_match('~^(\w+)(::|->)?(\w+)?(?:\(\))?$~', $Statement->data, $match) === 1) {
210
					if (!in_array($match[1], array('self', '$this'))) {
211
						$this->queueClass($match[1]);
212
					}
213
				}
214
			}
215
		}
216
	}
217
218
	private function parseTokens($source) {
219
		$mode = null;
220
		$namespace = '';
221
222
		$tokens = token_get_all($source);
223
		$token = reset($tokens);
224
		while ($token) {
225
			switch ($token[0]) {
226
				case T_NAMESPACE:
227
					$mode = T_NAMESPACE;
228
					break;
229
230
				case T_NS_SEPARATOR:
231
				case T_STRING:
232
					if ($mode === T_NAMESPACE) {
233
						$namespace .= $token[1];
234
					}
235
					break;
236
237
				case ';':
238
					$mode = null;
239
					break;
240
241
				case T_CLASS:
242
				case T_INTERFACE:
243
					$Class = new Entity\ParserClass($this, $tokens, $this->lastStatements);
244
					$this->Classes[strtolower($Class->name)] = $Class;
245
					$this->lastStatements = null;
246
					break;
247
248 View Code Duplication
				case T_FUNCTION:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
249
					$Function = new Entity\ParserFunction($this, $tokens, $this->lastStatements);
250
					$this->Functions[strtolower($Function->name)] = $Function;
251
					$this->lastStatements = null;
252
					break;
253
254
				case T_COMMENT:
255
					if ($this->lastStatements !== null) {
256
						$this->Statements = array_merge($this->Statements, $this->lastStatements);
257
						$this->lastStatements = null;
258
					}
259
					$Statements = $this->tokenToStatements($token);
260
					$this->queueClassesFromComments($Statements);
261
					$this->Statements = array_merge($this->Statements, $Statements);
262
					break;
263
264
				case T_DOC_COMMENT:
265
					if ($this->lastStatements !== null) {
266
						$this->Statements = array_merge($this->Statements, $this->lastStatements);
267
					}
268
					$Statements = $this->tokenToStatements($token);
269
					$this->queueClassesFromComments($Statements);
270
					$this->lastStatements = $Statements;
271
					break;
272
			}
273
274
			$token = next($tokens);
275
		}
276
	}
277
	
278
	private function parseFiles(Array $files, Array $defines = array())
279
	{
280
		$this->files_queued = $files;
281
282
		$index = 0;
283
		while (($file = array_shift($this->files_queued)) !== null) {
284
			$file = realpath($file);
285
286
			// @todo Test if this works
287
			if (in_array($file, $this->files_done)) {
288
				continue;
289
			}
290
291
			$this->current_file = $file;
292
			$this->files_done[] = $file;
293
			++$index;
294
295
			$this->Preprocessor->resetDefines();
296
			$this->Preprocessor->addDefines($defines);
297
			$source = $this->Preprocessor->preprocessFile($file);
298
299
			$this->parseTokens($source);
300
301
			if ($this->lastStatements !== null) {
302
				$this->Statements = array_merge($this->Statements, $this->lastStatements);
303
				$this->lastStatements = null;
304
			}
305
		}
306
307
		$this->current_file = null;
308
	}
309
310
	/**
311
	 * Inherit the statements
312
	 * @param \SwaggerGen\Parser\Php\Entity\ParserClass $Class
313
	 */
314
	private function inherit(Entity\ParserClass $Class)
315
	{
316
		$inherits = array_merge(array($Class->extends), $Class->implements);
317
		while (($inherit = array_shift($inherits)) !== null) {
318
			if (isset($this->Classes[strtolower($inherit)])) {
319
				$inheritedClass = $this->Classes[strtolower($inherit)];
320
				$this->inherit($inheritedClass);
321
322
				foreach ($inheritedClass->Methods as $name => $Method) {
323
					if (!isset($Class->Methods[$name])) {
324
						$Class->Methods[$name] = $Method;
325
					}
326
				}
327
			}
328
		}
329
	}
330
331
	/**
332
	 * Expands a set of comments with comments of methods referred to by
333
	 * rest\uses statements.
334
	 * @param \SwaggerGen\Statement[] $Statements
335
	 * @return \SwaggerGen\Statement[]
336
	 */
337
	private function expand(Array $Statements, Entity\ParserClass $Self = null)
338
	{
339
		$output = array();
340
341
		$match = null;
342
		foreach ($Statements as $Statement) {
343
			if ($Statement->command === 'uses' || $Statement->command === 'see') { //@todo either one, not both?
344
				if (preg_match('/^((?:\\w+)|\$this)(?:(::|->)(\\w+))?(?:\\(\\))?$/', strtolower($Statement->data), $match) === 1) {
345
					if (count($match) >= 3) {
346
						$Class = null;
347
						if (in_array($match[1], array('$this', 'self', 'static'))) {
348
							$Class = $Self;
349
						} elseif (isset($this->Classes[$match[1]])) {
350
							$Class = $this->Classes[$match[1]];
351
						}
352
353
						if ($Class) {
354
							if (isset($Class->Methods[$match[3]])) {
355
								$Method = $Class->Methods[$match[3]];
356
								$Method->Statements = $this->expand($Method->Statements, $Class);
357
								$output = array_merge($output, $Method->Statements);
358
							} else {
359
								throw new \SwaggerGen\Exception("Method '{$match[3]}' for class '{$match[1]}' not found");
360
							}
361
						} else {
362
							throw new \SwaggerGen\Exception("Class '{$match[1]}' not found");
363
						}
364
					} elseif (isset($this->Functions[$match[1]])) {
365
						$Function = $this->Functions[$match[1]];
366
						$Function->Statements = $this->expand($Function->Statements, null);
367
						$output = array_merge($output, $Function->Statements);
368
					} else {
369
						throw new \SwaggerGen\Exception("Function '{$match[1]}' not found");
370
					}
371
				}
372
			} else {
373
				$output[] = $Statement;
374
			}
375
		}
376
377
		return $output;
378
	}
379
380
}
381