1<?php
2/**
3 * Squiz_Sniffs_PHP_EmbeddedPhpSniff.
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_PHP_EmbeddedPhpSniff.
18 *
19 * Checks the indentation of embedded PHP code segments.
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_PHP_EmbeddedPhpSniff implements PHP_CodeSniffer_Sniff
31{
32
33
34    /**
35     * Returns an array of tokens this test wants to listen for.
36     *
37     * @return array
38     */
39    public function register()
40    {
41        return array(T_OPEN_TAG);
42
43    }//end register()
44
45
46    /**
47     * Processes this test, when one of its tokens is encountered.
48     *
49     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
50     * @param int                  $stackPtr  The position of the current token in the
51     *                                        stack passed in $tokens.
52     *
53     * @return void
54     */
55    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
56    {
57        $tokens = $phpcsFile->getTokens();
58
59        // If the close php tag is on the same line as the opening
60        // then we have an inline embedded PHP block.
61        $closeTag = $phpcsFile->findNext(T_CLOSE_TAG, $stackPtr);
62        if ($closeTag === false || $tokens[$stackPtr]['line'] !== $tokens[$closeTag]['line']) {
63            $this->_validateMultilineEmbeddedPhp($phpcsFile, $stackPtr);
64        } else {
65            $this->_validateInlineEmbeddedPhp($phpcsFile, $stackPtr);
66        }
67
68    }//end process()
69
70
71    /**
72     * Validates embedded PHP that exists on multiple lines.
73     *
74     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
75     * @param int                  $stackPtr  The position of the current token in the
76     *                                        stack passed in $tokens.
77     *
78     * @return void
79     */
80    private function _validateMultilineEmbeddedPhp(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
81    {
82        $tokens = $phpcsFile->getTokens();
83
84        $prevTag = $phpcsFile->findPrevious(T_OPEN_TAG, ($stackPtr - 1));
85        if ($prevTag === false) {
86            // This is the first open tag.
87            return;
88        }
89
90        $firstContent = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
91        $closingTag   = $phpcsFile->findNext(T_CLOSE_TAG, $stackPtr);
92        if ($closingTag !== false) {
93            $nextContent = $phpcsFile->findNext(T_WHITESPACE, ($closingTag + 1), $phpcsFile->numTokens, true);
94            if ($nextContent === false) {
95                // Final closing tag. It will be handled elsewhere.
96                return;
97            }
98
99            // We have an opening and a closing tag, that lie within other content.
100            if ($firstContent === $closingTag) {
101                $error = 'Empty embedded PHP tag found';
102                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'Empty');
103                if ($fix === true) {
104                    $phpcsFile->fixer->beginChangeset();
105                    for ($i = $stackPtr; $i <= $closingTag; $i++) {
106                        $phpcsFile->fixer->replaceToken($i, '');
107                    }
108
109                    $phpcsFile->fixer->endChangeset();
110                }
111
112                return;
113            }
114        }//end if
115
116        if ($tokens[$firstContent]['line'] === $tokens[$stackPtr]['line']) {
117            $error = 'Opening PHP tag must be on a line by itself';
118            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'ContentAfterOpen');
119            if ($fix === true) {
120                $first   = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr, true);
121                $padding = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content'])));
122                $phpcsFile->fixer->beginChangeset();
123                $phpcsFile->fixer->addNewline($stackPtr);
124                $phpcsFile->fixer->addContent($stackPtr, str_repeat(' ', $padding));
125                $phpcsFile->fixer->endChangeset();
126            }
127        } else {
128            // Check the indent of the first line, except if it is a scope closer.
129            if (isset($tokens[$firstContent]['scope_closer']) === false
130                || $tokens[$firstContent]['scope_closer'] !== $firstContent
131            ) {
132                // Check for a blank line at the top.
133                if ($tokens[$firstContent]['line'] > ($tokens[$stackPtr]['line'] + 1)) {
134                    // Find a token on the blank line to throw the error on.
135                    $i = $stackPtr;
136                    do {
137                        $i++;
138                    } while ($tokens[$i]['line'] !== ($tokens[$stackPtr]['line'] + 1));
139
140                    $error = 'Blank line found at start of embedded PHP content';
141                    $fix   = $phpcsFile->addFixableError($error, $i, 'SpacingBefore');
142                    if ($fix === true) {
143                        $phpcsFile->fixer->beginChangeset();
144                        for ($i = ($stackPtr + 1); $i < $firstContent; $i++) {
145                            if ($tokens[$i]['line'] === $tokens[$firstContent]['line']
146                                || $tokens[$i]['line'] === $tokens[$stackPtr]['line']
147                            ) {
148                                continue;
149                            }
150
151                            $phpcsFile->fixer->replaceToken($i, '');
152                        }
153
154                        $phpcsFile->fixer->endChangeset();
155                    }
156                }//end if
157
158                $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr);
159                if ($first === false) {
160                    $first  = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr);
161                    $indent = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content'])));
162                } else {
163                    $indent = ($tokens[($first + 1)]['column'] - 1);
164                }
165
166                $contentColumn = ($tokens[$firstContent]['column'] - 1);
167                if ($contentColumn !== $indent) {
168                    $error = 'First line of embedded PHP code must be indented %s spaces; %s found';
169                    $data  = array(
170                              $indent,
171                              $contentColumn,
172                             );
173                    $fix   = $phpcsFile->addFixableError($error, $firstContent, 'Indent', $data);
174                    if ($fix === true) {
175                        $padding = str_repeat(' ', $indent);
176                        if ($contentColumn === 0) {
177                            $phpcsFile->fixer->addContentBefore($firstContent, $padding);
178                        } else {
179                            $phpcsFile->fixer->replaceToken(($firstContent - 1), $padding);
180                        }
181                    }
182                }
183            }//end if
184        }//end if
185
186        $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
187        if ($tokens[$lastContent]['line'] === $tokens[$stackPtr]['line']
188            && trim($tokens[$lastContent]['content']) !== ''
189        ) {
190            $error = 'Opening PHP tag must be on a line by itself';
191            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'ContentBeforeOpen');
192            if ($fix === true) {
193                $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr);
194                if ($first === false) {
195                    $first   = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr);
196                    $padding = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content'])));
197                } else {
198                    $padding = ($tokens[($first + 1)]['column'] - 1);
199                }
200
201                $phpcsFile->fixer->addContentBefore($stackPtr, $phpcsFile->eolChar.str_repeat(' ', $padding));
202            }
203        } else {
204            // Find the first token on the first non-empty line we find.
205            for ($first = ($stackPtr - 1); $first > 0; $first--) {
206                if ($tokens[$first]['line'] === $tokens[$stackPtr]['line']) {
207                    continue;
208                } else if (trim($tokens[$first]['content']) !== '') {
209                    $first = $phpcsFile->findFirstOnLine(array(), $first, true);
210                    break;
211                }
212            }
213
214            $expected = 0;
215            if ($tokens[$first]['code'] === T_INLINE_HTML
216                && trim($tokens[$first]['content']) !== ''
217            ) {
218                $expected = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content'])));
219            } else if ($tokens[$first]['code'] === T_WHITESPACE) {
220                $expected = ($tokens[($first + 1)]['column'] - 1);
221            }
222
223            $expected += 4;
224            $found     = ($tokens[$stackPtr]['column'] - 1);
225            if ($found > $expected) {
226                $error = 'Opening PHP tag indent incorrect; expected no more than %s spaces but found %s';
227                $data  = array(
228                          $expected,
229                          $found,
230                         );
231                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'OpenTagIndent', $data);
232                if ($fix === true) {
233                    $phpcsFile->fixer->replaceToken(($stackPtr - 1), str_repeat(' ', $expected));
234                }
235            }
236        }//end if
237
238        if ($closingTag === false) {
239            return;
240        }
241
242        $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($closingTag - 1), ($stackPtr + 1), true);
243        $nextContent = $phpcsFile->findNext(T_WHITESPACE, ($closingTag + 1), null, true);
244
245        if ($tokens[$lastContent]['line'] === $tokens[$closingTag]['line']) {
246            $error = 'Closing PHP tag must be on a line by itself';
247            $fix   = $phpcsFile->addFixableError($error, $closingTag, 'ContentBeforeEnd');
248            if ($fix === true) {
249                $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $closingTag, true);
250                $phpcsFile->fixer->beginChangeset();
251                $phpcsFile->fixer->addContentBefore($closingTag, str_repeat(' ', ($tokens[$first]['column'] - 1)));
252                $phpcsFile->fixer->addNewlineBefore($closingTag);
253                $phpcsFile->fixer->endChangeset();
254            }
255        } else if ($tokens[$nextContent]['line'] === $tokens[$closingTag]['line']) {
256            $error = 'Closing PHP tag must be on a line by itself';
257            $fix   = $phpcsFile->addFixableError($error, $closingTag, 'ContentAfterEnd');
258            if ($fix === true) {
259                $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $closingTag, true);
260                $phpcsFile->fixer->beginChangeset();
261                $phpcsFile->fixer->addNewline($closingTag);
262                $phpcsFile->fixer->addContent($closingTag, str_repeat(' ', ($tokens[$first]['column'] - 1)));
263                $phpcsFile->fixer->endChangeset();
264            }
265        }//end if
266
267        $next = $phpcsFile->findNext(T_OPEN_TAG, ($closingTag + 1));
268        if ($next === false) {
269            return;
270        }
271
272        // Check for a blank line at the bottom.
273        if ((isset($tokens[$lastContent]['scope_closer']) === false
274            || $tokens[$lastContent]['scope_closer'] !== $lastContent)
275            && $tokens[$lastContent]['line'] < ($tokens[$closingTag]['line'] - 1)
276        ) {
277            // Find a token on the blank line to throw the error on.
278            $i = $closingTag;
279            do {
280                $i--;
281            } while ($tokens[$i]['line'] !== ($tokens[$closingTag]['line'] - 1));
282
283            $error = 'Blank line found at end of embedded PHP content';
284            $fix   = $phpcsFile->addFixableError($error, $i, 'SpacingAfter');
285            if ($fix === true) {
286                $phpcsFile->fixer->beginChangeset();
287                for ($i = ($lastContent + 1); $i < $closingTag; $i++) {
288                    if ($tokens[$i]['line'] === $tokens[$lastContent]['line']
289                        || $tokens[$i]['line'] === $tokens[$closingTag]['line']
290                    ) {
291                        continue;
292                    }
293
294                    $phpcsFile->fixer->replaceToken($i, '');
295                }
296
297                $phpcsFile->fixer->endChangeset();
298            }
299        }//end if
300
301    }//end _validateMultilineEmbeddedPhp()
302
303
304    /**
305     * Validates embedded PHP that exists on one line.
306     *
307     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
308     * @param int                  $stackPtr  The position of the current token in the
309     *                                        stack passed in $tokens.
310     *
311     * @return void
312     */
313    private function _validateInlineEmbeddedPhp(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
314    {
315        $tokens = $phpcsFile->getTokens();
316
317        // We only want one line PHP sections, so return if the closing tag is
318        // on the next line.
319        $closeTag = $phpcsFile->findNext(T_CLOSE_TAG, $stackPtr, null, false);
320        if ($tokens[$stackPtr]['line'] !== $tokens[$closeTag]['line']) {
321            return;
322        }
323
324        // Check that there is one, and only one space at the start of the statement.
325        $firstContent = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), ($closeTag - 1), true);
326
327        if ($firstContent === false) {
328            $error = 'Empty embedded PHP tag found';
329            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'Empty');
330            if ($fix === true) {
331                $phpcsFile->fixer->beginChangeset();
332                for ($i = $stackPtr; $i <= $closeTag; $i++) {
333                    $phpcsFile->fixer->replaceToken($i, '');
334                }
335
336                $phpcsFile->fixer->endChangeset();
337            }
338
339            return;
340        }
341
342        // The open tag token always contains a single space after it.
343        $leadingSpace = 1;
344        if ($tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) {
345            $leadingSpace = (strlen($tokens[($stackPtr + 1)]['content']) + 1);
346        }
347
348        if ($leadingSpace !== 1) {
349            $error = 'Expected 1 space after opening PHP tag; %s found';
350            $data  = array($leadingSpace);
351            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingAfterOpen', $data);
352            if ($fix === true) {
353                $phpcsFile->fixer->replaceToken(($stackPtr + 1), '');
354            }
355        }
356
357        $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($closeTag - 1), $stackPtr, true);
358        if ((isset($tokens[$prev]['scope_opener']) === false
359            || $tokens[$prev]['scope_opener'] !== $prev)
360            && (isset($tokens[$prev]['scope_closer']) === false
361            || $tokens[$prev]['scope_closer'] !== $prev)
362            && $tokens[$prev]['code'] !== T_SEMICOLON
363        ) {
364            $error = 'Inline PHP statement must end with a semicolon';
365            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSemicolon');
366            if ($fix === true) {
367                $phpcsFile->fixer->addContent($prev, ';');
368            }
369        } else if ($tokens[$prev]['code'] === T_SEMICOLON) {
370            $statementCount = 1;
371            for ($i = ($stackPtr + 1); $i < $prev; $i++) {
372                if ($tokens[$i]['code'] === T_SEMICOLON) {
373                    $statementCount++;
374                }
375            }
376
377            if ($statementCount > 1) {
378                $error = 'Inline PHP statement must contain a single statement; %s found';
379                $data  = array($statementCount);
380                $phpcsFile->addError($error, $stackPtr, 'MultipleStatements', $data);
381            }
382        }
383
384        $trailingSpace = 0;
385        if ($tokens[($closeTag - 1)]['code'] === T_WHITESPACE) {
386            $trailingSpace = strlen($tokens[($closeTag - 1)]['content']);
387        }
388
389        if ($trailingSpace !== 1) {
390            $error = 'Expected 1 space before closing PHP tag; %s found';
391            $data  = array($trailingSpace);
392            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBeforeClose', $data);
393            if ($fix === true) {
394                if ($trailingSpace === 0) {
395                    $phpcsFile->fixer->addContentBefore($closeTag, ' ');
396                } else {
397                    $phpcsFile->fixer->replaceToken(($closeTag - 1), ' ');
398                }
399            }
400        }
401
402    }//end _validateInlineEmbeddedPhp()
403
404
405}//end class
406