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

PHPCSStandards / PHP_CodeSniffer / 15253296250

26 May 2025 11:55AM UTC coverage: 78.632% (+0.3%) from 78.375%
15253296250

Pull #1105

github

web-flow
Merge d9441d98f into caf806050
Pull Request #1105: Skip tests when 'git' command is not available

19665 of 25009 relevant lines covered (78.63%)

88.67 hits per line

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

97.73
/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php
1
<?php
2
/**
3
 * Verifies that operators have valid spacing surrounding them.
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\WhiteSpace;
11

12
use PHP_CodeSniffer\Files\File;
13
use PHP_CodeSniffer\Sniffs\Sniff;
14
use PHP_CodeSniffer\Util\Tokens;
15

16
class OperatorSpacingSniff implements Sniff
17
{
18

19
    /**
20
     * Allow newlines instead of spaces.
21
     *
22
     * @var boolean
23
     */
24
    public $ignoreNewlines = false;
25

26
    /**
27
     * Don't check spacing for assignment operators.
28
     *
29
     * This allows multiple assignment statements to be aligned.
30
     *
31
     * @var boolean
32
     */
33
    public $ignoreSpacingBeforeAssignments = true;
34

35
    /**
36
     * A list of tokens that aren't considered as operands.
37
     *
38
     * @var string[]
39
     */
40
    private $nonOperandTokens = [];
41

42

43
    /**
44
     * Returns an array of tokens this test wants to listen for.
45
     *
46
     * @return array<int|string>
47
     */
48
    public function register()
3✔
49
    {
50
        /*
51
            First we setup an array of all the tokens that can come before
52
            a T_MINUS or T_PLUS token to indicate that the token is not being
53
            used as an operator.
54
        */
55

56
        // Trying to operate on a negative value; eg. ($var * -1).
57
        $this->nonOperandTokens = Tokens::OPERATORS;
3✔
58

59
        // Trying to compare a negative value; eg. ($var === -1).
60
        $this->nonOperandTokens += Tokens::COMPARISON_TOKENS;
3✔
61

62
        // Trying to compare a negative value; eg. ($var || -1 === $b).
63
        $this->nonOperandTokens += Tokens::BOOLEAN_OPERATORS;
3✔
64

65
        // Trying to assign a negative value; eg. ($var = -1).
66
        $this->nonOperandTokens += Tokens::ASSIGNMENT_TOKENS;
3✔
67

68
        // Returning/printing a negative value; eg. (return -1).
69
        $this->nonOperandTokens += [
2✔
70
            T_RETURN      => T_RETURN,
3✔
71
            T_ECHO        => T_ECHO,
3✔
72
            T_EXIT        => T_EXIT,
3✔
73
            T_PRINT       => T_PRINT,
3✔
74
            T_YIELD       => T_YIELD,
3✔
75
            T_FN_ARROW    => T_FN_ARROW,
3✔
76
            T_MATCH_ARROW => T_MATCH_ARROW,
3✔
77
        ];
2✔
78

79
        // Trying to use a negative value; eg. myFunction($var, -2).
80
        $this->nonOperandTokens += [
2✔
81
            T_CASE                => T_CASE,
3✔
82
            T_COLON               => T_COLON,
3✔
83
            T_COMMA               => T_COMMA,
3✔
84
            T_INLINE_ELSE         => T_INLINE_ELSE,
3✔
85
            T_INLINE_THEN         => T_INLINE_THEN,
3✔
86
            T_OPEN_CURLY_BRACKET  => T_OPEN_CURLY_BRACKET,
3✔
87
            T_OPEN_PARENTHESIS    => T_OPEN_PARENTHESIS,
3✔
88
            T_OPEN_SHORT_ARRAY    => T_OPEN_SHORT_ARRAY,
3✔
89
            T_OPEN_SQUARE_BRACKET => T_OPEN_SQUARE_BRACKET,
3✔
90
            T_STRING_CONCAT       => T_STRING_CONCAT,
3✔
91
        ];
2✔
92

93
        // Casting a negative value; eg. (array) -$a.
94
        $this->nonOperandTokens += Tokens::CAST_TOKENS;
3✔
95

96
        /*
97
            These are the tokens the sniff is looking for.
98
        */
99

100
        $targets   = Tokens::COMPARISON_TOKENS;
3✔
101
        $targets  += Tokens::OPERATORS;
3✔
102
        $targets  += Tokens::ASSIGNMENT_TOKENS;
3✔
103
        $targets[] = T_INLINE_THEN;
3✔
104
        $targets[] = T_INLINE_ELSE;
3✔
105
        $targets[] = T_INSTANCEOF;
3✔
106

107
        // Also register the contexts we want to specifically skip over.
108
        $targets[] = T_DECLARE;
3✔
109

110
        return $targets;
3✔
111

112
    }//end register()
