Completed
Push — master ( 0ef921...49b538 )
by Martijn
03:08
created

Parser::addDirs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
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
	{
74
		// Core comments
75
		$Statements = $this->statements;
76
77
		// Functions
78
		foreach ($this->Functions as $Function) {
79
			if ($Function->hasCommand('method')) {
80
				$Statements = array_merge($Statements, $Function->Statements);
81
			}
82
		}
83
84
		// Classes
85
		foreach ($this->Classes as $Class) {
86
			$Statements = array_merge($Statements, $Class->Statements);
87
			foreach ($Class->Methods as $Method) {
88
				if ($Method->hasCommand('method')) {
89
					$Statements = array_merge($Statements, $Method->Statements);
90
				}
91
			}
92
		}
93
94
		return $Statements;
95
	}
96
97
	public function parse($file, array $dirs = array(), array $defines = array())
98
	{
99
		$this->dirs = $this->common_dirs;
100
		foreach ($dirs as $dir) {
101
			$this->dirs[] = realpath($dir);
102
		}
103
104
		$this->parseFiles(array($file), $defines);
105
106
		// Inherit classes
107
		foreach ($this->Classes as $Class) {
108
			$this->inherit($Class);
109
		}
110
111
		// Expand functions with used and seen functions/methods.
112
		foreach ($this->Classes as $Class) {
113
			foreach ($Class->Methods as $Method) {
114
				$Method->Statements = $this->expand($Method->Statements, $Class);
115
			}
116
		}
117
118
		return $this->extractStatements();
119
	}
120
121
	/**
122
	 * Convert a T_*_COMMENT string to an array of Statements
123
	 * @param array $token
124
	 * @return \SwaggerGen\Statement[]
125
	 */
126
	public function tokenToStatements($token)
127
	{
128
		$comment = $token[1];
129
		$commentLineNumber = $token[2];
130
		$commentLines = array();
131
132
		$match = array();
133
		if (preg_match('~^/\*\*?\s*(.*)\s*\*\/$~sm', $comment, $match) === 1) {
134
			$lines = preg_split('~\n~', $match[0]);
135
			foreach ($lines as $line) {
136
				if (preg_match('~^\s*\*?\s*(.*?)\s*$~', $line, $match) === 1) {
137
					if (!empty($match[1])) {
138
						$commentLines[] = trim($match[1]);
139
					}
140
				}
141
			}
142
		} elseif (preg_match('~^//\s*(.*)$~', $comment, $match) === 1) {
143
			$commentLines[] = trim($match[1]);
144
		}
145
		// to commands
146
		$match = array();
147
		$command = null;
148
		$data = '';
149
		$commandLineNumber = 0;
150
		$statements = array();
151
		foreach ($commentLines as $lineNumber => $line) {
152
			// If new @-command, store any old and start new
153
			if ($command !== null && chr(ord($line)) === '@') {
154
				$statements[] = new \SwaggerGen\Statement($command, $data, $this->current_file, $commentLineNumber + $commandLineNumber);
155
				$command = null;
156
				$data = '';
157
			}
158
159
			if (preg_match('~^@' . preg_quote(self::COMMENT_TAG) . '\\\\([a-z][-a-z]*\\??)\\s*(.*)$~', $line, $match) === 1) {
160
				$command = $match[1];
161
				$data = $match[2];
162
				$commandLineNumber = $lineNumber;
163
			} elseif ($command !== null) {
164
				if ($lineNumber < count($commentLines) - 1) {
165
					$data .= ' ' . $line;
166
				} else {
167
					$data .= preg_replace('~\s*\**\/\s*$~', '', $line);
168
				}
169
			}
170
		}
171
172
		if ($command !== null) {
173
			$statements[] = new \SwaggerGen\Statement($command, $data, $this->current_file, $commentLineNumber + $commandLineNumber);
174
		}
175
176
		return $statements;
177
	}
178
179
	public function queueClass($classname)
180
	{
181
		foreach ($this->dirs as $dir) {
182
			$paths = array(
183
				$dir . DIRECTORY_SEPARATOR . $classname . '.php',
184
				$dir . DIRECTORY_SEPARATOR . $classname . '.class.php',
185
			);
186
187
			foreach ($paths as $path) {
188
				$realpath = realpath($path);
189
				if (in_array($realpath, $this->files_done)) {
190
					return;
191
				} elseif (is_file($realpath)) {
192
					$this->files_queued[] = $realpath;
193
					return;
194
				}
195
			}
196
		}
197
198
		// assume it's a class;
199
	}
200
201
	/**
202
	 * Add to the queue any classes based on the commands.
203
	 * @param \SwaggerGen\Statement[] $Statements
204
	 */
205
	public function queueClassesFromComments(array $Statements)
206
	{
207
		foreach ($Statements as $Statement) {
208
			if (in_array($Statement->getCommand(), array('uses', 'see'))) {
209
				$match = array();
210
				if (preg_match('~^(\w+)(::|->)?(\w+)?(?:\(\))?$~', $Statement->getData(), $match) === 1) {
211
					if (!in_array($match[1], array('self', '$this'))) {
212
						$this->queueClass($match[1]);
213
					}
214
				}
215
			}
216
		}
217
	}
218
219
	private function parseTokens($source)
