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

keradus / PHP-CS-Fixer / 14178008530

31 Mar 2025 02:35PM UTC coverage: 94.876% (+0.002%) from 94.874%
14178008530

push

github

web-flow
chore: make data providers key type `int` if all the keys are strings (#8550)

28163 of 29684 relevant lines covered (94.88%)

43.22 hits per line

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

99.16
/src/Tokenizer/Analyzer/FunctionsAnalyzer.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\Tokenizer\Analyzer;
16

17
use PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis;
18
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
19
use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
20
use PhpCsFixer\Tokenizer\CT;
21
use PhpCsFixer\Tokenizer\Token;
22
use PhpCsFixer\Tokenizer\Tokens;
23

24
/**
25
 * @internal
26
 */
27
final class FunctionsAnalyzer
28
{
29
    /**
30
     * @var array{tokens: string, imports: list<NamespaceUseAnalysis>, declarations: list<int>}
31
     */
32
    private array $functionsAnalysis = ['tokens' => '', 'imports' => [], 'declarations' => []];
33

34
    /**
35
     * Important: risky because of the limited (file) scope of the tool.
36
     */
37
    public function isGlobalFunctionCall(Tokens $tokens, int $index): bool
38
    {
39
        if (!$tokens[$index]->isGivenKind(T_STRING)) {
37✔
40
            return false;
37✔
41
        }
42

43
        $openParenthesisIndex = $tokens->getNextMeaningfulToken($index);
37✔
44

45
        if (!$tokens[$openParenthesisIndex]->equals('(')) {
37✔
46
            return false;
19✔
47
        }
48

49
        $previousIsNamespaceSeparator = false;
35✔
50
        $prevIndex = $tokens->getPrevMeaningfulToken($index);
35✔
51

52
        if ($tokens[$prevIndex]->isGivenKind(T_NS_SEPARATOR)) {
35✔
53
            $previousIsNamespaceSeparator = true;
5✔
54
            $prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
5✔
55
        }
56

57
        $possibleKind = [
35✔
58
            T_DOUBLE_COLON, T_FUNCTION, CT::T_NAMESPACE_OPERATOR, T_NEW, CT::T_RETURN_REF, T_STRING,
35✔
59
            ...Token::getObjectOperatorKinds(),
35✔
60
        ];
35✔
61

62
        // @TODO: drop condition when PHP 8.0+ is required
63
        if (\defined('T_ATTRIBUTE')) {
35✔
64
            $possibleKind[] = T_ATTRIBUTE;
35✔
65
        }
66

67
        if ($tokens[$prevIndex]->isGivenKind($possibleKind)) {
35✔
68
            return false;
23✔
69
        }
70

71
        if ($tokens[$tokens->getNextMeaningfulToken($openParenthesisIndex)]->isGivenKind(CT::T_FIRST_CLASS_CALLABLE)) {
26✔
72
            return false;
1✔
73
        }
74

75
        if ($previousIsNamespaceSeparator) {
25✔
76
            return true;
1✔
77
        }
78

79
        $functionName = strtolower($tokens[$index]->getContent());
24✔
80

81
        if ('set' === $functionName) {
24✔
82
            $closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesisIndex);
1✔
83
            $afterCloseParenthesisIndex = $tokens->getNextMeaningfulToken($closeParenthesisIndex);
1✔
84
            if ($tokens[$afterCloseParenthesisIndex]->equalsAny(['{', [T_DOUBLE_ARROW]])) {
1✔
85
                return false;
1✔
86
            }
87
        }
88

89
        if ($tokens->isChanged() || $tokens->getCodeHash() !== $this->functionsAnalysis['tokens']) {
24✔
90
            $this->buildFunctionsAnalysis($tokens);
24✔
91
        }
92

93
        // figure out in which namespace we are
94
        $scopeStartIndex = 0;
24✔
95
        $scopeEndIndex = \count($tokens) - 1;
24✔
96
        $inGlobalNamespace = false;
24✔
97

98
        foreach ($tokens->getNamespaceDeclarations() as $declaration) {
24✔
99
            $scopeStartIndex = $declaration->getScopeStartIndex();
24✔
100
            $scopeEndIndex = $declaration->getScopeEndIndex();
24✔
101

102
            if ($index >= $scopeStartIndex && $index <= $scopeEndIndex) {
24✔
103
                $inGlobalNamespace = $declaration->isGlobalNamespace();
24✔
104

105
                break;
24✔
106
            }
107
        }
108

109
        // check if the call is to a function declared in the same namespace as the call is done,
110
        // if the call is already in the global namespace than declared functions are in the same
111
        // global namespace and don't need checking
112

113
        if (!$inGlobalNamespace) {
24✔
114
            /** @var int $functionNameIndex */
115
            foreach ($this->functionsAnalysis['declarations'] as $functionNameIndex) {
4✔
116
                if ($functionNameIndex < $scopeStartIndex || $functionNameIndex > $scopeEndIndex) {
3✔
117
                    continue;
1✔
118
                }
119

120
                if (strtolower($tokens[$functionNameIndex]->getContent()) === $functionName) {
2✔
121
                    return false;
2✔
122
                }
123
            }
124
        }
125

126
        /** @var NamespaceUseAnalysis $functionUse */
127
        foreach ($this->functionsAnalysis['imports'] as $functionUse) {
22✔
128
            if ($functionUse->getStartIndex() < $scopeStartIndex || $functionUse->getEndIndex() > $scopeEndIndex) {
3✔
129
                continue;
1✔
130
            }
131

132
            if ($functionName !== strtolower($functionUse->getShortName())) {
3✔
133
                continue;
2✔
134
            }
135

136
            // global import like `use function \str_repeat;`
137
            return $functionUse->getShortName() === ltrim($functionUse->getFullName(), '\\');
1✔
138
        }
139

140
        if (AttributeAnalyzer::isAttribute($tokens, $index)) {
21✔
141
            return false;
1✔
142
        }
143

144
        return true;
20✔
145
    }
146

147
    /**
148
     * @return array<string, ArgumentAnalysis>
149
     */
150
    public function getFunctionArguments(Tokens $tokens, int $functionIndex): array
151
    {
152
        $argumentsStart = $tokens->getNextTokenOfKind($functionIndex, ['(']);
16✔
153
        $argumentsEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $argumentsStart);
16✔
154
        $argumentAnalyzer = new ArgumentsAnalyzer();
16✔
155
        $arguments = [];
16✔
156

157
        foreach ($argumentAnalyzer->getArguments($tokens, $argumentsStart, $argumentsEnd) as $start => $end) {
16✔
158
            $argumentInfo = $argumentAnalyzer->getArgumentInfo($tokens, $start, $end);
14✔
159
            $arguments[$argumentInfo->getName()] = $argumentInfo;
14✔
160
        }
161

162
        return $arguments;
16✔
163
    }
