• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHP_CodeSniffer / 13848627330

14 Mar 2025 02:29AM UTC coverage: 77.518% (+0.004%) from 77.514%
13848627330

push

github

jrfnl
Squiz/ClassDeclaration: allow for function attributes

The `Squiz.Classes.ClassDeclaration` sniff checks the number of blank lines _after_ a class closing brace to be exactly one blank line.

To prevent conflicts with other sniffs checking blank lines before function declarations, it bows out when the next thing after the class declaration is a function declaration and defers to the function declaration sniff to handle the "blank lines before function" check.
See squizlabs/PHP_CodeSniffer 2033 for historic context.

However, the above "bowing out" breaks when a function after a class has PHP 8.0+ attributes attached to it and the sniff would still the `NewlinesAfterCloseBrace` error.

Fixed now.

Includes tests.

Note: the new "errors" caused by the new tests are for the `MultipleClasses` code, which is unrelated to this fix and valid.

13 of 13 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

19295 of 24891 relevant lines covered (77.52%)

76.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.06
/src/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php
1
<?php
2
/**
3
 * Checks the declaration of the class and its inheritance is correct.
4
 *
5
 * @author    Greg Sherwood <gsherwood@squiz.net>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

10
namespace PHP_CodeSniffer\Standards\Squiz\Sniffs\Classes;
11

12
use PHP_CodeSniffer\Files\File;
13
use PHP_CodeSniffer\Standards\PSR2\Sniffs\Classes\ClassDeclarationSniff as PSR2ClassDeclarationSniff;
14
use PHP_CodeSniffer\Util\Tokens;
15

16
class ClassDeclarationSniff extends PSR2ClassDeclarationSniff
17
{
18

19

20
    /**
21
     * Processes this test, when one of its tokens is encountered.
22
     *
23
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
24
     * @param int                         $stackPtr  The position of the current token
25
     *                                               in the stack passed in $tokens.
26
     *
27
     * @return void
28
     */
29
    public function process(File $phpcsFile, $stackPtr)
3✔
30
    {
31
        // We want all the errors from the PSR2 standard, plus some of our own.
32
        parent::process($phpcsFile, $stackPtr);
3✔
33

34
        // Check that this is the only class or interface in the file.
35
        $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE], ($stackPtr + 1));
3✔
36
        if ($nextClass !== false) {
3✔
37
            // We have another, so an error is thrown.
38
            $error = 'Only one interface or class is allowed in a file';
3✔
39
            $phpcsFile->addError($error, $nextClass, 'MultipleClasses');
3✔
40
        }
41

42
    }//end process()
1✔
43

44

45
    /**
46
     * Processes the opening section of a class declaration.
47
     *
48
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
49
     * @param int                         $stackPtr  The position of the current token
50
     *                                               in the stack passed in $tokens.
51
     *
52
     * @return void
53
     */
54
    public function processOpen(File $phpcsFile, $stackPtr)
3✔
55
    {
56
        parent::processOpen($phpcsFile, $stackPtr);
3✔
57

58
        $tokens = $phpcsFile->getTokens();
3✔
59

60
        if ($tokens[($stackPtr - 1)]['code'] === T_WHITESPACE) {
3✔
61
            $prevContent = $tokens[($stackPtr - 1)]['content'];
3✔
62
            if ($prevContent !== $phpcsFile->eolChar) {
3✔
63
                $blankSpace = substr($prevContent, strpos($prevContent, $phpcsFile->eolChar));
3✔
64
                $spaces     = strlen($blankSpace);
3✔
65

66
                if ($tokens[($stackPtr - 2)]['code'] !== T_ABSTRACT
3✔
67
                    && $tokens[($stackPtr - 2)]['code'] !== T_FINAL
3✔
68
                    && $tokens[($stackPtr - 2)]['code'] !== T_READONLY
3✔
69
                ) {
70
                    if ($spaces !== 0) {
3✔
71
                        $type  = strtolower($tokens[$stackPtr]['content']);
3✔
72
                        $error = 'Expected 0 spaces before %s keyword; %s found';
3✔
73
                        $data  = [
2✔
74
                            $type,
3✔
75
                            $spaces,
3✔
76
                        ];
2✔
77

78
                        $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeKeyword', $data);
3✔
79
                        if ($fix === true) {
3✔
80
                            $phpcsFile->fixer->replaceToken(($stackPtr - 1), '');
3✔
81
                        }
82
                    }
83
                }
84
            }//end if
85
        }//end if
86

87
    }//end processOpen()
1✔
88

89

90
    /**
91
     * Processes the closing section of a class declaration.
92
     *
93
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
94
     * @param int                         $stackPtr  The position of the current token
95
     *                                               in the stack passed in $tokens.
96
     *
97
     * @return void
98
     */
99
    public function processClose(File $phpcsFile, $stackPtr)
