Skip to content

Commit

Permalink
FunctionsAnalyzer - better isGlobalFunctionCall detection
Browse files Browse the repository at this point in the history
  • Loading branch information
SpacePossum committed Feb 26, 2020
1 parent ed34dde commit 6a3d517
Show file tree
Hide file tree
Showing 4 changed files with 425 additions and 69 deletions.
2 changes: 1 addition & 1 deletion src/Fixer/FunctionNotation/ImplodeCallFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens)
{
$functionsAnalyzer = new FunctionsAnalyzer();

foreach ($tokens as $index => $token) {
for ($index = \count($tokens) - 1; $index > 0; --$index) {
if (!$tokens[$index]->equals([T_STRING, 'implode'], false)) {
continue;
}
Expand Down
146 changes: 143 additions & 3 deletions src/Tokenizer/Analyzer/FunctionsAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
namespace PhpCsFixer\Tokenizer\Analyzer;

use PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
Expand All @@ -23,6 +25,13 @@
final class FunctionsAnalyzer
{
/**
* @var array
*/
private $functionsAnalysis = ['tokens' => '', 'imports' => [], 'declarations' => []];

/**
* Important: risky because of the limited (file) scope of the tool.
*
* @param int $index
*
* @return bool
Expand All @@ -33,15 +42,85 @@ public function isGlobalFunctionCall(Tokens $tokens, $index)
return false;
}

$nextIndex = $tokens->getNextMeaningfulToken($index);

if (!$tokens[$nextIndex]->equals('(')) {
return false;
}

$previousIsNamespaceSeparator = false;
$prevIndex = $tokens->getPrevMeaningfulToken($index);

if ($tokens[$prevIndex]->isGivenKind(T_NS_SEPARATOR)) {
$previousIsNamespaceSeparator = true;
$prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
}

$nextIndex = $tokens->getNextMeaningfulToken($index);
if ($tokens[$prevIndex]->isGivenKind([T_DOUBLE_COLON, T_FUNCTION, CT::T_NAMESPACE_OPERATOR, T_NEW, T_OBJECT_OPERATOR, CT::T_RETURN_REF, T_STRING])) {
return false;
}

if ($previousIsNamespaceSeparator) {
return true;
}

if ($tokens->isChanged() || $tokens->getCodeHash() !== $this->functionsAnalysis['tokens']) {
$this->buildFunctionsAnalysis($tokens);
}

// figure out in which namespace we are
$namespaceAnalyzer = new NamespacesAnalyzer();

$declarations = $namespaceAnalyzer->getDeclarations($tokens);
$scopeStartIndex = 0;
$scopeEndIndex = \count($tokens) - 1;
$inGlobalNamespace = false;

foreach ($declarations as $declaration) {
$scopeStartIndex = $declaration->getScopeStartIndex();
$scopeEndIndex = $declaration->getScopeEndIndex();

if ($index >= $scopeStartIndex && $index <= $scopeEndIndex) {
$inGlobalNamespace = '' === $declaration->getFullName();

break;
}
}

$call = strtolower($tokens[$index]->getContent());

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

if (!$inGlobalNamespace) {
/** @var int $functionNameIndex */
foreach ($this->functionsAnalysis['declarations'] as $functionNameIndex) {
if ($functionNameIndex < $scopeStartIndex || $functionNameIndex > $scopeEndIndex) {
continue;
}

if (strtolower($tokens[$functionNameIndex]->getContent()) === $call) {
return false;
}
}
}

return !$tokens[$prevIndex]->isGivenKind([T_DOUBLE_COLON, T_FUNCTION, CT::T_NAMESPACE_OPERATOR, T_NEW, T_OBJECT_OPERATOR, CT::T_RETURN_REF, T_STRING])
&& $tokens[$nextIndex]->equals('(');
/** @var NamespaceUseAnalysis $functionUse */
foreach ($this->functionsAnalysis['imports'] as $functionUse) {
if ($functionUse->getStartIndex() < $scopeStartIndex || $functionUse->getEndIndex() > $scopeEndIndex) {
continue;
}

if ($call !== strtolower($functionUse->getShortName())) {
continue;
}

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

return true;
}

/**
Expand Down Expand Up @@ -119,4 +198,65 @@ public function isTheSameClassCall(Tokens $tokens, $index)
|| $tokens[$operatorIndex]->equals([T_DOUBLE_COLON, '::']) && $tokens[$referenceIndex]->equals([T_STRING, 'self'], false)
|| $tokens[$operatorIndex]->equals([T_DOUBLE_COLON, '::']) && $tokens[$referenceIndex]->equals([T_STATIC, 'static'], false);
}

private function buildFunctionsAnalysis(Tokens $tokens)
{
$this->functionsAnalysis = [
'tokens' => $tokens->getCodeHash(),
'imports' => [],
'declarations' => [],
];

// find declarations

if ($tokens->isTokenKindFound(T_FUNCTION)) {
$end = \count($tokens);

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

if ($tokens[$i]->equals('(')) { // anonymous class
$i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $i);
$i = $tokens->getNextTokenOfKind($i, ['{']);
}

$i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $i);

continue;
}

if (!$tokens[$i]->isGivenKind(T_FUNCTION)) {
continue;
}

$i = $tokens->getNextMeaningfulToken($i);

if ($tokens[$i]->isGivenKind(CT::T_RETURN_REF)) {
$i = $tokens->getNextMeaningfulToken($i);
}

if (!$tokens[$i]->isGivenKind(T_STRING)) {
continue;
}

$this->functionsAnalysis['declarations'][] = $i;
}
}

// find imported functions

$namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();

if ($tokens->isTokenKindFound(CT::T_FUNCTION_IMPORT)) {
$declarations = $namespaceUsesAnalyzer->getDeclarationsFromTokens($tokens);

foreach ($declarations as $declaration) {
if ($declaration->isFunction()) {
$this->functionsAnalysis['imports'][] = $declaration;
}
}
}
}
}
10 changes: 10 additions & 0 deletions tests/Fixer/FunctionNotation/NativeFunctionInvocationFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,16 @@ public function & strlen($name) {
'strict' => true,
],
],
[
'<?php
use function foo\json_decode;
json_decode($base);
',
null,
[
'include' => ['@all'],
],
],
];
}

Expand Down
Loading

0 comments on commit 6a3d517

Please sign in to comment.