113

114

115
    /**
116
     * Processes this sniff, when one of its tokens is encountered.
117
     *
118
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked.
119
     * @param int                         $stackPtr  The position of the current token in
120
     *                                               the stack passed in $tokens.
121
     *
122
     * @return void|int Optionally returns a stack pointer. The sniff will not be
123
     *                  called again on the current file until the returned stack
124
     *                  pointer is reached. Return `$phpcsFile->numTokens` to skip
125
     *                  the rest of the file.
126
     */
127
    public function process(File $phpcsFile, $stackPtr)
3✔
128
    {
129
        $tokens = $phpcsFile->getTokens();
3✔
130

131
        // Skip over declare statements as those should be handled by different sniffs.
132
        if ($tokens[$stackPtr]['code'] === T_DECLARE) {
3✔
133
            if (isset($tokens[$stackPtr]['parenthesis_closer']) === false) {
3✔
134
                // Parse error / live coding.
135
                return $phpcsFile->numTokens;
3✔
136
            }
137

138
            return $tokens[$stackPtr]['parenthesis_closer'];
3✔
139
        }
140

141
        if ($this->isOperator($phpcsFile, $stackPtr) === false) {
3✔
142
            return;
3✔
143
        }
144

145
        if ($tokens[$stackPtr]['code'] === T_BITWISE_AND) {
3✔
146
            // Check there is one space before the & operator.
147
            if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) {
3✔
148
                $error = 'Expected 1 space before "&" operator; 0 found';
3✔
149
                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceBeforeAmp');
3✔
150
                if ($fix === true) {
3✔
151
                    $phpcsFile->fixer->addContentBefore($stackPtr, ' ');
3✔
152
                }
153

154
                $phpcsFile->recordMetric($stackPtr, 'Space before operator', 0);
3✔
155
            } else {
156
                if ($tokens[($stackPtr - 2)]['line'] !== $tokens[$stackPtr]['line']) {
3✔
157
                    $found = 'newline';
×
158
                } else {
159
                    $found = $tokens[($stackPtr - 1)]['length'];
3✔
160
                }
161

162
                $phpcsFile->recordMetric($stackPtr, 'Space before operator', $found);
3✔
163
                if ($found !== 1
3✔
164
                    && ($found !== 'newline' || $this->ignoreNewlines === false)
3✔
165
                ) {
166
                    $error = 'Expected 1 space before "&" operator; %s found';
3✔
167
                    $data  = [$found];
3✔
168
                    $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBeforeAmp', $data);
3✔
169
                    if ($fix === true) {
3✔
170
                        $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' ');
3✔
171
                    }
172
                }
173
            }//end if
174

175
            $hasNext = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
3✔
176
            if ($hasNext === false) {
3✔
177
                // Live coding/parse error at end of file.
178
                return;
×
179
            }
180

181
            // Check there is one space after the & operator.
182
            if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) {
3✔
183
                $error = 'Expected 1 space after "&" operator; 0 found';
3✔
184
                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceAfterAmp');
3✔
185
                if ($fix === true) {
3✔
186
                    $phpcsFile->fixer->addContent($stackPtr, ' ');
3✔
187
                }
188

189
                $phpcsFile->recordMetric($stackPtr, 'Space after operator', 0);
3✔
190
            } else {
191
                if ($tokens[($stackPtr + 2)]['line'] !== $tokens[$stackPtr]['line']) {
3✔
192
                    $found = 'newline';
×
193
                } else {
194
                    $found = $tokens[($stackPtr + 1)]['length'];
3✔
195
                }
196

197
                $phpcsFile->recordMetric($stackPtr, 'Space after operator', $found);
3✔
198
                if ($found !== 1
3✔
199
                    && ($found !== 'newline' || $this->ignoreNewlines === false)
3✔
200
                ) {
201
                    $error = 'Expected 1 space after "&" operator; %s found';
3✔
202
                    $data  = [$found];
3✔
203
                    $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingAfterAmp', $data);
3✔
204
                    if ($fix === true) {
3✔
205
                        $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' ');
3✔
206
                    }
207
                }