3✔
100
    {
101
        $tokens = $phpcsFile->getTokens();
3✔
102
        if (isset($tokens[$stackPtr]['scope_closer']) === false) {
3✔
103
            return;
×
104
        }
105

106
        $closeBrace = $tokens[$stackPtr]['scope_closer'];
3✔
107

108
        // Check that the closing brace has one blank line after it.
109
        for ($nextContent = ($closeBrace + 1); $nextContent < $phpcsFile->numTokens; $nextContent++) {
3✔
110
            // Ignore comments on the same line as the brace.
111
            if ($tokens[$nextContent]['line'] === $tokens[$closeBrace]['line']
3✔
112
                && ($tokens[$nextContent]['code'] === T_WHITESPACE
3✔
113
                || $tokens[$nextContent]['code'] === T_COMMENT
3✔
114
                || isset(Tokens::$phpcsCommentTokens[$tokens[$nextContent]['code']]) === true)
3✔
115
            ) {
116
                continue;
3✔
117
            }
118

119
            if ($tokens[$nextContent]['code'] !== T_WHITESPACE) {
3✔
120
                break;
3✔
121
            }
122
        }
123

124
        if ($nextContent === $phpcsFile->numTokens) {
3✔
125
            // Ignore the line check as this is the very end of the file.
UNCOV
126
            $difference = 1;
×
127
        } else {
128
            $difference = ($tokens[$nextContent]['line'] - $tokens[$closeBrace]['line'] - 1);
3✔
129
        }
130

131
        $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($closeBrace - 1), $stackPtr, true);
3✔
132

133
        if ($difference === -1
3✔
134
            || $tokens[$lastContent]['line'] === $tokens[$closeBrace]['line']
3✔
135
        ) {
136
            $error = 'Closing %s brace must be on a line by itself';
3✔
137
            $data  = [$tokens[$stackPtr]['content']];
3✔
138
            $fix   = $phpcsFile->addFixableError($error, $closeBrace, 'CloseBraceSameLine', $data);
3✔
139
            if ($fix === true) {
3✔
140
                if ($difference === -1) {
3✔
141
                    $phpcsFile->fixer->addNewlineBefore($nextContent);
3✔
142
                }
143

144
                if ($tokens[$lastContent]['line'] === $tokens[$closeBrace]['line']) {
3✔
145
                    $phpcsFile->fixer->addNewlineBefore($closeBrace);
3✔
146
                }
147
            }
148
        } else if ($tokens[($closeBrace - 1)]['code'] === T_WHITESPACE) {
3✔
149
            $prevContent = $tokens[($closeBrace - 1)]['content'];
3✔
150
            if ($prevContent !== $phpcsFile->eolChar) {
3✔
151
                $blankSpace = substr($prevContent, strpos($prevContent, $phpcsFile->eolChar));
3✔
152
                $spaces     = strlen($blankSpace);
3✔
153
                if ($spaces !== 0) {
3✔
154
                    if ($tokens[($closeBrace - 1)]['line'] !== $tokens[$closeBrace]['line']) {
3✔
155
                        $error = 'Expected 0 spaces before closing brace; newline found';
3✔
156
                        $phpcsFile->addError($error, $closeBrace, 'NewLineBeforeCloseBrace');
3✔
157
                    } else {
158
                        $error = 'Expected 0 spaces before closing brace; %s found';
3✔
159
                        $data  = [$spaces];
3✔
160
                        $fix   = $phpcsFile->addFixableError($error, $closeBrace, 'SpaceBeforeCloseBrace', $data);
3✔
161
                        if ($fix === true) {
3✔
162
                            $phpcsFile->fixer->replaceToken(($closeBrace - 1), '');
3✔
163
                        }
164
                    }
165
                }
166
            }
167
        }//end if
168

169
        if ($difference !== -1 && $difference !== 1) {
3✔
170
            for ($nextSignificant = $nextContent; $nextSignificant < $phpcsFile->numTokens; $nextSignificant++) {
3✔
171
                if ($tokens[$nextSignificant]['code'] === T_WHITESPACE) {
3✔
172
                    continue;
3✔
173
                }
174

175
                if ($tokens[$nextSignificant]['code'] === T_DOC_COMMENT_OPEN_TAG) {
3✔
176
                    $nextSignificant = $tokens[$nextSignificant]['comment_closer'];
3✔
177
                    continue;
3✔
178
                }
179

180
                if ($tokens[$nextSignificant]['code'] === T_ATTRIBUTE
3✔
181
                    && isset($tokens[$nextSignificant]['attribute_closer']) === true
3✔
182
                ) {
183
                    $nextSignificant = $tokens[$nextSignificant]['attribute_closer'];
3✔
184
                    continue;
3✔
185
                }
186

187
                break;
3✔
188
            }
189

190
            if ($tokens[$nextSignificant]['code'] === T_FUNCTION) {
3✔
191
                return;
3✔
192
            }
193

194
            $error = 'Closing brace of a %s must be followed by a single blank line; found %s';
3✔
195
            $data  = [
2✔
196
                $tokens[$stackPtr]['content'],
3✔
197
                $difference,
3✔
198
            ];
2✔
199
            $fix   = $phpcsFile->addFixableError($error, $closeBrace, 'NewlinesAfterCloseBrace', $data);
3✔
200
            if ($fix === true) {
3✔
201
                if ($difference === 0) {
3✔
202
                    $first = $phpcsFile->findFirstOnLine([], $nextContent, true);
3✔
203
                    $phpcsFile->fixer->addNewlineBefore($first);
3✔
204
                } else {
205
                    $phpcsFile->fixer->beginChangeset();
3✔
206
                    for ($i = ($closeBrace + 1); $i < $nextContent; $i++) {
3✔
207
                        if ($tokens[$i]['line'] <= ($tokens[$closeBrace]['line'] + 1)) {
3✔
208
                            continue;
3✔
209
                        } else if ($tokens[$i]['line'] === $tokens[$nextContent]['line']) {
3✔
210
                            break;
3✔
211
                        }
212

213
                        $phpcsFile->fixer->replaceToken($i, '');
3✔
214
                    }
215

216
                    $phpcsFile->fixer->endChangeset();
3✔
217
                }
218
            }
219
        }//end if
220

221
    }//end processClose()
1✔
222

223

224
}//end class
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc