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

keradus / PHP-CS-Fixer / 22042339290

15 Feb 2026 08:14PM UTC coverage: 92.957% (-0.2%) from 93.171%
22042339290

push

github

keradus
test: check PHP env in CI jobs

29302 of 31522 relevant lines covered (92.96%)

44.04 hits per line

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

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

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

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

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

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

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

135
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
136
    {
137
        return new FixerConfigurationResolver([
82✔
138
            (new FixerOptionBuilder('modernize_stripos', 'Whether to modernize `stripos` calls as well.'))
82✔
139
                ->setAllowedTypes(['bool'])
82✔
140
                ->setDefault(Future::getV4OrV3(true, false))
82✔
141
                ->getOption(),
82✔
142
        ]);
82✔
143
    }
144

145
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
146
    {
147
        $functionsAnalyzer = new FunctionsAnalyzer();
59✔
148
        $argumentsAnalyzer = new ArgumentsAnalyzer();
59✔
149

150
        $modernizeCandidates = [[\T_STRING, 'strpos']];
59✔
151
        if ($this->configuration['modernize_stripos']) {
59✔
152
            $modernizeCandidates[] = [\T_STRING, 'stripos'];
22✔
153
        }
154

155
        for ($index = \count($tokens) - 1; $index > 0; --$index) {
59✔
156
            // find candidate function call
157
            if (!$tokens[$index]->equalsAny($modernizeCandidates, false) || !$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
59✔
158
                continue;
59✔
159
            }
160

161
            // assert called with 2 arguments
162
            $openIndex = $tokens->getNextMeaningfulToken($index);
46✔
163
            $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
46✔
164
            $arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
46✔
165

166
            if (2 !== \count($arguments)) {
46✔
167
                continue;
2✔
168
            }
169

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

173
            if (null === $compareTokens) {
44✔
174
                $compareTokens = $this->getCompareTokens($tokens, $closeIndex, 1); // look ahead
24✔
175
            }
176

177
            if (null !== $compareTokens) {
44✔
178
                $isCaseInsensitive = $tokens[$index]->equals([\T_STRING, 'stripos'], false);
39✔
179
                $this->fixCall($tokens, $index, $compareTokens, $isCaseInsensitive);
39✔
180
            }
181
        }
182
    }
183

184
    /**
185
     * @param array{operator_index: int, operand_index: int} $operatorIndices
186
     */
187
    private function fixCall(Tokens $tokens, int $functionIndex, array $operatorIndices, bool $isCaseInsensitive): void
188
    {
189
        foreach (self::REPLACEMENTS as $replacement) {
39✔
190
            if (!$tokens[$operatorIndices['operator_index']]->equals($replacement['operator'])) {
39✔
191
                continue;
28✔
192
            }
193

194
            if (!$tokens[$operatorIndices['operand_index']]->equals($replacement['operand'], false)) {
39✔
195
                continue;
13✔
196
            }
197

198
            $tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndices['operator_index']);
39✔
199
            $tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndices['operand_index']);
39✔
200
            $tokens->clearTokenAndMergeSurroundingWhitespace($functionIndex);
39✔
201

202
            if ($replacement['negate']) {
39✔
203
                $negateInsertIndex = $functionIndex;
22✔
204

205
                $prevFunctionIndex = $tokens->getPrevMeaningfulToken($functionIndex);
22✔
206
                if ($tokens[$prevFunctionIndex]->isGivenKind(\T_NS_SEPARATOR)) {
22✔
207
                    $negateInsertIndex = $prevFunctionIndex;
4✔
208
                }
209

210
                $tokens->insertAt($negateInsertIndex, new Token('!'));
22✔
211
                ++$functionIndex;
22✔
212
            }
213

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

216
            if ($isCaseInsensitive) {
39✔
217
                $this->wrapArgumentsWithStrToLower($tokens, $functionIndex);
22✔
218
            }
219

220
            break;
39✔
221
        }
222
    }
223

224
    private function wrapArgumentsWithStrToLower(Tokens $tokens, int $functionIndex): void