208
            }//end if
209

210
            return;
3✔
211
        }//end if
212

213
        $operator = $tokens[$stackPtr]['content'];
3✔
214

215
        if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE
3✔
216
            && (($tokens[($stackPtr - 1)]['code'] === T_INLINE_THEN
3✔
217
            && $tokens[($stackPtr)]['code'] === T_INLINE_ELSE) === false)
3✔
218
        ) {
219
            $error = "Expected 1 space before \"$operator\"; 0 found";
3✔
220
            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceBefore');
3✔
221
            if ($fix === true) {
3✔
222
                $phpcsFile->fixer->addContentBefore($stackPtr, ' ');
3✔
223
            }
224

225
            $phpcsFile->recordMetric($stackPtr, 'Space before operator', 0);
3✔
226
        } else if (isset(Tokens::ASSIGNMENT_TOKENS[$tokens[$stackPtr]['code']]) === false
3✔
227
            || $this->ignoreSpacingBeforeAssignments === false
3✔
228
        ) {
229
            // Throw an error for assignments only if enabled using the sniff property
230
            // because other standards allow multiple spaces to align assignments.
231
            $prevNonWhitespace = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
3✔
232
            if ($tokens[$prevNonWhitespace]['line'] !== $tokens[$stackPtr]['line']) {
3✔
233
                $found = 'newline';
3✔
234
            } else {
235
                $found = $tokens[($stackPtr - 1)]['length'];
3✔
236
            }
237

238
            $phpcsFile->recordMetric($stackPtr, 'Space before operator', $found);
3✔
239
            if ($found !== 1
3✔
240
                && ($found !== 'newline' || $this->ignoreNewlines === false)
3✔
241
            ) {
242
                $error = 'Expected 1 space before "%s"; %s found';
3✔
243
                $data  = [
2✔
244
                    $operator,
3✔
245
                    $found,
3✔
246
                ];
2✔
247

248
                if (isset(Tokens::COMMENT_TOKENS[$tokens[$prevNonWhitespace]['code']]) === true) {
3✔
249
                    // Throw a non-fixable error if the token on the previous line is a comment token,
250
                    // as in that case it's not for the sniff to decide where the comment should be moved to
251
                    // and it would get us into unfixable situations as the new line char is included
252
                    // in the contents of the comment token.
253
                    $phpcsFile->addError($error, $stackPtr, 'SpacingBefore', $data);
3✔
254
                } else {
255
                    $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBefore', $data);
3✔
256
                    if ($fix === true) {
3✔
257
                        $phpcsFile->fixer->beginChangeset();
3✔
258
                        if ($found === 'newline') {
3✔
259
                            $i = ($stackPtr - 2);
3✔
260
                            while ($tokens[$i]['code'] === T_WHITESPACE) {
3✔
261
                                $phpcsFile->fixer->replaceToken($i, '');
3✔
262
                                $i--;
3✔
263
                            }
264
                        }
265

266
                        $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' ');
3✔
267
                        $phpcsFile->fixer->endChangeset();
3✔
268
                    }
269
                }//end if
270
            }//end if
271
        }//end if
272

273
        $hasNext = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
3✔
274
        if ($hasNext === false) {
3✔
275
            // Live coding/parse error at end of file.
276
            return;
3✔
277
        }
278

