1<?php
2/**
3 * Squiz_Sniffs_Commenting_InlineCommentSniff.
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 * Squiz_Sniffs_Commenting_InlineCommentSniff.
18 *
19 * Checks that there is adequate spacing between comments.
20 *
21 * @category  PHP
22 * @package   PHP_CodeSniffer
23 * @author    Greg Sherwood <gsherwood@squiz.net>
24 * @author    Marc McIntyre <mmcintyre@squiz.net>
25 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
26 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
27 * @version   Release: @package_version@
28 * @link      http://pear.php.net/package/PHP_CodeSniffer
29 */
30class Squiz_Sniffs_Commenting_InlineCommentSniff implements PHP_CodeSniffer_Sniff
31{
32
33    /**
34     * A list of tokenizers this sniff supports.
35     *
36     * @var array
37     */
38    public $supportedTokenizers = array(
39                                   'PHP',
40                                   'JS',
41                                  );
42
43
44    /**
45     * Returns an array of tokens this test wants to listen for.
46     *
47     * @return array
48     */
49    public function register()
50    {
51        return array(
52                T_COMMENT,
53                T_DOC_COMMENT_OPEN_TAG,
54               );
55
56    }//end register()
57
58
59    /**
60     * Processes this test, when one of its tokens is encountered.
61     *
62     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
63     * @param int                  $stackPtr  The position of the current token in the
64     *                                        stack passed in $tokens.
65     *
66     * @return void
67     */
68    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
69    {
70        $tokens = $phpcsFile->getTokens();
71
72        // If this is a function/class/interface doc block comment, skip it.
73        // We are only interested in inline doc block comments, which are
74        // not allowed.
75        if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) {
76            $nextToken = $phpcsFile->findNext(
77                PHP_CodeSniffer_Tokens::$emptyTokens,
78                ($stackPtr + 1),
79                null,
80                true
81            );
82
83            $ignore = array(
84                       T_CLASS,
85                       T_INTERFACE,
86                       T_TRAIT,
87                       T_FUNCTION,
88                       T_CLOSURE,
89                       T_PUBLIC,
90                       T_PRIVATE,
91                       T_PROTECTED,
92                       T_FINAL,
93                       T_STATIC,
94                       T_ABSTRACT,
95                       T_CONST,
96                       T_PROPERTY,
97                       T_INCLUDE,
98                       T_INCLUDE_ONCE,
99                       T_REQUIRE,
100                       T_REQUIRE_ONCE,
101                      );
102
103            if (in_array($tokens[$nextToken]['code'], $ignore) === true) {
104                return;
105            }
106
107            if ($phpcsFile->tokenizerType === 'JS') {
108                // We allow block comments if a function or object
109                // is being assigned to a variable.
110                $ignore    = PHP_CodeSniffer_Tokens::$emptyTokens;
111                $ignore[]  = T_EQUAL;
112                $ignore[]  = T_STRING;
113                $ignore[]  = T_OBJECT_OPERATOR;
114                $nextToken = $phpcsFile->findNext($ignore, ($nextToken + 1), null, true);
115                if ($tokens[$nextToken]['code'] === T_FUNCTION
116                    || $tokens[$nextToken]['code'] === T_CLOSURE
117                    || $tokens[$nextToken]['code'] === T_OBJECT
118                    || $tokens[$nextToken]['code'] === T_PROTOTYPE
119                ) {
120                    return;
121                }
122            }
123
124            $prevToken = $phpcsFile->findPrevious(
125                PHP_CodeSniffer_Tokens::$emptyTokens,
126                ($stackPtr - 1),
127                null,
128                true
129            );
130
131            if ($tokens[$prevToken]['code'] === T_OPEN_TAG) {
132                return;
133            }
134
135            if ($tokens[$stackPtr]['content'] === '/**') {
136                $error = 'Inline doc block comments are not allowed; use "/* Comment */" or "// Comment" instead';
137                $phpcsFile->addError($error, $stackPtr, 'DocBlock');
138            }
139        }//end if
140
141        if ($tokens[$stackPtr]['content']{0} === '#') {
142            $error = 'Perl-style comments are not allowed; use "// Comment" instead';
143            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'WrongStyle');
144            if ($fix === true) {
145                $comment = ltrim($tokens[$stackPtr]['content'], "# \t");
146                $phpcsFile->fixer->replaceToken($stackPtr, "// $comment");
147            }
148        }
149
150        // We don't want end of block comments. If the last comment is a closing
151        // curly brace.
152        $previousContent = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
153        if ($tokens[$previousContent]['line'] === $tokens[$stackPtr]['line']) {
154            if ($tokens[$previousContent]['code'] === T_CLOSE_CURLY_BRACKET) {
155                return;
156            }
157
158            // Special case for JS files.
159            if ($tokens[$previousContent]['code'] === T_COMMA
160                || $tokens[$previousContent]['code'] === T_SEMICOLON
161            ) {
162                $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($previousContent - 1), null, true);
163                if ($tokens[$lastContent]['code'] === T_CLOSE_CURLY_BRACKET) {
164                    return;
165                }
166            }
167        }
168
169        $comment = rtrim($tokens[$stackPtr]['content']);
170
171        // Only want inline comments.
172        if (substr($comment, 0, 2) !== '//') {
173            return;
174        }
175
176        if (trim(substr($comment, 2)) !== '') {
177            $spaceCount = 0;
178            $tabFound   = false;
179
180            $commentLength = strlen($comment);
181            for ($i = 2; $i < $commentLength; $i++) {
182                if ($comment[$i] === "\t") {
183                    $tabFound = true;
184                    break;
185                }
186
187                if ($comment[$i] !== ' ') {
188                    break;
189                }
190
191                $spaceCount++;
192            }
193
194            $fix = false;
195            if ($tabFound === true) {
196                $error = 'Tab found before comment text; expected "// %s" but found "%s"';
197                $data  = array(
198                          ltrim(substr($comment, 2)),
199                          $comment,
200                         );
201                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'TabBefore', $data);
202            } else if ($spaceCount === 0) {
203                $error = 'No space found before comment text; expected "// %s" but found "%s"';
204                $data  = array(
205                          substr($comment, 2),
206                          $comment,
207                         );
208                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceBefore', $data);
209            } else if ($spaceCount > 1) {
210                $error = 'Expected 1 space before comment text but found %s; use block comment if you need indentation';
211                $data  = array(
212                          $spaceCount,
213                          substr($comment, (2 + $spaceCount)),
214                          $comment,
215                         );
216                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBefore', $data);
217            }//end if
218
219            if ($fix === true) {
220                $newComment = '// '.ltrim($tokens[$stackPtr]['content'], "/\t ");
221                $phpcsFile->fixer->replaceToken($stackPtr, $newComment);
222            }
223        }//end if
224
225        // The below section determines if a comment block is correctly capitalised,
226        // and ends in a full-stop. It will find the last comment in a block, and
227        // work its way up.
228        $nextComment = $phpcsFile->findNext(array(T_COMMENT), ($stackPtr + 1), null, false);
229        if (($nextComment !== false)
230            && (($tokens[$nextComment]['line']) === ($tokens[$stackPtr]['line'] + 1))
231        ) {
232            return;
233        }
234
235        $topComment  = $stackPtr;
236        $lastComment = $stackPtr;
237        while (($topComment = $phpcsFile->findPrevious(array(T_COMMENT), ($lastComment - 1), null, false)) !== false) {
238            if ($tokens[$topComment]['line'] !== ($tokens[$lastComment]['line'] - 1)) {
239                break;
240            }
241
242            $lastComment = $topComment;
243        }
244
245        $topComment  = $lastComment;
246        $commentText = '';
247
248        for ($i = $topComment; $i <= $stackPtr; $i++) {
249            if ($tokens[$i]['code'] === T_COMMENT) {
250                $commentText .= trim(substr($tokens[$i]['content'], 2));
251            }
252        }
253
254        if ($commentText === '') {
255            $error = 'Blank comments are not allowed';
256            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'Empty');
257            if ($fix === true) {
258                $phpcsFile->fixer->replaceToken($stackPtr, '');
259            }
260
261            return;
262        }
263
264        if (preg_match('/^\p{Ll}/u', $commentText) === 1) {
265            $error = 'Inline comments must start with a capital letter';
266            $phpcsFile->addError($error, $topComment, 'NotCapital');
267        }
268
269        // Only check the end of comment character if the start of the comment
270        // is a letter, indicating that the comment is just standard text.
271        if (preg_match('/^\p{L}/u', $commentText) === 1) {
272            $commentCloser   = $commentText[(strlen($commentText) - 1)];
273            $acceptedClosers = array(
274                                'full-stops'        => '.',
275                                'exclamation marks' => '!',
276                                'or question marks' => '?',
277                               );
278
279            if (in_array($commentCloser, $acceptedClosers) === false) {
280                $error = 'Inline comments must end in %s';
281                $ender = '';
282                foreach ($acceptedClosers as $closerName => $symbol) {
283                    $ender .= ' '.$closerName.',';
284                }
285
286                $ender = trim($ender, ' ,');
287                $data  = array($ender);
288                $phpcsFile->addError($error, $stackPtr, 'InvalidEndChar', $data);
289            }
290        }
291
292        // Finally, the line below the last comment cannot be empty if this inline
293        // comment is on a line by itself.
294        if ($tokens[$previousContent]['line'] < $tokens[$stackPtr]['line']) {
295            $start = false;
296            for ($i = ($stackPtr + 1); $i < $phpcsFile->numTokens; $i++) {
297                if ($tokens[$i]['line'] === ($tokens[$stackPtr]['line'] + 1)) {
298                    if ($tokens[$i]['code'] !== T_WHITESPACE) {
299                        return;
300                    }
301                } else if ($tokens[$i]['line'] > ($tokens[$stackPtr]['line'] + 1)) {
302                    break;
303                }
304            }
305
306            $error = 'There must be no blank line following an inline comment';
307            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingAfter');
308            if ($fix === true) {
309                $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
310                $phpcsFile->fixer->beginChangeset();
311                for ($i = ($stackPtr + 1); $i < $next; $i++) {
312                    if ($tokens[$i]['line'] === $tokens[$next]['line']) {
313                        break;
314                    }
315
316                    $phpcsFile->fixer->replaceToken($i, '');
317                }
318
319                $phpcsFile->fixer->endChangeset();
320            }
321        }//end if
322
323    }//end process()
324
325
326}//end class
327