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

keradus / PHP-CS-Fixer / 17678835382

12 Sep 2025 03:24PM UTC coverage: 94.69% (-0.06%) from 94.75%
17678835382

push

github

keradus
fix typo

1 of 1 new or added line in 1 file covered. (100.0%)

1042 existing lines in 177 files now uncovered.

28424 of 30018 relevant lines covered (94.69%)

45.5 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\Tokenizer\Analyzer\ArgumentsAnalyzer;
27
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
28
use PhpCsFixer\Tokenizer\Token;
29
use PhpCsFixer\Tokenizer\Tokens;
30

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

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

77
    private bool $modernizeStripos = false;
78

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

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

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

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

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

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

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

143
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
144
    {
145
        return new FixerConfigurationResolver([
82✔
146
            (new FixerOptionBuilder('modernize_stripos', 'Whether to modernize `stripos` calls as well.'))
82✔
147
                ->setAllowedTypes(['bool'])
82✔
148
                ->setDefault(false) // @TODO change to "true" on next major 4.0
82✔
149
                ->getOption(),
82✔
150
        ]);
82✔
151
    }
152

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

228
            break;
39✔
229
        }
230
    }
231

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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