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

keradus / PHP-CS-Fixer / 18051010410

26 Sep 2025 10:40PM UTC coverage: 94.308% (-0.02%) from 94.331%
18051010410

push

github

web-flow
chore: use accidentally missing `@auto:risky` (#9102)

28583 of 30308 relevant lines covered (94.31%)

45.24 hits per line

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

97.69
/src/Fixer/Alias/ModernizeStrposFixer.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\Alias;
16

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

32
/**
33
 * @phpstan-type _AutogeneratedInputConfiguration array{
34
 *  modernize_stripos?: bool,
35
 * }
36
 * @phpstan-type _AutogeneratedComputedConfiguration array{
37
 *  modernize_stripos: bool,
38
 * }
39
 *
40
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
41
 *
42
 * @author Alexander M. Turek <me@derrabus.de>
43
 *
44
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
45
 */
46
final class ModernizeStrposFixer extends AbstractFixer implements ConfigurableFixerInterface
47
{
48
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
49
    use ConfigurableFixerTrait;
50

51
    private const REPLACEMENTS = [
52
        [
53
            'operator' => [\T_IS_IDENTICAL, '==='],
54
            'operand' => [\T_LNUMBER, '0'],
55
            'replacement' => [\T_STRING, 'str_starts_with'],
56
            'negate' => false,
57
        ],
58
        [
59
            'operator' => [\T_IS_NOT_IDENTICAL, '!=='],
60
            'operand' => [\T_LNUMBER, '0'],
61
            'replacement' => [\T_STRING, 'str_starts_with'],
62
            'negate' => true,
63
        ],
64
        [
65
            'operator' => [\T_IS_NOT_IDENTICAL, '!=='],
66
            'operand' => [\T_STRING, 'false'],
67
            'replacement' => [\T_STRING, 'str_contains'],
68
            'negate' => false,
69
        ],
70
        [
71
            'operator' => [\T_IS_IDENTICAL, '==='],
72
            'operand' => [\T_STRING, 'false'],
73
            'replacement' => [\T_STRING, 'str_contains'],
74
            'negate' => true,
75
        ],
76
    ];
77

78
    private bool $modernizeStripos = false;
79

80
    public function getDefinition(): FixerDefinitionInterface
81
    {
82
        return new FixerDefinition(
3✔
83
            'Replace `strpos()` and `stripos()` calls with `str_starts_with()` or `str_contains()` if possible.',
3✔
84
            [
3✔
85
                new CodeSample(
3✔
86
                    <<<'PHP'
3✔
87
                        <?php
88
                        if (strpos($haystack, $needle) === 0) {}
89
                        if (strpos($haystack, $needle) !== 0) {}
90
                        if (strpos($haystack, $needle) !== false) {}
91
                        if (strpos($haystack, $needle) === false) {}
92

93
                        PHP,
3✔
94
                ),
3✔
95
                new CodeSample(
3✔
96
                    <<<'PHP'
3✔
97
                        <?php
98
                        if (strpos($haystack, $needle) === 0) {}
99
                        if (strpos($haystack, $needle) !== 0) {}
100
                        if (strpos($haystack, $needle) !== false) {}
101
                        if (strpos($haystack, $needle) === false) {}
102
                        if (stripos($haystack, $needle) === 0) {}
103
                        if (stripos($haystack, $needle) !== 0) {}
104
                        if (stripos($haystack, $needle) !== false) {}
105
                        if (stripos($haystack, $needle) === false) {}
106

107
                        PHP,
3✔
108
                    ['modernize_stripos' => true]
3✔
109
                ),
3✔
110
            ],
3✔
111
            null,
3✔
112
            'Risky if `strpos`, `stripos`, `str_starts_with`, `str_contains` or `strtolower` functions are overridden.'
3✔
113
        );
3✔
114
    }
115

116
    /**
117
     * {@inheritdoc}
118
     *
119
     * Must run before BinaryOperatorSpacesFixer, NoExtraBlankLinesFixer, NoSpacesInsideParenthesisFixer, NoTrailingWhitespaceFixer, NotOperatorWithSpaceFixer, NotOperatorWithSuccessorSpaceFixer, PhpUnitDedicateAssertFixer, SingleSpaceAfterConstructFixer, SingleSpaceAroundConstructFixer, SpacesInsideParenthesesFixer.
120
     * Must run after StrictComparisonFixer.
121
     */
122
    public function getPriority(): int
123
    {
124
        return 37;
1✔
125
    }
126

127
    public function isCandidate(Tokens $tokens): bool
128
    {
129
        return $tokens->isTokenKindFound(\T_STRING) && $tokens->isAnyTokenKindsFound([\T_IS_IDENTICAL, \T_IS_NOT_IDENTICAL]);
71✔
130
    }
131

132
    public function isRisky(): bool
133
    {
134
        return true;
1✔
135
    }
136

137
    protected function configurePostNormalisation(): void
138
    {
139
        if (isset($this->configuration['modernize_stripos']) && true === $this->configuration['modernize_stripos']) {
82✔
140
            $this->modernizeStripos = true;
23✔
141
        }
142
    }
143

144
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
145
    {
146
        return new FixerConfigurationResolver([
82✔
147
            (new FixerOptionBuilder('modernize_stripos', 'Whether to modernize `stripos` calls as well.'))
82✔
148
                ->setAllowedTypes(['bool'])
82✔
149
                ->setDefault(Future::getV4OrV3(true, false))
82✔
150
                ->getOption(),
82✔
151
        ]);
82✔
152
    }
153

154
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
155
    {
156
        $functionsAnalyzer = new FunctionsAnalyzer();
59✔
157
        $argumentsAnalyzer = new ArgumentsAnalyzer();
59✔
158

159
        $modernizeCandidates = [[\T_STRING, 'strpos']];
59✔
160
        if ($this->modernizeStripos) {
59✔
161
            $modernizeCandidates[] = [\T_STRING, 'stripos'];
22✔
162
        }
163

164
        for ($index = \count($tokens) - 1; $index > 0; --$index) {
59✔
165
            // find candidate function call
166
            if (!$tokens[$index]->equalsAny($modernizeCandidates, false) || !$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
59✔
167
                continue;
59✔
168
            }
169

170
            // assert called with 2 arguments
171
            $openIndex = $tokens->getNextMeaningfulToken($index);
46✔
172
            $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
46✔
173
            $arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
46✔
174

175
            if (2 !== \count($arguments)) {
46✔
176
                continue;
2✔
177
            }
178

179
            // check if part condition and fix if needed
180
            $compareTokens = $this->getCompareTokens($tokens, $index, -1); // look behind
44✔
181

182
            if (null === $compareTokens) {
44✔
183
                $compareTokens = $this->getCompareTokens($tokens, $closeIndex, 1); // look ahead
24✔
184
            }
185

186
            if (null !== $compareTokens) {
44✔
187
                $isCaseInsensitive = $tokens[$index]->equals([\T_STRING, 'stripos'], false);
39✔
188
                $this->fixCall($tokens, $index, $compareTokens, $isCaseInsensitive);
39✔
189
            }
190
        }
191
    }
192

193
    /**
194
     * @param array{operator_index: int, operand_index: int} $operatorIndices
195
     */
196
    private function fixCall(Tokens $tokens, int $functionIndex, array $operatorIndices, bool $isCaseInsensitive): void
197
    {
198
        foreach (self::REPLACEMENTS as $replacement) {
39✔
199
            if (!$tokens[$operatorIndices['operator_index']]->equals($replacement['operator'])) {
39✔
200
                continue;
28✔
201
            }
202

203
            if (!$tokens[$operatorIndices['operand_index']]->equals($replacement['operand'], false)) {
39✔
204
                continue;
13✔
205
            }
206

207
            $tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndices['operator_index']);
39✔
208
            $tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndices['operand_index']);
39✔
209
            $tokens->clearTokenAndMergeSurroundingWhitespace($functionIndex);
39✔
210

211
            if ($replacement['negate']) {
39✔
212
                $negateInsertIndex = $functionIndex;
22✔
213

214
                $prevFunctionIndex = $tokens->getPrevMeaningfulToken($functionIndex);
22✔
215
                if ($tokens[$prevFunctionIndex]->isGivenKind(\T_NS_SEPARATOR)) {
22✔
216
                    $negateInsertIndex = $prevFunctionIndex;
4✔
217
                }
218

219
                $tokens->insertAt($negateInsertIndex, new Token('!'));
22✔
220
                ++$functionIndex;
22✔
221
            }
222

223
            $tokens->insertAt($functionIndex, new Token($replacement['replacement']));
39✔
224

225
            if ($isCaseInsensitive) {
39✔
226
                $this->wrapArgumentsWithStrToLower($tokens, $functionIndex);
22✔
227
            }
228

229
            break;
39✔
230
        }
231
    }
