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

keradus / PHP-CS-Fixer / 17319949156

29 Aug 2025 09:20AM UTC coverage: 94.696% (-0.05%) from 94.744%
17319949156

push

github

keradus
CS

28333 of 29920 relevant lines covered (94.7%)

45.63 hits per line

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

96.12
/src/Fixer/AbstractShortOperatorFixer.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <fabien@symfony.com>
9
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14

15
namespace PhpCsFixer\Fixer;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer;
19
use PhpCsFixer\Tokenizer\Analyzer\RangeAnalyzer;
20
use PhpCsFixer\Tokenizer\CT;
21
use PhpCsFixer\Tokenizer\Token;
22
use PhpCsFixer\Tokenizer\Tokens;
23

24
/**
25
 * @internal
26
 *
27
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
28
 */
29
abstract class AbstractShortOperatorFixer extends AbstractFixer
30
{
31
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
32
    {
33
        $alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer();
110✔
34

35
        for ($index = \count($tokens) - 1; $index > 3; --$index) {
110✔
36
            if (!$this->isOperatorTokenCandidate($tokens, $index)) {
110✔
37
                continue;
110✔
38
            }
39

40
            // get what is before the operator
41

42
            $beforeRange = $this->getBeforeOperatorRange($tokens, $index);
107✔
43
            $equalsIndex = $tokens->getPrevMeaningfulToken($beforeRange['start']);
107✔
44

45
            // make sure that before that is '='
46

47
            if (!$tokens[$equalsIndex]->equals('=')) {
107✔
48
                continue;
13✔
49
            }
50

51
            // get and check what is before '='
52

53
            $assignRange = $this->getBeforeOperatorRange($tokens, $equalsIndex);
98✔
54
            $beforeAssignmentIndex = $tokens->getPrevMeaningfulToken($assignRange['start']);
98✔
55

56
            if ($tokens[$beforeAssignmentIndex]->equals(':')) {
98✔
57
                if (!$this->belongsToSwitchOrAlternativeSyntax($alternativeSyntaxAnalyzer, $tokens, $beforeAssignmentIndex)) {
6✔
58
                    continue;
2✔
59
                }
60
            } elseif (!$tokens[$beforeAssignmentIndex]->equalsAny([';', '{', '}', '(', ')', ',', [\T_OPEN_TAG], [\T_RETURN]])) {
92✔
61
                continue;
2✔
62
            }
63

64
            // check if "assign" and "before" the operator are (functionally) the same
65

66
            if (RangeAnalyzer::rangeEqualsRange($tokens, $assignRange, $beforeRange)) {
96✔
67
                $this->shortenOperation($tokens, $equalsIndex, $index, $assignRange, $beforeRange);
78✔
68

69
                continue;
78✔
70
            }
71

72
            if (!$this->isOperatorCommutative($tokens[$index])) {
18✔
73
                continue;
7✔
74
            }
75

76
            $afterRange = $this->getAfterOperatorRange($tokens, $index);
12✔
77

78
            // check if "assign" and "after" the operator are (functionally) the same
79
            if (!RangeAnalyzer::rangeEqualsRange($tokens, $assignRange, $afterRange)) {
12✔
80
                continue;
1✔
81
            }
82

83
            $this->shortenOperation($tokens, $equalsIndex, $index, $assignRange, $afterRange);
11✔
84
        }
85
    }
86

87
    abstract protected function getReplacementToken(Token $token): Token;
88

89
    abstract protected function isOperatorTokenCandidate(Tokens $tokens, int $index): bool;
90

91
    /**
92
     * @param array{start: int, end: int} $assignRange
93
     * @param array{start: int, end: int} $operatorRange
94
     */
95
    private function shortenOperation(
96
        Tokens $tokens,
97
        int $equalsIndex,
98
        int $operatorIndex,
99
        array $assignRange,
100
        array $operatorRange
101
    ): void {
102
        $tokens[$equalsIndex] = $this->getReplacementToken($tokens[$operatorIndex]);
89✔
103
        $tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndex);
89✔
104
        $this->clearMeaningfulFromRange($tokens, $operatorRange);
89✔
105

106
        foreach ([$equalsIndex, $assignRange['end']] as $i) {
89✔
107
            $i = $tokens->getNonEmptySibling($i, 1);
89✔
108

109
            if ($tokens[$i]->isWhitespace(" \t")) {
89✔
110
                $tokens[$i] = new Token([\T_WHITESPACE, ' ']);
83✔
111
            } elseif (!$tokens[$i]->isWhitespace()) {
9✔
112
                $tokens->insertAt($i, new Token([\T_WHITESPACE, ' ']));
6✔
113
            }
114
        }
115
    }
116

117
    /**
118
     * @return array{start: int, end: int}
119
     */
120
    private function getAfterOperatorRange(Tokens $tokens, int $index): array
121
    {
122
        $index = $tokens->getNextMeaningfulToken($index);
12✔
123
        $range = ['start' => $index];
12✔
124

125
        while (true) {
12✔
126
            $nextIndex = $tokens->getNextMeaningfulToken($index);
12✔
127

128
            if (null === $nextIndex || $tokens[$nextIndex]->equalsAny([';', ',', [\T_CLOSE_TAG]])) {
12✔
129
                break;
11✔
130
            }
131

132
            $blockType = Tokens::detectBlockType($tokens[$nextIndex]);
1✔
133

134
            if (null === $blockType) {
1✔
135
                $index = $nextIndex;
×
136

137
                continue;
×
138
            }
139

140
            if (false === $blockType['isStart']) {
1✔
141
                break;
1✔
142
            }
143

144
            $index = $tokens->findBlockEnd($blockType['type'], $nextIndex);
×
145
        }
146

147
        $range['end'] = $index;
12✔
148

149
        return $range;
12✔
150
    }
