Skip to content

Commit f4b9350

Browse files
committed
Add parameterized NoUnusedImportsFixer from Arnaud Le Blanc
1 parent 153b007 commit f4b9350

File tree

3 files changed

+285
-2
lines changed

3 files changed

+285
-2
lines changed

README.MD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ composer require codedruids/php-coding-standards
2323
* `LaravelPhpdocAlignmentFixer`
2424
<br>PHPDoc space alignment fixer for conforming with [Laravel](https://laravel.com) coding standards.
2525
<br>Origin: [laravel/pint](https://github.com/laravel/pint) (@nunomaduro)
26+
* `NoUnusedImportsFixer` (variant)
27+
<br>Variant of PHP-CS-Fixer which adds 'treat_same_namespace_as_unused' parameter
28+
<br>Origin: [arnaud-lb/PHP-CS-Fixer](https://github.com/arnaud-lb/PHP-CS-Fixer) (@arnaud-lb)
2629

2730
## License
2831

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codedruids/php-coding-standards",
3-
"description": "Collection of coding standards that haven't made it into packages yet",
3+
"description": "Collection of fixers/sniffs for coding standards that haven't made it into packages yet",
44
"type": "library",
55
"license": "MIT",
66
"authors": [
@@ -17,7 +17,8 @@
1717
},
1818
"autoload": {
1919
"psr-4": {
20-
"Laravel\\": "src/Laravel"
20+
"Laravel\\": "src/Laravel",
21+
"ALeBlanc\\": "src/ALeBlanc"
2122
}
2223
}
2324
}
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
<?php
2+
3+
namespace ALeBlanc\CodingStandards\Fixers;
4+
5+
use PhpCsFixer\AbstractFixer;
6+
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
7+
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
8+
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
9+
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
10+
use PhpCsFixer\FixerDefinition\CodeSample;
11+
use PhpCsFixer\FixerDefinition\FixerDefinition;
12+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
13+
use PhpCsFixer\Preg;
14+
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
15+
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
16+
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
17+
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
18+
use PhpCsFixer\Tokenizer\Token;
19+
use PhpCsFixer\Tokenizer\Tokens;
20+
21+
/**
22+
* Fixer copied from https://github.com/arnaud-lb/PHP-CS-Fixer
23+
*
24+
* Original author: Dariusz Rumiński <dariusz.ruminski@gmail.com>
25+
* Modifications to add 'treat_same_namespace_as_unused' parameter: Arnaud Le Blanc <arnaud.lb@gmail.com>
26+
* Additional compatibility modifications: Leith Caldwell <leith@codedruids.com>
27+
*/
28+
final class NoUnusedImportsFixer extends AbstractFixer implements ConfigurableFixerInterface
29+
{
30+
/**
31+
* {@inheritDoc}
32+
*
33+
* Make it a bit more obvious this is a fork of the original by using a different name
34+
*/
35+
public function getName(): string
36+
{
37+
return 'ALeBlanc/no_unused_imports';
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
public function getDefinition(): FixerDefinitionInterface
44+
{
45+
return new FixerDefinition(
46+
'Unused `use` statements must be removed.',
47+
[new CodeSample("<?php\nuse \\DateTime;\nuse \\Exception;\n\nnew DateTime();\n")]
48+
);
49+
}
50+
51+
/**
52+
* {@inheritdoc}
53+
*/
54+
public function getPriority(): int
55+
{
56+
// should be run after the SingleImportPerStatementFixer
57+
return -10;
58+
}
59+
60+
/**
61+
* {@inheritdoc}
62+
*/
63+
public function isCandidate(Tokens $tokens): bool
64+
{
65+
return $tokens->isTokenKindFound(T_USE);
66+
}
67+
68+
/**
69+
* {@inheritdoc}
70+
*/
71+
public function supports(\SplFileInfo $file): bool
72+
{
73+
$path = $file->getPathname();
74+
75+
/*
76+
* @deprecated this exception will be removed on 3.0
77+
* some fixtures are auto-generated by Symfony and may contain unused use statements
78+
*/
79+
if (false !== strpos($path, \DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR)
80+
&& false === strpos($path, \DIRECTORY_SEPARATOR.'tests'.\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR)
81+
) {
82+
return false;
83+
}
84+
85+
return true;
86+
}
87+
88+
/**
89+
* {@inheritdoc}
90+
*/
91+
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
92+
{
93+
$useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
94+
95+
if (0 === \count($useDeclarations)) {
96+
return;
97+
}
98+
99+
foreach ((new NamespacesAnalyzer())->getDeclarations($tokens) as $namespace) {
100+
$currentNamespaceUseDeclarations = array_filter(
101+
$useDeclarations,
102+
function (NamespaceUseAnalysis $useDeclaration) use ($namespace) {
103+
return
104+
$useDeclaration->getStartIndex() >= $namespace->getScopeStartIndex()
105+
&& $useDeclaration->getEndIndex() <= $namespace->getScopeEndIndex()
106+
;
107+
}
108+
);
109+
110+
$usagesSearchIgnoredIndexes = [];
111+
112+
foreach ($currentNamespaceUseDeclarations as $useDeclaration) {
113+
$usagesSearchIgnoredIndexes[$useDeclaration->getStartIndex()] = $useDeclaration->getEndIndex();
114+
}
115+
116+
foreach ($currentNamespaceUseDeclarations as $useDeclaration) {
117+
if (!$this->importIsUsed($tokens, $namespace, $usagesSearchIgnoredIndexes, $useDeclaration->getShortName())) {
118+
$this->removeUseDeclaration($tokens, $useDeclaration);
119+
}
120+
}
121+
122+
if ($this->configuration['treat_same_namespace_as_unused']) {
123+
$this->removeUsesInSameNamespace($tokens, $currentNamespaceUseDeclarations, $namespace);
124+
}
125+
}
126+
}
127+
128+
/**
129+
* {@inheritdoc}
130+
*/
131+
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
132+
{
133+
return new FixerConfigurationResolver([
134+
(new FixerOptionBuilder('treat_same_namespace_as_unused', 'whether to treat imports in the same namespace as unused'))
135+
->setAllowedTypes(['bool'])
136+
->setDefault(true)
137+
->getOption(),
138+
]);
139+
}
140+
141+
private function importIsUsed(Tokens $tokens, NamespaceAnalysis $namespace, array $ignoredIndexes, $shortName)
142+
{
143+
for ($index = $namespace->getScopeStartIndex(); $index <= $namespace->getScopeEndIndex(); ++$index) {
144+
if (isset($ignoredIndexes[$index])) {
145+
$index = $ignoredIndexes[$index];
146+
147+
continue;
148+
}
149+
150+
$token = $tokens[$index];
151+
152+
if (
153+
$token->isGivenKind(T_STRING)
154+
&& 0 === strcasecmp($shortName, $token->getContent())
155+
&& !$tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind([T_NS_SEPARATOR, T_CONST, T_OBJECT_OPERATOR])
156+
) {
157+
return true;
158+
}
159+
160+
if ($token->isComment() && Preg::match(
161+
'/(?<![[:alnum:]])(?<!\\\\)'.$shortName.'(?![[:alnum:]])/i',
162+
$token->getContent()
163+
)) {
164+
return true;
165+
}
166+
}
167+
168+
return false;
169+
}
170+
171+
private function removeUseDeclaration(Tokens $tokens, NamespaceUseAnalysis $useDeclaration)
172+
{
173+
for ($index = $useDeclaration->getEndIndex() - 1; $index >= $useDeclaration->getStartIndex(); --$index) {
174+
if ($tokens[$index]->isComment()) {
175+
continue;
176+
}
177+
178+
if (!$tokens[$index]->isWhitespace() || false === strpos($tokens[$index]->getContent(), "\n")) {
179+
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
180+
181+
continue;
182+
}
183+
184+
// when multi line white space keep the line feed if the previous token is a comment
185+
$prevIndex = $tokens->getPrevNonWhitespace($index);
186+
if ($tokens[$prevIndex]->isComment()) {
187+
$content = $tokens[$index]->getContent();
188+
$tokens[$index] = new Token([T_WHITESPACE, substr($content, strrpos($content, "\n"))]); // preserve indent only
189+
} else {
190+
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
191+
}
192+
}
193+
194+
if ($tokens[$useDeclaration->getEndIndex()]->equals(';')) { // do not remove `? >`
195+
$tokens->clearAt($useDeclaration->getEndIndex());
196+
}
197+
198+
// remove white space above and below where the `use` statement was
199+
200+
$prevIndex = $useDeclaration->getStartIndex() - 1;
201+
$prevToken = $tokens[$prevIndex];
202+
203+
if ($prevToken->isWhitespace()) {
204+
$content = rtrim($prevToken->getContent(), " \t");
205+
206+
if ('' === $content) {
207+
$tokens->clearAt($prevIndex);
208+
} else {
209+
$tokens[$prevIndex] = new Token([T_WHITESPACE, $content]);
210+
}
211+
212+
$prevToken = $tokens[$prevIndex];
213+
}
214+
215+
if (!isset($tokens[$useDeclaration->getEndIndex() + 1])) {
216+
return;
217+
}
218+
219+
$nextIndex = $tokens->getNonEmptySibling($useDeclaration->getEndIndex(), 1);
220+
if (null === $nextIndex) {
221+
return;
222+
}
223+
224+
$nextToken = $tokens[$nextIndex];
225+
226+
if ($nextToken->isWhitespace()) {
227+
$content = Preg::replace(
228+
"#^\r\n|^\n#",
229+
'',
230+
ltrim($nextToken->getContent(), " \t"),
231+
1
232+
);
233+
234+
if ('' !== $content) {
235+
$tokens[$nextIndex] = new Token([T_WHITESPACE, $content]);
236+
} else {
237+
$tokens->clearAt($nextIndex);
238+
}
239+
240+
$nextToken = $tokens[$nextIndex];
241+
}
242+
243+
if ($prevToken->isWhitespace() && $nextToken->isWhitespace()) {
244+
$content = $prevToken->getContent().$nextToken->getContent();
245+
246+
if ('' !== $content) {
247+
$tokens[$nextIndex] = new Token([T_WHITESPACE, $content]);
248+
} else {
249+
$tokens->clearAt($nextIndex);
250+
}
251+
252+
$tokens->clearAt($prevIndex);
253+
}
254+
}
255+
256+
private function removeUsesInSameNamespace(Tokens $tokens, array $useDeclarations, NamespaceAnalysis $namespaceDeclaration)
257+
{
258+
$namespace = $namespaceDeclaration->getFullName();
259+
$nsLength = \strlen($namespace.'\\');
260+
261+
foreach ($useDeclarations as $useDeclaration) {
262+
if ($useDeclaration->isAliased()) {
263+
continue;
264+
}
265+
266+
$useDeclarationFullName = ltrim($useDeclaration->getFullName(), '\\');
267+
268+
if (0 !== strpos($useDeclarationFullName, $namespace.'\\')) {
269+
continue;
270+
}
271+
272+
$partName = substr($useDeclarationFullName, $nsLength);
273+
274+
if (false === strpos($partName, '\\')) {
275+
$this->removeUseDeclaration($tokens, $useDeclaration);
276+
}
277+
}
278+
}
279+
}

0 commit comments

Comments
 (0)