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

keradus / PHP-CS-Fixer / 12791833934

15 Jan 2025 03:40PM UTC coverage: 94.966% (-0.009%) from 94.975%
12791833934

push

github

web-flow
Merge branch 'master' into docker_alpine

52 of 54 new or added lines in 4 files covered. (96.3%)

3 existing lines in 1 file now uncovered.

27882 of 29360 relevant lines covered (94.97%)

43.1 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
 * @author Alexander M. Turek <me@derrabus.de>
33
 *
34
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
35
 *
36
 * @phpstan-type _AutogeneratedInputConfiguration array{
37
 *  modernize_stripos?: bool
38
 * }
39
 * @phpstan-type _AutogeneratedComputedConfiguration array{
40
 *  modernize_stripos: bool
41
 * }
42
 */
43
final class ModernizeStrposFixer extends AbstractFixer implements ConfigurableFixerInterface
44
{
45
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
46
    use ConfigurableFixerTrait;
47

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

75
    private bool $modernizeStripos = false;
76

77
    public function getDefinition(): FixerDefinitionInterface
78
    {
79
        return new FixerDefinition(
3✔
80
            'Replace `strpos()` and `stripos()` calls with `str_starts_with()` or `str_contains()` if possible.',
3✔
81
            [
3✔
82
                new CodeSample(
3✔
83
                    '<?php
3✔
84
if (strpos($haystack, $needle) === 0) {}
85
if (strpos($haystack, $needle) !== 0) {}
86
if (strpos($haystack, $needle) !== false) {}
87
if (strpos($haystack, $needle) === false) {}
88
',
3✔
89
                ),
3✔
90
                new CodeSample(
3✔
91
                    '<?php
3✔
92
if (strpos($haystack, $needle) === 0) {}
93
if (strpos($haystack, $needle) !== 0) {}
94
if (strpos($haystack, $needle) !== false) {}
95
if (strpos($haystack, $needle) === false) {}
96
if (stripos($haystack, $needle) === 0) {}
97
if (stripos($haystack, $needle) !== 0) {}
98
if (stripos($haystack, $needle) !== false) {}
99
if (stripos($haystack, $needle) === false) {}
100
',
3✔
101
                    ['modernize_stripos' => true]
3✔
102
                ),
3✔
103
            ],
3✔
104
            null,
3✔
105
            'Risky if `strpos`, `stripos`, `str_starts_with`, `str_contains` or `strtolower` functions are overridden.'
3✔
106
        );
3✔
107
    }
108

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

222
            break;
39✔
223
        }
224
    }
225

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

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

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

241
        $isSecondArgumentTokenWhiteSpace = $tokens[array_key_last($arguments)]->isGivenKind(T_WHITESPACE);
22✔
242

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

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

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

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

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

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

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

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

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

289
        $operandIndex = $tokens->getMeaningfulTokenSibling($operatorIndex, $direction);
44✔
290

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

295
        $operand = $tokens[$operandIndex];
44✔
296

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

301
        $precedenceTokenIndex = $tokens->getMeaningfulTokenSibling($operandIndex, $direction);
43✔
302

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

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

310
    private function isOfHigherPrecedence(Token $token): bool
311
    {
312
        static $operatorsKinds = [
43✔
313
            T_DEC,                 // --
43✔
314
            T_INC,                 // ++
43✔
315
            T_INSTANCEOF,          // instanceof
43✔
316
            T_IS_GREATER_OR_EQUAL, // >=
43✔
317
            T_IS_SMALLER_OR_EQUAL, // <=
43✔
318
            T_POW,                 // **
43✔
319
            T_SL,                  // <<
43✔
320
            T_SR,                  // >>
43✔
321
        ];
43✔
322

323
        static $operatorsPerContent = [
43✔
324
            '!',
43✔
325
            '%',
43✔
326
            '*',
43✔
327
            '+',
43✔
328
            '-',
43✔
329
            '.',
43✔
330
            '/',
43✔
331
            '<',
43✔
332
            '>',
43✔
333
            '~',
43✔
334
        ];
43✔
335

336
        return $token->isGivenKind($operatorsKinds) || $token->equalsAny($operatorsPerContent);
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