1<?php
2/**
3 * Class Declaration Test.
4 *
5 * PHP version 5
6 *
7 * @category  PHP
8 * @package   PHP_CodeSniffer
9 * @author    Greg Sherwood <gsherwood@squiz.net>
10 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
11 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
12 * @link      http://pear.php.net/package/PHP_CodeSniffer
13 */
14
15if (class_exists('PEAR_Sniffs_Classes_ClassDeclarationSniff', true) === false) {
16    $error = 'Class PEAR_Sniffs_Classes_ClassDeclarationSniff not found';
17    throw new PHP_CodeSniffer_Exception($error);
18}
19
20/**
21 * Class Declaration Test.
22 *
23 * Checks the declaration of the class and its inheritance is correct.
24 *
25 * @category  PHP
26 * @package   PHP_CodeSniffer
27 * @author    Greg Sherwood <gsherwood@squiz.net>
28 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
29 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
30 * @version   Release: @package_version@
31 * @link      http://pear.php.net/package/PHP_CodeSniffer
32 */
33class PSR2_Sniffs_Classes_ClassDeclarationSniff extends PEAR_Sniffs_Classes_ClassDeclarationSniff
34{
35
36
37    /**
38     * Processes this test, when one of its tokens is encountered.
39     *
40     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
41     * @param int                  $stackPtr  The position of the current token
42     *                                         in the stack passed in $tokens.
43     *
44     * @return void
45     */
46    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
47    {
48        // We want all the errors from the PEAR standard, plus some of our own.
49        parent::process($phpcsFile, $stackPtr);
50
51        // Just in case.
52        $tokens = $phpcsFile->getTokens();
53        if (isset($tokens[$stackPtr]['scope_opener']) === false) {
54            return;
55        }
56
57        $this->processOpen($phpcsFile, $stackPtr);
58        $this->processClose($phpcsFile, $stackPtr);
59
60    }//end process()
61
62
63    /**
64     * Processes the opening section of a class declaration.
65     *
66     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
67     * @param int                  $stackPtr  The position of the current token
68     *                                        in the stack passed in $tokens.
69     *
70     * @return void
71     */
72    public function processOpen(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
73    {
74        $tokens       = $phpcsFile->getTokens();
75        $stackPtrType = strtolower($tokens[$stackPtr]['content']);
76
77        // Check alignment of the keyword and braces.
78        if ($tokens[($stackPtr - 1)]['code'] === T_WHITESPACE) {
79            $prevContent = $tokens[($stackPtr - 1)]['content'];
80            if ($prevContent !== $phpcsFile->eolChar) {
81                $blankSpace = substr($prevContent, strpos($prevContent, $phpcsFile->eolChar));
82                $spaces     = strlen($blankSpace);
83
84                if (in_array($tokens[($stackPtr - 2)]['code'], array(T_ABSTRACT, T_FINAL)) === true
85                    && $spaces !== 1
86                ) {
87                    $prevContent = strtolower($tokens[($stackPtr - 2)]['content']);
88                    $error       = 'Expected 1 space between %s and %s keywords; %s found';
89                    $data        = array(
90                                    $prevContent,
91                                    $stackPtrType,
92                                    $spaces,
93                                   );
94
95                    $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeKeyword', $data);
96                    if ($fix === true) {
97                        $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' ');
98                    }
99                }
100            } else if ($tokens[($stackPtr - 2)]['code'] === T_ABSTRACT
101                || $tokens[($stackPtr - 2)]['code'] === T_FINAL
102            ) {
103                $prevContent = strtolower($tokens[($stackPtr - 2)]['content']);
104                $error       = 'Expected 1 space between %s and %s keywords; newline found';
105                $data        = array(
106                                $prevContent,
107                                $stackPtrType,
108                               );
109
110                $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NewlineBeforeKeyword', $data);
111                if ($fix === true) {
112                    $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' ');
113                }
114            }//end if
115        }//end if
116
117        // We'll need the indent of the class/interface declaration for later.
118        $classIndent = 0;
119        for ($i = ($stackPtr - 1); $i > 0; $i--) {
120            if ($tokens[$i]['line'] === $tokens[$stackPtr]['line']) {
121                continue;
122            }
123
124            // We changed lines.
125            if ($tokens[($i + 1)]['code'] === T_WHITESPACE) {
126                $classIndent = strlen($tokens[($i + 1)]['content']);
127            }
128
129            break;
130        }
131
132        $className = $phpcsFile->findNext(T_STRING, $stackPtr);
133
134        // Spacing of the keyword.
135        $gap = $tokens[($stackPtr + 1)]['content'];
136        if (strlen($gap) !== 1) {
137            $found = strlen($gap);
138            $error = 'Expected 1 space between %s keyword and %s name; %s found';
139            $data  = array(
140                      $stackPtrType,
141                      $stackPtrType,
142                      $found,
143                     );
144
145            $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterKeyword', $data);
146            if ($fix === true) {
147                $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' ');
148            }
149        }
150
151        // Check after the class/interface name.
152        if ($tokens[($className + 2)]['line'] === $tokens[$className]['line']) {
153            $gap = $tokens[($className + 1)]['content'];
154            if (strlen($gap) !== 1) {
155                $found = strlen($gap);
156                $error = 'Expected 1 space after %s name; %s found';
157                $data  = array(
158                          $stackPtrType,
159                          $found,
160                         );
161
162                $fix = $phpcsFile->addFixableError($error, $className, 'SpaceAfterName', $data);
163                if ($fix === true) {
164                    $phpcsFile->fixer->replaceToken(($className + 1), ' ');
165                }
166            }
167        }
168
169        $openingBrace = $tokens[$stackPtr]['scope_opener'];
170
171        // Check positions of the extends and implements keywords.
172        foreach (array('extends', 'implements') as $keywordType) {
173            $keyword = $phpcsFile->findNext(constant('T_'.strtoupper($keywordType)), ($stackPtr + 1), $openingBrace);
174            if ($keyword !== false) {
175                if ($tokens[$keyword]['line'] !== $tokens[$stackPtr]['line']) {
176                    $error = 'The '.$keywordType.' keyword must be on the same line as the %s name';
177                    $data  = array($stackPtrType);
178                    $fix   = $phpcsFile->addFixableError($error, $keyword, ucfirst($keywordType).'Line', $data);
179                    if ($fix === true) {
180                        $phpcsFile->fixer->beginChangeset();
181                        for ($i = ($stackPtr + 1); $i < $keyword; $i++) {
182                            if ($tokens[$i]['line'] !== $tokens[($i + 1)]['line']) {
183                                $phpcsFile->fixer->substrToken($i, 0, (strlen($phpcsFile->eolChar) * -1));
184                            }
185                        }
186
187                        $phpcsFile->fixer->addContentBefore($keyword, ' ');
188                        $phpcsFile->fixer->endChangeset();
189                    }
190                } else {
191                    // Check the whitespace before. Whitespace after is checked
192                    // later by looking at the whitespace before the first class name
193                    // in the list.
194                    $gap = strlen($tokens[($keyword - 1)]['content']);
195                    if ($gap !== 1) {
196                        $error = 'Expected 1 space before '.$keywordType.' keyword; %s found';
197                        $data  = array($gap);
198                        $fix   = $phpcsFile->addFixableError($error, $keyword, 'SpaceBefore'.ucfirst($keywordType), $data);
199                        if ($fix === true) {
200                            $phpcsFile->fixer->replaceToken(($keyword - 1), ' ');
201                        }
202                    }
203                }//end if
204            }//end if
205        }//end foreach
206
207        // Check each of the extends/implements class names. If the extends/implements
208        // keyword is the last content on the line, it means we need to check for
209        // the multi-line format, so we do not include the class names
210        // from the extends/implements list in the following check.
211        // Note that classes can only extend one other class, so they can't use a
212        // multi-line extends format, whereas an interface can extend multiple
213        // other interfaces, and so uses a multi-line extends format.
214        if ($tokens[$stackPtr]['code'] === T_INTERFACE) {
215            $keywordTokenType = T_EXTENDS;
216        } else {
217            $keywordTokenType = T_IMPLEMENTS;
218        }
219
220        $implements          = $phpcsFile->findNext($keywordTokenType, ($stackPtr + 1), $openingBrace);
221        $multiLineImplements = false;
222        if ($implements !== false) {
223            $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($openingBrace - 1), $implements, true);
224            if ($tokens[$prev]['line'] !== $tokens[$implements]['line']) {
225                $multiLineImplements = true;
226            }
227        }
228
229        $find = array(
230                 T_STRING,
231                 $keywordTokenType,
232                );
233
234        $classNames = array();
235        $nextClass  = $phpcsFile->findNext($find, ($className + 2), ($openingBrace - 1));
236        while ($nextClass !== false) {
237            $classNames[] = $nextClass;
238            $nextClass    = $phpcsFile->findNext($find, ($nextClass + 1), ($openingBrace - 1));
239        }
240
241        $classCount         = count($classNames);
242        $checkingImplements = false;
243        $implementsToken    = null;
244        foreach ($classNames as $i => $className) {
245            if ($tokens[$className]['code'] === $keywordTokenType) {
246                $checkingImplements = true;
247                $implementsToken    = $className;
248                continue;
249            }
250
251            if ($checkingImplements === true
252                && $multiLineImplements === true
253                && ($tokens[($className - 1)]['code'] !== T_NS_SEPARATOR
254                || $tokens[($className - 2)]['code'] !== T_STRING)
255            ) {
256                $prev = $phpcsFile->findPrevious(
257                    array(
258                     T_NS_SEPARATOR,
259                     T_WHITESPACE,
260                    ),
261                    ($className - 1),
262                    $implements,
263                    true
264                );
265
266                if ($prev === $implementsToken && $tokens[$className]['line'] !== ($tokens[$prev]['line'] + 1)) {
267                    if ($keywordTokenType === T_EXTENDS) {
268                        $error = 'The first item in a multi-line extends list must be on the line following the extends keyword';
269                        $fix   = $phpcsFile->addFixableError($error, $className, 'FirstExtendsInterfaceSameLine');
270                    } else {
271                        $error = 'The first item in a multi-line implements list must be on the line following the implements keyword';
272                        $fix   = $phpcsFile->addFixableError($error, $className, 'FirstInterfaceSameLine');
273                    }
274
275                    if ($fix === true) {
276                        $phpcsFile->fixer->beginChangeset();
277                        for ($i = ($prev + 1); $i < $className; $i++) {
278                            if ($tokens[$i]['code'] !== T_WHITESPACE) {
279                                break;
280                            }
281
282                            $phpcsFile->fixer->replaceToken($i, '');
283                        }
284
285                        $phpcsFile->fixer->addNewline($prev);
286                        $phpcsFile->fixer->endChangeset();
287                    }
288                } else if ($tokens[$prev]['line'] !== ($tokens[$className]['line'] - 1)) {
289                    if ($keywordTokenType === T_EXTENDS) {
290                        $error = 'Only one interface may be specified per line in a multi-line extends declaration';
291                        $fix   = $phpcsFile->addFixableError($error, $className, 'ExtendsInterfaceSameLine');
292                    } else {
293                        $error = 'Only one interface may be specified per line in a multi-line implements declaration';
294                        $fix   = $phpcsFile->addFixableError($error, $className, 'InterfaceSameLine');
295                    }
296
297                    if ($fix === true) {
298                        $phpcsFile->fixer->beginChangeset();
299                        for ($i = ($prev + 1); $i < $className; $i++) {
300                            if ($tokens[$i]['code'] !== T_WHITESPACE) {
301                                break;
302                            }
303
304                            $phpcsFile->fixer->replaceToken($i, '');
305                        }
306
307                        $phpcsFile->fixer->addNewline($prev);
308                        $phpcsFile->fixer->endChangeset();
309                    }
310                } else {
311                    $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($className - 1), $implements);
312                    if ($tokens[$prev]['line'] !== $tokens[$className]['line']) {
313                        $found = 0;
314                    } else {
315                        $found = strlen($tokens[$prev]['content']);
316                    }
317
318                    $expected = ($classIndent + $this->indent);
319                    if ($found !== $expected) {
320                        $error = 'Expected %s spaces before interface name; %s found';
321                        $data  = array(
322                                  $expected,
323                                  $found,
324                                 );
325                        $fix   = $phpcsFile->addFixableError($error, $className, 'InterfaceWrongIndent', $data);
326                        if ($fix === true) {
327                            $padding = str_repeat(' ', $expected);
328                            if ($found === 0) {
329                                $phpcsFile->fixer->addContent($prev, $padding);
330                            } else {
331                                $phpcsFile->fixer->replaceToken($prev, $padding);
332                            }
333                        }
334                    }
335                }//end if
336            } else if ($tokens[($className - 1)]['code'] !== T_NS_SEPARATOR
337                || $tokens[($className - 2)]['code'] !== T_STRING
338            ) {
339                // Not part of a longer fully qualified class name.
340                if ($tokens[($className - 1)]['code'] === T_COMMA
341                    || ($tokens[($className - 1)]['code'] === T_NS_SEPARATOR
342                    && $tokens[($className - 2)]['code'] === T_COMMA)
343                ) {
344                    $error = 'Expected 1 space before "%s"; 0 found';
345                    $data  = array($tokens[$className]['content']);
346                    $fix   = $phpcsFile->addFixableError($error, ($nextComma + 1), 'NoSpaceBeforeName', $data);
347                    if ($fix === true) {
348                        $phpcsFile->fixer->addContentBefore(($nextComma + 1), ' ');
349                    }
350                } else {
351                    if ($tokens[($className - 1)]['code'] === T_NS_SEPARATOR) {
352                        $prev = ($className - 2);
353                    } else {
354                        $prev = ($className - 1);
355                    }
356
357                    $spaceBefore = strlen($tokens[$prev]['content']);
358                    if ($spaceBefore !== 1) {
359                        $error = 'Expected 1 space before "%s"; %s found';
360                        $data  = array(
361                                  $tokens[$className]['content'],
362                                  $spaceBefore,
363                                 );
364
365                        $fix = $phpcsFile->addFixableError($error, $className, 'SpaceBeforeName', $data);
366                        if ($fix === true) {
367                            $phpcsFile->fixer->replaceToken($prev, ' ');
368                        }
369                    }
370                }//end if
371            }//end if
372
373            if ($checkingImplements === true
374                && $tokens[($className + 1)]['code'] !== T_NS_SEPARATOR
375                && $tokens[($className + 1)]['code'] !== T_COMMA
376            ) {
377                if ($i !== ($classCount - 1)) {
378                    // This is not the last class name, and the comma
379                    // is not where we expect it to be.
380                    if ($tokens[($className + 2)]['code'] !== $keywordTokenType) {
381                        $error = 'Expected 0 spaces between "%s" and comma; %s found';
382                        $data  = array(
383                                  $tokens[$className]['content'],
384                                  strlen($tokens[($className + 1)]['content']),
385                                 );
386
387                        $fix = $phpcsFile->addFixableError($error, $className, 'SpaceBeforeComma', $data);
388                        if ($fix === true) {
389                            $phpcsFile->fixer->replaceToken(($className + 1), '');
390                        }
391                    }
392                }
393
394                $nextComma = $phpcsFile->findNext(T_COMMA, $className);
395            } else {
396                $nextComma = ($className + 1);
397            }//end if
398        }//end foreach
399
400    }//end processOpen()
401
402
403    /**
404     * Processes the closing section of a class declaration.
405     *
406     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
407     * @param int                  $stackPtr  The position of the current token
408     *                                        in the stack passed in $tokens.
409     *
410     * @return void
411     */
412    public function processClose(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
413    {
414        $tokens = $phpcsFile->getTokens();
415
416        // Check that the closing brace comes right after the code body.
417        $closeBrace  = $tokens[$stackPtr]['scope_closer'];
418        $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($closeBrace - 1), null, true);
419        if ($prevContent !== $tokens[$stackPtr]['scope_opener']
420            && $tokens[$prevContent]['line'] !== ($tokens[$closeBrace]['line'] - 1)
421        ) {
422            $error = 'The closing brace for the %s must go on the next line after the body';
423            $data  = array($tokens[$stackPtr]['content']);
424            $fix   = $phpcsFile->addFixableError($error, $closeBrace, 'CloseBraceAfterBody', $data);
425
426            if ($fix === true) {
427                $phpcsFile->fixer->beginChangeset();
428                for ($i = ($prevContent + 1); $i < $closeBrace; $i++) {
429                    $phpcsFile->fixer->replaceToken($i, '');
430                }
431
432                if (strpos($tokens[$prevContent]['content'], $phpcsFile->eolChar) === false) {
433                    $phpcsFile->fixer->replaceToken($closeBrace, $phpcsFile->eolChar.$tokens[$closeBrace]['content']);
434                }
435
436                $phpcsFile->fixer->endChangeset();
437            }
438        }//end if
439
440        // Check the closing brace is on it's own line, but allow
441        // for comments like "//end class".
442        $nextContent = $phpcsFile->findNext(array(T_WHITESPACE, T_COMMENT), ($closeBrace + 1), null, true);
443        if ($tokens[$nextContent]['content'] !== $phpcsFile->eolChar
444            && $tokens[$nextContent]['line'] === $tokens[$closeBrace]['line']
445        ) {
446            $type  = strtolower($tokens[$stackPtr]['content']);
447            $error = 'Closing %s brace must be on a line by itself';
448            $data  = array($type);
449            $phpcsFile->addError($error, $closeBrace, 'CloseBraceSameLine', $data);
450        }
451
452    }//end processClose()
453
454
455}//end class
456