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

keradus / PHP-CS-Fixer / 17377459942

01 Sep 2025 12:19PM UTC coverage: 94.684% (-0.009%) from 94.693%
17377459942

push

github

web-flow
chore: `Tokens::offsetSet` - explicit validation of input (#9004)

1 of 5 new or added lines in 1 file covered. (20.0%)

306 existing lines in 60 files now uncovered.

28390 of 29984 relevant lines covered (94.68%)

45.5 hits per line

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

99.07
/src/Fixer/PhpUnit/PhpUnitExpectationFixer.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\PhpUnit;
16

17
use PhpCsFixer\Fixer\AbstractPhpUnitFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
22
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
23
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
24
use PhpCsFixer\FixerDefinition\CodeSample;
25
use PhpCsFixer\FixerDefinition\FixerDefinition;
26
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
27
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
28
use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
29
use PhpCsFixer\Tokenizer\Token;
30
use PhpCsFixer\Tokenizer\Tokens;
31

32
/**
33
 * @phpstan-type _AutogeneratedInputConfiguration array{
34
 *  target?: '5.2'|'5.6'|'8.4'|'newest',
35
 * }
36
 * @phpstan-type _AutogeneratedComputedConfiguration array{
37
 *  target: '5.2'|'5.6'|'8.4'|'newest',
38
 * }
39
 *
40
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
41
 *
42
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
43
 *
44
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
45
 */
46
final class PhpUnitExpectationFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
47
{
48
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
49
    use ConfigurableFixerTrait;
50

51
    /**
52
     * @var array<string, string>
53
     */
54
    private array $methodMap = [];
55

56
    public function getDefinition(): FixerDefinitionInterface
57
    {
58
        return new FixerDefinition(
3✔
59
            'Usages of `->setExpectedException*` methods MUST be replaced by `->expectException*` methods.',
3✔
60
            [
3✔
61
                new CodeSample(
3✔
62
                    <<<'PHP'
3✔
63
                        <?php
64
                        final class MyTest extends \PHPUnit_Framework_TestCase
65
                        {
66
                            public function testFoo()
67
                            {
68
                                $this->setExpectedException("RuntimeException", "Msg", 123);
69
                                foo();
70
                            }
71

72
                            public function testBar()
73
                            {
74
                                $this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
75
                                bar();
76
                            }
77
                        }
78

79
                        PHP
3✔
80
                ),
3✔
81
                new CodeSample(
3✔
82
                    <<<'PHP'
3✔
83
                        <?php
84
                        final class MyTest extends \PHPUnit_Framework_TestCase
85
                        {
86
                            public function testFoo()
87
                            {
88
                                $this->setExpectedException("RuntimeException", null, 123);
89
                                foo();
90
                            }
91

92
                            public function testBar()
93
                            {
94
                                $this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
95
                                bar();
96
                            }
97
                        }
98

99
                        PHP,
3✔
100
                    ['target' => PhpUnitTargetVersion::VERSION_8_4]
3✔
101
                ),
3✔
102
                new CodeSample(
3✔
103
                    <<<'PHP'
3✔
104
                        <?php
105
                        final class MyTest extends \PHPUnit_Framework_TestCase
106
                        {
107
                            public function testFoo()
108
                            {
109
                                $this->setExpectedException("RuntimeException", null, 123);
110
                                foo();
111
                            }
112

113
                            public function testBar()
114
                            {
115
                                $this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
116
                                bar();
117
                            }
118
                        }
119

120
                        PHP,
3✔
121
                    ['target' => PhpUnitTargetVersion::VERSION_5_6]
3✔
122
                ),
3✔
123
                new CodeSample(
3✔
124
                    <<<'PHP'
3✔
125
                        <?php
126
                        final class MyTest extends \PHPUnit_Framework_TestCase
127
                        {
128
                            public function testFoo()
129
                            {
130
                                $this->setExpectedException("RuntimeException", "Msg", 123);
131
                                foo();
132
                            }
133

134
                            public function testBar()
135
                            {
136
                                $this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
137
                                bar();
138
                            }
139
                        }
140

141
                        PHP,
3✔
142
                    ['target' => PhpUnitTargetVersion::VERSION_5_2]
3✔
143
                ),
3✔
144
            ],
3✔
145
            null,
3✔
146
            'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
3✔
147
        );
3✔
148
    }
149

150
    /**
151
     * {@inheritdoc}
152
     *
153
     * Must run after PhpUnitNoExpectationAnnotationFixer.
154
     */
155
    public function getPriority(): int
156
    {
157
        return 0;
1✔
158
    }
159

160
    public function isRisky(): bool
161
    {
162
        return true;
1✔
163
    }
164

165
    protected function configurePostNormalisation(): void
166
    {
167
        $this->methodMap = [
24✔
168
            'setExpectedException' => 'expectExceptionMessage',
24✔
169
        ];
24✔
170

171
        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_6)) {
24✔
172
            $this->methodMap['setExpectedExceptionRegExp'] = 'expectExceptionMessageRegExp';
24✔
173
        }
174

175
        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_8_4)) {
24✔
176
            $this->methodMap['setExpectedExceptionRegExp'] = 'expectExceptionMessageMatches';
24✔
177
            $this->methodMap['expectExceptionMessageRegExp'] = 'expectExceptionMessageMatches';
24✔
178
        }
179
    }
180

