Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 112 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
AlphabeticArraySortSniff | |
0.00% |
0 / 112 |
|
0.00% |
0 / 6 |
650 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
processDocTag | |
0.00% |
0 / 63 |
|
0.00% |
0 / 1 |
156 | |||
warnOnFirstMismatch | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
rebuildSortedArray | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
20 | |||
sortStatements | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * Check if the keys of an array are sorted and autofix it. |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with this program; if not, write to the Free Software Foundation, Inc., |
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
19 | * http://www.gnu.org/copyleft/gpl.html |
20 | * |
21 | * @file |
22 | */ |
23 | |
24 | namespace MediaWiki\Sniffs\Arrays; |
25 | |
26 | use ArrayIterator; |
27 | use PHP_CodeSniffer\Files\File; |
28 | use PHP_CodeSniffer\Sniffs\Sniff; |
29 | use PHP_CodeSniffer\Util\Tokens; |
30 | |
31 | class AlphabeticArraySortSniff implements Sniff { |
32 | |
33 | private const ANNOTATION_NAME = '@phpcs-require-sorted-array'; |
34 | |
35 | /** |
36 | * @inheritDoc |
37 | */ |
38 | public function register(): array { |
39 | return [ T_DOC_COMMENT_OPEN_TAG ]; |
40 | } |
41 | |
42 | /** |
43 | * @param File $phpcsFile |
44 | * @param int $stackPtr The current token index. |
45 | * @return void |
46 | */ |
47 | public function process( File $phpcsFile, $stackPtr ) { |
48 | $tokens = $phpcsFile->getTokens(); |
49 | $end = $tokens[$stackPtr]['comment_closer']; |
50 | foreach ( $tokens[$stackPtr]['comment_tags'] as $tag ) { |
51 | if ( $tokens[$tag]['content'] === self::ANNOTATION_NAME ) { |
52 | $this->processDocTag( $phpcsFile, $tokens, $tag, $end ); |
53 | break; |
54 | } |
55 | } |
56 | } |
57 | |
58 | /** |
59 | * @param File $phpcsFile |
60 | * @param array[] $tokens |
61 | * @param int $tagPtr Token position of the tag |
62 | * @param int $docEnd Token position of the end of the doc comment |
63 | */ |
64 | private function processDocTag( File $phpcsFile, array $tokens, int $tagPtr, int $docEnd ): void { |
65 | $arrayToken = $phpcsFile->findNext( [ T_OPEN_SHORT_ARRAY, T_ARRAY ], $docEnd + 1 ); |
66 | if ( $arrayToken === false || ( |
67 | // On the same line or one line after the doc block |
68 | $tokens[$docEnd]['line'] !== $tokens[$arrayToken]['line'] && |
69 | $tokens[$docEnd]['line'] !== $tokens[$arrayToken]['line'] - 1 ) |
70 | ) { |
71 | $phpcsFile->addWarning( |
72 | 'No array found after %s', |
73 | $tagPtr, |
74 | 'Unsupported', |
75 | [ self::ANNOTATION_NAME, $tokens[$arrayToken]['content'] ] |
76 | ); |
77 | return; |
78 | } |
79 | |
80 | if ( !isset( $tokens[$arrayToken]['bracket_opener'] ) ) { |
81 | // Live coding |
82 | return; |
83 | } |
84 | |
85 | $endArray = $tokens[$arrayToken]['bracket_closer'] - 1; |
86 | $startArray = $phpcsFile->findNext( |
87 | Tokens::$emptyTokens, |
88 | $tokens[$arrayToken]['bracket_opener'] + 1, |
89 | $endArray, |
90 | true |
91 | ); |
92 | if ( $startArray === false ) { |
93 | // Empty array |
94 | return; |
95 | } |
96 | $endArray = $phpcsFile->findPrevious( Tokens::$emptyTokens, $endArray, $startArray, true ); |
97 | if ( $tokens[$endArray]['code'] === T_COMMA ) { |
98 | // Ignore trailing commas |
99 | $endArray--; |
100 | } |
101 | |
102 | $keys = []; |
103 | $duplicateCounter = 0; |
104 | $next = $startArray; |
105 | while ( $next <= $endArray ) { |
106 | $endStatement = $phpcsFile->findEndOfStatement( $next, [ T_DOUBLE_ARROW ] ); |
107 | if ( $endStatement >= $endArray ) { |
108 | // Not going ahead on our own end |
109 | $endStatement = $endArray; |
110 | $endItem = $endArray; |
111 | } else { |
112 | // Do not track comma |
113 | $endItem = $endStatement - 1; |
114 | } |
115 | $keyToken = $phpcsFile->findNext( Tokens::$emptyTokens, $next, $endItem + 1, true ); |
116 | |
117 | $arrayKey = $tokens[$keyToken]['content']; |
118 | if ( isset( $keys[$arrayKey] ) ) { |
119 | $phpcsFile->addWarning( |
120 | 'Found duplicate key "%s" on array required sorting', |
121 | $keyToken, |
122 | 'Duplicate', |
123 | [ $arrayKey ] |
124 | ); |
125 | $duplicateCounter++; |
126 | // Make the key unique to get a stable sort result and to handle this token as well |
127 | $arrayKey .= "\0" . $duplicateCounter; |
128 | } |
129 | |
130 | $keys[$arrayKey] = [ |
131 | 'key' => $keyToken, |
132 | 'end' => $endItem, |
133 | 'startLocation' => $next, |
134 | 'endLocation' => $endStatement, |
135 | ]; |
136 | $next = $endStatement + 1; |
137 | } |
138 | |
139 | $sortedKeys = $this->sortStatements( $keys ); |
140 | if ( $sortedKeys === array_keys( $keys ) ) { |
141 | return; |
142 | } |
143 | |
144 | $fix = $phpcsFile->addFixableWarning( |
145 | 'Array is not sorted alphabetically', |
146 | $tagPtr, |
147 | 'Unsorted' |
148 | ); |
149 | |
150 | if ( $fix ) { |
151 | $this->rebuildSortedArray( $phpcsFile, $sortedKeys, $keys, $startArray ); |
152 | } else { |
153 | $this->warnOnFirstMismatch( $phpcsFile, $sortedKeys, $keys ); |
154 | } |
155 | } |
156 | |
157 | /** |
158 | * Add a warning on first mismatched key to make it easier found the wrong key in the array. |
159 | * On each key could make warning on all keys, when the first is already out of order |
160 | * |
161 | * @param File $phpcsFile |
162 | * @param string[] $sorted |
163 | * @param array[] $unsorted |
164 | */ |
165 | private function warnOnFirstMismatch( File $phpcsFile, array $sorted, array $unsorted ): void { |
166 | $iteratorUnsorted = new ArrayIterator( $unsorted ); |
167 | foreach ( $sorted as $sortedKey ) { |
168 | $unsortedKey = $iteratorUnsorted->key(); |
169 | if ( $sortedKey !== $unsortedKey ) { |
170 | $unsortedToken = $iteratorUnsorted->current(); |
171 | $phpcsFile->addFixableWarning( |
172 | 'This key is out of order (Needs %s, got %s)', |
173 | $unsortedToken['key'], |
174 | 'UnsortedHint', |
175 | [ $sortedKey, $unsortedKey ] |
176 | ); |
177 | break; |
178 | } |
179 | $iteratorUnsorted->next(); |
180 | } |
181 | } |
182 | |
183 | /** |
184 | * When autofix is wanted, rebuild the content of the array and use it |
185 | * Get the comma and line indents between each items from the current order. |
186 | * Add the key and values in sorted order. |
187 | * |
188 | * @param File $phpcsFile |
189 | * @param string[] $sorted |
190 | * @param array[] $unsorted |
191 | * @param int $stackPtr |
192 | */ |
193 | private function rebuildSortedArray( File $phpcsFile, array $sorted, array $unsorted, int $stackPtr ): void { |
194 | $phpcsFile->fixer->beginChangeset(); |
195 | $iteratorSorted = new ArrayIterator( $sorted ); |
196 | $newArray = ''; |
197 | $lastEnd = false; |
198 | foreach ( $unsorted as $values ) { |
199 | // Add comma and indent between the items |
200 | if ( $lastEnd !== false ) { |
201 | $newArray .= $phpcsFile->getTokensAsString( |
202 | $lastEnd + 1, |
203 | $values['key'] - $lastEnd - 1, |
204 | // keep tabs on multiline statements |
205 | true |
206 | ); |
207 | } |
208 | $lastEnd = $values['end']; |
209 | |
210 | // Add the array item |
211 | $sortedKey = $iteratorSorted->current(); |
212 | $unsortedToken = $unsorted[$sortedKey]; |
213 | $newArray .= $phpcsFile->getTokensAsString( |
214 | $unsortedToken['key'], |
215 | $unsortedToken['end'] - $unsortedToken['key'] + 1, |
216 | // keep tabs on multiline statements |
217 | true |
218 | ); |
219 | $iteratorSorted->next(); |
220 | |
221 | // remove at old location including comma and indent |
222 | for ( $i = $unsortedToken['startLocation']; $i <= $unsortedToken['endLocation']; $i++ ) { |
223 | $phpcsFile->fixer->replaceToken( $i, '' ); |
224 | } |
225 | } |
226 | $phpcsFile->fixer->addContent( $stackPtr, $newArray ); |
227 | $phpcsFile->fixer->endChangeset(); |
228 | } |
229 | |
230 | /** |
231 | * This sorts the array keys |
232 | * |
233 | * @param array[] $statementList Array mapping keys to tokens |
234 | * @return string[] Sorted list of keys |
235 | */ |
236 | private function sortStatements( array $statementList ): array { |
237 | $map = []; |
238 | foreach ( $statementList as $key => $_ ) { |
239 | $map[$key] = trim( $key, "'\"" ); |
240 | } |
241 | natcasesort( $map ); |
242 | // @phan-suppress-next-line PhanTypeMismatchReturn False positive as array_keys can return list<string> |
243 | return array_keys( $map ); |
244 | } |
245 | |
246 | } |