Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.09% covered (warning)
75.09%
208 / 277
61.54% covered (warning)
61.54%
16 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
php_exporter
75.09% covered (warning)
75.09%
208 / 277
61.54% covered (warning)
61.54%
16 / 26
262.81
0.00% covered (danger)
0.00%
0 / 1
 __construct
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 get_events
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_current_event
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 set_content
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 crawl_phpbb_directory_php
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_recursive_file_list
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 export_events_for_wiki
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 export_events_for_rst
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 export_events_for_bbcode
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 crawl_php_file
91.30% covered (success)
91.30%
63 / 69
0.00% covered (danger)
0.00%
0 / 1
22.32
 version_is_filtered
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
 get_event_name
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 preg_match_event_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_vars_from_array
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 get_vars_from_single_line_array
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 get_vars_from_multi_line_array
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 get_vars_from_docblock
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
10
 find_since
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 find_changed
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 find_event
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 find_tag
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
11
 find_description
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 validate_since
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 validate_changed
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
2.09
 validate_event
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 validate_vars_docblock_array
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
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
14namespace phpbb\event;
15
16/**
17* Class php_exporter
18* Crawls through a list of files and grabs all php-events
19*/
20class 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}