220
	{
221
		$mode = null;
222
		$namespace = '';
223
224
		$tokens = token_get_all($source);
225
		$token = reset($tokens);
226
		while ($token) {
227
			switch ($token[0]) {
228
				case T_NAMESPACE:
229
					$mode = T_NAMESPACE;
230
					break;
231
232
				case T_NS_SEPARATOR:
233
				case T_STRING:
234
					if ($mode === T_NAMESPACE) {
235
						$namespace .= $token[1];
236
					}
237
					break;
238
239
				case ';':
240
					$mode = null;
241
					break;
242
243
				case T_CLASS:
244
				case T_INTERFACE:
245
					$Class = new Entity\ParserClass($this, $tokens, $this->lastStatements);
246
					$this->Classes[strtolower($Class->name)] = $Class;
247
					$this->lastStatements = null;
248
					break;
249
250 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...
251
					$Function = new Entity\ParserFunction($this, $tokens, $this->lastStatements);
252
					$this->Functions[strtolower($Function->name)] = $Function;
253
					$this->lastStatements = null;
254
					break;
255
256
				case T_COMMENT:
257
					if ($this->lastStatements !== null) {
258
						$this->statements = array_merge($this->statements, $this->lastStatements);
259
						$this->lastStatements = null;
260
					}
261
					$Statements = $this->tokenToStatements($token);
262
					$this->queueClassesFromComments($Statements);
263
					$this->statements = array_merge($this->statements, $Statements);
264
					break;
265
266
				case T_DOC_COMMENT:
267
					if ($this->lastStatements !== null) {
268
						$this->statements = array_merge($this->statements, $this->lastStatements);
269
					}
270
					$Statements = $this->tokenToStatements($token);
271
					$this->queueClassesFromComments($Statements);
272
					$this->lastStatements = $Statements;
273
					break;
274
			}
275
276
			$token = next($tokens);
277
		}
278
	}
279
280
	private function parseFiles(array $files, array $defines = array())
281
	{
282
		$this->files_queued = $files;
283
284
		$index = 0;
285
		while (($file = array_shift($this->files_queued)) !== null) {
286
			$file = realpath($file);
287
288
			// @todo Test if this works
289
			if (in_array($file, $this->files_done)) {
290
				continue;
291
			}
292
293
			$this->current_file = $file;
294
			$this->files_done[] = $file;
295
			++$index;
296
297
			$this->Preprocessor->resetDefines();
298
			$this->Preprocessor->addDefines($defines);
299
			$source = $this->Preprocessor->preprocessFile($file);
300
301
			$this->parseTokens($source);
302
303
			if ($this->lastStatements !== null) {
304
				$this->statements = array_merge($this->statements, $this->lastStatements);
305
				$this->lastStatements = null;
306
			}
307
		}
308
309
		$this->current_file = null;
310
	}
311
312
	/**
313
	 * Inherit the statements
314
	 * @param \SwaggerGen\Parser\Php\Entity\ParserClass $Class
315
	 */
316
	private function inherit(Entity\ParserClass $Class)
317
	{
318
		$inherits = array_merge(array($Class->extends), $Class->implements);
319
		while (($inherit = array_shift($inherits)) !== null) {
320
			if (isset($this->Classes[strtolower($inherit)])) {
321
				$inheritedClass = $this->Classes[strtolower($inherit)];
322
				$this->inherit($inheritedClass);
323
324
				foreach ($inheritedClass->Methods as $name => $Method) {
325
					if (!isset($Class->Methods[$name])) {
326
						$Class->Methods[$name] = $Method;
327
					}
328
				}
329
			}
330
		}
331
	}
332
333
	/**
334
	 * Expands a set of comments with comments of methods referred to by
335
	 * rest\uses statements.
336
	 * @param \SwaggerGen\Statement[] $Statements
337
	 * @return \SwaggerGen\Statement[]
338
	 */
339
	private function expand(array $Statements, Entity\ParserClass $Self = null)
340
	{
341
		$output = array();
342
343
		$match = null;
344
		foreach ($Statements as $Statement) {
345
			if (in_array($Statement->getCommand(), array('uses', 'see'))) { 
346
				if (preg_match('/^((?:\\w+)|\$this)(?:(::|->)(\\w+))?(?:\\(\\))?$/', strtolower($Statement->getData()), $match) === 1) {
347
					if (count($match) >= 3) {
348
						$Class = null;
349
						if (in_array($match[1], array('$this', 'self', 'static'))) {
350
							$Class = $Self;
351
						} elseif (isset($this->Classes[$match[1]])) {
352
							$Class = $this->Classes[$match[1]];
353
						}
354
355
						if ($Class) {
356
							if (isset($Class->Methods[$match[3]])) {
357
								$Method = $Class->Methods[$match[3]];
358
								$Method->Statements = $this->expand($Method->Statements, $Class);
359
								$output = array_merge($output, $Method->Statements);
360
							} else {
361
								throw new \SwaggerGen\Exception("Method '{$match[3]}' for class '{$match[1]}' not found");
362
							}
363
						} else {
364
							throw new \SwaggerGen\Exception("Class '{$match[1]}' not found");
365
						}
366
					} elseif (isset($this->Functions[$match[1]])) {
367
						$Function = $this->Functions[$match[1]];
368
						$Function->Statements = $this->expand($Function->Statements, null);
369
						$output = array_merge($output, $Function->Statements);
370
					} else {
371
						throw new \SwaggerGen\Exception("Function '{$match[1]}' not found");
372
					}
373
				}
374
			} else {
375
				$output[] = $Statement;
376
			}
377
		}
378
379
		return $output;
380
	}
381
382
}
383