1<?php
2/**
3 * Parses and verifies the file doc comment.
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-2014 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
16/**
17 * Parses and verifies the file doc comment.
18 *
19 * @category  PHP
20 * @package   PHP_CodeSniffer
21 * @author    Greg Sherwood <gsherwood@squiz.net>
22 * @author    Marc McIntyre <mmcintyre@squiz.net>
23 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
24 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
25 * @version   Release: @package_version@
26 * @link      http://pear.php.net/package/PHP_CodeSniffer
27 */
28
29class Squiz_Sniffs_Commenting_FileCommentSniff implements PHP_CodeSniffer_Sniff
30{
31
32    /**
33     * A list of tokenizers this sniff supports.
34     *
35     * @var array
36     */
37    public $supportedTokenizers = array(
38                                   'PHP',
39                                   'JS',
40                                  );
41
42
43    /**
44     * Returns an array of tokens this test wants to listen for.
45     *
46     * @return array
47     */
48    public function register()
49    {
50        return array(T_OPEN_TAG);
51
52    }//end register()
53
54
55    /**
56     * Processes this test, when one of its tokens is encountered.
57     *
58     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
59     * @param int                  $stackPtr  The position of the current token
60     *                                        in the stack passed in $tokens.
61     *
62     * @return int
63     */
64    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
65    {
66        $this->currentFile = $phpcsFile;
67
68        $tokens       = $phpcsFile->getTokens();
69        $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
70
71        if ($tokens[$commentStart]['code'] === T_COMMENT) {
72            $phpcsFile->addError('You must use "/**" style comments for a file comment', $commentStart, 'WrongStyle');
73            $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'yes');
74            return ($phpcsFile->numTokens + 1);
75        } else if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT_OPEN_TAG) {
76            $phpcsFile->addError('Missing file doc comment', $stackPtr, 'Missing');
77            $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'no');
78            return ($phpcsFile->numTokens + 1);
79        }
80
81        $commentEnd = $tokens[$commentStart]['comment_closer'];
82
83        $nextToken = $phpcsFile->findNext(
84            T_WHITESPACE,
85            ($commentEnd + 1),
86            null,
87            true
88        );
89
90        $ignore = array(
91                   T_CLASS,
92                   T_INTERFACE,
93                   T_TRAIT,
94                   T_FUNCTION,
95                   T_CLOSURE,
96                   T_PUBLIC,
97                   T_PRIVATE,
98                   T_PROTECTED,
99                   T_FINAL,
100                   T_STATIC,
101                   T_ABSTRACT,
102                   T_CONST,
103                   T_PROPERTY,
104                   T_INCLUDE,
105                   T_INCLUDE_ONCE,
106                   T_REQUIRE,
107                   T_REQUIRE_ONCE,
108                  );
109
110        if (in_array($tokens[$nextToken]['code'], $ignore) === true) {
111            $phpcsFile->addError('Missing file doc comment', $stackPtr, 'Missing');
112            $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'no');
113            return ($phpcsFile->numTokens + 1);
114        }
115
116        $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'yes');
117
118        // No blank line between the open tag and the file comment.
119        if ($tokens[$commentStart]['line'] > ($tokens[$stackPtr]['line'] + 1)) {
120            $error = 'There must be no blank lines before the file comment';
121            $phpcsFile->addError($error, $stackPtr, 'SpacingAfterOpen');
122        }
123
124        // Exactly one blank line after the file comment.
125        $next = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true);
126        if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2)) {
127            $error = 'There must be exactly one blank line after the file comment';
128            $phpcsFile->addError($error, $commentEnd, 'SpacingAfterComment');
129        }
130
131        // Required tags in correct order.
132        $required = array(
133                     '@package'    => true,
134                     '@subpackage' => true,
135                     '@author'     => true,
136                     '@copyright'  => true,
137                    );
138
139        $foundTags = array();
140        foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
141            $name       = $tokens[$tag]['content'];
142            $isRequired = isset($required[$name]);
143
144            if ($isRequired === true && in_array($name, $foundTags) === true) {
145                $error = 'Only one %s tag is allowed in a file comment';
146                $data  = array($name);
147                $phpcsFile->addError($error, $tag, 'Duplicate'.ucfirst(substr($name, 1)).'Tag', $data);
148            }
149
150            $foundTags[] = $name;
151
152            if ($isRequired === false) {
153                continue;
154            }
155
156            $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
157            if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
158                $error = 'Content missing for %s tag in file comment';
159                $data  = array($name);
160                $phpcsFile->addError($error, $tag, 'Empty'.ucfirst(substr($name, 1)).'Tag', $data);
161                continue;
162            }
163
164            if ($name === '@author') {
165                if ($tokens[$string]['content'] !== 'Squiz Pty Ltd <products@squiz.net>') {
166                    $error = 'Expected "Squiz Pty Ltd <products@squiz.net>" for author tag';
167                    $fix   = $phpcsFile->addFixableError($error, $tag, 'IncorrectAuthor');
168                    if ($fix === true) {
169                        $expected = 'Squiz Pty Ltd <products@squiz.net>';
170                        $phpcsFile->fixer->replaceToken($string, $expected);
171                    }
172                }
173            } else if ($name === '@copyright') {
174                if (preg_match('/^([0-9]{4})(-[0-9]{4})? (Squiz Pty Ltd \(ABN 77 084 670 600\))$/', $tokens[$string]['content']) === 0) {
175                    $error = 'Expected "xxxx-xxxx Squiz Pty Ltd (ABN 77 084 670 600)" for copyright declaration';
176                    $fix   = $phpcsFile->addFixableError($error, $tag, 'IncorrectCopyright');
177                    if ($fix === true) {
178                        $matches = array();
179                        preg_match('/^(([0-9]{4})(-[0-9]{4})?)?.*$/', $tokens[$string]['content'], $matches);
180                        if (isset($matches[1]) === false) {
181                            $matches[1] = date('Y');
182                        }
183
184                        $expected = $matches[1].' Squiz Pty Ltd (ABN 77 084 670 600)';
185                        $phpcsFile->fixer->replaceToken($string, $expected);
186                    }
187                }
188            }//end if
189        }//end foreach
190
191        // Check if the tags are in the correct position.
192        $pos = 0;
193        foreach ($required as $tag => $true) {
194            if (in_array($tag, $foundTags) === false) {
195                $error = 'Missing %s tag in file comment';
196                $data  = array($tag);
197                $phpcsFile->addError($error, $commentEnd, 'Missing'.ucfirst(substr($tag, 1)).'Tag', $data);
198            }
199
200            if (isset($foundTags[$pos]) === false) {
201                break;
202            }
203
204            if ($foundTags[$pos] !== $tag) {
205                $error = 'The tag in position %s should be the %s tag';
206                $data  = array(
207                          ($pos + 1),
208                          $tag,
209                         );
210                $phpcsFile->addError($error, $tokens[$commentStart]['comment_tags'][$pos], ucfirst(substr($tag, 1)).'TagOrder', $data);
211            }
212
213            $pos++;
214        }//end foreach
215
216        // Ignore the rest of the file.
217        return ($phpcsFile->numTokens + 1);
218
219    }//end process()
220
221
222}//end class
223