232

233
    private function wrapArgumentsWithStrToLower(Tokens $tokens, int $functionIndex): void
234
    {
235
        $argumentsAnalyzer = new ArgumentsAnalyzer();
22✔
236
        $shouldAddNamespace = $tokens[$functionIndex - 1]->isGivenKind(\T_NS_SEPARATOR);
22✔
237

238
        $openIndex = $tokens->getNextMeaningfulToken($functionIndex);
22✔
239
        $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
22✔
240
        $arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
22✔
241

242
        $firstArgumentIndexStart = array_key_first($arguments);
22✔
243
        if (!isset($arguments[$firstArgumentIndexStart])) {
22✔
244
            return;
×
245
        }
246
        $firstArgumentIndexEnd = $arguments[$firstArgumentIndexStart] + 3 + ($shouldAddNamespace ? 1 : 0);
22✔
247

248
        $isSecondArgumentTokenWhiteSpace = $tokens[array_key_last($arguments)]->isGivenKind(\T_WHITESPACE);
22✔
249

250
        if ($isSecondArgumentTokenWhiteSpace) {
22✔
251
            $secondArgumentIndexStart = $tokens->getNextMeaningfulToken(array_key_last($arguments));
21✔
252
        } else {
253
            $secondArgumentIndexStart = array_key_last($arguments);
1✔
254
        }
255

256
        $secondArgumentIndexStart += 3 + ($shouldAddNamespace ? 1 : 0);
22✔
257
        if (!isset($arguments[array_key_last($arguments)])) {
22✔
258
            return;
×
259
        }
260
        $secondArgumentIndexEnd = $arguments[array_key_last($arguments)] + 6 + ($shouldAddNamespace ? 1 : 0) + ($isSecondArgumentTokenWhiteSpace ? 1 : 0);
22✔
261

262
        if ($shouldAddNamespace) {
22✔
263
            $tokens->insertAt($firstArgumentIndexStart, new Token([\T_NS_SEPARATOR, '\\']));
4✔
264
            ++$firstArgumentIndexStart;
4✔
265
        }
266

267
        $tokens->insertAt($firstArgumentIndexStart, [new Token([\T_STRING, 'strtolower']), new Token('(')]);
22✔
268
        $tokens->insertAt($firstArgumentIndexEnd, new Token(')'));
22✔
269

270
        if ($shouldAddNamespace) {
22✔
271
            $tokens->insertAt($secondArgumentIndexStart, new Token([\T_NS_SEPARATOR, '\\']));
4✔
272
            ++$secondArgumentIndexStart;
4✔
273
        }
274

275
        $tokens->insertAt($secondArgumentIndexStart, [new Token([\T_STRING, 'strtolower']), new Token('(')]);
22✔
276
        $tokens->insertAt($secondArgumentIndexEnd, new Token(')'));
22✔
277
    }
