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

keradus / PHP-CS-Fixer / 17319949156

29 Aug 2025 09:20AM UTC coverage: 94.696% (-0.05%) from 94.744%
17319949156

push

github

keradus
CS

28333 of 29920 relevant lines covered (94.7%)

45.63 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
if (strpos($haystack, $needle) === 0) {}
87
if (strpos($haystack, $needle) !== 0) {}
88
if (strpos($haystack, $needle) !== false) {}
89
if (strpos($haystack, $needle) === false) {}
90
',
3✔
91
                ),
3✔
92
                new CodeSample(
3✔
93
                    '<?php
3✔
94
if (strpos($haystack, $needle) === 0) {}
95
if (strpos($haystack, $needle) !== 0) {}
96
if (strpos($haystack, $needle) !== false) {}
97
if (strpos($haystack, $needle) === false) {}
98
if (stripos($haystack, $needle) === 0) {}
99
if (stripos($haystack, $needle) !== 0) {}
100
if (stripos($haystack, $needle) !== false) {}
101
if (stripos($haystack, $needle) === false) {}
102
',
3✔
103
                    ['modernize_stripos' => true]
3✔
104
                ),
3✔
105
            ],
3✔
106
            null,
3✔
107
            'Risky if `strpos`, `stripos`, `str_starts_with`, `str_contains` or `strtolower` functions are overridden.'
3✔
108
        );
3✔
109
    }
110

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

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

127
    public function isRisky(): bool
128
    {
129
        return true;
1✔
130
    }
131

132
    protected function configurePostNormalisation(): void
133
    {
134
        if (isset($this->configuration['modernize_stripos']) && true === $this->configuration['modernize_stripos']) {
82✔
135
            $this->modernizeStripos = true;
23✔
136
        }
137
    }
138

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

149
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
150
    {
151
        $functionsAnalyzer = new FunctionsAnalyzer();
59✔
152
        $argumentsAnalyzer = new ArgumentsAnalyzer();
59✔
153

154
        $modernizeCandidates = [[\T_STRING, 'strpos']];
59✔
155
        if ($this->modernizeStripos) {
59✔
156
            $modernizeCandidates[] = [\T_STRING, 'stripos'];
22✔
157
        }
158

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

165
            // assert called with 2 arguments
166
            $openIndex = $tokens->getNextMeaningfulToken($index);
46✔
167
            $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
46✔
168
            $arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
46✔
169

170
            if (2 !== \count($arguments)) {
46✔
171
                continue;
2✔
172
            }
173

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

177
            if (null === $compareTokens) {
44✔
178
                $compareTokens = $this->getCompareTokens($tokens, $closeIndex, 1); // look ahead
24✔
179
            }
180

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

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

198
            if (!$tokens[$operatorIndices['operand_index']]->equals($replacement['operand'], false)) {
39✔
199
                continue;
13✔
200
            }
201

202
            $tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndices['operator_index']);
39✔
203
            $tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndices['operand_index']);
39✔
204
            $tokens->clearTokenAndMergeSurroundingWhitespace($functionIndex);
39✔
205

206
            if ($replacement['negate']) {
39✔
207
                $negateInsertIndex = $functionIndex;
22✔
208

209
                $prevFunctionIndex = $tokens->getPrevMeaningfulToken($functionIndex);
22✔
210
                if ($tokens[$prevFunctionIndex]->isGivenKind(\T_NS_SEPARATOR)) {
22✔
211
                    $negateInsertIndex = $prevFunctionIndex;
4✔
212
                }
213

214
                $tokens->insertAt($negateInsertIndex, new Token('!'));
22✔
215
                ++$functionIndex;
22✔
216
            }
217

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

220
            if ($isCaseInsensitive) {
39✔
221
                $this->wrapArgumentsWithStrToLower($tokens, $functionIndex);
22✔
222
            }
223

224
            break;
39✔
225
        }
226
    }
227

228
    private function wrapArgumentsWithStrToLower(Tokens $tokens, int $functionIndex): void
