Skip to content

Commit e041ee6

Browse files
✨ Improve AlphabeticallySortedUseSniff
1 parent 31b0423 commit e041ee6

File tree

5 files changed

+304
-181
lines changed

5 files changed

+304
-181
lines changed

SymfonyCustom/Sniffs/Namespaces/AlphabeticallySortedUseSniff.php

Lines changed: 132 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -4,150 +4,190 @@
44

55
use PHP_CodeSniffer\Files\File;
66
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use PHP_CodeSniffer\Util\Tokens;
8+
use SymfonyCustom\Sniffs\SniffHelper;
79

810
/**
9-
* Ensures USE blocks are alphabetically sorted.
11+
* Class AlphabeticallySortedUseSniff
1012
*/
1113
class AlphabeticallySortedUseSniff implements Sniff
1214
{
1315
/**
14-
* @var bool
15-
*/
16-
private $caseSensitive = false;
17-
18-
/**
19-
* Returns an array of tokens this test wants to listen for.
20-
*
21-
* @return array
16+
* @return int[]
2217
*/
2318
public function register()
2419
{
25-
return [T_USE];
20+
return [T_OPEN_TAG, T_NAMESPACE];
2621
}
2722

2823
/**
29-
* Processes this test, when one of its tokens is encountered.
30-
*
31-
* @param File $phpcsFile The file being scanned.
32-
* @param int $stackPtr The position of the current token in
33-
* the stack passed in $tokens.
24+
* @param File $phpcsFile
25+
* @param int $stackPtr
3426
*
35-
* @return void
27+
* @return int
3628
*/
3729
public function process(File $phpcsFile, $stackPtr)
3830
{
39-
if (true === $this->shouldIgnoreUse($phpcsFile, $stackPtr)) {
40-
return;
41-
}
31+
$tokens = $phpcsFile->getTokens();
4232

43-
$previousUse = $phpcsFile->findPrevious(T_USE, $stackPtr - 1);
33+
if (!SniffHelper::isNamespace($phpcsFile, $stackPtr)) {
34+
$namespace = $phpcsFile->findNext(T_NAMESPACE, $stackPtr + 1);
4435

45-
if (false === $previousUse) {
46-
return;
36+
if ($namespace) {
37+
return $namespace;
38+
}
4739
}
4840

49-
// Look for the real previous USE
50-
while (true === $this->shouldIgnoreUse($phpcsFile, $previousUse)) {
51-
$previousUse = $phpcsFile->findPrevious(T_USE, $previousUse - 1);
41+
$uses = $this->getUseStatements($phpcsFile, $stackPtr);
5242

53-
if (false === $previousUse) {
54-
return;
43+
$lastUse = null;
44+
foreach ($uses as $use) {
45+
if (!$lastUse) {
46+
$lastUse = $use;
47+
continue;
5548
}
56-
}
5749

58-
$namespace = $this->getNamespaceUsed($phpcsFile, $stackPtr);
59-
$previousNamespace = $this->getNamespaceUsed($phpcsFile, $previousUse);
50+
$order = $this->compareUseStatements($use, $lastUse);
51+
52+
if ($order < 0) {
53+
$error = 'Use statements are incorrectly ordered. The first wrong one is %s';
54+
$data = [$use['name']];
6055

61-
if ($this->compareNamespaces($namespace, $previousNamespace) < 0) {
62-
$error = 'Namespaces used are not correctly sorted';
63-
$phpcsFile->addError($error, $stackPtr, 'AlphabeticallySortedUse');
56+
$phpcsFile->addError($error, $use['ptrUse'], 'IncorrectOrder', $data);
57+
58+
return $stackPtr + 1;
59+
}
60+
61+
// Check empty lines between use statements.
62+
// There must be no empty lines between use statements.
63+
$lineDiff = $tokens[$use['ptrUse']]['line'] - $tokens[$lastUse['ptrUse']]['line'];
64+
if ($lineDiff > 1) {
65+
$error = 'There must not be any empty line between use statement of the same type';
66+
$fix = $phpcsFile->addFixableError($error, $use['ptrUse'], 'EmptyLine');
67+
68+
if ($fix) {
69+
$phpcsFile->fixer->beginChangeset();
70+
for ($i = $lastUse['ptrEnd'] + 1; $i < $use['ptrUse']; ++$i) {
71+
if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) {
72+
$phpcsFile->fixer->replaceToken($i, '');
73+
--$lineDiff;
74+
75+
if (1 === $lineDiff) {
76+
break;
77+
}
78+
}
79+
}
80+
$phpcsFile->fixer->endChangeset();
81+
}
82+
} elseif (0 === $lineDiff) {
83+
$error = 'Each use statement must be in new line';
84+
$fix = $phpcsFile->addFixableError($error, $use['ptrUse'], 'TheSameLine');
85+
86+
if ($fix) {
87+
$phpcsFile->fixer->addNewline($lastUse['ptrEnd']);
88+
}
89+
}
90+
91+
$lastUse = $use;
6492
}
93+
94+
return T_OPEN_TAG === $tokens[$stackPtr]['code']
95+
? $phpcsFile->numTokens + 1
96+
: $stackPtr + 1;
6597
}
6698

6799
/**
68-
* Check if this USE statement is part of the namespace block.
100+
* @param File $phpcsFile
101+
* @param int $scopePtr
69102
*
70-
* @param File $phpcsFile The file being scanned.
71-
* @param int $stackPtr The position of the current token in
72-
* the stack passed in $tokens.
73-
*
74-
* @return bool
103+
* @return array
75104
*/
76-
private function shouldIgnoreUse(File $phpcsFile, $stackPtr)
105+
private function getUseStatements(File $phpcsFile, $scopePtr)
77106
{
78107
$tokens = $phpcsFile->getTokens();
79108

80-
// Ignore USE keywords inside closures.
81-
$next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
82-
if (T_OPEN_PARENTHESIS === $tokens[$next]['code']) {
83-
return true;
109+
$uses = [];
110+
111+
if (isset($tokens[$scopePtr]['scope_opener'])) {
112+
$start = $tokens[$scopePtr]['scope_opener'];
113+
$end = $tokens[$scopePtr]['scope_closer'];
114+
} else {
115+
$start = $scopePtr;
116+
$end = null;
84117
}
85118

86-
// Ignore USE keywords inside class and trait
87-
if ($phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT]) === true) {
88-
return true;
119+
$use = $phpcsFile->findNext(T_USE, $start + 1, $end);
120+
while (false !== $use && T_USE === $tokens[$use]['code']) {
121+
if (!SniffHelper::isGlobalUse($phpcsFile, $use)
122+
|| (null !== $end
123+
&& (!isset($tokens[$use]['conditions'][$scopePtr])
124+
|| $tokens[$use]['level'] !== $tokens[$scopePtr]['level'] + 1))
125+
) {
126+
$use = $phpcsFile->findNext(Tokens::$emptyTokens, $use + 1, $end, true);
127+
continue;
128+
}
129+
130+
// find semicolon as the end of the global use scope
131+
$endOfScope = $phpcsFile->findNext([T_SEMICOLON], $use + 1);
132+
133+
$startOfName = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $use + 1, $endOfScope);
134+
135+
$type = 'class';
136+
if (T_STRING === $tokens[$startOfName]['code']) {
137+
$lowerContent = strtolower($tokens[$startOfName]['content']);
138+
if ('function' === $lowerContent || 'const' === $lowerContent) {
139+
$type = $lowerContent;
140+
141+
$startOfName = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $startOfName + 1, $endOfScope);
142+
}
143+
}
144+
145+
$uses[] = [
146+
'ptrUse' => $use,
147+
'name' => trim($phpcsFile->getTokensAsString($startOfName, $endOfScope - $startOfName)),
148+
'ptrEnd' => $endOfScope,
149+
'string' => trim($phpcsFile->getTokensAsString($use, $endOfScope - $use + 1)),
150+
'type' => $type,
151+
];
152+
153+
$use = $phpcsFile->findNext(Tokens::$emptyTokens, $endOfScope + 1, $end, true);
89154
}
90155

91-
return false;
156+
return $uses;
92157
}
93158

94159
/**
95-
* Get full namespace imported
160+
* @param array $a
161+
* @param array $b
96162
*
97-
* @param File $phpcsFile The file being scanned.
98-
* @param int $stackPtr The position of the current token in
99-
* the stack passed in $tokens.
100-
*
101-
* @return string
163+
* @return int
102164
*/
103-
private function getNamespaceUsed(File $phpcsFile, $stackPtr)
165+
private function compareUseStatements(array $a, array $b)
104166
{
105-
$tokens = $phpcsFile->getTokens();
106-
$namespace = '';
107-
108-
$start = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $stackPtr);
109-
$end = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $start, null, true);
167+
if ($a['type'] === $b['type']) {
168+
return strcasecmp(
169+
$this->clearName($a['name']),
170+
$this->clearName($b['name'])
171+
);
172+
}
110173

