Subversion Repositories Applications.framework

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
5 aurelien 1
<?php
2
/**
3
 * Parses doc comments.
4
 *
5
 * PHP version 5
6
 *
7
 * @category  PHP
8
 * @package   PHP_CodeSniffer
9
 * @author    Greg Sherwood <gsherwood@squiz.net>
10
 * @author    Marc McIntyre <mmcintyre@squiz.net>
11
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
12
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
13
 * @version   CVS: $Id: AbstractParser.php,v 1.22 2008/12/04 00:35:14 squiz Exp $
14
 * @link      http://pear.php.net/package/PHP_CodeSniffer
15
 */
16
 
17
if (class_exists('PHP_CodeSniffer_CommentParser_SingleElement', true) === false) {
18
    $error = 'Class PHP_CodeSniffer_CommentParser_SingleElement not found';
19
    throw new PHP_CodeSniffer_Exception($error);
20
}
21
 
22
if (class_exists('PHP_CodeSniffer_CommentParser_CommentElement', true) === false) {
23
    $error = 'Class PHP_CodeSniffer_CommentParser_CommentElement not found';
24
    throw new PHP_CodeSniffer_Exception($error);
25
}
26
 
27
if (class_exists('PHP_CodeSniffer_CommentParser_ParserException', true) === false) {
28
    $error = 'Class PHP_CodeSniffer_CommentParser_ParserException not found';
29
    throw new PHP_CodeSniffer_Exception($error);
30
}
31
 
32
/**
33
 * Parses doc comments.
34
 *
35
 * This abstract parser handles the following tags:
36
 *
37
 * <ul>
38
 *  <li>The short description and the long description</li>
39
 *  <li>@see</li>
40
 *  <li>@link</li>
41
 *  <li>@deprecated</li>
42
 *  <li>@since</li>
43
 * </ul>
44
 *
45
 * Extending classes should implement the getAllowedTags() method to return the
46
 * tags that they wish to process, ommiting the tags that this base class
47
 * processes. When one of these tags in encountered, the process&lt;tag_name&gt;
48
 * method is called on that class. For example, if a parser's getAllowedTags()
49
 * method returns \@param as one of its tags, the processParam method will be
50
 * called so that the parser can process such a tag.
51
 *
52
 * The method is passed the tokens that comprise this tag. The tokens array
53
 * includes the whitespace that exists between the tokens, as seperate tokens.
54
 * It's up to the method to create a element that implements the DocElement
55
 * interface, which should be returned. The AbstractDocElement class is a helper
56
 * class that can be used to handle most of the parsing of the tokens into their
57
 * individual sub elements. It requires that you construct it with the element
58
 * previous to the element currently being processed, which can be acquired
59
 * with the protected $previousElement class member of this class.
60
 *
61
 * @category  PHP
62
 * @package   PHP_CodeSniffer
63
 * @author    Greg Sherwood <gsherwood@squiz.net>
64
 * @author    Marc McIntyre <mmcintyre@squiz.net>
65
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
66
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
67
 * @version   Release: 1.2.0RC1
68
 * @link      http://pear.php.net/package/PHP_CodeSniffer
69
 */