278

279
    /**
280
     * @param -1|1 $direction
281
     *
282
     * @return null|array{operator_index: int, operand_index: int}
283
     */
284
    private function getCompareTokens(Tokens $tokens, int $offsetIndex, int $direction): ?array
285
    {
286
        $operatorIndex = $tokens->getMeaningfulTokenSibling($offsetIndex, $direction);
44✔
287

288
        if (null !== $operatorIndex && $tokens[$operatorIndex]->isGivenKind(\T_NS_SEPARATOR)) {
44✔
289
            $operatorIndex = $tokens->getMeaningfulTokenSibling($operatorIndex, $direction);
8✔
290
        }
291

292
        if (null === $operatorIndex || !$tokens[$operatorIndex]->isGivenKind([\T_IS_IDENTICAL, \T_IS_NOT_IDENTICAL])) {
44✔
293
            return null;
24✔
294
        }
295

296
        $operandIndex = $tokens->getMeaningfulTokenSibling($operatorIndex, $direction);
44✔
297

298
        if (null === $operandIndex) {
44✔
299
            return null;
×
300
        }
301

302
        $operand = $tokens[$operandIndex];
44✔
303

304
        if (!$operand->equals([\T_LNUMBER, '0']) && !$operand->equals([\T_STRING, 'false'], false)) {
44✔
305
            return null;
1✔
306
        }
307

308
        $precedenceTokenIndex = $tokens->getMeaningfulTokenSibling($operandIndex, $direction);
43✔
309

310
        if (null !== $precedenceTokenIndex && $this->isOfHigherPrecedence($tokens[$precedenceTokenIndex])) {
43✔
311
            return null;
4✔
312
        }
313

314
        return ['operator_index' => $operatorIndex, 'operand_index' => $operandIndex];
39✔
315
    }
316

317
    private function isOfHigherPrecedence(Token $token): bool
318
    {
319
        return
43✔
320
            $token->isGivenKind([
43✔
321
                \T_DEC,                 // --
43✔
322
                \T_INC,                 // ++
43✔
323
                \T_INSTANCEOF,          // instanceof
43✔
324
                \T_IS_GREATER_OR_EQUAL, // >=
43✔
325
                \T_IS_SMALLER_OR_EQUAL, // <=
43✔
326
                \T_POW,                 // **
43✔
327
                \T_SL,                  // <<
43✔
328
                \T_SR,                  // >>
43✔
329
            ])
43✔
330
            || $token->equalsAny([
43✔
331
                '!',
43✔
332
                '%',
43✔
333
                '*',
43✔
334
                '+',
43✔
335
                '-',
43✔
336
                '.',
43✔
337
                '/',
43✔
338
                '<',
43✔
339
                '>',
43✔
340
                '~',
43✔
341
            ]);
43✔
342
    }
343
}
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