111-
for ($i = $start; $i < $end; $i++) {
112-
$namespace .= $tokens[$i]['content'];
174+
if ('class' === $a['type'] || ('function' === $a['type'] && 'const' === $b['type'])) {
175+
return -1;
113176
}
114177

115-
return $namespace;
178+
return 1;
116179
}
117180

118181
/**
119-
* @param string $namespace1
120-
* @param string $namespace2
182+
* @param string $name
121183
*
122-
* @return int
184+
* @return string
123185
*/
124-
private function compareNamespaces($namespace1, $namespace2)
186+
private function clearName($name)
125187
{
126-
$array1 = explode('\\', $namespace1);
127-
$length1 = count($array1);
128-
$array2 = explode('\\', $namespace2);
129-
$length2 = count($array2);
130-
131-
for ($i = 0; $i < $length1; $i++) {
132-
if ($i >= $length2) {
133-
// $namespace2 is shorter than $namespace1 and they have the same beginning so $namespace1 > $namespace2
134-
return 1;
135-
}
136-
137-
if (true === $this->caseSensitive && strcmp($array1[$i], $array2[$i]) !== 0) {
138-
return strcmp($array1[$i], $array2[$i]);
139-
}
140-
141-
if (false === $this->caseSensitive && strcasecmp($array1[$i], $array2[$i]) !== 0) {
142-
return strcasecmp($array1[$i], $array2[$i]);
143-
}
144-
}
145-
146-
if ($length1 === $length2) {
147-
return 0;
148-
}
188+
// Handle grouped use
189+
$name = explode('{', $name)[0];
149190

150-
// $namespace1 is shorter than $namespace2 and they have the same beginning so $namespace1 < $namespace2
151-
return -1;
191+
return str_replace('\\', ' ', $name);
152192
}
153193
}

0 commit comments

Comments
 (0)