225
    {
226
        $argumentsAnalyzer = new ArgumentsAnalyzer();
22✔
227
        $shouldAddNamespace = $tokens[$functionIndex - 1]->isGivenKind(\T_NS_SEPARATOR);
22✔
228

229
        $openIndex = $tokens->getNextMeaningfulToken($functionIndex);
22✔
230
        $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
22✔
231
        $arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
22✔
232

233
        $firstArgumentIndexStart = array_key_first($arguments);
22✔
234
        if (!isset($arguments[$firstArgumentIndexStart])) {
22✔
235
            return;
×
236
        }
237
        $firstArgumentIndexEnd = $arguments[$firstArgumentIndexStart] + 3 + ($shouldAddNamespace ? 1 : 0);
22✔
238

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

241
        if ($isSecondArgumentTokenWhiteSpace) {
22✔
242
            $secondArgumentIndexStart = $tokens->getNextMeaningfulToken(array_key_last($arguments));
21✔
243
        } else {
244
            $secondArgumentIndexStart = array_key_last($arguments);
1✔
245
        }
246

247
        $secondArgumentIndexStart += 3 + ($shouldAddNamespace ? 1 : 0);
22✔
248
        if (!isset($arguments[array_key_last($arguments)])) {
22✔
249
            return;
×
250
        }
251
        $secondArgumentIndexEnd = $arguments[array_key_last($arguments)] + 6 + ($shouldAddNamespace ? 1 : 0) + ($isSecondArgumentTokenWhiteSpace ? 1 : 0);
22✔
252

253
        if ($shouldAddNamespace) {
22✔
254
            $tokens->insertAt($firstArgumentIndexStart, new Token([\T_NS_SEPARATOR, '\\']));
4✔
255
            ++$firstArgumentIndexStart;
4✔
256
        }
257

258
        $tokens->insertAt($firstArgumentIndexStart, [new Token([\T_STRING, 'strtolower']), new Token('(')]);
22✔
259
        $tokens->insertAt($firstArgumentIndexEnd, new Token(')'));
22✔
260

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

266
        $tokens->insertAt($secondArgumentIndexStart, [new Token([\T_STRING, 'strtolower']), new Token('(')]);
22✔
267
        $tokens->insertAt($secondArgumentIndexEnd, new Token(')'));
22✔
268
    }
269

270
    /**
271
     * @param -1|1 $direction
272
     *
273
     * @return null|array{operator_index: int, operand_index: int}
274
     */
275
    private function getCompareTokens(Tokens $tokens, int $offsetIndex, int $direction): ?array
276
    {
277
        $operatorIndex = $tokens->getMeaningfulTokenSibling($offsetIndex, $direction);
44✔
278

279
        if (null !== $operatorIndex && $tokens[$operatorIndex]->isGivenKind(\T_NS_SEPARATOR)) {
44✔
280
            $operatorIndex = $tokens->getMeaningfulTokenSibling($operatorIndex, $direction);
8✔
281
        }
282

283
        if (null === $operatorIndex || !$tokens[$operatorIndex]->isGivenKind([\T_IS_IDENTICAL, \T_IS_NOT_IDENTICAL])) {
44✔
284
            return null;
24✔
285
        }
286

287
        $operandIndex = $tokens->getMeaningfulTokenSibling($operatorIndex, $direction);
44✔
288

289
        if (null === $operandIndex) {
44✔
290
            return null;
×
291
        }
292

293
        $operand = $tokens[$operandIndex];
44✔
294

295
        if (!$operand->equals([\T_LNUMBER, '0']) && !$operand->equals([\T_STRING, 'false'], false)) {
44✔
296
            return null;
1✔
297
        }
298

299
        $precedenceTokenIndex = $tokens->getMeaningfulTokenSibling($operandIndex, $direction);
43✔
300

301
        if (null !== $precedenceTokenIndex && $this->isOfHigherPrecedence($tokens[$precedenceTokenIndex])) {
43✔
302
            return null;
4✔
303
        }
304

305
        return ['operator_index' => $operatorIndex, 'operand_index' => $operandIndex];
39✔
306
    }
307

308
    private function isOfHigherPrecedence(Token $token): bool
309
    {
310
        return
43✔
311
            $token->isGivenKind([
43✔
312
                \T_DEC,                 // --
43✔
313
                \T_INC,                 // ++
43✔
314
                \T_INSTANCEOF,          // instanceof
43✔
315
                \T_IS_GREATER_OR_EQUAL, // >=
43✔
316
                \T_IS_SMALLER_OR_EQUAL, // <=
43✔
317
                \T_POW,                 // **
43✔
318
                \T_SL,                  // <<
43✔
319
                \T_SR,                  // >>
43✔
320
            ])
43✔
321
            || $token->equalsAny([
43✔
322
                '!',
43✔
323
                '%',
43✔
324
                '*',
43✔
325
                '+',
43✔
326
                '-',
43✔
327
                '.',
43✔
328
                '/',
43✔
329
                '<',
43✔
330
                '>',
43✔
331
                '~',
43✔
332
            ]);
43✔
333
    }
334
}
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