164

165
    public function getFunctionReturnType(Tokens $tokens, int $methodIndex): ?TypeAnalysis
166
    {
167
        $argumentsStart = $tokens->getNextTokenOfKind($methodIndex, ['(']);
9✔
168
        $argumentsEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $argumentsStart);
9✔
169
        $typeColonIndex = $tokens->getNextMeaningfulToken($argumentsEnd);
9✔
170

171
        if (!$tokens[$typeColonIndex]->isGivenKind(CT::T_TYPE_COLON)) {
9✔
172
            return null;
3✔
173
        }
174

175
        $type = '';
6✔
176
        $typeStartIndex = $tokens->getNextMeaningfulToken($typeColonIndex);
6✔
177
        $typeEndIndex = $typeStartIndex;
6✔
178
        $functionBodyStart = $tokens->getNextTokenOfKind($typeColonIndex, ['{', ';', [T_DOUBLE_ARROW]]);
6✔
179

180
        for ($i = $typeStartIndex; $i < $functionBodyStart; ++$i) {
6✔
181
            if ($tokens[$i]->isWhitespace() || $tokens[$i]->isComment()) {
6✔
182
                continue;
6✔
183
            }
184

185
            $type .= $tokens[$i]->getContent();
6✔
186
            $typeEndIndex = $i;
6✔
187
        }
