Skip to content

Commit 955e929

Browse files
author
Vincent Langlet
committed
✨ Add UnusedUseSniff
1 parent 31b207c commit 955e929

File tree

4 files changed

+264
-0
lines changed

4 files changed

+264
-0
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
namespace Symfony3Custom\Sniffs\Namespaces;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use PHP_CodeSniffer\Util\Tokens;
8+
9+
/**
10+
* Checks for "use" statements that are not needed in a file.
11+
*/
12+
class UnusedUseSniff implements Sniff
13+
{
14+
/**
15+
* Returns an array of tokens this test wants to listen for.
16+
*
17+
* @return array
18+
*/
19+
public function register()
20+
{
21+
return array(T_USE);
22+
}
23+
24+
/**
25+
* Processes this test, when one of its tokens is encountered.
26+
*
27+
* @param File $phpcsFile The file being scanned.
28+
* @param int $stackPtr The position of the current token in the stack passed in $tokens.
29+
*
30+
* @return void
31+
*/
32+
public function process(File $phpcsFile, $stackPtr)
33+
{
34+
$tokens = $phpcsFile->getTokens();
35+
//T_DOC_COMMENT_STRING;PHPCS_T_DOC_COMMENT_TAG;
36+
37+
// Only check use statements in the global scope.
38+
if (empty($tokens[$stackPtr]['conditions']) === false) {
39+
return;
40+
}
41+
42+
// Seek to the end of the statement and get the string before the semi colon.
43+
$semiColon = $phpcsFile->findEndOfStatement($stackPtr);
44+
if (T_SEMICOLON !== $tokens[$semiColon]['code']) {
45+
return;
46+
}
47+
48+
$classPtr = $phpcsFile->findPrevious(
49+
Tokens::$emptyTokens,
50+
($semiColon - 1),
51+
null,
52+
true
53+
);
54+
55+
if (T_STRING !== $tokens[$classPtr]['code']) {
56+
return;
57+
}
58+
59+
// Search where the class name is used. PHP treats class names case insensitive,
60+
// that's why we cannot search for the exact class name string
61+
// and need to iterate over all T_STRING tokens in the file.
62+
$classUsed = $phpcsFile->findNext(T_STRING, ($classPtr + 1));
63+
$lowerClassName = strtolower($tokens[$classPtr]['content']);
64+
65+
// Check if the referenced class is in the same namespace as the current file.
66+
// If it is then the use statement is not necessary.
67+
$namespacePtr = $phpcsFile->findPrevious([T_NAMESPACE], $stackPtr);
68+
69+
// Check if the use statement does aliasing with the "as" keyword.
70+
// Aliasing is allowed even in the same namespace.
71+
$aliasUsed = $phpcsFile->findPrevious(T_AS, ($classPtr - 1), $stackPtr);
72+
73+
if (false !== $namespacePtr && false === $aliasUsed) {
74+
$nsEnd = $phpcsFile->findNext(
75+
[T_NS_SEPARATOR, T_STRING, T_WHITESPACE],
76+
($namespacePtr + 1),
77+
null,
78+
true
79+
);
80+
81+
$namespace = trim($phpcsFile->getTokensAsString(($namespacePtr + 1), ($nsEnd - $namespacePtr - 1)));
82+
83+
$useNamespacePtr = $phpcsFile->findNext([T_STRING], ($stackPtr + 1));
84+
$useNamespaceEnd = $phpcsFile->findNext(
85+
[T_NS_SEPARATOR, T_STRING],
86+
($useNamespacePtr + 1),
87+
null,
88+
true
89+
);
90+
91+
$useNamespace = rtrim(
92+
$phpcsFile->getTokensAsString(
93+
$useNamespacePtr,
94+
($useNamespaceEnd - $useNamespacePtr - 1)
95+
),
96+
'\\'
97+
);
98+
99+
if (strcasecmp($namespace, $useNamespace) === 0) {
100+
$classUsed = false;
101+
}
102+
}
103+
104+
while (false !== $classUsed) {
105+
if (strtolower($tokens[$classUsed]['content']) === $lowerClassName) {
106+
// If the name is used in a PHP 7 function return type declaration stop.
107+
if (T_RETURN_TYPE === $tokens[$classUsed]['code']) {
108+
return;
109+
}
110+
111+
$beforeUsage = $phpcsFile->findPrevious(
112+
Tokens::$emptyTokens,
113+
($classUsed - 1),
114+
null,
115+
true
116+
);
117+
118+
// If a backslash is used before the class name then this is some other use statement.
119+
if (T_USE !== $tokens[$beforeUsage]['code'] && T_NS_SEPARATOR !== $tokens[$beforeUsage]['code']) {
120+
return;
121+
}
122+
123+
// Trait use statement within a class.
124+
if (T_USE === $tokens[$beforeUsage]['code'] && false === empty($tokens[$beforeUsage]['conditions'])) {
125+
return;
126+
}
127+
}
128+
129+
$classUsed = $phpcsFile->findNext(
130+
[T_STRING, T_DOC_COMMENT_STRING, T_RETURN_TYPE],
131+
($classUsed + 1)
132+
);
133+
}
134+
135+
// Check for doc comment tags
136+
foreach ($tokens as $token) {
137+
if ('T_DOC_COMMENT_TAG' === $token['type']) {
138+
// Handle comment tag as @Route(..) or @ORM\Id
139+
if (preg_match('/^@'.$lowerClassName.'(?![a-zA-Z])/i', $token['content']) === 1) {
140+
return;
141+
};
142+
}
143+
}
144+
145+
$warning = 'Unused use statement';
146+
$fix = $phpcsFile->addFixableError($warning, $stackPtr, 'UnusedUse');
147+
148+
if (true === $fix) {
149+
// Remove the whole use statement line.
150+
$phpcsFile->fixer->beginChangeset();
151+
for ($i = $stackPtr; $i <= $semiColon; $i++) {
152+
$phpcsFile->fixer->replaceToken($i, '');
153+
}
154+
155+
// Also remove whitespace after the semicolon (new lines).
156+
while (true === isset($tokens[$i]) && T_WHITESPACE === $tokens[$i]['code']) {
157+
$phpcsFile->fixer->replaceToken($i, '');
158+
if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) {
159+
break;
160+
}
161+
162+
$i++;
163+
}
164+
165+
$phpcsFile->fixer->endChangeset();
166+
}
167+
}
168+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace MyProject;
4+
5+
use BarClass as Bar;
6+
use Foo;
7+
use Route;
8+
use ORM;
9+
use Truc;
10+
use Machin;
11+
use Unused;
12+
13+
class Container
14+
{
15+
/**
16+
* @Foo
17+
* @Route("/{id}")
18+
* @ORM\Column(type="integer")
19+
*
20+
* @param Toto
21+
* @var Truc
22+
* @return Machin
23+
*/
24+
function test (Bar $bar)
25+
{
26+
return;
27+
}
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace MyProject;
4+
5+
use BarClass as Bar;
6+
use Foo;
7+
use Route;
8+
use ORM;
9+
use Truc;
10+
use Machin;
11+
12+
class Container
13+
{
14+
/**
15+
* @Foo
16+
* @Route("/{id}")
17+
* @ORM\Column(type="integer")
18+
*
19+
* @param Toto
20+
* @var Truc
21+
* @return Machin
22+
*/
23+
function test (Bar $bar)
24+
{
25+
return;
26+
}
27+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Symfony3Custom\Tests\Namespaces;
4+
5+
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
6+
7+
/**
8+
* Unit test class for the UnusedUse sniff.
9+
*
10+
* @group Symfony3Custom
11+
*/
12+
class UnusedUseUnitTest extends AbstractSniffUnitTest
13+
{
14+
/**
15+
* Returns the lines where errors should occur.
16+
*
17+
* The key of the array should represent the line number and the value
18+
* should represent the number of errors that should occur on that line.
19+
*
20+
* @return array<int, int>
21+
*/
22+
public function getErrorList()
23+
{
24+
return array(
25+
11 => 1,
26+
);
27+
}
28+
29+
/**
30+
* Returns the lines where warnings should occur.
31+
*
32+
* The key of the array should represent the line number and the value
33+
* should represent the number of errors that should occur on that line.
34+
*
35+
* @return array(int => int)
36+
*/
37+
public function getWarningList()
38+
{
39+
return array();
40+
}
41+
}

0 commit comments

Comments
 (0)