Skip to content

Commit ed4a5ed

Browse files
bkdotcomkukulich
authored andcommitted
SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys: New sniff
1 parent 539c056 commit ed4a5ed

17 files changed

+1258
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Slevomat Coding Standard for [PHP_CodeSniffer](https://github.com/squizlabs/PHP_
2929

3030
🚧 = [Sniff check can be suppressed locally](#suppressing-sniffs-locally)
3131

32+
- [SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys](doc/arrays.md#slevomatcodingstandardarrayalphabeticallysortedbykeys) 🔧
3233
- [SlevomatCodingStandard.Arrays.DisallowImplicitArrayCreation](doc/arrays.md#slevomatcodingstandardarraysdisallowimplicitarraycreation)
3334
- [SlevomatCodingStandard.Arrays.MultiLineArrayEndBracketPlacement](doc/arrays.md#slevomatcodingstandardarraysmultilinearrayendbracketplacement-) 🔧
3435
- [SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace](doc/arrays.md#slevomatcodingstandardarrayssinglelinearraywhitespace-) 🔧
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Helpers;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use function arsort;
7+
use function end;
8+
use function in_array;
9+
use function key;
10+
use function min;
11+
use function strlen;
12+
use function strnatcasecmp;
13+
use function strpos;
14+
use const T_COMMA;
15+
use const T_OPEN_SHORT_ARRAY;
16+
use const T_WHITESPACE;
17+
18+
/**
19+
* @internal
20+
*/
21+
class ArrayHelper
22+
{
23+
24+
/** @var array<int, array<string, array<int, int|string>|int|string>> */
25+
protected static $tokens;
26+
27+
/**
28+
* @return ArrayKeyValue[]
29+
*/
30+
public static function parse(File $phpcsFile, int $pointer): array
31+
{
32+
self::$tokens = $phpcsFile->getTokens();
33+
$token = self::$tokens[$pointer];
34+
[$pointerOpener, $pointerCloser] = self::openClosePointers($token);
35+
$tokenOpener = self::$tokens[$pointerOpener];
36+
$lineIndents = [''];
37+
$keyValues = [];
38+
$skipUntilPointer = null;
39+
$tokenEnd = null;
40+
$pointer = $pointerOpener + 1;
41+
for (; $pointer < $pointerCloser; $pointer++) {
42+
$token = self::$tokens[$pointer];
43+
if ($token['line'] > $tokenOpener['line'] || in_array($token['code'], TokenHelper::$ineffectiveTokenCodes, true) === false) {
44+
break;
45+
}
46+
}
47+
$pointerStart = $pointer;
48+
49+
for (; $pointer < $pointerCloser; $pointer++) {
50+
if ($pointer < $skipUntilPointer) {
51+
continue;
52+
}
53+
54+
$token = self::$tokens[$pointer];
55+
56+
if (in_array($token['code'], TokenHelper::$arrayTokenCodes, true)) {
57+
$pointerCloserWalk = self::openClosePointers($token)[1];
58+
$skipUntilPointer = $pointerCloserWalk;
59+
continue;
60+
}
61+
if (isset($token['scope_closer']) && $token['scope_closer'] > $pointer) {
62+
$skipUntilPointer = $token['scope_closer'];
63+
continue;
64+
}
65+
if (isset($token['parenthesis_closer'])) {
66+
$skipUntilPointer = $token['parenthesis_closer'];
67+
continue;
68+
}
69+
$nextEffective = in_array($token['code'], TokenHelper::$ineffectiveTokenCodes, true)
70+
? TokenHelper::findNextEffective($phpcsFile, $pointer)
71+
: TokenHelper::findNextEffective($phpcsFile, $pointer + 1);
72+
if (
73+
isset($lineIndents[$token['line']]) === false
74+
&& in_array($token['code'], TokenHelper::$ineffectiveTokenCodes, true) === false
75+
) {
76+
$firstPointerOnLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $pointer);
77+
$firstEffective = TokenHelper::findNextEffective($phpcsFile, $firstPointerOnLine);
78+
$lineIndents[$token['line']] = TokenHelper::getContent($phpcsFile, $firstPointerOnLine, $firstEffective - 1);
79+
}
80+
81+
$startNewKeyValue = $tokenEnd !== null
82+
? self::parseTestStartKeyVal($pointer, $tokenEnd, end($lineIndents))
83+
: false;
84+
if ($startNewKeyValue) {
85+
if ($nextEffective === $pointerCloser && in_array($token['code'], TokenHelper::$ineffectiveTokenCodes, true)) {
86+
// there are no more key/values
87+
$firstPointerOnLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $pointer);
88+
if ($pointer === $firstPointerOnLine) {
89+
// end last key/value on the prev token
90+
$pointer--;
91+
}
92+
break;
93+
}
94+
$startNewKeyValue = false;
95+
$tokenEnd = null;
96+
$keyValues[] = new ArrayKeyValue($phpcsFile, $pointerStart, $pointer - 1);
97+
$pointerStart = $pointer;
98+
}
99+
if ($token['code'] === T_COMMA || $tokenEnd !== null) {
100+
$tokenEnd = $token;
101+
}
102+
}
103+
104+
$pointer = min($pointer, $pointerCloser - 1);
105+
$keyValues[] = new ArrayKeyValue($phpcsFile, $pointerStart, $pointer);
106+
107+
self::$tokens = [];
108+
return $keyValues;
109+
}
110+
111+
/**
112+
* @param ArrayKeyValue[] $keyValues
113+
*/
114+
public static function getIndentation(array $keyValues): ?string
115+
{
116+
$indents = [];
117+
foreach ($keyValues as $keyValue) {
118+
$indent = $keyValue->getIndent() ?? 'null';
119+
$indents[$indent] = isset($indents[$indent])
120+
? $indents[$indent] + 1
121+
: 1;
122+
}
123+
arsort($indents);
124+
$indent = key($indents);
125+
return $indent !== 'null'
126+
? (string) $indent
127+
: null;
128+
}
129+
130+
/**
131+
* @param ArrayKeyValue[] $keyValues
132+
*/
133+
public static function isKeyed(array $keyValues): bool
134+
{
135+
foreach ($keyValues as $keyValue) {
136+
if ($keyValue->getKey() !== null) {
137+
return true;
138+
}
139+
}
140+
return false;
141+
}
142+
143+
/**
144+
* @param ArrayKeyValue[] $keyValues
145+
*/
146+
public static function isKeyedAll(array $keyValues): bool
147+
{
148+
foreach ($keyValues as $keyValue) {
149+
if ($keyValue->getKey() === null) {
150+
return false;
151+
}
152+
}
153+
return true;
154+
}
155+
156+
/**
157+
* Test if non-empty array with opening & closing brackets on separate lines
158+
*/
159+
public static function isMultiLine(File $phpcsFile, int $pointer): bool
160+
{
161+
$tokens = $phpcsFile->getTokens();
162+
$token = $tokens[$pointer];
163+
[$pointerOpener, $pointerCloser] = self::openClosePointers($token);
164+
$tokenOpener = $tokens[$pointerOpener];
165+
$tokenCloser = $tokens[$pointerCloser];
166+
167+
/** @var int $pointerPreviousToClose */
168+
$pointerPreviousToClose = TokenHelper::findPreviousEffective($phpcsFile, $pointerCloser - 1);
169+
170+
return $tokenOpener['line'] !== $tokenCloser['line']
171+
&& $pointerPreviousToClose !== $pointerOpener;
172+
}
173+
174+
/**
175+
* @param ArrayKeyValue[] $keyValues
176+
*/
177+
public static function isSortedByKey(array $keyValues): bool
178+
{
179+
$prev = '';
180+
foreach ($keyValues as $keyValue) {
181+
$cmp = strnatcasecmp((string) $prev, (string) $keyValue->getKey());
182+
if ($cmp === 1) {
183+
return false;
184+
}
185+
$prev = $keyValue->getKey();
186+
}
187+
return true;
188+
}
189+
190+
/**
191+
* @param array<string, array<int, int|string>|int|string> $token
192+
* @return int[]
193+
*/
194+
public static function openClosePointers(array $token): array
195+
{
196+
$isShortArray = $token['code'] === T_OPEN_SHORT_ARRAY;
197+
$pointerOpener = $isShortArray
198+
? $token['bracket_opener']
199+
: $token['parenthesis_opener'];
200+
$pointerCloser = $isShortArray
201+
? $token['bracket_closer']
202+
: $token['parenthesis_closer'];
203+
return [(int) $pointerOpener, (int) $pointerCloser];
204+
}
205+
206+
/**
207+
* Test whether we should begin collecting the next key/value tokens
208+
*
209+
* @param array<string, array<int, int|string>|int|string> $tokenPrev
210+
*/
211+
private static function parseTestStartKeyVal(int $pointer, array $tokenPrev, string $lastIndent): bool
212+
{
213+
$token = self::$tokens[$pointer];
214+
215+
// token['column'] cannot be relied on if tabs are being used
216+
// this simply checks if indent contains tab and is the same as the prev indent plus additional
217+
$testTabIndent = static function ($indent, $indentPrev) {
218+
return strpos($indent, "\t") !== false
219+
&& strpos($indent, $indentPrev) === 0
220+
&& strlen($indent) > strlen($indentPrev);
221+
};
222+
223+
$startNew = true;
224+
225+
if (
226+
in_array($token['code'], TokenHelper::$ineffectiveTokenCodes, true)
227+
&& $token['line'] === $tokenPrev['line']
228+
) {
229+
// we're whitespace or comment after the value...
230+
$startNew = false;
231+
} elseif (
232+
$token['code'] === T_WHITESPACE
233+
&& in_array($tokenPrev['code'], TokenHelper::$inlineCommentTokenCodes, true)
234+
&& in_array(self::$tokens[$pointer + 1]['code'], TokenHelper::$inlineCommentTokenCodes, true)
235+
&& (self::$tokens[$pointer + 1]['column'] >= $tokenPrev['column']
236+
|| $testTabIndent($token['content'], $lastIndent)
237+
)
238+
) {
239+
// 'key' => 'value' // tokenPrev is this comment
240+
// // we're in the preceeding whitespace
241+
$startNew = false;
242+
}
243+
244+
return $startNew;
245+
}
246+
247+
}

0 commit comments

Comments
 (0)