70
abstract class PHP_CodeSniffer_CommentParser_AbstractParser
71
{
72
 
73
    /**
74
     * The comment element that appears in the doc comment.
75
     *
76
     * @var PHP_CodeSniffer_CommentParser_CommentElement
77
     */
78
    protected $comment = null;
79
 
80
    /**
81
     * The string content of the comment.
82
     *
83
     * @var string
84
     */
85
    protected $commentString = '';
86
 
87
    /**
88
     * The file that the comment exists in.
89
     *
90
     * @var PHP_CodeSniffer_File
91
     */
92
    protected $phpcsFile = null;
93
 
94
    /**
95
     * The word tokens that appear in the comment.
96
     *
97
     * Whitespace tokens also appear in this stack, but are separate tokens
98
     * from words.
99
     *
100
     * @var array(string)
101
     */
102
    protected $words = array();
103
 
104
    /**
105
     * The previous doc element that was processed.
106
     *
107
     * null if the current element being processed is the first element in the
108
     * doc comment.
109
     *
110
     * @var PHP_CodeSniffer_CommentParser_DocElement
111
     */
112
    protected $previousElement = null;
113
 
114
    /**
115
     * A list of see elements that appear in this doc comment.
116
     *
117
     * @var array(PHP_CodeSniffer_CommentParser_SingleElement)
118
     */
119
    protected $sees = array();
120
 
121
    /**
122
     * A list of see elements that appear in this doc comment.
123
     *
124
     * @var array(PHP_CodeSniffer_CommentParser_SingleElement)
125
     */
126
    protected $deprecated = null;
127
 
128
    /**
129
     * A list of see elements that appear in this doc comment.
130
     *
131
     * @var array(PHP_CodeSniffer_CommentParser_SingleElement)
132
     */
133
    protected $links = array();
134
 
135
    /**
136
     * A element to represent \@since tags.
137
     *
138
     * @var PHP_CodeSniffer_CommentParser_SingleElement
139
     */
140
    protected $since = null;
141
 
142
    /**
143
     * True if the comment has been parsed.
144
     *
145
     * @var boolean
146
     */
147
    private $_hasParsed = false;
148
 
149
    /**
150
     * The tags that this class can process.
151
     *
152
     * @var array(string)
153
     */
154
    private static $_tags = array(
155
                             'see'        => false,
156
                             'link'       => false,
157
                             'deprecated' => true,
158
                             'since'      => true,
159
                            );
160
 
161
    /**
162
     * An array of unknown tags.
163
     *
164
     * @var array(string)
165
     */
166
    public $unknown = array();
167
 
168
    /**
169
     * The order of tags.
170
     *
171
     * @var array(string)
172
     */
173
    public $orders = array();
174
 
175
 
176
    /**
177
     * Constructs a Doc Comment Parser.
178
     *
179
     * @param string               $comment   The comment to parse.
180
     * @param PHP_CodeSniffer_File $phpcsFile The file that this comment is in.
181
     */
182
    public function __construct($comment, PHP_CodeSniffer_File $phpcsFile)
183
    {
184
        $this->commentString = $comment;
185
        $this->phpcsFile     = $phpcsFile;
186
 
187
    }//end __construct()
188
 
189
 
190
    /**
191
     * Initiates the parsing of the doc comment.
192
     *
193
     * @return void
194
     * @throws PHP_CodeSniffer_CommentParser_ParserException If the parser finds a
195
     *                                                       problem with the
196
     *                                                       comment.
197
     */
198
    public function parse()
199
    {
200
        if ($this->_hasParsed === false) {
201
            $this->_parse($this->commentString);
202
        }
203
 
204
    }//end parse()
205
 
206
 
207
    /**
208
     * Parse the comment.
209
     *
210
     * @param string $comment The doc comment to parse.
211
     *
212
     * @return void
213
     * @see _parseWords()
214
     */
215
    private function _parse($comment)
216
    {
217
        // Firstly, remove the comment tags and any stars from the left side.
218
        $lines = explode($this->phpcsFile->eolChar, $comment);
219
        foreach ($lines as &$line) {
220
            $line = trim($line);
221
 
222
            if ($line !== '') {
223
                if (substr($line, 0, 3) === '/**') {
224
                    $line = substr($line, 3);
225
                } else if (substr($line, -2, 2) === '*/') {
226
                    $line = substr($line, 0, -2);
227
                } else if ($line{0} === '*') {
228
                    $line = substr($line, 1);
229
                }
230
 
231
                // Add the words to the stack, preserving newlines. Other parsers
232
                // might be interested in the spaces between words, so tokenize
233
                // spaces as well as separate tokens.
234
                $flags = (PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
235
                $words = preg_split(
236
                    '|(\s+)|',
237
                    $line.$this->phpcsFile->eolChar,
238
                    -1,
239
                    $flags
240
                );
241
 
242
                $this->words = array_merge($this->words, $words);
243
            }
244
        }//end foreach
245
 
246
        $this->_parseWords();
247
 
248
    }//end _parse()
249
 
250
 
251
    /**
252
     * Parses each word within the doc comment.
253
     *
254
     * @return void
255
     * @see _parse()
256
     * @throws PHP_CodeSniffer_CommentParser_ParserException If more than the allowed
257
     *                                                       number of occurances of
258
     *                                                       a tag is found.
259
     */
260
    private function _parseWords()
261
    {
262
        $allowedTags     = (self::$_tags + $this->getAllowedTags());
263
        $allowedTagNames = array_keys($allowedTags);
264
        $foundTags       = array();
265
        $prevTagPos      = false;
266
        $wordWasEmpty    = true;
267
 
268
        foreach ($this->words as $wordPos => $word) {
269
 
270
            if (trim($word) !== '') {
271
                $wordWasEmpty = false;
272
            }
273
 
274
            if ($word{0} === '@') {
275
 
276
                $tag = substr($word, 1);
277
 
278
                // Filter out @ tags in the comment description.
279
                // A real comment tag should have whitespace and a newline before it.
280
                if (isset($this->words[($wordPos - 1)]) === false
281
                    || trim($this->words[($wordPos - 1)]) !== ''
282
                ) {
283
                    continue;
284
                }
285
 
286
                if (isset($this->words[($wordPos - 2)]) === false
287
                    || $this->words[($wordPos - 2)] !== $this->phpcsFile->eolChar
288
                ) {
289
                    continue;
290
                }
291
 
292
                $foundTags[] = $tag;
293
 
294
                if ($prevTagPos !== false) {
295
                    // There was a tag before this so let's process it.
296
                    $prevTag = substr($this->words[$prevTagPos], 1);
297
                    $this->parseTag($prevTag, $prevTagPos, ($wordPos - 1));
298
                } else {
299
                    // There must have been a comment before this tag, so
300
                    // let's process that.
301
                    $this->parseTag('comment', 0, ($wordPos - 1));
302
                }
303
 
304
                $prevTagPos = $wordPos;
305
 
306
                if (in_array($tag, $allowedTagNames) === false) {
307
                    // This is not a tag that we process, but let's check to
308
                    // see if it is a tag we know about. If we don't know about it,
309
                    // we add it to a list of unknown tags.
310
                    $knownTags = array(
311
                                  'abstract',
312
                                  'access',
313
                                  'example',
314
                                  'filesource',
315
                                  'global',
316
                                  'ignore',
317
                                  'internal',
318
                                  'name',
319
                                  'static',
320
                                  'staticvar',
321
                                  'todo',
322
                                  'tutorial',
323
                                  'uses',
324
                                  'package_version@',
325
                                 );
326
 
327
                    if (in_array($tag, $knownTags) === false) {
328
                        $this->unknown[] = array(
329
                                            'tag'  => $tag,
330
                                            'line' => $this->getLine($wordPos),
331
                                           );
332
                    }
333
 
334
                }//end if
335
 
336
            }//end if
337
        }//end foreach
338
 
339
        // Only process this tag if there was something to process.
340
        if ($wordWasEmpty === false) {
341
            if ($prevTagPos === false) {
342
                // There must only be a comment in this doc comment.
343
                $this->parseTag('comment', 0, count($this->words));
344
            } else {
345
                // Process the last tag element.
346
                $prevTag  = substr($this->words[$prevTagPos], 1);
347
                $numWords = count($this->words);
348
                $endPos   = $numWords;
349
 
350
                if ($prevTag === 'package' || $prevTag === 'subpaackage') {
351
                    // These are single-word tags, so anything after a newline
352
                    // is really a comment.
353
                    for ($endPos = $prevTagPos; $endPos < $numWords; $endPos++) {
354
                        if (strpos($this->words[$endPos], $this->phpcsFile->eolChar) !== false) {
355
                            break;
356
                        }
357
                    }
358
                }
359
 
360
                $this->parseTag($prevTag, $prevTagPos, $endPos);
361
 
362
                if ($endPos !== $numWords) {
363
                    // Process the final comment, if it is not empty.
364
                    $tokens  = array_slice($this->words, ($endPos + 1), $numWords);
365
                    $content = implode('', $tokens);
366
                    if (trim($content) !== '') {
367
                        $this->parseTag('comment', ($endPos + 1), $numWords);
368
                    }
369
                }
370
            }//end if
371
        }//end if
372
 
373
    }//end _parseWords()
374
 
375
 
376
    /**
377
     * Returns the line that the token exists on in the doc comment.
378
     *
379
     * @param int $tokenPos The position in the words stack to find the line
380
     *                      number for.
381
     *
382
     * @return int
383
     */
384
    protected function getLine($tokenPos)
385
    {
386
        $newlines = 0;
387
        for ($i = 0; $i < $tokenPos; $i++) {
388
            $newlines += substr_count($this->phpcsFile->eolChar, $this->words[$i]);
389
        }
390
 
391
        return $newlines;
392
 
393
    }//end getLine()
394
 
395
 
396
    /**
397
     * Parses see tag element within the doc comment.
398
     *
399
     * @param array(string) $tokens The word tokens that comprise this element.
400
     *
401
     * @return DocElement The element that represents this see comment.
402
     */
403
    protected function parseSee($tokens)
404
    {
405
        $see = new PHP_CodeSniffer_CommentParser_SingleElement(
406
            $this->previousElement,
407
            $tokens,
408
            'see',
409
            $this->phpcsFile
410
        );
411
 
412
        $this->sees[] = $see;
413
        return $see;
414
 
415
    }//end parseSee()
416
 
417
 
418
    /**
419
     * Parses the comment element that appears at the top of the doc comment.
420
     *
421
     * @param array(string) $tokens The word tokens that comprise tihs element.
422
     *
423
     * @return DocElement The element that represents this comment element.
424
     */
425
    protected function parseComment($tokens)
426
    {
427
        $this->comment = new PHP_CodeSniffer_CommentParser_CommentElement(
428
            $this->previousElement,
429
            $tokens,
430
            $this->phpcsFile
431
        );
432
 
433
        return $this->comment;
434
 
435
    }//end parseComment()
436
 
437
 
438
    /**
439
     * Parses \@deprecated tags.
440
     *
441
     * @param array(string) $tokens The word tokens that comprise tihs element.
442
     *
443
     * @return DocElement The element that represents this deprecated tag.
444
     */
445
    protected function parseDeprecated($tokens)
446
    {
447
        $this->deprecated = new PHP_CodeSniffer_CommentParser_SingleElement(
448
            $this->previousElement,
449
            $tokens,
450
            'deprecated',
451
            $this->phpcsFile
452
        );
453
 
454
        return $this->deprecated;
455
 
456
    }//end parseDeprecated()
457
 
458
 
459
    /**
460
     * Parses \@since tags.
461
     *
462
     * @param array(string) $tokens The word tokens that comprise this element.
463
     *
464
     * @return SingleElement The element that represents this since tag.
465
     */
466
    protected function parseSince($tokens)
467
    {
468
        $this->since = new PHP_CodeSniffer_CommentParser_SingleElement(
469
            $this->previousElement,
470
            $tokens,
471
            'since',
472
            $this->phpcsFile
473
        );
474
 
475
        return $this->since;
476
 
477
    }//end parseSince()
478
 
479
 
480
    /**
481
     * Parses \@link tags.
482
     *
483
     * @param array(string) $tokens The word tokens that comprise this element.
484
     *
485
     * @return SingleElement The element that represents this link tag.
486
     */
487
    protected function parseLink($tokens)
488
    {
489
        $link = new PHP_CodeSniffer_CommentParser_SingleElement(
490
            $this->previousElement,
491
            $tokens,
492
            'link',
493
            $this->phpcsFile
494
        );
495
 
496
        $this->links[] = $link;
497
        return $link;
498
 
499
    }//end parseLink()
500
 
501
 
502
    /**
503
     * Returns the see elements that appear in this doc comment.
504
     *
505
     * @return array(SingleElement)
506
     */
507
    public function getSees()
508
    {
509
        return $this->sees;
510
 
511
    }//end getSees()
512
 
513
 
514
    /**
515
     * Returns the comment element that appears at the top of this doc comment.
516
     *
517
     * @return CommentElement
518
     */
519
    public function getComment()
520
    {
521
        return $this->comment;
522
 
523
    }//end getComment()
524
 
525
 
526
    /**
527
     * Returns the link elements found in this comment.
528
     *
529
     * Returns an empty array if no links are found in the comment.
530
     *
531
     * @return array(SingleElement)
532
     */
533
    public function getLinks()
534
    {
535
        return $this->links;
536
 
537
    }//end getLinks()
538
 
539
 
540
    /**
541
     * Returns the deprecated element found in this comment.
542
     *
543
     * Returns null if no element exists in the comment.
544
     *
545
     * @return SingleElement
546
     */
547
    public function getDeprecated()
548
    {
549
        return $this->deprecated;
550
 
551
    }//end getDeprecated()
552
 
553
 
554
    /**
555
     * Returns the since element found in this comment.
556
     *
557
     * Returns null if no element exists in the comment.
558
     *
559
     * @return SingleElement
560
     */
561
    public function getSince()
562
    {
563
        return $this->since;
564
 
565
    }//end getSince()
566
 
567
 
568
    /**
569
     * Parses the specified tag.
570
     *
571
     * @param string $tag   The tag name to parse (omitting the @ sybmol from
572
     *                      the tag)
573
     * @param int    $start The position in the word tokens where this element
574
     *                      started.
575
     * @param int    $end   The position in the word tokens where this element
576
     *                      ended.
577
     *
578
     * @return void
579
     * @throws Exception If the process method for the tag cannot be found.
580
     */
581
    protected function parseTag($tag, $start, $end)
582
    {
583
        $tokens = array_slice($this->words, ($start + 1), ($end - $start));
584
 
585
        $allowedTags     = (self::$_tags + $this->getAllowedTags());
586
        $allowedTagNames = array_keys($allowedTags);
587
        if ($tag === 'comment' || in_array($tag, $allowedTagNames) === true) {
588
            $method = 'parse'.$tag;
589
            if (method_exists($this, $method) === false) {
590
                $error = 'Method '.$method.' must be implemented to process '.$tag.' tags';
591
                throw new Exception($error);
592
            }
593
 
594
            $this->previousElement = $this->$method($tokens);
595
        } else {
596
            $this->previousElement = new PHP_CodeSniffer_CommentParser_SingleElement(
597
                $this->previousElement,
598
                $tokens,
599
                $tag,
600
                $this->phpcsFile
601
            );
602
        }
603
 
604
        $this->orders[] = $tag;
605
 
606
        if ($this->previousElement === null
607
            || ($this->previousElement instanceof PHP_CodeSniffer_CommentParser_DocElement) === false
608
        ) {
609
            throw new Exception('Parse method must return a DocElement');
610
        }
611
 
612
    }//end parseTag()
613
 
614
 
615
    /**
616
     * Returns a list of tags that this comment parser allows for it's comment.
617
     *
618
     * Each tag should indicate if only one entry of this tag can exist in the
619
     * comment by specifying true as the array value, or false if more than one
620
     * is allowed. Each tag should ommit the @ symbol. Only tags other than
621
     * the standard tags should be returned.
622
     *
623
     * @return array(string => boolean)
624
     */
625
    protected abstract function getAllowedTags();
626
 
627
 
628
    /**
629
     * Returns the tag orders (index => tagName).
630
     *
631
     * @return array
632
     */
633
    public function getTagOrders()
634
    {
635
        return $this->orders;
636
 
637
    }//end getTagOrders()
638
 
639
 
640
    /**
641
     * Returns the unknown tags.
642
     *
643
     * @return array
644
     */
645
    public function getUnknown()
646
    {
647
        return $this->unknown;
648
 
649
    }//end getUnknown()
650
 
651
 
652
}//end class
653
 
654
?>