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

keradus / PHP-CS-Fixer / 13747606772

08 Mar 2025 10:38PM UTC coverage: 94.847% (-0.08%) from 94.929%
13747606772

push

github

web-flow
fix: `MbStrFunctionsFixer` - fix imports (#8474)

23 of 24 new or added lines in 1 file covered. (95.83%)

181 existing lines in 6 files now uncovered.

28127 of 29655 relevant lines covered (94.85%)

43.08 hits per line

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

91.67
/src/Fixer/Alias/MbStrFunctionsFixer.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\FixerDefinition\CodeSample;
19
use PhpCsFixer\FixerDefinition\FixerDefinition;
20
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
21
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
22
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
23
use PhpCsFixer\Tokenizer\CT;
24
use PhpCsFixer\Tokenizer\Token;
25
use PhpCsFixer\Tokenizer\Tokens;
26

27
/**
28
 * @author Filippo Tessarotto <zoeslam@gmail.com>
29
 */
30
final class MbStrFunctionsFixer extends AbstractFixer
31
{
32
    /**
33
     * list of the string-related function names and their mb_ equivalent.
34
     *
35
     * @var array<
36
     *     string,
37
     *     array{
38
     *         alternativeName: string,
39
     *         argumentCount: list<int>,
40
     *     },
41
     * >
42
     */
43
    private static array $functionsMap = [
44
        'str_split' => ['alternativeName' => 'mb_str_split', 'argumentCount' => [1, 2, 3]],
45
        'stripos' => ['alternativeName' => 'mb_stripos', 'argumentCount' => [2, 3]],
46
        'stristr' => ['alternativeName' => 'mb_stristr', 'argumentCount' => [2, 3]],
47
        'strlen' => ['alternativeName' => 'mb_strlen', 'argumentCount' => [1]],
48
        'strpos' => ['alternativeName' => 'mb_strpos', 'argumentCount' => [2, 3]],
49
        'strrchr' => ['alternativeName' => 'mb_strrchr', 'argumentCount' => [2]],
50
        'strripos' => ['alternativeName' => 'mb_strripos', 'argumentCount' => [2, 3]],
51
        'strrpos' => ['alternativeName' => 'mb_strrpos', 'argumentCount' => [2, 3]],
52
        'strstr' => ['alternativeName' => 'mb_strstr', 'argumentCount' => [2, 3]],
53
        'strtolower' => ['alternativeName' => 'mb_strtolower', 'argumentCount' => [1]],
54
        'strtoupper' => ['alternativeName' => 'mb_strtoupper', 'argumentCount' => [1]],
55
        'substr' => ['alternativeName' => 'mb_substr', 'argumentCount' => [2, 3]],
56
        'substr_count' => ['alternativeName' => 'mb_substr_count', 'argumentCount' => [2, 3, 4]],
57
    ];
58

59
    /**
60
     * @var array<
61
     *     string,
62
     *     array{
63
     *         alternativeName: string,
64
     *         argumentCount: list<int>,
65
     *     },
66
     * >
67
     */
68
    private array $functions;
69

70
    public function __construct()
71
    {
72
        parent::__construct();
28✔
73

74
        if (\PHP_VERSION_ID >= 8_03_00) {
28✔
75
            self::$functionsMap['str_pad'] = ['alternativeName' => 'mb_str_pad', 'argumentCount' => [1, 2, 3, 4]];
28✔
76
        }
77

78
        if (\PHP_VERSION_ID >= 8_04_00) {
28✔
79
            self::$functionsMap['trim'] = ['alternativeName' => 'mb_trim', 'argumentCount' => [1, 2]];
×
80
            self::$functionsMap['ltrim'] = ['alternativeName' => 'mb_ltrim', 'argumentCount' => [1, 2]];
×
81
            self::$functionsMap['rtrim'] = ['alternativeName' => 'mb_rtrim', 'argumentCount' => [1, 2]];
×
82
        }
83

84
        $this->functions = array_filter(
28✔
85
            self::$functionsMap,
28✔
86
            static fn (array $mapping): bool => (new \ReflectionFunction($mapping['alternativeName']))->isInternal()
28✔
87
        );
28✔
88
    }
89

90
    public function isCandidate(Tokens $tokens): bool
91
    {
92
        return $tokens->isTokenKindFound(T_STRING);
20✔
93
    }
94

95
    public function isRisky(): bool
96
    {
97
        return true;
1✔
98
    }
99

100
    /**
101
     * {@inheritdoc}
102
     *
103
     * Must run before NativeFunctionInvocationFixer.
104
     */
105
    public function getPriority(): int
106
    {
107
        return 2;
1✔
108
    }
109

110
    public function getDefinition(): FixerDefinitionInterface
111
    {
112
        return new FixerDefinition(
3✔
113
            'Replace non multibyte-safe functions with corresponding mb function.',
3✔
114
            [
3✔
115
                new CodeSample(
3✔
116
                    '<?php
3✔
117
$a = strlen($a);
118
$a = strpos($a, $b);
119
$a = strrpos($a, $b);
120
$a = substr($a, $b);
121
$a = strtolower($a);
122
$a = strtoupper($a);
123
$a = stripos($a, $b);
124
$a = strripos($a, $b);
125
$a = strstr($a, $b);
126
$a = stristr($a, $b);
127
$a = strrchr($a, $b);
128
$a = substr_count($a, $b);
129
'
3✔
130
                ),
3✔
131
            ],
3✔
132
            null,
3✔
133
            'Risky when any of the functions are overridden, or when relying on the string byte size rather than its length in characters.'
3✔
134
        );
3✔
135
    }
136

137
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
138
    {
139
        $argumentsAnalyzer = new ArgumentsAnalyzer();
19✔
140
        $functionsAnalyzer = new FunctionsAnalyzer();
19✔
141

142
        for ($index = $tokens->count() - 1; $index > 0; --$index) {
19✔
143
            if (!$tokens[$index]->isGivenKind(T_STRING)) {
19✔
144
                continue;
19✔
145
            }
146

147
            $lowercasedContent = strtolower($tokens[$index]->getContent());
19✔
148
            if (!isset($this->functions[$lowercasedContent])) {
19✔
149
                continue;
13✔
150
            }
151

152
            // is it a global function call?
153
            if ($functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
19✔
154
                $openParenthesis = $tokens->getNextMeaningfulToken($index);
11✔
155
                $closeParenthesis = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis);
11✔
156
                $numberOfArguments = $argumentsAnalyzer->countArguments($tokens, $openParenthesis, $closeParenthesis);
11✔
157
                if (!\in_array($numberOfArguments, $this->functions[$lowercasedContent]['argumentCount'], true)) {
11✔
158
                    continue;
2✔
159
                }
160
                $tokens[$index] = new Token([T_STRING, $this->functions[$lowercasedContent]['alternativeName']]);
9✔
161

162
                continue;
9✔
163
            }
164

165
            // is it a global function import?
166
            $functionIndex = $tokens->getPrevMeaningfulToken($index);
9✔
167
            if (!$tokens[$functionIndex]->isGivenKind(CT::T_FUNCTION_IMPORT)) {
9✔
168
                continue;
9✔
169
            }
170
            $useIndex = $tokens->getPrevMeaningfulToken($functionIndex);
1✔
171
            if (!$tokens[$useIndex]->isGivenKind(T_USE)) {
1✔
NEW
172
                continue;
×
173
            }
174
            $tokens[$index] = new Token([T_STRING, $this->functions[$lowercasedContent]['alternativeName']]);
1✔
175
        }
176
    }
177
}
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