Skip to content

Commit

Permalink
NoUnusedImportsFixer - Fix undetected unused imports when type mismatch
Browse files Browse the repository at this point in the history
  • Loading branch information
julienfalque authored and keradus committed Aug 29, 2021
1 parent 1127221 commit 1fc2bda
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 33 deletions.
68 changes: 48 additions & 20 deletions src/Fixer/Import/NoUnusedImportsFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\GotoLabelAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;

/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
Expand Down Expand Up @@ -73,24 +75,18 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
}

foreach ((new NamespacesAnalyzer())->getDeclarations($tokens) as $namespace) {
$currentNamespaceUseDeclarations = array_filter(
$useDeclarations,
static function (NamespaceUseAnalysis $useDeclaration) use ($namespace) {
return
$useDeclaration->getStartIndex() >= $namespace->getScopeStartIndex()
&& $useDeclaration->getEndIndex() <= $namespace->getScopeEndIndex()
;
}
);

$usagesSearchIgnoredIndexes = [];
$currentNamespaceUseDeclarations = [];
$currentNamespaceUseDeclarationIndexes = [];

foreach ($currentNamespaceUseDeclarations as $useDeclaration) {
$usagesSearchIgnoredIndexes[$useDeclaration->getStartIndex()] = $useDeclaration->getEndIndex();
foreach ($useDeclarations as $useDeclaration) {
if ($useDeclaration->getStartIndex() >= $namespace->getScopeStartIndex() && $useDeclaration->getEndIndex() <= $namespace->getScopeEndIndex()) {
$currentNamespaceUseDeclarations[] = $useDeclaration;
$currentNamespaceUseDeclarationIndexes[$useDeclaration->getStartIndex()] = $useDeclaration->getEndIndex();
}
}

foreach ($currentNamespaceUseDeclarations as $useDeclaration) {
if (!$this->isImportUsed($tokens, $namespace, $usagesSearchIgnoredIndexes, $useDeclaration->getShortName())) {
if (!$this->isImportUsed($tokens, $namespace, $useDeclaration, $currentNamespaceUseDeclarationIndexes)) {
$this->removeUseDeclaration($tokens, $useDeclaration);
}
}
Expand All @@ -100,10 +96,19 @@ static function (NamespaceUseAnalysis $useDeclaration) use ($namespace) {
}

/**
* @param array<int, int> $ignoredIndexes
* @param array<int, int> $ignoredIndexes indexes of the use statements themselves that should not be checked as being "used"
*/
private function isImportUsed(Tokens $tokens, NamespaceAnalysis $namespace, array $ignoredIndexes, string $shortName): bool
private function isImportUsed(Tokens $tokens, NamespaceAnalysis $namespace, NamespaceUseAnalysis $import, array $ignoredIndexes): bool
{
$analyzer = new TokensAnalyzer($tokens);
$gotoLabelAnalyzer = new GotoLabelAnalyzer();

$tokensNotBeforeFunctionCall = [T_NEW];
// @TODO: drop condition when PHP 8.0+ is required
if (\defined('T_ATTRIBUTE')) {
$tokensNotBeforeFunctionCall[] = T_ATTRIBUTE;
}

$namespaceEndIndex = $namespace->getScopeEndIndex();
for ($index = $namespace->getScopeStartIndex(); $index <= $namespaceEndIndex; ++$index) {
if (isset($ignoredIndexes[$index])) {
Expand All @@ -115,6 +120,10 @@ private function isImportUsed(Tokens $tokens, NamespaceAnalysis $namespace, arra
$token = $tokens[$index];

if ($token->isGivenKind(T_STRING)) {
if (0 !== strcasecmp($import->getShortName(), $token->getContent())) {
continue;
}

$prevMeaningfulToken = $tokens[$tokens->getPrevMeaningfulToken($index)];

if ($prevMeaningfulToken->isGivenKind(T_NAMESPACE)) {
Expand All @@ -124,10 +133,29 @@ private function isImportUsed(Tokens $tokens, NamespaceAnalysis $namespace, arra
}

if (
0 === strcasecmp($shortName, $token->getContent())
&& !$prevMeaningfulToken->isGivenKind([T_NS_SEPARATOR, T_CONST, T_DOUBLE_COLON])
&& !$prevMeaningfulToken->isObjectOperator()
$prevMeaningfulToken->isGivenKind([T_NS_SEPARATOR, T_FUNCTION, T_CONST, T_DOUBLE_COLON])
|| $prevMeaningfulToken->isObjectOperator()
) {
continue;
}

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

if ($gotoLabelAnalyzer->belongsToGoToLabel($tokens, $nextMeaningfulIndex)) {
continue;
}

$nextMeaningfulToken = $tokens[$nextMeaningfulIndex];

if ($analyzer->isConstantInvocation($index)) {
$type = NamespaceUseAnalysis::TYPE_CONSTANT;
} elseif ($nextMeaningfulToken->equals('(') && !$prevMeaningfulToken->isGivenKind($tokensNotBeforeFunctionCall)) {
$type = NamespaceUseAnalysis::TYPE_FUNCTION;
} else {
$type = NamespaceUseAnalysis::TYPE_CLASS;
}

if ($import->getType() === $type) {
return true;
}

Expand All @@ -136,7 +164,7 @@ private function isImportUsed(Tokens $tokens, NamespaceAnalysis $namespace, arra

if ($token->isComment()
&& Preg::match(
'/(?<![[:alnum:]\$])(?<!\\\\)'.$shortName.'(?![[:alnum:]])/i',
'/(?<![[:alnum:]\$])(?<!\\\\)'.$import->getShortName().'(?![[:alnum:]])/i',
$token->getContent()
)
) {
Expand Down
2 changes: 1 addition & 1 deletion src/Tokenizer/Analyzer/Analysis/NamespaceUseAnalysis.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
*/
final class NamespaceUseAnalysis implements StartEndTokenAwareAnalysis
{
public const TYPE_CLASS = 1;
public const TYPE_CLASS = 1; // "classy" could be class, interface or trait
public const TYPE_FUNCTION = 2;
public const TYPE_CONSTANT = 3;

Expand Down
2 changes: 1 addition & 1 deletion src/Tokenizer/Analyzer/GotoLabelAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ public function belongsToGoToLabel(Tokens $tokens, int $index): bool

$prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulTokenIndex);

return $tokens[$prevMeaningfulTokenIndex]->equalsAny([';', '{', '}', [T_OPEN_TAG]]);
return $tokens[$prevMeaningfulTokenIndex]->equalsAny([':', ';', '{', '}', [T_OPEN_TAG]]);
}
}
209 changes: 205 additions & 4 deletions tests/Fixer/Import/NoUnusedImportsFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ public function doSomething($foo)
<?php
use Foo as Bar;
use A\MyTrait1;
class MyParent
{
Expand All @@ -422,6 +423,7 @@ class MyParent
use Foo;
use Foo as Bar;
use A\MyTrait1;
class MyParent
{
Expand Down Expand Up @@ -1085,6 +1087,192 @@ public function __invoke(StoreRequest $request)
{}
}',
],
'unused import matching function call' => [
'<?php
namespace Foo;
bar();',
'<?php
namespace Foo;
use Bar;
bar();',
],
'unused import matching function declaration' => [
'<?php
namespace Foo;
function bar () {}',
'<?php
namespace Foo;
use Bar;
function bar () {}',
],
'unused import matching method declaration' => [
'<?php
namespace Foo;
class Foo {
public function bar () {}
}',
'<?php
namespace Foo;
use Bar;
class Foo {
public function bar () {}
}',
],
'unused import matching constant usage' => [
'<?php
namespace Foo;
echo BAR;',
'<?php
namespace Foo;
use Bar;
echo BAR;',
],
'unused import matching class constant' => [
'<?php
namespace Foo;
class Foo {
const BAR = 1;
}',
'<?php
namespace Foo;
use Bar;
class Foo {
const BAR = 1;
}',
],
'unused function import matching class usage' => [
'<?php
namespace Foo;
new Bar();
Baz::method();',
'<?php
namespace Foo;
use function bar;
use function baz;
new Bar();
Baz::method();',
],
'unused function import matching method call' => [
'<?php
namespace Foo;
Foo::bar();',
'<?php
namespace Foo;
use function bar;
Foo::bar();',
],
'unused function import matching method declaration' => [
'<?php
namespace Foo;
class Foo {
public function bar () {}
}',
'<?php
namespace Foo;
use function bar;
class Foo {
public function bar () {}
}',
],
'unused function import matching constant usage' => [
'<?php
namespace Foo;
echo BAR;',
'<?php
namespace Foo;
use function bar;
echo BAR;',
],
'unused function import matching class constant' => [
'<?php
namespace Foo;
class Foo {
const BAR = 1;
}',
'<?php
namespace Foo;
use function bar;
class Foo {
const BAR = 1;
}',
],
'unused constant import matching function call' => [
'<?php
namespace Foo;
bar();',
'<?php
namespace Foo;
use const BAR;
bar();',
],
'unused constant import matching function declaration' => [
'<?php
namespace Foo;
function bar () {}',
'<?php
namespace Foo;
use const BAR;
function bar () {}',
],
'unused constant import matching method declaration' => [
'<?php
namespace Foo;
class Foo {
public function bar () {}
}',
'<?php
namespace Foo;
use const BAR;
class Foo {
public function bar () {}
}',
],
'unused constant import matching class constant' => [
'<?php
namespace Foo;
class Foo {
const BAR = 1;
}',
'<?php
namespace Foo;
use const BAR;
class Foo {
const BAR = 1;
}',
],
'attribute without braces' => [
'<?php
use Foo;
class Controller
{
#[Foo]
public function foo() {}
}',
],
'attribute with braces' => [
'<?php
use Foo;
class Controller
{
#[Foo()]
public function foo() {}
}',
],
'go to' => [
'<?php
Bar1:
Bar2:
Bar3:
',
'<?php
use Bar1;
use const Bar2;
use function Bar3;
Bar1:
Bar2:
Bar3:
',
],
];
}

Expand Down Expand Up @@ -1181,10 +1369,16 @@ public function testFixPrePHP80(): void

/**
* @requires PHP 8.0
* @dataProvider providePhp80Cases
*/
public function testFix80(): void
public function testFix80(string $expected, ?string $input = null): void
{
$this->doTest(
$this->doTest($expected, $input);
}

public function providePhp80Cases()
{
yield [
'<?php
Expand All @@ -1197,7 +1391,14 @@ public function testFix80(): void
$x = $foo?->bar;
$y = foo?->bar();
'
);
',
];

yield 'with union type in non-capturing catch' => [
'<?php
use Foo;
use Bar;
try {} catch (Foo | Bar) {}',
];
}
}
1 change: 0 additions & 1 deletion tests/Fixtures/Integration/misc/PHP7_0.test
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ use const some\a\ConstC;
use function some\a\fn_a;
use function some\a\fn_b;
use function some\a\fn_c;
use function some\x\CC as C;
use some\x\ClassX;
use function some\x\D;
use const some\x\E;
Expand Down
2 changes: 0 additions & 2 deletions tests/Fixtures/Integration/misc/PHP7_2.test
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ PHP 7.2 test.

namespace Foo;

use Bar\A;

class C
{
public function A($a, object $b): object
Expand Down
Loading

0 comments on commit 1fc2bda

Please sign in to comment.