Skip to content

Commit 57b10e3

Browse files
committed
bug #4846 FunctionsAnalyzer - better isGlobalFunctionCall detection (SpacePossum)
This PR was merged into the 2.15 branch. Discussion ---------- FunctionsAnalyzer - better isGlobalFunctionCall detection Fixes #3051 Replaces #4251 Commits ------- 6a3d517 FunctionsAnalyzer - better isGlobalFunctionCall detection
2 parents 088428f + 6a3d517 commit 57b10e3

File tree

4 files changed

+425
-69
lines changed

4 files changed

+425
-69
lines changed

src/Fixer/FunctionNotation/ImplodeCallFixer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens)
7575
{
7676
$functionsAnalyzer = new FunctionsAnalyzer();
7777

78-
foreach ($tokens as $index => $token) {
78+
for ($index = \count($tokens) - 1; $index > 0; --$index) {
7979
if (!$tokens[$index]->equals([T_STRING, 'implode'], false)) {
8080
continue;
8181
}

src/Tokenizer/Analyzer/FunctionsAnalyzer.php

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
namespace PhpCsFixer\Tokenizer\Analyzer;
1414

1515
use PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis;
16+
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
1617
use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
1718
use PhpCsFixer\Tokenizer\CT;
19+
use PhpCsFixer\Tokenizer\Token;
1820
use PhpCsFixer\Tokenizer\Tokens;
1921

2022
/**
@@ -23,6 +25,13 @@
2325
final class FunctionsAnalyzer
2426
{
2527
/**
28+
* @var array
29+
*/
30+
private $functionsAnalysis = ['tokens' => '', 'imports' => [], 'declarations' => []];
31+
32+
/**
33+
* Important: risky because of the limited (file) scope of the tool.
34+
*
2635
* @param int $index
2736
*
2837
* @return bool
@@ -33,15 +42,85 @@ public function isGlobalFunctionCall(Tokens $tokens, $index)
3342
return false;
3443
}
3544

45+
$nextIndex = $tokens->getNextMeaningfulToken($index);
46+
47+
if (!$tokens[$nextIndex]->equals('(')) {
48+
return false;
49+
}
50+
51+
$previousIsNamespaceSeparator = false;
3652
$prevIndex = $tokens->getPrevMeaningfulToken($index);
53+
3754
if ($tokens[$prevIndex]->isGivenKind(T_NS_SEPARATOR)) {
55+
$previousIsNamespaceSeparator = true;
3856
$prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
3957
}
4058

41-
$nextIndex = $tokens->getNextMeaningfulToken($index);
59+
if ($tokens[$prevIndex]->isGivenKind([T_DOUBLE_COLON, T_FUNCTION, CT::T_NAMESPACE_OPERATOR, T_NEW, T_OBJECT_OPERATOR, CT::T_RETURN_REF, T_STRING])) {
60+
return false;
61+
}
62+
63+
if ($previousIsNamespaceSeparator) {
64+
return true;
65+
}
66+
67+
if ($tokens->isChanged() || $tokens->getCodeHash() !== $this->functionsAnalysis['tokens']) {
68+
$this->buildFunctionsAnalysis($tokens);
69+
}
70+
71+
// figure out in which namespace we are
72+
$namespaceAnalyzer = new NamespacesAnalyzer();
73+
74+
$declarations = $namespaceAnalyzer->getDeclarations($tokens);
75+
$scopeStartIndex = 0;
76+
$scopeEndIndex = \count($tokens) - 1;
77+
$inGlobalNamespace = false;
78+
79+
foreach ($declarations as $declaration) {
80+
$scopeStartIndex = $declaration->getScopeStartIndex();
81+
$scopeEndIndex = $declaration->getScopeEndIndex();
82+
83+
if ($index >= $scopeStartIndex && $index <= $scopeEndIndex) {
84+
$inGlobalNamespace = '' === $declaration->getFullName();
85+
86+
break;
87+
}
88+
}
89+
90+
$call = strtolower($tokens[$index]->getContent());
91+
92+
// check if the call is to a function declared in the same namespace as the call is done,
93+
// if the call is already in the global namespace than declared functions are in the same
94+
// global namespace and don't need checking
95+
96+
if (!$inGlobalNamespace) {
97+
/** @var int $functionNameIndex */
98+
foreach ($this->functionsAnalysis['declarations'] as $functionNameIndex) {
99+
if ($functionNameIndex < $scopeStartIndex || $functionNameIndex > $scopeEndIndex) {
100+
continue;
101+
}
102+
103+
if (strtolower($tokens[$functionNameIndex]->getContent()) === $call) {
104+
return false;
105+
}
106+
}
107+
}
42108

43-
return !$tokens[$prevIndex]->isGivenKind([T_DOUBLE_COLON, T_FUNCTION, CT::T_NAMESPACE_OPERATOR, T_NEW, T_OBJECT_OPERATOR, CT::T_RETURN_REF, T_STRING])
44-
&& $tokens[$nextIndex]->equals('(');
109+
/** @var NamespaceUseAnalysis $functionUse */
110+
foreach ($this->functionsAnalysis['imports'] as $functionUse) {
111+
if ($functionUse->getStartIndex() < $scopeStartIndex || $functionUse->getEndIndex() > $scopeEndIndex) {
112+
continue;
113+
}
114+
115+
if ($call !== strtolower($functionUse->getShortName())) {
116+
continue;
117+
}
118+
119+
// global import like `use function \str_repeat;`
120+
return $functionUse->getShortName() === ltrim($functionUse->getFullName(), '\\');
121+
}
122+
123+
return true;
45124
}
46125

47126
/**
@@ -119,4 +198,65 @@ public function isTheSameClassCall(Tokens $tokens, $index)
119198
|| $tokens[$operatorIndex]->equals([T_DOUBLE_COLON, '::']) && $tokens[$referenceIndex]->equals([T_STRING, 'self'], false)
120199
|| $tokens[$operatorIndex]->equals([T_DOUBLE_COLON, '::']) && $tokens[$referenceIndex]->equals([T_STATIC, 'static'], false);
121200
}
201+
202+
private function buildFunctionsAnalysis(Tokens $tokens)
203+
{
204+
$this->functionsAnalysis = [
205+
'tokens' => $tokens->getCodeHash(),
206+
'imports' => [],
207+
'declarations' => [],
208+
];
209+
210+
// find declarations
211+
212+
if ($tokens->isTokenKindFound(T_FUNCTION)) {
213+
$end = \count($tokens);
214+
215+
for ($i = 0; $i < $end; ++$i) {
216+
// skip classy, we are looking for functions not methods
217+
if ($tokens[$i]->isGivenKind(Token::getClassyTokenKinds())) {
218+
$i = $tokens->getNextTokenOfKind($i, ['(', '{']);
219+
220+
if ($tokens[$i]->equals('(')) { // anonymous class
221+
$i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $i);
222+
$i = $tokens->getNextTokenOfKind($i, ['{']);
223+
}
224+
225+
$i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $i);
226+
227+
continue;
228+
}
229+
230+
if (!$tokens[$i]->isGivenKind(T_FUNCTION)) {
231+
continue;
232+
}
233+
234+
$i = $tokens->getNextMeaningfulToken($i);
235+
236+
if ($tokens[$i]->isGivenKind(CT::T_RETURN_REF)) {
237+
$i = $tokens->getNextMeaningfulToken($i);
238+
}
239+
240+
if (!$tokens[$i]->isGivenKind(T_STRING)) {
241+
continue;
242+
}
243+
244+
$this->functionsAnalysis['declarations'][] = $i;
245+
}
246+
}
247+
248+
// find imported functions
249+
250+
$namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
251+
252+
if ($tokens->isTokenKindFound(CT::T_FUNCTION_IMPORT)) {
253+
$declarations = $namespaceUsesAnalyzer->getDeclarationsFromTokens($tokens);
254+
255+
foreach ($declarations as $declaration) {
256+
if ($declaration->isFunction()) {
257+
$this->functionsAnalysis['imports'][] = $declaration;
258+
}
259+
}
260+
}
261+
}
122262
}

tests/Fixer/FunctionNotation/NativeFunctionInvocationFixerTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,16 @@ public function & strlen($name) {
592592
'strict' => true,
593593
],
594594
],
595+
[
596+
'<?php
597+
use function foo\json_decode;
598+
json_decode($base);
599+
',
600+
null,
601+
[
602+
'include' => ['@all'],
603+
],
604+
],
595605
];
596606
}
597607

0 commit comments

Comments
 (0)