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

keradus / PHP-CS-Fixer / 16303177127

15 Jul 2025 06:22PM UTC coverage: 94.758% (-0.05%) from 94.806%
16303177127

push

github

keradus
bumped version

28199 of 29759 relevant lines covered (94.76%)

45.91 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
abstract class AbstractShortOperatorFixer extends AbstractFixer
28
{
29
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
30
    {
31
        $alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer();
110✔
32

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

38
            // get what is before the operator
39

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

43
            // make sure that before that is '='
44

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

49
            // get and check what is before '='
50

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

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

62
            // check if "assign" and "before" the operator are (functionally) the same
63

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

67
                continue;
78✔
68
            }
69

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

74
            $afterRange = $this->getAfterOperatorRange($tokens, $index);
12✔
75

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

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

85
    abstract protected function getReplacementToken(Token $token): Token;
86

87
    abstract protected function isOperatorTokenCandidate(Tokens $tokens, int $index): bool;
88

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

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

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

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

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

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

130
            $blockType = Tokens::detectBlockType($tokens[$nextIndex]);
1✔
131

132
            if (null === $blockType) {
1✔
133
                $index = $nextIndex;
×
134

135
                continue;
×
136
            }
137

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

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

145
        $range['end'] = $index;
12✔
146

147
        return $range;
12✔
148
    }
149

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

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

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

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

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

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

174
        $range = ['end' => $previousIndex];
106✔
175
        $index = $previousIndex;
106✔
176

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

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

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

197
                $previousIndex = $blockStart;
17✔
198
            }
199

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

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

211
        $range['start'] = $index;
106✔
212

213
        return $range;
106✔
214
    }
215

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

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

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

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

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

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

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

255
        $index = $tokens->getPrevMeaningfulToken($index);
6✔
256

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

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