1<?php
2/**
3 * Parses and verifies the doc comments for files.
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-2012 Squiz Pty Ltd (ABN 77 084 670 600)
12 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
13 * @link      http://pear.php.net/package/PHP_CodeSniffer
14 */
15
16if (class_exists('PHP_CodeSniffer_CommentParser_ClassCommentParser', true) === false) {
17    throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_ClassCommentParser not found');
18}
19
20/**
21 * Parses and verifies the doc comments for files.
22 *
23 * Verifies that :
24 * <ul>
25 *  <li>A doc comment exists.</li>
26 *  <li>There is a blank newline after the short description.</li>
27 *  <li>There is a blank newline between the long and short description.</li>
28 *  <li>There is a blank newline between the long description and tags.</li>
29 *  <li>A PHP version is specified.</li>
30 *  <li>Check the order of the tags.</li>
31 *  <li>Check the indentation of each tag.</li>
32 *  <li>Check required and optional tags and the format of their content.</li>
33 * </ul>
34 *
35 * @category  PHP
36 * @package   PHP_CodeSniffer
37 * @author    Greg Sherwood <gsherwood@squiz.net>
38 * @author    Marc McIntyre <mmcintyre@squiz.net>
39 * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600)
40 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
41 * @version   Release: 1.5.1
42 * @link      http://pear.php.net/package/PHP_CodeSniffer
43 */
44
45class PhOSCo_Sniffs_Commenting_FileCommentSniff implements PHP_CodeSniffer_Sniff {
46
47    /**
48     * The header comment parser for the current file.
49     *
50     * @var PHP_CodeSniffer_Comment_Parser_ClassCommentParser
51     */
52    protected $commentParser = null;
53
54    /**
55     * The current PHP_CodeSniffer_File object we are processing.
56     *
57     * @var PHP_CodeSniffer_File
58     */
59    protected $currentFile = null;
60
61    /**
62     * Tags in correct order and related info.
63     *
64     * @var array
65     */
66    protected $tags = array(
67            'author' => array('required' => true, 'allow_multiple' => true,
68                              'order_text' => 'follows @subpackage (if used) or @package',),
69            'copyright' => array('required' => false, 'allow_multiple' => true, 'order_text' => 'follows @author',),
70            'license' => array('required' => true, 'allow_multiple' => false,
71                               'order_text' => 'follows @copyright (if used) or @author',),
72            'version' => array('required' => false, 'allow_multiple' => false, 'order_text' => 'follows @license',),
73            'see' => array('required' => false, 'allow_multiple' => true, 'order_text' => 'follows @link',),
74            'since' => array('required' => false, 'allow_multiple' => false,
75                             'order_text' => 'follows @see (if used) or @link',),
76            'deprecated' => array('required' => false, 'allow_multiple' => false,
77                                  'order_text' => 'follows @since (if used) or @see (if used) or @link',),);
78
79    /**
80     * Returns an array of tokens this test wants to listen for.
81     *
82     * @return array
83     */
84    public function register() {
85        return array(T_OPEN_TAG);
86
87    }//end register()
88
89    /**
90     * Processes this test, when one of its tokens is encountered.
91     *
92     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
93     * @param int                  $stackPtr  The position of the current token
94     *                                        in the stack passed in $tokens.
95     *
96     * @return void
97     */
98    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) {
99        $this->currentFile = $phpcsFile;
100
101        // We are only interested if this is the first open tag.
102        if ($stackPtr !== 0) {
103            if ($phpcsFile->findPrevious(T_OPEN_TAG, ($stackPtr - 1)) !== false) {
104                return;
105            }
106        }
107
108        $tokens = $phpcsFile->getTokens();
109
110        // Find the next non whitespace token.
111        $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
112
113        // Allow declare() statements at the top of the file.
114        if ($tokens[$commentStart]['code'] === T_DECLARE) {
115            $semicolon = $phpcsFile->findNext(T_SEMICOLON, ($commentStart + 1));
116            $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($semicolon + 1), null, true);
117        }
118
119        // Ignore vim header.
120        if ($tokens[$commentStart]['code'] === T_COMMENT) {
121            if (strstr($tokens[$commentStart]['content'], 'vim:') !== false) {
122                $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($commentStart + 1), null, true);
123            }
124        }
125
126        $errorToken = ($stackPtr + 1);
127        if (isset($tokens[$errorToken]) === false) {
128            $errorToken--;
129        }
130
131        if ($tokens[$commentStart]['code'] === T_CLOSE_TAG) {
132            // We are only interested if this is the first open tag.
133            return;
134        } else if ($tokens[$commentStart]['code'] === T_COMMENT) {
135            $error = 'You must use "/**" style comments for a file comment';
136            $phpcsFile->addError($error, $errorToken, 'WrongStyle');
137            return;
138        } else if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT) {
139            $phpcsFile->addError('Missing file doc comment', $errorToken, 'Missing');
140            return;
141        } else {
142
143            // Extract the header comment docblock.
144            $commentEnd = $phpcsFile->findNext(T_DOC_COMMENT, ($commentStart + 1), null, true);
145
146            $commentEnd--;
147
148            // Check if there is only 1 doc comment between the
149            // open tag and class token.
150            $nextToken = array(T_ABSTRACT, T_CLASS, T_FUNCTION, T_DOC_COMMENT,);
151
152            $commentNext = $phpcsFile->findNext($nextToken, ($commentEnd + 1));
153            if ($commentNext !== false && $tokens[$commentNext]['code'] !== T_DOC_COMMENT) {
154                // Found a class token right after comment doc block.
155                $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), $commentNext, false,
156                    $phpcsFile->eolChar);
157
158                if ($newlineToken !== false) {
159                    $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($newlineToken + 1), $commentNext, false,
160                        $phpcsFile->eolChar);
161
162                    if ($newlineToken === false) {
163                        // No blank line between the class token and the doc block.
164                        // The doc block is most likely a class comment.
165                        $error = 'Missing file doc comment';
166                        $phpcsFile->addError($error, $errorToken, 'Missing');
167                        return;
168                    }
169                }
170            }//end if
171
172            $comment = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
173
174            // Parse the header comment docblock.
175            try {
176                $this->commentParser = new PHP_CodeSniffer_CommentParser_ClassCommentParser($comment, $phpcsFile);
177                $this->commentParser->parse();
178            } catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
179                $line = ($e->getLineWithinComment() + $commentStart);
180                $phpcsFile->addError($e->getMessage(), $line, 'FailedParse');
181                return;
182            }
183
184            $comment = $this->commentParser->getComment();
185            if (is_null($comment) === true) {
186                $error = 'File doc comment is empty';
187                $phpcsFile->addError($error, $commentStart, 'Empty');
188                return;
189            }
190
191            // No extra newline before short description.
192            $short = $comment->getShortComment();
193            $newlineCount = 0;
194            $newlineSpan = strspn($short, $phpcsFile->eolChar);
195            if ($short !== '' && $newlineSpan > 0) {
196                $error = 'Extra newline(s) found before file comment short description';
197                $phpcsFile->addError($error, ($commentStart + 1), 'SpacingBefore');
198            }
199
200            $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
201
202            // Exactly one blank line between short and long description.
203            $long = $comment->getLongComment();
204            if (empty($long) === false) {
205                $between = $comment->getWhiteSpaceBetween();
206                $newlineBetween = substr_count($between, $phpcsFile->eolChar);
207                if ($newlineBetween !== 2) {
208                    $error = 'There must be exactly one blank line between descriptions in file comment';
209                    $phpcsFile->addError($error, ($commentStart + $newlineCount + 1), 'DescriptionSpacing');
210                }
211
212                $newlineCount += $newlineBetween;
213            }
214
215            // Exactly one blank line before tags.
216            $tags = $this->commentParser->getTagOrders();
217            if (count($tags) > 1) {
218                $newlineSpan = $comment->getNewlineAfter();
219                if ($newlineSpan !== 2) {
220                    $error = 'There must be exactly one blank line before the tags in file comment';
221                    if ($long !== '') {
222                        $newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
223                    }
224
225                    $phpcsFile->addError($error, ($commentStart + $newlineCount), 'SpacingBeforeTags');
226                    $short = rtrim($short, $phpcsFile->eolChar . ' ');
227                }
228            }
229
230            // Check the PHP Version.
231            $this->processPHPVersion($commentStart, $commentEnd, $long);
232
233            // Check each tag.
234            $this->processTags($commentStart, $commentEnd);
235        }//end if
236
237    }//end process()
238
239    /**
240     * Check that the PHP version is specified.
241     *
242     * @param int    $commentStart Position in the stack where the comment started.
243     * @param int    $commentEnd   Position in the stack where the comment ended.
244     * @param string $commentText  The text of the function comment.
245     *
246     * @return void
247     */
248    protected function processPHPVersion($commentStart, $commentEnd, $commentText) {
249        if (strstr(strtolower($commentText), 'php version') === false) {
250            $error = 'PHP version not specified';
251            $this->currentFile->addWarning($error, $commentEnd, 'MissingVersion');
252        }
253
254    }//end processPHPVersion()
255
256    /**
257     * Processes each required or optional tag.
258     *
259     * @param int $commentStart Position in the stack where the comment started.
260     * @param int $commentEnd   Position in the stack where the comment ended.
261     *
262     * @return void
263     */
264    protected function processTags($commentStart, $commentEnd) {
265        $docBlock = (get_class($this) === 'PhOSCo_Sniffs_Commenting_FileCommentSniff') ? 'file' : 'class';
266        $foundTags = $this->commentParser->getTagOrders();
267        $orderIndex = 0;
268        $indentation = array();
269        $longestTag = 0;
270        $errorPos = 0;
271
272        foreach ($this->tags as $tag => $info) {
273
274            // Required tag missing.
275            if ($info['required'] === true && in_array($tag, $foundTags) === false) {
276                $error = 'Missing @%s tag in %s comment';
277                $data = array($tag, $docBlock,);
278                $this->currentFile->addError($error, $commentEnd, 'MissingTag', $data);
279                continue;
280            }
281
282            // Get the line number for current tag.
283            $tagName = ucfirst($tag);
284            if ($info['allow_multiple'] === true) {
285                $tagName .= 's';
286            }
287
288            $getMethod = 'get' . $tagName;
289            $tagElement = $this->commentParser->$getMethod();
290            if (is_null($tagElement) === true || empty($tagElement) === true) {
291                continue;
292            }
293
294            $errorPos = $commentStart;
295            if (is_array($tagElement) === false) {
296                $errorPos = ($commentStart + $tagElement->getLine());
297            }
298
299            // Get the tag order.
300            $foundIndexes = array_keys($foundTags, $tag);
301
302            if (count($foundIndexes) > 1) {
303                // Multiple occurrence not allowed.
304                if ($info['allow_multiple'] === false) {
305                    $error = 'Only 1 @%s tag is allowed in a %s comment';
306                    $data = array($tag, $docBlock,);
307                    $this->currentFile->addError($error, $errorPos, 'DuplicateTag', $data);
308                } else {
309                    // Make sure same tags are grouped together.
310                    $i = 0;
311                    $count = $foundIndexes[0];
312                    foreach ($foundIndexes as $index) {
313                        if ($index !== $count) {
314                            $errorPosIndex = ($errorPos + $tagElement[$i]->getLine());
315                            $error = '@%s tags must be grouped together';
316                            $data = array($tag);
317                            $this->currentFile->addError($error, $errorPosIndex, 'TagsNotGrouped', $data);
318                        }
319
320                        $i++;
321                        $count++;
322                    }
323                }
324            }//end if
325
326            // Check tag order.
327            if ($foundIndexes[0] > $orderIndex) {
328                $orderIndex = $foundIndexes[0];
329            } else {
330                if (is_array($tagElement) === true && empty($tagElement) === false) {
331                    $errorPos += $tagElement[0]->getLine();
332                }
333
334                $error = 'The @%s tag is in the wrong order; the tag %s';
335                $data = array($tag, $info['order_text'],);
336                $this->currentFile->addError($error, $errorPos, 'WrongTagOrder', $data);
337            }
338
339            // Store the indentation for checking.
340            $len = strlen($tag);
341            if ($len > $longestTag) {
342                $longestTag = $len;
343            }
344
345            if (is_array($tagElement) === true) {
346                foreach ($tagElement as $key => $element) {
347                    $indentation[] = array('tag' => $tag, 'space' => $this->getIndentation($tag, $element),
348                                           'line' => $element->getLine(),);
349                }
350            } else {
351                $indentation[] = array('tag' => $tag, 'space' => $this->getIndentation($tag, $tagElement),);
352            }
353
354            $method = 'process' . $tagName;
355            if (method_exists($this, $method) === true) {
356                // Process each tag if a method is defined.
357                call_user_func(array($this, $method), $errorPos);
358            } else {
359                if (is_array($tagElement) === true) {
360                    foreach ($tagElement as $key => $element) {
361                        $element->process($this->currentFile, $commentStart, $docBlock);
362                    }
363                } else {
364                    $tagElement->process($this->currentFile, $commentStart, $docBlock);
365                }
366            }
367        }//end foreach
368
369        foreach ($indentation as $indentInfo) {
370            if ($indentInfo['space'] !== 0 && $indentInfo['space'] !== ($longestTag + 1)) {
371                $expected = (($longestTag - strlen($indentInfo['tag'])) + 1);
372                $space = ($indentInfo['space'] - strlen($indentInfo['tag']));
373                $error = '@%s tag comment indented incorrectly; expected %s spaces but found %s';
374                $data = array($indentInfo['tag'], $expected, $space,);
375
376                $getTagMethod = 'get' . ucfirst($indentInfo['tag']);
377
378                if ($this->tags[$indentInfo['tag']]['allow_multiple'] === true) {
379                    $line = $indentInfo['line'];
380                } else {
381                    $tagElem = $this->commentParser->$getTagMethod();
382                    $line = $tagElem->getLine();
383                }
384
385                $this->currentFile->addError($error, ($commentStart + $line), 'TagIndent', $data);
386            }
387        }
388
389    }//end processTags()
390
391    /**
392     * Get the indentation information of each tag.
393     *
394     * @param string                                   $tagName    The name of the
395     *                                                             doc comment
396     *                                                             element.
397     * @param PHP_CodeSniffer_CommentParser_DocElement $tagElement The doc comment
398     *                                                             element.
399     *
400     * @return void
401     */
402    protected function getIndentation($tagName, $tagElement) {
403        if ($tagElement instanceof PHP_CodeSniffer_CommentParser_SingleElement) {
404            if ($tagElement->getContent() !== '') {
405                return (strlen($tagName) + substr_count($tagElement->getWhitespaceBeforeContent(), ' '));
406            }
407        } else if ($tagElement instanceof PHP_CodeSniffer_CommentParser_PairElement) {
408            if ($tagElement->getValue() !== '') {
409                return (strlen($tagName) + substr_count($tagElement->getWhitespaceBeforeValue(), ' '));
410            }
411        }
412
413        return 0;
414
415    }//end getIndentation()
416
417    /**
418     * Process the category tag.
419     *
420     * @param int $errorPos The line number where the error occurs.
421     *
422     * @return void
423     */
424    protected function processCategory($errorPos) {
425        $category = $this->commentParser->getCategory();
426        if ($category !== null) {
427            $content = $category->getContent();
428            if ($content !== '') {
429                if (PHP_CodeSniffer::isUnderscoreName($content) !== true) {
430                    $newContent = str_replace(' ', '_', $content);
431                    $nameBits = explode('_', $newContent);
432                    $firstBit = array_shift($nameBits);
433                    $newName = ucfirst($firstBit) . '_';
434                    foreach ($nameBits as $bit) {
435                        $newName .= ucfirst($bit) . '_';
436                    }
437
438                    $error = 'Category name "%s" is not valid; consider "%s" instead';
439                    $validName = trim($newName, '_');
440                    $data = array($content, $validName,);
441                    $this->currentFile->addError($error, $errorPos, 'InvalidCategory', $data);
442                }
443            } else {
444                $error = '@category tag must contain a name';
445                $this->currentFile->addError($error, $errorPos, 'EmptyCategory');
446            }
447        }
448
449    }//end processCategory()
450
451    /**
452     * Process the package tag.
453     *
454     * @param int $errorPos The line number where the error occurs.
455     *
456     * @return void
457     */
458    protected function processPackage($errorPos) {
459        $package = $this->commentParser->getPackage();
460        if ($package === null) {
461            return;
462        }
463
464        $content = $package->getContent();
465        if ($content === '') {
466            $error = '@package tag must contain a name';
467            $this->currentFile->addError($error, $errorPos, 'EmptyPackage');
468            return;
469        }
470
471        if (PHP_CodeSniffer::isUnderscoreName($content) === true) {
472            return;
473        }
474
475        $newContent = str_replace(' ', '_', $content);
476        $newContent = preg_replace('/[^A-Za-z_]/', '', $newContent);
477        $nameBits = explode('_', $newContent);
478        $firstBit = array_shift($nameBits);
479        $newName = strtoupper($firstBit{0}) . substr($firstBit, 1) . '_';
480        foreach ($nameBits as $bit) {
481            $newName .= strtoupper($bit{0}) . substr($bit, 1) . '_';
482        }
483
484        $error = 'Package name "%s" is not valid; consider "%s" instead';
485        $validName = trim($newName, '_');
486        $data = array($content, $validName,);
487        $this->currentFile->addError($error, $errorPos, 'InvalidPackage', $data);
488
489    }//end processPackage()
490
491    /**
492     * Process the subpackage tag.
493     *
494     * @param int $errorPos The line number where the error occurs.
495     *
496     * @return void
497     */
498    protected function processSubpackage($errorPos) {
499        $package = $this->commentParser->getSubpackage();
500        if ($package !== null) {
501            $content = $package->getContent();
502            if ($content !== '') {
503                if (PHP_CodeSniffer::isUnderscoreName($content) !== true) {
504                    $newContent = str_replace(' ', '_', $content);
505                    $nameBits = explode('_', $newContent);
506                    $firstBit = array_shift($nameBits);
507                    $newName = strtoupper($firstBit{0}) . substr($firstBit, 1) . '_';
508                    foreach ($nameBits as $bit) {
509                        $newName .= strtoupper($bit{0}) . substr($bit, 1) . '_';
510                    }
511
512                    $error = 'Subpackage name "%s" is not valid; consider "%s" instead';
513                    $validName = trim($newName, '_');
514                    $data = array($content, $validName,);
515                    $this->currentFile->addError($error, $errorPos, 'InvalidSubpackage', $data);
516                }
517            } else {
518                $error = '@subpackage tag must contain a name';
519                $this->currentFile->addError($error, $errorPos, 'EmptySubpackage');
520            }
521        }
522
523    }//end processSubpackage()
524
525    /**
526     * Process the author tag(s) that this header comment has.
527     *
528     * This function is different from other _process functions
529     * as $authors is an array of SingleElements, so we work out
530     * the errorPos for each element separately
531     *
532     * @param int $commentStart The position in the stack where
533     *                          the comment started.
534     *
535     * @return void
536     */
537    protected function processAuthors($commentStart) {
538        $authors = $this->commentParser->getAuthors();
539        // Report missing return.
540        if (empty($authors) === false) {
541            foreach ($authors as $author) {
542                $errorPos = ($commentStart + $author->getLine());
543                $content = $author->getContent();
544                if ($content !== '') {
545                    $local = '\da-zA-Z-_+';
546                    // Dot character cannot be the first or last character
547                    // in the local-part.
548                    $localMiddle = $local . '.\w';
549                    if (preg_match(
550                        '/^([^<]*)\s+<([' . $local . ']([' . $localMiddle . ']*[' . $local
551                            . '])*@[\da-zA-Z][-.\w]*[\da-zA-Z]\.[a-zA-Z]{2,7})>$/', $content) === 0) {
552                        $error = 'Content of the @author tag must be in the form "Display Name <username@example.com>"';
553                        $this->currentFile->addError($error, $errorPos, 'InvalidAuthors');
554                    }
555                } else {
556                    $error = 'Content missing for @author tag in %s comment';
557                    $docBlock = (get_class($this) === 'PhOSCo_Sniffs_Commenting_FileCommentSniff') ? 'file' : 'class';
558                    $data = array($docBlock);
559                    $this->currentFile->addError($error, $errorPos, 'EmptyAuthors', $data);
560                }
561            }
562        }
563
564    }//end processAuthors()
565
566    /**
567     * Process the copyright tags.
568     *
569     * @param int $commentStart The position in the stack where
570     *                          the comment started.
571     *
572     * @return void
573     */
574    protected function processCopyrights($commentStart) {
575        $copyrights = $this->commentParser->getCopyrights();
576        foreach ($copyrights as $copyright) {
577            $errorPos = ($commentStart + $copyright->getLine());
578            $content = $copyright->getContent();
579            if ($content !== '') {
580                $matches = array();
581                if (preg_match('/^([0-9]{4})((.{1})([0-9]{4}))? (.+)$/', $content, $matches) !== 0) {
582                    // Check earliest-latest year order.
583                    if ($matches[3] !== '') {
584                        if ($matches[3] !== '-') {
585                            $error = 'A hyphen must be used between the earliest and latest year';
586                            $this->currentFile->addError($error, $errorPos, 'CopyrightHyphen');
587                        }
588
589                        if ($matches[4] !== '' && $matches[4] < $matches[1]) {
590                            $error = "Invalid year span \"$matches[1]$matches[3]$matches[4]\" found; consider \"$matches[4]-$matches[1]\" instead";
591                            $this->currentFile->addWarning($error, $errorPos, 'InvalidCopyright');
592                        }
593                    }
594                } else {
595                    $error = '@copyright tag must contain a year and the name of the copyright holder';
596                    $this->currentFile->addError($error, $errorPos, 'EmptyCopyright');
597                }
598            } else {
599                $error = '@copyright tag must contain a year and the name of the copyright holder';
600                $this->currentFile->addError($error, $errorPos, 'EmptyCopyright');
601            }//end if
602        }//end if
603
604    }//end processCopyrights()
605
606    /**
607     * Process the license tag.
608     *
609     * @param int $errorPos The line number where the error occurs.
610     *
611     * @return void
612     */
613    protected function processLicense($errorPos) {
614        $license = $this->commentParser->getLicense();
615        if ($license !== null) {
616            $value = $license->getValue();
617            $comment = $license->getComment();
618            if ($value === '' || $comment === '') {
619                $error = '@license tag must contain a URL and a license name';
620                $this->currentFile->addError($error, $errorPos, 'EmptyLicense');
621            }
622        }
623
624    }//end processLicense()
625
626    /**
627     * Process the version tag.
628     *
629     * @param int $errorPos The line number where the error occurs.
630     *
631     * @return void
632     */
633    protected function processVersion($errorPos) {
634        $version = $this->commentParser->getVersion();
635        if ($version !== null) {
636            $content = $version->getContent();
637            $matches = array();
638            if (empty($content) === true) {
639                $error = 'Content missing for @version tag in file comment';
640                $this->currentFile->addError($error, $errorPos, 'EmptyVersion');
641            } else if (strstr($content, 'CVS:') === false && strstr($content, 'SVN:') === false
642                && strstr($content, 'GIT:') === false && strstr($content, 'HG:') === false) {
643                $error = 'Invalid version "%s" in file comment; consider "CVS: <cvs_id>" or "SVN: <svn_id>" or "GIT: <git_id>" or "HG: <hg_id>" instead';
644                $data = array($content);
645                $this->currentFile->addWarning($error, $errorPos, 'InvalidVersion', $data);
646            }
647        }
648
649    }//end processVersion()
650
651}//end class
652
653?>
654