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: |
|
|
|
|
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
|
|
|
|
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.