Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
75.09% |
208 / 277 |
|
61.54% |
16 / 26 |
CRAP | |
0.00% |
0 / 1 |
php_exporter | |
75.09% |
208 / 277 |
|
61.54% |
16 / 26 |
262.81 | |
0.00% |
0 / 1 |
__construct | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
2.00 | |||
get_events | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
set_current_event | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
set_content | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
crawl_phpbb_directory_php | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
get_recursive_file_list | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
export_events_for_wiki | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
export_events_for_rst | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
export_events_for_bbcode | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
crawl_php_file | |
91.30% |
63 / 69 |
|
0.00% |
0 / 1 |
22.32 | |||
version_is_filtered | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
4 | |||
get_event_name | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
3 | |||
preg_match_event_name | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
get_vars_from_array | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
get_vars_from_single_line_array | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
get_vars_from_multi_line_array | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
get_vars_from_docblock | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
10 | |||
find_since | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
find_changed | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
find_event | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
find_tag | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
11 | |||
find_description | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 | |||
validate_since | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
validate_changed | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
2.09 | |||
validate_event | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
3.14 | |||
validate_vars_docblock_array | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | namespace phpbb\event; |
15 | |
16 | /** |
17 | * Class php_exporter |
18 | * Crawls through a list of files and grabs all php-events |
19 | */ |
20 | class php_exporter |
21 | { |
22 | /** @var string Path where we look for files*/ |
23 | protected $path; |
24 | |
25 | /** @var string phpBB Root Path */ |
26 | protected $root_path; |
27 | |
28 | /** @var string The minimum version for the events to return */ |
29 | protected $min_version; |
30 | |
31 | /** @var string The maximum version for the events to return */ |
32 | protected $max_version; |
33 | |
34 | /** @var string */ |
35 | protected $current_file; |
36 | |
37 | /** @var string */ |
38 | protected $current_event; |
39 | |
40 | /** @var int */ |
41 | protected $current_event_line; |
42 | |
43 | /** @var array */ |
44 | protected $events; |
45 | |
46 | /** @var array */ |
47 | protected $file_lines; |
48 | |
49 | /** |
50 | * @param string $phpbb_root_path |
51 | * @param mixed $extension String 'vendor/ext' to filter, null for phpBB core |
52 | * @param string $min_version |
53 | * @param string $max_version |
54 | */ |
55 | public function __construct($phpbb_root_path, $extension = null, $min_version = null, $max_version = null) |
56 | { |
57 | $this->root_path = $phpbb_root_path; |
58 | $this->path = $phpbb_root_path; |
59 | $this->events = $this->file_lines = array(); |
60 | $this->current_file = $this->current_event = ''; |
61 | $this->current_event_line = 0; |
62 | $this->min_version = $min_version; |
63 | $this->max_version = $max_version; |
64 | |
65 | $this->path = $this->root_path; |
66 | if ($extension) |
67 | { |
68 | $this->path .= 'ext/' . $extension . '/'; |
69 | } |
70 | } |
71 | |
72 | /** |
73 | * Get the list of all events |
74 | * |
75 | * @return array Array with events: name => details |
76 | */ |
77 | public function get_events() |
78 | { |
79 | return $this->events; |
80 | } |
81 | |
82 | /** |
83 | * Set current event data |
84 | * |
85 | * @param string $name Name of the current event (used for error messages) |
86 | * @param int $line Line where the current event is placed in |
87 | * @return void |
88 | */ |
89 | public function set_current_event($name, $line) |
90 | { |
91 | $this->current_event = $name; |
92 | $this->current_event_line = $line; |
93 | } |
94 | |
95 | /** |
96 | * Set the content of this file |
97 | * |
98 | * @param array $content Array with the lines of the file |
99 | * @return void |
100 | */ |
101 | public function set_content($content) |
102 | { |
103 | $this->file_lines = $content; |
104 | } |
105 | |
106 | /** |
107 | * Crawl the phpBB/ directory for php events |
108 | * @return int The number of events found |
109 | */ |
110 | public function crawl_phpbb_directory_php() |
111 | { |
112 | $files = $this->get_recursive_file_list(); |
113 | $this->events = array(); |
114 | foreach ($files as $file) |
115 | { |
116 | $this->crawl_php_file($file); |
117 | } |
118 | ksort($this->events); |
119 | |
120 | return count($this->events); |
121 | } |
122 | |
123 | /** |
124 | * Returns a list of files in $dir |
125 | * |
126 | * @return array List of files (including the path) |
127 | */ |
128 | public function get_recursive_file_list() |
129 | { |
130 | try |
131 | { |
132 | $iterator = new \RecursiveIteratorIterator( |
133 | new \phpbb\event\recursive_event_filter_iterator( |
134 | new \RecursiveDirectoryIterator( |
135 | $this->path, |
136 | \FilesystemIterator::SKIP_DOTS |
137 | ), |
138 | $this->path |
139 | ), |
140 | \RecursiveIteratorIterator::LEAVES_ONLY |
141 | ); |
142 | } |
143 | catch (\Exception $e) |
144 | { |
145 | return array(); |
146 | } |
147 | |
148 | $files = array(); |
149 | foreach ($iterator as $file_info) |
150 | { |
151 | /** @var \RecursiveDirectoryIterator $inner_iterator */ |
152 | $inner_iterator = $iterator->getInnerIterator(); |
153 | $relative_path = $inner_iterator->getSubPathname(); |
154 | $files[] = str_replace(DIRECTORY_SEPARATOR, '/', $relative_path); |
155 | } |
156 | |
157 | return $files; |
158 | } |
159 | |
160 | /** |
161 | * Format the php events as a wiki table |
162 | * |
163 | * @param string $action |
164 | * @return string |
165 | * @deprecated 3.3.5-RC1 (To be removed: 4.0.0-a1) |
166 | */ |
167 | public function export_events_for_wiki($action = '') |
168 | { |
169 | if ($action === 'diff') |
170 | { |
171 | $wiki_page = '=== PHP Events (Hook Locations) ===' . "\n"; |
172 | } |
173 | else |
174 | { |
175 | $wiki_page = '= PHP Events (Hook Locations) =' . "\n"; |
176 | } |
177 | $wiki_page .= '{| class="sortable zebra" cellspacing="0" cellpadding="5"' . "\n"; |
178 | $wiki_page .= '! Identifier !! Placement !! Arguments !! Added in Release !! Explanation' . "\n"; |
179 | foreach ($this->events as $event) |
180 | { |
181 | $wiki_page .= '|- id="' . $event['event'] . '"' . "\n"; |
182 | $wiki_page .= '| [[#' . $event['event'] . '|' . $event['event'] . ']] || ' . $event['file'] . ' || ' . implode(', ', $event['arguments']) . ' || ' . $event['since'] . ' || ' . $event['description'] . "\n"; |
183 | } |
184 | $wiki_page .= '|}' . "\n"; |
185 | |
186 | return $wiki_page; |
187 | } |
188 | |
189 | /** |
190 | * Format the PHP events as a rst table |
191 | * |
192 | * @param string $action |
193 | * @return string |
194 | */ |
195 | public function export_events_for_rst(string $action = ''): string |
196 | { |
197 | $rst_exporter = new rst_exporter(); |
198 | $rst_exporter->add_section_header($action === 'diff' ? 'h3' : 'h2', 'PHP Events'); |
199 | $rst_exporter->set_columns([ |
200 | 'event' => 'Identifier', |
201 | 'file' => 'Placement', |
202 | 'arguments' => 'Arguments', |
203 | 'since' => 'Added in Release', |
204 | 'description' => 'Explanation', |
205 | ]); |
206 | $rst_exporter->generate_events_table($this->events); |
207 | |
208 | return $rst_exporter->get_rst_output(); |
209 | } |
210 | |
211 | /** |
212 | * Format the PHP events as a BBCode list |
213 | * |
214 | * @param string $action |
215 | * @return string |
216 | */ |
217 | public function export_events_for_bbcode(string $action = ''): string |
218 | { |
219 | if ($action === 'diff') |
220 | { |
221 | $bbcode_text = '[size=150]PHP Events[/size]' . "\n"; |
222 | } |
223 | else |
224 | { |
225 | $bbcode_text = '[size=200]PHP Events[/size]' . "\n"; |
226 | } |
227 | |
228 | foreach ($this->events as $event) |
229 | { |
230 | $bbcode_text .= "[list]\n"; |
231 | $bbcode_text .= "[*][b]{$event['event']}[/b]\n"; |
232 | $bbcode_text .= "Placement: {$event['file']}\n"; |
233 | $bbcode_text .= 'Arguments: ' . implode(', ', $event['arguments']) . "\n"; |
234 | $bbcode_text .= "Added in Release: {$event['since']}\n"; |
235 | $bbcode_text .= "Explanation: {$event['description']}\n"; |
236 | $bbcode_text .= "[/list]\n"; |
237 | } |
238 | |
239 | return $bbcode_text; |
240 | } |
241 | |
242 | /** |
243 | * @param string $file |
244 | * @return int Number of events found in this file |
245 | * @throws \LogicException |
246 | */ |
247 | public function crawl_php_file($file) |
248 | { |
249 | $this->current_file = $file; |
250 | $this->file_lines = array(); |
251 | $content = file_get_contents($this->path . $this->current_file); |
252 | $num_events_found = 0; |
253 | |
254 | if (strpos($content, 'dispatcher->trigger_event(') || strpos($content, 'dispatcher->dispatch(')) |
255 | { |
256 | $this->set_content(explode("\n", $content)); |
257 | for ($i = 0, $num_lines = count($this->file_lines); $i < $num_lines; $i++) |
258 | { |
259 | $event_line = false; |
260 | $found_trigger_event = strpos($this->file_lines[$i], 'dispatcher->trigger_event('); |
261 | $found_use_vars = strpos($this->file_lines[$i], ', compact($vars)'); |
262 | $arguments = array(); |
263 | if ($found_trigger_event !== false) |
264 | { |
265 | $event_line = $i; |
266 | $this->set_current_event($this->get_event_name($event_line, false), $event_line); |
267 | |
268 | if ($found_use_vars) |
269 | { |
270 | // Find variables of the event |
271 | $arguments = $this->get_vars_from_array(); |
272 | $doc_vars = $this->get_vars_from_docblock(); |
273 | $this->validate_vars_docblock_array($arguments, $doc_vars); |
274 | } |
275 | } |
276 | else |
277 | { |
278 | $found_dispatch = strpos($this->file_lines[$i], 'dispatcher->dispatch('); |
279 | if ($found_dispatch !== false) |
280 | { |
281 | $event_line = $i; |
282 | $this->set_current_event($this->get_event_name($event_line, true), $event_line); |
283 | } |
284 | } |
285 | |
286 | if ($event_line) |
287 | { |
288 | // Validate @event |
289 | $event_line_num = $this->find_event(); |
290 | $this->validate_event($this->current_event, $this->file_lines[$event_line_num]); |
291 | |
292 | // Validate @since |
293 | $since_line_num = $this->find_since(); |
294 | $since = $this->validate_since($this->file_lines[$since_line_num]); |
295 | |
296 | $changed_line_nums = $this->find_changed('changed'); |
297 | if (empty($changed_line_nums)) |
298 | { |
299 | $changed_line_nums = $this->find_changed('change'); |
300 | } |
301 | $changed_versions = array(); |
302 | if (!empty($changed_line_nums)) |
303 | { |
304 | foreach ($changed_line_nums as $changed_line_num) |
305 | { |
306 | $changed_versions[] = $this->validate_changed($this->file_lines[$changed_line_num]); |
307 | } |
308 | } |
309 | |
310 | if (!$this->version_is_filtered($since)) |
311 | { |
312 | $valid_version = false; |
313 | foreach ($changed_versions as $changed) |
314 | { |
315 | $valid_version = $valid_version || $this->version_is_filtered($changed); |
316 | } |
317 | |
318 | if (!$valid_version) |
319 | { |
320 | continue; |
321 | } |
322 | } |
323 | |
324 | // Find event description line |
325 | $description_line_num = $this->find_description(); |
326 | $description_lines = array(); |
327 | |
328 | while (true) |
329 | { |
330 | $description_line = substr(trim($this->file_lines[$description_line_num]), strlen('*')); |
331 | $description_line = trim(str_replace("\t", " ", $description_line)); |
332 | |
333 | // Reached end of description if line is a tag |
334 | if (strlen($description_line) && $description_line[0] == '@') |
335 | { |
336 | break; |
337 | } |
338 | |
339 | $description_lines[] = $description_line; |
340 | $description_line_num++; |
341 | } |
342 | |
343 | // If there is an empty line between description and first tag, remove it |
344 | if (!strlen(end($description_lines))) |
345 | { |
346 | array_pop($description_lines); |
347 | } |
348 | |
349 | $description = trim(implode('<br>', $description_lines)); |
350 | sort($arguments); |
351 | |
352 | if (isset($this->events[$this->current_event])) |
353 | { |
354 | if ($this->events[$this->current_event]['arguments'] != $arguments || |
355 | $this->events[$this->current_event]['since'] != $since) |
356 | { |
357 | throw new \LogicException("The event '{$this->current_event}' from file " |
358 | . "'{$this->current_file}:{$event_line_num}' already exists in file " |
359 | . "'{$this->events[$this->current_event]['file']}'", 10); |
360 | } |
361 | |
362 | $this->events[$this->current_event]['file'] .= '<br>' . $this->current_file; |
363 | } |
364 | else |
365 | { |
366 | $this->events[$this->current_event] = array( |
367 | 'event' => $this->current_event, |
368 | 'file' => $this->current_file, |
369 | 'arguments' => $arguments, |
370 | 'since' => $since, |
371 | 'description' => $description, |
372 | ); |
373 | $num_events_found++; |
374 | } |
375 | } |
376 | } |
377 | } |
378 | |
379 | return $num_events_found; |
380 | } |
381 | |
382 | /** |
383 | * The version to check |
384 | * |
385 | * @param string $version |
386 | * @return bool |
387 | */ |
388 | protected function version_is_filtered($version) |
389 | { |
390 | return (!$this->min_version || phpbb_version_compare($this->min_version, $version, '<=')) |
391 | && (!$this->max_version || phpbb_version_compare($this->max_version, $version, '>=')); |
392 | } |
393 | |
394 | /** |
395 | * Find the name of the event inside the dispatch() line |
396 | * |
397 | * @param int $event_line |
398 | * @param bool $is_dispatch Do we look for dispatch() or trigger_event() ? |
399 | * @return string Name of the event |
400 | * @throws \LogicException |
401 | */ |
402 | public function get_event_name($event_line, $is_dispatch) |
403 | { |
404 | $event_text_line = $this->file_lines[$event_line]; |
405 | $event_text_line = ltrim($event_text_line, "\t "); |
406 | |
407 | if ($is_dispatch) |
408 | { |
409 | $regex = '#\$[a-z](?:[a-z0-9_]|->)*'; |
410 | $regex .= '->dispatch\((\[)?'; |
411 | $regex .= '\'' . $this->preg_match_event_name() . '(?(1)\', \'(?2))+\''; |
412 | $regex .= '(?(1)\])\);#'; |
413 | } |
414 | else |
415 | { |
416 | $regex = '#(?:extract\()?\$[a-z](?:[a-z0-9_]|->)*'; |
417 | $regex .= '->trigger_event\((\[)?'; |
418 | $regex .= '\'' . $this->preg_match_event_name() . '(?(1)\', \'(?2))+\''; |
419 | $regex .= '(?(1)\])(?:, compact\(\$vars\)\))?\);#'; |
420 | } |
421 | |
422 | $match = array(); |
423 | preg_match($regex, $event_text_line, $match); |
424 | if (!isset($match[2])) |
425 | { |
426 | throw new \LogicException("Can not find event name in line '{$event_text_line}' " |
427 | . "in file '{$this->current_file}:{$event_line}'", 1); |
428 | } |
429 | |
430 | return $match[2]; |
431 | } |
432 | |
433 | /** |
434 | * Returns a regex match for the event name |
435 | * |
436 | * @return string |
437 | */ |
438 | protected function preg_match_event_name() |
439 | { |
440 | return '([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+)'; |
441 | } |
442 | |
443 | /** |
444 | * Find the $vars array |
445 | * |
446 | * @return array List of variables |
447 | * @throws \LogicException |
448 | */ |
449 | public function get_vars_from_array() |
450 | { |
451 | $line = ltrim($this->file_lines[$this->current_event_line - 1], "\t"); |
452 | if ($line === ');' || $line === '];') |
453 | { |
454 | $vars_array = $this->get_vars_from_multi_line_array(); |
455 | } |
456 | else |
457 | { |
458 | $vars_array = $this->get_vars_from_single_line_array($line); |
459 | } |
460 | |
461 | foreach ($vars_array as $var) |
462 | { |
463 | if (!preg_match('#^[a-z_][a-z0-9_]*$#i', $var)) |
464 | { |
465 | throw new \LogicException("Found invalid var '{$var}' in array for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3); |
466 | } |
467 | } |
468 | |
469 | sort($vars_array); |
470 | return $vars_array; |
471 | } |
472 | |
473 | /** |
474 | * Find the variables in single line array |
475 | * |
476 | * @param string $line |
477 | * @param bool $throw_multiline Throw an exception when there are too |
478 | * many arguments in one line. |
479 | * @return array List of variables |
480 | * @throws \LogicException |
481 | */ |
482 | public function get_vars_from_single_line_array($line, $throw_multiline = true) |
483 | { |
484 | $match = array(); |
485 | preg_match('#^\$vars = (?:(\[)|array\()\'([a-z0-9_\' ,]+)\'(?(1)\]|\));$#i', $line, $match); |
486 | |
487 | if (isset($match[2])) |
488 | { |
489 | $vars_array = explode("', '", $match[2]); |
490 | if ($throw_multiline && count($vars_array) > 6) |
491 | { |
492 | throw new \LogicException('Should use multiple lines for $vars definition ' |
493 | . "for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); |
494 | } |
495 | return $vars_array; |
496 | } |
497 | else |
498 | { |
499 | throw new \LogicException("Can not find '\$vars = array();'-line for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); |
500 | } |
501 | } |
502 | |
503 | /** |
504 | * Find the variables in single line array |
505 | * |
506 | * @return array List of variables |
507 | * @throws \LogicException |
508 | */ |
509 | public function get_vars_from_multi_line_array() |
510 | { |
511 | $current_vars_line = 2; |
512 | $var_lines = array(); |
513 | while (!in_array(ltrim($this->file_lines[$this->current_event_line - $current_vars_line], "\t"), ['$vars = array(', '$vars = ['])) |
514 | { |
515 | $var_lines[] = substr(trim($this->file_lines[$this->current_event_line - $current_vars_line]), 0, -1); |
516 | |
517 | $current_vars_line++; |
518 | if ($current_vars_line > $this->current_event_line) |
519 | { |
520 | // Reached the start of the file |
521 | throw new \LogicException("Can not find end of \$vars array for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); |
522 | } |
523 | } |
524 | |
525 | return $this->get_vars_from_single_line_array('$vars = array(' . implode(", ", $var_lines) . ');', false); |
526 | } |
527 | |
528 | /** |
529 | * Find the $vars array |
530 | * |
531 | * @return array List of variables |
532 | * @throws \LogicException |
533 | */ |
534 | public function get_vars_from_docblock() |
535 | { |
536 | $doc_vars = array(); |
537 | $current_doc_line = 1; |
538 | $found_comment_end = false; |
539 | while (ltrim($this->file_lines[$this->current_event_line - $current_doc_line], "\t") !== '/**') |
540 | { |
541 | if (ltrim($this->file_lines[$this->current_event_line - $current_doc_line], "\t ") === '*/') |
542 | { |
543 | $found_comment_end = true; |
544 | } |
545 | |
546 | if ($found_comment_end) |
547 | { |
548 | $var_line = trim($this->file_lines[$this->current_event_line - $current_doc_line]); |
549 | $var_line = preg_replace('!\s+!', ' ', $var_line); |
550 | if (strpos($var_line, '* @var ') === 0) |
551 | { |
552 | $doc_line = explode(' ', $var_line, 5); |
553 | if (count($doc_line) !== 5) |
554 | { |
555 | throw new \LogicException("Found invalid line '{$this->file_lines[$this->current_event_line - $current_doc_line]}' " |
556 | . "for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); |
557 | } |
558 | $doc_vars[] = $doc_line[3]; |
559 | } |
560 | } |
561 | |
562 | $current_doc_line++; |
563 | if ($current_doc_line > $this->current_event_line) |
564 | { |
565 | // Reached the start of the file |
566 | throw new \LogicException("Can not find end of docblock for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); |
567 | } |
568 | } |
569 | |
570 | if (empty($doc_vars)) |
571 | { |
572 | // Reached the start of the file |
573 | throw new \LogicException("Can not find @var lines for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3); |
574 | } |
575 | |
576 | foreach ($doc_vars as $var) |
577 | { |
578 | if (!preg_match('#^[a-z_][a-z0-9_]*$#i', $var)) |
579 | { |
580 | throw new \LogicException("Found invalid @var '{$var}' in docblock for event " |
581 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 4); |
582 | } |
583 | } |
584 | |
585 | sort($doc_vars); |
586 | return $doc_vars; |
587 | } |
588 | |
589 | /** |
590 | * Find the "@since" Information line |
591 | * |
592 | * @return int Absolute line number |
593 | * @throws \LogicException |
594 | */ |
595 | public function find_since() |
596 | { |
597 | return $this->find_tag('since', array('event', 'var')); |
598 | } |
599 | |
600 | /** |
601 | * Find the "@changed" Information lines |
602 | * |
603 | * @param string $tag_name Should be 'change', not 'changed' |
604 | * @return array Absolute line numbers |
605 | * @throws \LogicException |
606 | */ |
607 | public function find_changed($tag_name) |
608 | { |
609 | $lines = array(); |
610 | $last_line = 0; |
611 | try |
612 | { |
613 | while ($line = $this->find_tag($tag_name, array('since'), $last_line)) |
614 | { |
615 | $lines[] = $line; |
616 | $last_line = $line; |
617 | } |
618 | } |
619 | catch (\LogicException $e) |
620 | { |
621 | // Not changed? No problem! |
622 | } |
623 | |
624 | return $lines; |
625 | } |
626 | |
627 | /** |
628 | * Find the "@event" Information line |
629 | * |
630 | * @return int Absolute line number |
631 | */ |
632 | public function find_event() |
633 | { |
634 | return $this->find_tag('event', array()); |
635 | } |
636 | |
637 | /** |
638 | * Find a "@*" Information line |
639 | * |
640 | * @param string $find_tag Name of the tag we are trying to find |
641 | * @param array $disallowed_tags List of tags that must not appear between |
642 | * the tag and the actual event |
643 | * @param int $skip_to_line Skip lines until this one |
644 | * @return int Absolute line number |
645 | * @throws \LogicException |
646 | */ |
647 | public function find_tag($find_tag, $disallowed_tags, $skip_to_line = 0) |
648 | { |
649 | $find_tag_line = $skip_to_line ? $this->current_event_line - $skip_to_line + 1 : 0; |
650 | $found_comment_end = ($skip_to_line) ? true : false; |
651 | while (strpos(ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t "), '* @' . $find_tag . ' ') !== 0) |
652 | { |
653 | if ($found_comment_end && ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t") === '/**') |
654 | { |
655 | // Reached the start of this doc block |
656 | throw new \LogicException("Can not find '@{$find_tag}' information for event " |
657 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); |
658 | } |
659 | |
660 | foreach ($disallowed_tags as $disallowed_tag) |
661 | { |
662 | if ($found_comment_end && strpos(ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t "), '* @' . $disallowed_tag) === 0) |
663 | { |
664 | // Found @var after the @since |
665 | throw new \LogicException("Found '@{$disallowed_tag}' information after '@{$find_tag}' for event " |
666 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3); |
667 | } |
668 | } |
669 | |
670 | if (ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t ") === '*/') |
671 | { |
672 | $found_comment_end = true; |
673 | } |
674 | |
675 | $find_tag_line++; |
676 | if ($find_tag_line >= $this->current_event_line) |
677 | { |
678 | // Reached the start of the file |
679 | throw new \LogicException("Can not find '@{$find_tag}' information for event " |
680 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); |
681 | } |
682 | } |
683 | |
684 | return $this->current_event_line - $find_tag_line; |
685 | } |
686 | |
687 | /** |
688 | * Find a "@*" Information line |
689 | * |
690 | * @return int Absolute line number |
691 | * @throws \LogicException |
692 | */ |
693 | public function find_description() |
694 | { |
695 | $find_desc_line = 0; |
696 | while (ltrim($this->file_lines[$this->current_event_line - $find_desc_line], "\t") !== '/**') |
697 | { |
698 | $find_desc_line++; |
699 | if ($find_desc_line > $this->current_event_line) |
700 | { |
701 | // Reached the start of the file |
702 | throw new \LogicException("Can not find a description for event " |
703 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); |
704 | } |
705 | } |
706 | |
707 | $find_desc_line = $this->current_event_line - $find_desc_line + 1; |
708 | |
709 | $desc = trim($this->file_lines[$find_desc_line]); |
710 | if (strpos($desc, '* @') === 0 || $desc[0] !== '*' || substr($desc, 1) == '') |
711 | { |
712 | // First line of the doc block is a @-line, empty or only contains "*" |
713 | throw new \LogicException("Can not find a description for event " |
714 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); |
715 | } |
716 | |
717 | return $find_desc_line; |
718 | } |
719 | |
720 | /** |
721 | * Validate "@since" Information |
722 | * |
723 | * @param string $line |
724 | * @return string |
725 | * @throws \LogicException |
726 | */ |
727 | public function validate_since($line) |
728 | { |
729 | $match = array(); |
730 | preg_match('#^\* @since (\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?)$#', ltrim($line, "\t "), $match); |
731 | if (!isset($match[1])) |
732 | { |
733 | throw new \LogicException("Invalid '@since' information for event " |
734 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'"); |
735 | } |
736 | |
737 | return $match[1]; |
738 | } |
739 | |
740 | /** |
741 | * Validate "@changed" Information |
742 | * |
743 | * @param string $line |
744 | * @return string |
745 | * @throws \LogicException |
746 | */ |
747 | public function validate_changed($line) |
748 | { |
749 | $match = array(); |
750 | $line = str_replace("\t", ' ', ltrim($line, "\t ")); |
751 | preg_match('#^\* @changed (\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?)( (?:.*))?$#', $line, $match); |
752 | if (!isset($match[2])) |
753 | { |
754 | throw new \LogicException("Invalid '@changed' information for event " |
755 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'"); |
756 | } |
757 | |
758 | return $match[2]; |
759 | } |
760 | |
761 | /** |
762 | * Validate "@event" Information |
763 | * |
764 | * @param string $event_name |
765 | * @param string $line |
766 | * @return string |
767 | * @throws \LogicException |
768 | */ |
769 | public function validate_event($event_name, $line) |
770 | { |
771 | $event = substr(ltrim($line, "\t "), strlen('* @event ')); |
772 | |
773 | if ($event !== trim($event)) |
774 | { |
775 | throw new \LogicException("Invalid '@event' information for event " |
776 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); |
777 | } |
778 | |
779 | if ($event !== $event_name) |
780 | { |
781 | throw new \LogicException("Event name does not match '@event' tag for event " |
782 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); |
783 | } |
784 | |
785 | return $event; |
786 | } |
787 | |
788 | /** |
789 | * Validates that two arrays contain the same strings |
790 | * |
791 | * @param array $vars_array Variables found in the array line |
792 | * @param array $vars_docblock Variables found in the doc block |
793 | * @return void |
794 | * @throws \LogicException |
795 | */ |
796 | public function validate_vars_docblock_array($vars_array, $vars_docblock) |
797 | { |
798 | $vars_array = array_unique($vars_array); |
799 | $vars_docblock = array_unique($vars_docblock); |
800 | $sizeof_vars_array = count($vars_array); |
801 | |
802 | if ($sizeof_vars_array !== count($vars_docblock) || $sizeof_vars_array !== count(array_intersect($vars_array, $vars_docblock))) |
803 | { |
804 | throw new \LogicException("\$vars array does not match the list of '@var' tags for event " |
805 | . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'"); |
806 | } |
807 | } |
808 | } |