188

189
        return new TypeAnalysis($type, $typeStartIndex, $typeEndIndex);
6✔
190
    }
191

192
    public function isTheSameClassCall(Tokens $tokens, int $index): bool
193
    {
194
        if (!$tokens->offsetExists($index)) {
10✔
195
            throw new \InvalidArgumentException(\sprintf('Token index %d does not exist.', $index));
1✔
196
        }
197

198
        $operatorIndex = $tokens->getPrevMeaningfulToken($index);
9✔
199

200
        if (null === $operatorIndex) {
9✔
201
            return false;
9✔
202
        }
203

204
        if (!$tokens[$operatorIndex]->isObjectOperator() && !$tokens[$operatorIndex]->isGivenKind(T_DOUBLE_COLON)) {
9✔
205
            return false;
9✔
206
        }
207

208
        $referenceIndex = $tokens->getPrevMeaningfulToken($operatorIndex);
9✔
209

210
        if (null === $referenceIndex) {
9✔
211
            return false;
×
212
        }
213

214
        if (!$tokens[$referenceIndex]->equalsAny([[T_VARIABLE, '$this'], [T_STRING, 'self'], [T_STATIC, 'static']], false)) {
9✔
215
            return false;
2✔
216
        }
217

218
        return $tokens[$tokens->getNextMeaningfulToken($index)]->equals('(');
7✔
219
    }
220

221
    private function buildFunctionsAnalysis(Tokens $tokens): void
222
    {
223
        $this->functionsAnalysis = [
24✔
224
            'tokens' => $tokens->getCodeHash(),
24✔
225
            'imports' => [],
24✔
226
            'declarations' => [],
24✔
227
        ];
24✔
228

229
        // find declarations
230

231
        if ($tokens->isTokenKindFound(T_FUNCTION)) {
24✔
232
            $end = \count($tokens);
12✔
233

234
            for ($i = 0; $i < $end; ++$i) {
12✔
235
                // skip classy, we are looking for functions not methods
236
                if ($tokens[$i]->isGivenKind(Token::getClassyTokenKinds())) {
12✔
237
                    $i = $tokens->getNextTokenOfKind($i, ['(', '{']);
4✔
238

239
                    if ($tokens[$i]->equals('(')) { // anonymous class
4✔
240
                        $i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $i);
1✔
241
                        $i = $tokens->getNextTokenOfKind($i, ['{']);
1✔
242
                    }
243

244
                    $i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $i);
4✔
245

246
                    continue;
4✔
247
                }
248

249
                if (!$tokens[$i]->isGivenKind(T_FUNCTION)) {
12✔
250
                    continue;
12✔
251
                }
252

253
                $i = $tokens->getNextMeaningfulToken($i);
8✔
254

255
                if ($tokens[$i]->isGivenKind(CT::T_RETURN_REF)) {
8✔
256
                    $i = $tokens->getNextMeaningfulToken($i);
1✔
257
                }
258

259
                if (!$tokens[$i]->isGivenKind(T_STRING)) {
8✔
260
                    continue;
1✔
261
                }
262

263
                $this->functionsAnalysis['declarations'][] = $i;
8✔
264
            }
265
        }
266

267
        // find imported functions
268

269
        $namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
24✔
270

271
        if ($tokens->isTokenKindFound(CT::T_FUNCTION_IMPORT)) {
24✔
272
            $declarations = $namespaceUsesAnalyzer->getDeclarationsFromTokens($tokens);
3✔
273

274
            foreach ($declarations as $declaration) {
3✔
275
                if ($declaration->isFunction()) {
3✔
276
                    $this->functionsAnalysis['imports'][] = $declaration;
3✔
277
                }
278
            }
279
        }
280
    }
281
}
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