151

152
    /**
153
     * @return array{start: int, end: int}
154
     */
155
    private function getBeforeOperatorRange(Tokens $tokens, int $index): array
156
    {
157
        static $blockOpenTypes;
107✔
158

159
        if (null === $blockOpenTypes) {
107✔
160
            $blockOpenTypes = [',']; // not a true "block type", but speeds up things
2✔
161

162
            foreach (Tokens::getBlockEdgeDefinitions() as $definition) {
2✔
163
                $blockOpenTypes[] = $definition['start'];
2✔
164
            }
165
        }
166

167
        $controlStructureWithoutBracesTypes = [\T_IF, \T_ELSE, \T_ELSEIF, \T_FOR, \T_FOREACH, \T_WHILE];
107✔
168

169
        $previousIndex = $tokens->getPrevMeaningfulToken($index);
107✔
170
        $previousToken = $tokens[$previousIndex];
107✔
171

172
        if ($tokens[$previousIndex]->equalsAny($blockOpenTypes)) {
107✔
173
            return ['start' => $index, 'end' => $index];
1✔
174
        }
175

176
        $range = ['end' => $previousIndex];
106✔
177
        $index = $previousIndex;
106✔
178

179
        while ($previousToken->equalsAny([
106✔
180
            '$',
106✔
181
            ']',
106✔
182
            ')',
106✔
183
            [CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
106✔
184
            [CT::T_DYNAMIC_PROP_BRACE_CLOSE],
106✔
185
            [CT::T_DYNAMIC_VAR_BRACE_CLOSE],
106✔
186
            [\T_NS_SEPARATOR],
106✔
187
            [\T_STRING],
106✔
188
            [\T_VARIABLE],
106✔
189
        ])) {
106✔
190
            $blockType = Tokens::detectBlockType($previousToken);
104✔
191

192
            if (null !== $blockType) {
104✔
193
                $blockStart = $tokens->findBlockStart($blockType['type'], $previousIndex);
20✔
194

195
                if ($tokens[$previousIndex]->equals(')') && $tokens[$tokens->getPrevMeaningfulToken($blockStart)]->isGivenKind($controlStructureWithoutBracesTypes)) {
20✔
196
                    break; // we went too far back
3✔
197
                }
198

199
                $previousIndex = $blockStart;
17✔
200
            }
201

202
            $index = $previousIndex;
104✔
203
            $previousIndex = $tokens->getPrevMeaningfulToken($previousIndex);
104✔
204
            $previousToken = $tokens[$previousIndex];
104✔
205
        }
206

207
        if ($previousToken->isGivenKind(\T_OBJECT_OPERATOR)) {
106✔
208
            $index = $this->getBeforeOperatorRange($tokens, $previousIndex)['start'];
4✔
209
        } elseif ($previousToken->isGivenKind(\T_PAAMAYIM_NEKUDOTAYIM)) {
106✔
210
            $index = $this->getBeforeOperatorRange($tokens, $tokens->getPrevMeaningfulToken($previousIndex))['start'];
1✔
211
        }
212

213
        $range['start'] = $index;
106✔
214

215
        return $range;
106✔
216
    }
217

218
    /**
219
     * @param array{start: int, end: int} $range
220
     */
221
    private function clearMeaningfulFromRange(Tokens $tokens, array $range): void
222
    {
223
        // $range['end'] must be meaningful!
224
        for ($i = $range['end']; $i >= $range['start']; $i = $tokens->getPrevMeaningfulToken($i)) {
89✔
225
            $tokens->clearTokenAndMergeSurroundingWhitespace($i);
89✔
226
        }
227
    }
228

229
    private function isOperatorCommutative(Token $operatorToken): bool
230
    {
231
        if ($operatorToken->isGivenKind(\T_COALESCE)) {
18✔
232
            return false;
2✔
233
        }
234

235
        // check for commutative kinds
236
        if ($operatorToken->equalsAny(['*', '|', '&', '^'])) { // note that for arrays in PHP `+` is not commutative
16✔
237
            return true;
12✔
238
        }
239

240
        // check for non-commutative kinds
241
        if ($operatorToken->equalsAny(['-', '/', '.', '%', '+'])) {
5✔
242
            return false;
5✔
243
        }
244

245
        throw new \InvalidArgumentException(\sprintf('Not supported operator "%s".', $operatorToken->toJson()));
×
246
    }
247

248
    private function belongsToSwitchOrAlternativeSyntax(AlternativeSyntaxAnalyzer $alternativeSyntaxAnalyzer, Tokens $tokens, int $index): bool
249
    {
250
        $candidate = $index;
6✔
251
        $index = $tokens->getPrevMeaningfulToken($candidate);
6✔
252

253
        if ($tokens[$index]->isGivenKind(\T_DEFAULT)) {
6✔
254
            return true;
2✔
255
        }
256

257
        $index = $tokens->getPrevMeaningfulToken($index);
6✔
258

259
        if ($tokens[$index]->isGivenKind(\T_CASE)) {
6✔
260
            return true;
2✔
261
        }
262

263
        return $alternativeSyntaxAnalyzer->belongsToAlternativeSyntax($tokens, $candidate);
4✔
264
    }
265
}
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