229
    {
230
        $argumentsAnalyzer = new ArgumentsAnalyzer();
22✔
231
        $shouldAddNamespace = $tokens[$functionIndex - 1]->isGivenKind(\T_NS_SEPARATOR);
22✔
232

233
        $openIndex = $tokens->getNextMeaningfulToken($functionIndex);
22✔
234
        $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
22✔
235
        $arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
22✔
236

237
        $firstArgumentIndexStart = array_key_first($arguments);
22✔
238
        if (!isset($arguments[$firstArgumentIndexStart])) {
22✔
239
            return;
×
240
        }
241
        $firstArgumentIndexEnd = $arguments[$firstArgumentIndexStart] + 3 + ($shouldAddNamespace ? 1 : 0);
22✔
242

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

245
        if ($isSecondArgumentTokenWhiteSpace) {
22✔
246
            $secondArgumentIndexStart = $tokens->getNextMeaningfulToken(array_key_last($arguments));
21✔
247
        } else {
248
            $secondArgumentIndexStart = array_key_last($arguments);
1✔
249
        }
250

251
        $secondArgumentIndexStart += 3 + ($shouldAddNamespace ? 1 : 0);
22✔
252
        if (!isset($arguments[array_key_last($arguments)])) {
22✔
253
            return;
×
254
        }
255
        $secondArgumentIndexEnd = $arguments[array_key_last($arguments)] + 6 + ($shouldAddNamespace ? 1 : 0) + ($isSecondArgumentTokenWhiteSpace ? 1 : 0);
22✔
256

257
        if ($shouldAddNamespace) {
22✔
258
            $tokens->insertAt($firstArgumentIndexStart, new Token([\T_NS_SEPARATOR, '\\']));
4✔
259
            ++$firstArgumentIndexStart;
4✔
260
        }
261

262
        $tokens->insertAt($firstArgumentIndexStart, [new Token([\T_STRING, 'strtolower']), new Token('(')]);
22✔
263
        $tokens->insertAt($firstArgumentIndexEnd, new Token(')'));
22✔
264

265
        if ($shouldAddNamespace) {
22✔
266
            $tokens->insertAt($secondArgumentIndexStart, new Token([\T_NS_SEPARATOR, '\\']));
4✔
267
            ++$secondArgumentIndexStart;
4✔
268
        }
269

270
        $tokens->insertAt($secondArgumentIndexStart, [new Token([\T_STRING, 'strtolower']), new Token('(')]);
22✔
271
        $tokens->insertAt($secondArgumentIndexEnd, new Token(')'));
22✔
272
    }
273

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

283
        if (null !== $operatorIndex && $tokens[$operatorIndex]->isGivenKind(\T_NS_SEPARATOR)) {
44✔
284
            $operatorIndex = $tokens->getMeaningfulTokenSibling($operatorIndex, $direction);
8✔
285
        }
286

287
        if (null === $operatorIndex || !$tokens[$operatorIndex]->isGivenKind([\T_IS_IDENTICAL, \T_IS_NOT_IDENTICAL])) {
44✔
288
            return null;
24✔
289
        }
290

291
        $operandIndex = $tokens->getMeaningfulTokenSibling($operatorIndex, $direction);
44✔
292

293
        if (null === $operandIndex) {
44✔
294
            return null;
×
295
        }
296

297
        $operand = $tokens[$operandIndex];
44✔
298

299
        if (!$operand->equals([\T_LNUMBER, '0']) && !$operand->equals([\T_STRING, 'false'], false)) {
44✔
300
            return null;
1✔
301
        }
302

303
        $precedenceTokenIndex = $tokens->getMeaningfulTokenSibling($operandIndex, $direction);
43✔
304

305
        if (null !== $precedenceTokenIndex && $this->isOfHigherPrecedence($tokens[$precedenceTokenIndex])) {
43✔
306
            return null;
4✔
307
        }
308

309
        return ['operator_index' => $operatorIndex, 'operand_index' => $operandIndex];
39✔
310
    }
311

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