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