1<?php
2/**
3 * Squiz_Sniffs_ControlStructures_LongConditionClosingCommentSniff.
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_ControlStructures_LongConditionClosingCommentSniff.
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 */
28class Squiz_Sniffs_Commenting_LongConditionClosingCommentSniff implements PHP_CodeSniffer_Sniff
29{
30
31    /**
32     * A list of tokenizers this sniff supports.
33     *
34     * @var array
35     */
36    public $supportedTokenizers = array(
37                                   'PHP',
38                                   'JS',
39                                  );
40
41    /**
42     * The openers that we are interested in.
43     *
44     * @var array(int)
45     */
46    private static $_openers = array(
47                                T_SWITCH,
48                                T_IF,
49                                T_FOR,
50                                T_FOREACH,
51                                T_WHILE,
52                                T_TRY,
53                                T_CASE,
54                               );
55
56    /**
57     * The length that a code block must be before
58     * requiring a closing comment.
59     *
60     * @var int
61     */
62    public $lineLimit = 20;
63
64    /**
65     * The format the end comment should be in.
66     *
67     * The placeholder %s will be replaced with the type of condition opener.
68     *
69     * @var string
70     */
71    public $commentFormat = '//end %s';
72
73
74    /**
75     * Returns an array of tokens this test wants to listen for.
76     *
77     * @return array
78     */
79    public function register()
80    {
81        return array(T_CLOSE_CURLY_BRACKET);
82
83    }//end register()
84
85
86    /**
87     * Processes this test, when one of its tokens is encountered.
88     *
89     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
90     * @param int                  $stackPtr  The position of the current token in the
91     *                                        stack passed in $tokens.
92     *
93     * @return void
94     */
95    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
96    {
97        $tokens = $phpcsFile->getTokens();
98
99        if (isset($tokens[$stackPtr]['scope_condition']) === false) {
100            // No scope condition. It is a function closer.
101            return;
102        }
103
104        $startCondition = $tokens[$tokens[$stackPtr]['scope_condition']];
105        $startBrace     = $tokens[$tokens[$stackPtr]['scope_opener']];
106        $endBrace       = $tokens[$stackPtr];
107
108        // We are only interested in some code blocks.
109        if (in_array($startCondition['code'], self::$_openers) === false) {
110            return;
111        }
112
113        if ($startCondition['code'] === T_IF) {
114            // If this is actually an ELSE IF, skip it as the brace
115            // will be checked by the original IF.
116            $else = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$stackPtr]['scope_condition'] - 1), null, true);
117            if ($tokens[$else]['code'] === T_ELSE) {
118                return;
119            }
120
121            // IF statements that have an ELSE block need to use
122            // "end if" rather than "end else" or "end elseif".
123            do {
124                $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
125                if ($tokens[$nextToken]['code'] === T_ELSE || $tokens[$nextToken]['code'] === T_ELSEIF) {
126                    // Check for ELSE IF (2 tokens) as opposed to ELSEIF (1 token).
127                    if ($tokens[$nextToken]['code'] === T_ELSE
128                        && isset($tokens[$nextToken]['scope_closer']) === false
129                    ) {
130                        $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($nextToken + 1), null, true);
131                        if ($tokens[$nextToken]['code'] !== T_IF
132                            || isset($tokens[$nextToken]['scope_closer']) === false
133                        ) {
134                            // Not an ELSE IF or is an inline ELSE IF.
135                            break;
136                        }
137                    }
138
139                    if (isset($tokens[$nextToken]['scope_closer']) === false) {
140                        // There isn't going to be anywhere to print the "end if" comment
141                        // because there is no closer.
142                        return;
143                    }
144
145                    // The end brace becomes the ELSE's end brace.
146                    $stackPtr = $tokens[$nextToken]['scope_closer'];
147                    $endBrace = $tokens[$stackPtr];
148                } else {
149                    break;
150                }//end if
151            } while (isset($tokens[$nextToken]['scope_closer']) === true);
152        }//end if
153
154        if ($startCondition['code'] === T_TRY) {
155            // TRY statements need to check until the end of all CATCH statements.
156            do {
157                $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
158                if ($tokens[$nextToken]['code'] === T_CATCH) {
159                    // The end brace becomes the CATCH's end brace.
160                    $stackPtr = $tokens[$nextToken]['scope_closer'];
161                    $endBrace = $tokens[$stackPtr];
162                } else {
163                    break;
164                }
165            } while (isset($tokens[$nextToken]['scope_closer']) === true);
166        }
167
168        $lineDifference = ($endBrace['line'] - $startBrace['line']);
169
170        $expected = sprintf($this->commentFormat, $startCondition['content']);
171        $comment  = $phpcsFile->findNext(array(T_COMMENT), $stackPtr, null, false);
172
173        if (($comment === false) || ($tokens[$comment]['line'] !== $endBrace['line'])) {
174            if ($lineDifference >= $this->lineLimit) {
175                $error = 'End comment for long condition not found; expected "%s"';
176                $data  = array($expected);
177                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'Missing', $data);
178
179                if ($fix === true) {
180                    $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
181                    if ($next !== false && $tokens[$next]['line'] === $tokens[$stackPtr]['line']) {
182                        $expected .= $phpcsFile->eolChar;
183                    }
184
185                    $phpcsFile->fixer->addContent($stackPtr, $expected);
186                }
187            }
188
189            return;
190        }
191
192        if (($comment - $stackPtr) !== 1) {
193            $error = 'Space found before closing comment; expected "%s"';
194            $data  = array($expected);
195            $phpcsFile->addError($error, $stackPtr, 'SpacingBefore', $data);
196        }
197
198        if (trim($tokens[$comment]['content']) !== $expected) {
199            $found = trim($tokens[$comment]['content']);
200            $error = 'Incorrect closing comment; expected "%s" but found "%s"';
201            $data  = array(
202                      $expected,
203                      $found,
204                     );
205
206            $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Invalid', $data);
207            if ($fix === true) {
208                $phpcsFile->fixer->replaceToken($comment, $expected.$phpcsFile->eolChar);
209            }
210
211            return;
212        }
213
214    }//end process()
215
216
217}//end class
218