181
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
182
    {
183
        return new FixerConfigurationResolver([
24✔
184
            (new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
24✔
185
                ->setAllowedTypes(['string'])
24✔
186
                ->setAllowedValues([PhpUnitTargetVersion::VERSION_5_2, PhpUnitTargetVersion::VERSION_5_6, PhpUnitTargetVersion::VERSION_8_4, PhpUnitTargetVersion::VERSION_NEWEST])
24✔
187
                ->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
24✔
188
                ->getOption(),
24✔
189
        ]);
24✔
190
    }
191

192
    protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
193
    {
194
        foreach (Token::getObjectOperatorKinds() as $objectOperator) {
15✔
195
            $this->applyPhpUnitClassFixWithObjectOperator($tokens, $startIndex, $endIndex, $objectOperator);
15✔
196
        }
197
    }
198

199
    private function applyPhpUnitClassFixWithObjectOperator(Tokens $tokens, int $startIndex, int $endIndex, int $objectOperator): void
200
    {
201
        $argumentsAnalyzer = new ArgumentsAnalyzer();
15✔
202

203
        $oldMethodSequence = [
15✔
204
            [\T_VARIABLE, '$this'],
15✔
205
            [$objectOperator],
15✔
206
            [\T_STRING],
15✔
207
        ];
15✔
208

209
        for ($index = $startIndex; $startIndex < $endIndex; ++$index) {
15✔
210
            $match = $tokens->findSequence($oldMethodSequence, $index);
15✔
211

212
            if (null === $match) {
15✔
213
                return;
15✔
214
            }
215

216
            [$thisIndex, , $index] = array_keys($match);
15✔
217

218
            if (!isset($this->methodMap[$tokens[$index]->getContent()])) {
15✔
219
                continue;
15✔
220
            }
221

222
            $openIndex = $tokens->getNextTokenOfKind($index, ['(']);
15✔
223
            $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
15✔
224
            $commaIndex = $tokens->getPrevMeaningfulToken($closeIndex);
15✔
225
            if ($tokens[$commaIndex]->equals(',')) {
15✔
226
                $tokens->removeTrailingWhitespace($commaIndex);
2✔
227
                $tokens->clearAt($commaIndex);
2✔
228
            }
229

230
            $arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
15✔
231
            $argumentsCnt = \count($arguments);
15✔
232

233
            $argumentsReplacements = ['expectException', $this->methodMap[$tokens[$index]->getContent()], 'expectExceptionCode'];
15✔
234

235
            $indent = $this->whitespacesConfig->getLineEnding().WhitespacesAnalyzer::detectIndent($tokens, $thisIndex);
15✔
236

237
            $isMultilineWhitespace = false;
15✔
238

239
            for ($cnt = $argumentsCnt - 1; $cnt >= 1; --$cnt) {
15✔
240
                $argStart = array_keys($arguments)[$cnt];
13✔
241
                $argBefore = $tokens->getPrevMeaningfulToken($argStart);
13✔
242

243
                if (!isset($argumentsReplacements[$cnt])) {
13✔
UNCOV
244
                    throw new \LogicException(\sprintf('Unexpected index %d to find replacement method.', $cnt));
×
245
                }
246

247
                if ('expectExceptionMessage' === $argumentsReplacements[$cnt]) {
13✔
248
                    $paramIndicatorIndex = $tokens->getNextMeaningfulToken($argBefore);
12✔
249
                    $afterParamIndicatorIndex = $tokens->getNextMeaningfulToken($paramIndicatorIndex);
12✔
250

251
                    if (
252
                        $tokens[$paramIndicatorIndex]->equals([\T_STRING, 'null'], false)
12✔
253
                        && $tokens[$afterParamIndicatorIndex]->equals(')')
12✔
254
                    ) {
255
                        if ($tokens[$argBefore + 1]->isWhitespace()) {
2✔
256
                            $tokens->clearTokenAndMergeSurroundingWhitespace($argBefore + 1);
1✔
257
                        }
258
                        $tokens->clearTokenAndMergeSurroundingWhitespace($argBefore);
2✔
259
                        $tokens->clearTokenAndMergeSurroundingWhitespace($paramIndicatorIndex);
2✔
260

261
                        continue;
2✔
262
                    }
263
                }
264

265
                $isMultilineWhitespace = $isMultilineWhitespace || ($tokens[$argStart]->isWhitespace() && !$tokens[$argStart]->isWhitespace(" \t"));
13✔
266
                $tokensOverrideArgStart = [
13✔
267
                    new Token([\T_WHITESPACE, $indent]),
13✔
268
                    new Token([\T_VARIABLE, '$this']),
13✔
269
                    new Token([\T_OBJECT_OPERATOR, '->']),
13✔
270
                    new Token([\T_STRING, $argumentsReplacements[$cnt]]),
13✔
271
                    new Token('('),
13✔
272
                ];
13✔
273
                $tokensOverrideArgBefore = [
13✔
274
                    new Token(')'),
13✔
275
                    new Token(';'),
13✔
276
                ];
13✔
277

278
                if ($isMultilineWhitespace) {
13✔
279
                    $tokensOverrideArgStart[] = new Token([\T_WHITESPACE, $indent.$this->whitespacesConfig->getIndent()]);
1✔
280
                    array_unshift($tokensOverrideArgBefore, new Token([\T_WHITESPACE, $indent]));
1✔
281
                }
282

283
                if ($tokens[$argStart]->isWhitespace()) {
13✔
284
                    $tokens->overrideRange($argStart, $argStart, $tokensOverrideArgStart);
12✔
285
                } else {
286
                    $tokens->insertAt($argStart, $tokensOverrideArgStart);
1✔
287
                }
288

289
                $tokens->overrideRange($argBefore, $argBefore, $tokensOverrideArgBefore);
13✔
290
            }
291

292
            $methodName = 'expectException';
15✔
293
            if ('expectExceptionMessageRegExp' === $tokens[$index]->getContent()) {
15✔
294
                $methodName = $this->methodMap[$tokens[$index]->getContent()];
2✔
295
            }
296
            $tokens[$index] = new Token([\T_STRING, $methodName]);
15✔
297
        }
298
    }
299
}
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