279
        if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) {
3✔
280
            // Skip short ternary such as: "$foo = $bar ?: true;".
281
            if (($tokens[$stackPtr]['code'] === T_INLINE_THEN
3✔
282
                && $tokens[($stackPtr + 1)]['code'] === T_INLINE_ELSE)
3✔
283
            ) {
284
                return;
3✔
285
            }
286

287
            $error = "Expected 1 space after \"$operator\"; 0 found";
3✔
288
            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceAfter');
3✔
289
            if ($fix === true) {
3✔
290
                $phpcsFile->fixer->addContent($stackPtr, ' ');
3✔
291
            }
292

293
            $phpcsFile->recordMetric($stackPtr, 'Space after operator', 0);
3✔
294
        } else {
295
            if (isset($tokens[($stackPtr + 2)]) === true
3✔
296
                && $tokens[($stackPtr + 2)]['line'] !== $tokens[$stackPtr]['line']
3✔
297
            ) {
298
                $found = 'newline';
3✔
299
            } else {
300
                $found = $tokens[($stackPtr + 1)]['length'];
3✔
301
            }
302

303
            $phpcsFile->recordMetric($stackPtr, 'Space after operator', $found);
3✔
304
            if ($found !== 1
3✔
305
                && ($found !== 'newline' || $this->ignoreNewlines === false)
3✔
306
            ) {
307
                $error = 'Expected 1 space after "%s"; %s found';
3✔
308
                $data  = [
2✔
309
                    $operator,
3✔
310
                    $found,
3✔
311
                ];
2✔
312

313
                $nextNonWhitespace = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
3✔
314
                if ($nextNonWhitespace !== false
3✔
315
                    && isset(Tokens::COMMENT_TOKENS[$tokens[$nextNonWhitespace]['code']]) === true
3✔
316
                    && $found === 'newline'
3✔
317
                ) {
318
                    // Don't auto-fix when it's a comment or PHPCS annotation on a new line as
319
                    // it causes fixer conflicts and can cause the meaning of annotations to change.
320
                    $phpcsFile->addError($error, $stackPtr, 'SpacingAfter', $data);
3✔
321
                } else {
322
                    $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingAfter', $data);
3✔
323
                    if ($fix === true) {
3✔
324
                        $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' ');
3✔
325
                    }
326
                }
327
            }//end if
328
        }//end if
329

330
    }//end process()
1✔
331

332

333
    /**
334
     * Checks if an operator is actually a different type of token in the current context.
335
     *
336
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked.
337
     * @param int                         $stackPtr  The position of the operator in
338
     *                                               the stack.
339
     *
340
     * @return boolean
341
     */
342
    protected function isOperator(File $phpcsFile, $stackPtr)
3✔
343
    {
344
        $tokens = $phpcsFile->getTokens();
3✔
345

346
        if ($tokens[$stackPtr]['code'] === T_DECLARE) {
3✔
347
            return false;
×
348
        }
349

350
        // Skip default values in function declarations.
351
        // Skip declare statements.
352
        if ($tokens[$stackPtr]['code'] === T_EQUAL) {
3✔
353
            if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
3✔
354
                $parenthesis = array_keys($tokens[$stackPtr]['nested_parenthesis']);
3✔
355
                $bracket     = array_pop($parenthesis);
3✔
356
                if (isset($tokens[$bracket]['parenthesis_owner']) === true) {
3✔
357
                    $function = $tokens[$bracket]['parenthesis_owner'];
3✔
358
                    if ($tokens[$function]['code'] === T_FUNCTION
3✔
359
                        || $tokens[$function]['code'] === T_CLOSURE
3✔
360
                        || $tokens[$function]['code'] === T_FN
3✔
361
                    ) {
362
                        return false;
3✔
363
                    }
364
                }
365
            }
366
        }
367

368
        if ($tokens[$stackPtr]['code'] === T_EQUAL) {
3✔
369
            // Skip for '=&' case.
370
            if (isset($tokens[($stackPtr + 1)]) === true
3✔
371
                && $tokens[($stackPtr + 1)]['code'] === T_BITWISE_AND
3✔
372
            ) {
373
                return false;
3✔
374
            }
375
        }
376

377
        if ($tokens[$stackPtr]['code'] === T_BITWISE_AND) {
3✔
378
            // If it's not a reference, then we expect one space either side of the
379
            // bitwise operator.
380
            if ($phpcsFile->isReference($stackPtr) === true) {
3✔
381
                return false;
3✔
382
            }
383
        }
384

385
        if ($tokens[$stackPtr]['code'] === T_MINUS || $tokens[$stackPtr]['code'] === T_PLUS) {
3✔
386
            // Check minus spacing, but make sure we aren't just assigning
387
            // a minus value or returning one.
388
            $prev = $phpcsFile->findPrevious(Tokens::EMPTY_TOKENS, ($stackPtr - 1), null, true);
3✔
389
            if (isset($this->nonOperandTokens[$tokens[$prev]['code']]) === true) {
3✔
390
                return false;
3✔
391
            }
392
        }//end if
393

394
        return true;
3✔
395

396
    }//end isOperator()
397

398

399
}//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

© 2025 Coveralls, Inc