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

keradus / PHP-CS-Fixer / 17279562118

27 Aug 2025 09:47PM UTC coverage: 94.693%. Remained the same
17279562118

push

github

keradus
CS

28316 of 29903 relevant lines covered (94.69%)

45.61 hits per line

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

98.0
/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
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
31
 */
32
final class MbStrFunctionsFixer extends AbstractFixer
33
{
34
    /**
35
     * list of the string-related function names and their mb_ equivalent.
36
     *
37
     * @var array<
38
     *     string,
39
     *     array{
40
     *         alternativeName: string,
41
     *         argumentCount: list<int>,
42
     *     },
43
     * >
44
     */
45
    private static array $functionsMap = [
46
        'str_split' => ['alternativeName' => 'mb_str_split', 'argumentCount' => [1, 2, 3]],
47
        'stripos' => ['alternativeName' => 'mb_stripos', 'argumentCount' => [2, 3]],
48
        'stristr' => ['alternativeName' => 'mb_stristr', 'argumentCount' => [2, 3]],
49
        'strlen' => ['alternativeName' => 'mb_strlen', 'argumentCount' => [1]],
50
        'strpos' => ['alternativeName' => 'mb_strpos', 'argumentCount' => [2, 3]],
51
        'strrchr' => ['alternativeName' => 'mb_strrchr', 'argumentCount' => [2]],
52
        'strripos' => ['alternativeName' => 'mb_strripos', 'argumentCount' => [2, 3]],
53
        'strrpos' => ['alternativeName' => 'mb_strrpos', 'argumentCount' => [2, 3]],
54
        'strstr' => ['alternativeName' => 'mb_strstr', 'argumentCount' => [2, 3]],
55
        'strtolower' => ['alternativeName' => 'mb_strtolower', 'argumentCount' => [1]],
56
        'strtoupper' => ['alternativeName' => 'mb_strtoupper', 'argumentCount' => [1]],
57
        'substr' => ['alternativeName' => 'mb_substr', 'argumentCount' => [2, 3]],
58
        'substr_count' => ['alternativeName' => 'mb_substr_count', 'argumentCount' => [2, 3, 4]],
59
    ];
60

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

72
    public function __construct()
73
    {
74
        parent::__construct();
33✔
75

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

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

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

92
    public function isCandidate(Tokens $tokens): bool
93
    {
94
        return $tokens->isTokenKindFound(\T_STRING);
24✔
95
    }
96

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

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

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

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

144
        for ($index = $tokens->count() - 1; $index > 0; --$index) {
23✔
145
            if (!$tokens[$index]->isGivenKind(\T_STRING)) {
23✔
146
                continue;
23✔
147
            }
148

149
            $lowercasedContent = strtolower($tokens[$index]->getContent());
23✔
150
            if (!isset($this->functions[$lowercasedContent])) {
23✔
151
                continue;
17✔
152
            }
153

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

164
                continue;
13✔
165
            }
166

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