Skip to content

Commit 139d8a0

Browse files
authored
Merge pull request #54 from PHPCSStandards/feature/arrays-isshortarray-make-compatible-with-older-phpcs
Arrays::isShortArray(): fix compatibility with older PHPCS versions
2 parents 64b31cc + 5ebc202 commit 139d8a0

File tree

4 files changed

+368
-2
lines changed

4 files changed

+368
-2
lines changed

PHPCSUtils/Tokens/Collections.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,27 @@ class Collections
4040
\T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY,
4141
];
4242

43+
/**
44+
* Tokens which are used to create arrays.
45+
*
46+
* List which is backward-compatible with PHPCS < 3.3.0.
47+
* Should only be used selectively.
48+
*
49+
* @since 1.0.0
50+
*
51+
* @see \PHPCSUtils\Tokens\Collections::$shortArrayTokensBC Related property containing only tokens used
52+
* for short arrays (cross-version).
53+
*
54+
* @var array <int|string> => <int|string>
55+
*/
56+
public static $arrayTokensBC = [
57+
\T_ARRAY => \T_ARRAY,
58+
\T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY,
59+
\T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY,
60+
\T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET,
61+
\T_CLOSE_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET,
62+
];
63+
4364
/**
4465
* Modifier keywords which can be used for a class declaration.
4566
*
@@ -257,6 +278,26 @@ class Collections
257278
\T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY,
258279
];
259280

281+
/**
282+
* Tokens which are used for short arrays.
283+
*
284+
* List which is backward-compatible with PHPCS < 3.3.0.
285+
* Should only be used selectively.
286+
*
287+
* @since 1.0.0
288+
*
289+
* @see \PHPCSUtils\Tokens\Collections::$arrayTokensBC Related property containing all tokens used for arrays
290+
* (cross-version).
291+
*
292+
* @var array <int|string> => <int|string>
293+
*/
294+
public static $shortArrayTokensBC = [
295+
\T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY,
296+
\T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY,
297+
\T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET,
298+
\T_CLOSE_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET,
299+
];
300+
260301
/**
261302
* Tokens which are used for short lists.
262303
*

PHPCSUtils/Utils/Arrays.php

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
namespace PHPCSUtils\Utils;
1212

1313
use PHP_CodeSniffer\Files\File;
14+
use PHP_CodeSniffer\Util\Tokens;
15+
use PHPCSUtils\BackCompat\Helper;
1416
use PHPCSUtils\Tokens\Collections;
1517
use PHPCSUtils\Utils\Lists;
1618

@@ -26,25 +28,129 @@ class Arrays
2628
* Determine whether a `T_OPEN/CLOSE_SHORT_ARRAY` token is a short array() construct
2729
* and not a short list.
2830
*
31+
* This method also accepts `T_OPEN/CLOSE_SQUARE_BRACKET` tokens to allow it to be
32+
* PHPCS cross-version compatible as the short array tokenizing has been plagued by
33+
* a number of bugs over time.
34+
*
2935
* @since 1.0.0
3036
*
3137
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
3238
* @param int $stackPtr The position of the short array bracket token.
3339
*
3440
* @return bool True if the token passed is the open/close bracket of a short array.
35-
* False if the token is a short list bracket or not one of the accepted tokens.
41+
* False if the token is a short list bracket, a plain square bracket
42+
* or not one of the accepted tokens.
3643
*/
3744
public static function isShortArray(File $phpcsFile, $stackPtr)
3845
{
3946
$tokens = $phpcsFile->getTokens();
4047

4148
// Is this one of the tokens this function handles ?
4249
if (isset($tokens[$stackPtr]) === false
43-
|| isset(Collections::$shortArrayTokens[$tokens[$stackPtr]['code']]) === false
50+
|| isset(Collections::$shortArrayTokensBC[$tokens[$stackPtr]['code']]) === false
4451
) {
4552
return false;
4653
}
4754

55+
// All known tokenizer bugs are in PHPCS versions before 3.3.0.
56+
$phpcsVersion = Helper::getVersion();
57+
58+
/*
59+
* Deal with square brackets which may be incorrectly tokenized short arrays.
60+
*/
61+
if (isset(Collections::$shortArrayTokens[$tokens[$stackPtr]['code']]) === false) {
62+
if (\version_compare($phpcsVersion, '3.3.0', '>=')) {
63+
// These will just be properly tokenized, plain square brackets. No need for further checks.
64+
return false;
65+
}
66+
67+
$opener = $stackPtr;
68+
if ($tokens[$stackPtr]['code'] === \T_CLOSE_SQUARE_BRACKET) {
69+
$opener = $tokens[$stackPtr]['bracket_opener'];
70+
}
71+
72+
if (isset($tokens[$opener]['bracket_closer']) === false) {
73+
return false;
74+
}
75+
76+
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true);
77+
78+
if (\version_compare($phpcsVersion, '2.8.0', '>=')) {
79+
/*
80+
* BC: Work around a bug in the tokenizer of PHPCS 2.8.0 - 3.2.3 where a `[` would be
81+
* tokenized as T_OPEN_SQUARE_BRACKET instead of T_OPEN_SHORT_ARRAY if it was
82+
* preceded by a PHP open tag at the very start of the file.
83+
*
84+
* If we have square brackets which are not that specific situation, they are just plain
85+
* square brackets.
86+
*
87+
* @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1971
88+
*/
89+
if ($prevNonEmpty !== 0 || $tokens[$prevNonEmpty]['code'] !== \T_OPEN_TAG) {
90+
return false;
91+
}
92+
}
93+
94+
if (\version_compare($phpcsVersion, '2.8.0', '<')) {
95+
/*
96+
* BC: Work around a bug in the tokenizer of PHPCS < 2.8.0 where a `[` would be
97+
* tokenized as T_OPEN_SQUARE_BRACKET instead of T_OPEN_SHORT_ARRAY if it was
98+
* preceded by a close curly of a control structure.
99+
*
100+
* If we have square brackets which are not that specific situation, they are just plain
101+
* square brackets.
102+
*
103+
* @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1284
104+
*/
105+
if ($tokens[$prevNonEmpty]['code'] !== \T_CLOSE_CURLY_BRACKET
106+
|| isset($tokens[$prevNonEmpty]['scope_condition']) === false
107+
) {
108+
return false;
109+
}
110+
}
111+
} else {
112+
/*
113+
* Deal with short array brackets which may be incorrectly tokenized plain square brackets.
114+
*/
115+
if (\version_compare($phpcsVersion, '2.9.0', '<')) {
116+
$opener = $stackPtr;
117+
if ($tokens[$stackPtr]['code'] === \T_CLOSE_SHORT_ARRAY) {
118+
$opener = $tokens[$stackPtr]['bracket_opener'];
119+
}
120+
121+
/*
122+
* BC: Work around a bug in the tokenizer of PHPCS < 2.9.0 where array dereferencing
123+
* of short array and string literals would be incorrectly tokenized as short array.
124+
* I.e. the square brackets in `'PHP'[0]` would be tokenized as short array.
125+
*
126+
* @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1381
127+
*/
128+
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true);
129+
if ($tokens[$prevNonEmpty]['code'] === \T_CLOSE_SHORT_ARRAY
130+
|| $tokens[$prevNonEmpty]['code'] === \T_CONSTANT_ENCAPSED_STRING
131+
) {
132+
return false;
133+
}
134+
135+
/*
136+
* BC: Work around a bug in the tokenizer of PHPCS 2.8.0 and 2.8.1 where array dereferencing
137+
* of a variable variable would be incorrectly tokenized as short array.
138+
*
139+
* @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1284
140+
*/
141+
if (\version_compare($phpcsVersion, '2.8.0', '>=')
142+
&& $tokens[$prevNonEmpty]['code'] === \T_CLOSE_CURLY_BRACKET
143+
) {
144+
$openCurly = $tokens[$prevNonEmpty]['bracket_opener'];
145+
$beforeCurlies = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($openCurly - 1), null, true);
146+
if ($tokens[$beforeCurlies]['code'] === \T_DOLLAR) {
147+
return false;
148+
}
149+
}
150+
}
151+
}
152+
153+
// In all other circumstances, make sure this isn't a short list instead of a short array.
48154
return (Lists::isShortList($phpcsFile, $stackPtr) === false);
49155
}
50156
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
/**
3+
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4+
*
5+
* @package PHPCSUtils
6+
* @copyright 2019 PHPCSUtils Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSUtils
9+
*/
10+
11+
namespace PHPCSUtils\Tests\Utils\Arrays;
12+
13+
use PHPCSUtils\TestUtils\UtilityMethodTestCase;
14+
use PHPCSUtils\Utils\Arrays;
15+
16+
/**
17+
* Tests for specific PHPCS tokenizer issues which can affect the \PHPCSUtils\Utils\Arrays::isShortArray() method.
18+
*
19+
* @covers \PHPCSUtils\Utils\Arrays::isShortArray
20+
*
21+
* @group arrays
22+
*
23+
* @since 1.0.0
24+
*/
25+
class IsShortArrayTokenizerBC1Test extends UtilityMethodTestCase
26+
{
27+
28+
/**
29+
* Full path to the test case file associated with this test class.
30+
*
31+
* @var string
32+
*/
33+
protected static $caseFile = '';
34+
35+
/**
36+
* Initialize PHPCS & tokenize the test case file.
37+
*
38+
* Overloaded to re-use the `$caseFile` from the Lists::isShortList() test.
39+
*
40+
* @beforeClass
41+
*
42+
* @return void
43+
*/
44+
public static function setUpTestFile()
45+
{
46+
self::$caseFile = \dirname(__DIR__) . '/Lists/IsShortListTokenizerBC1Test.inc';
47+
parent::setUpTestFile();
48+
}
49+
50+
/**
51+
* Test correctly determining whether a short array open token is a short array,
52+
* even when the token is incorrectly tokenized.
53+
*
54+
* @dataProvider dataIsShortArray
55+
*
56+
* @param string $testMarker The comment which prefaces the target token in the test file.
57+
* @param bool $expected The expected boolean return value.
58+
*
59+
* @return void
60+
*/
61+
public function testIsShortArray($testMarker, $expected)
62+
{
63+
$stackPtr = $this->getTargetToken($testMarker, [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET]);
64+
$result = Arrays::isShortArray(self::$phpcsFile, $stackPtr);
65+
66+
$this->assertSame($expected, $result);
67+
}
68+
69+
/**
70+
* Data provider.
71+
*
72+
* @see testIsShortArray() For the array format.
73+
*
74+
* @return array
75+
*/
76+
public function dataIsShortArray()
77+
{
78+
return [
79+
'issue-1971-list-first-in-file' => [
80+
'/* testTokenizerIssue1971PHPCSlt330gt271A */',
81+
false,
82+
],
83+
'issue-1971-list-first-in-file-nested' => [
84+
'/* testTokenizerIssue1971PHPCSlt330gt271B */',
85+
false,
86+
],
87+
'issue-1381-array-dereferencing-1-array' => [
88+
'/* testTokenizerIssue1381PHPCSlt290A1 */',
89+
true,
90+
],
91+
'issue-1381-array-dereferencing-1-deref' => [
92+
'/* testTokenizerIssue1381PHPCSlt290A2 */',
93+
false,
94+
],
95+
'issue-1381-array-dereferencing-2' => [
96+
'/* testTokenizerIssue1381PHPCSlt290B */',
97+
false,
98+
],
99+
'issue-1381-array-dereferencing-3' => [
100+
'/* testTokenizerIssue1381PHPCSlt290C */',
101+
false,
102+
],
103+
'issue-1381-array-dereferencing-4' => [
104+
'/* testTokenizerIssue1381PHPCSlt290D1 */',
105+
false,
106+
],
107+
'issue-1381-array-dereferencing-4-deref-deref' => [
108+
'/* testTokenizerIssue1381PHPCSlt290D2 */',
109+
false,
110+
],
111+
'issue-1284-short-list-directly-after-close-curly-control-structure' => [
112+
'/* testTokenizerIssue1284PHPCSlt280A */',
113+
false,
114+
],
115+
'issue-1284-short-array-directly-after-close-curly-control-structure' => [
116+
'/* testTokenizerIssue1284PHPCSlt280B */',
117+
true,
118+
],
119+
'issue-1284-array-access-variable-variable' => [
120+
'/* testTokenizerIssue1284PHPCSlt290C */',
121+
false,
122+
],
123+
'issue-1284-array-access-variable-property' => [
124+
'/* testTokenizerIssue1284PHPCSlt280D */',
125+
false,
126+
],
127+
];
128+
}
129+
}

0 commit comments

Comments
 (0)