Skip to content

Commit 0643697

Browse files
committed
Utils\Arrays: add new getDoubleArrowPtr() utility method
This adds a new utility method: * `getDoubleArrowPtr()` - to find the position of a double arrow within an array item. Returns the integer stackPtr position or false if the array item doesn't have a key. Note: this method will handle nested `foreach` and keyed `yield` structures correctly. Includes dedicated unit tests.
1 parent e438508 commit 0643697

File tree

3 files changed

+346
-0
lines changed

3 files changed

+346
-0
lines changed

PHPCSUtils/Utils/Arrays.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace PHPCSUtils\Utils;
1212

13+
use PHP_CodeSniffer\Exceptions\RuntimeException;
1314
use PHP_CodeSniffer\Files\File;
1415
use PHP_CodeSniffer\Util\Tokens;
1516
use PHPCSUtils\BackCompat\Helper;
@@ -24,6 +25,17 @@
2425
class Arrays
2526
{
2627

28+
/**
29+
* The tokens to target to find the double arrow in an array item.
30+
*
31+
* @var array <int|string> => <int|string>
32+
*/
33+
private static $doubleArrowTargets = [
34+
\T_DOUBLE_ARROW => \T_DOUBLE_ARROW,
35+
\T_ARRAY => \T_ARRAY,
36+
\T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY,
37+
];
38+
2739
/**
2840
* Determine whether a `T_OPEN/CLOSE_SHORT_ARRAY` token is a short array() construct
2941
* and not a short list.
@@ -217,4 +229,63 @@ public static function getOpenClose(File $phpcsFile, $stackPtr, $isShortArray =
217229

218230
return false;
219231
}
232+
233+
/**
234+
* Get the stack pointer position of the double arrow within an array item.
235+
*
236+
* Expects to be passed the array item start and end tokens as retrieved via
237+
* {@see \PHPCSUtils\Utils\PassedParameters::getParameters()}.
238+
*
239+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being examined.
240+
* @param int $start Stack pointer to the start of the array item.
241+
* @param int $end Stack pointer to the end of the array item (inclusive).
242+
*
243+
* @return int|false Stack pointer to the double arrow if this array item has a key or false otherwise.
244+
*
245+
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the start or end positions are invalid.
246+
*/
247+
public static function getDoubleArrowPtr(File $phpcsFile, $start, $end)
248+
{
249+
$tokens = $phpcsFile->getTokens();
250+
251+
if (isset($tokens[$start], $tokens[$end]) === false || $start > $end) {
252+
throw new RuntimeException(
253+
'Invalid start and/or end position passed to getDoubleArrowPtr().'
254+
. ' Received: $start ' . $start . ', $end ' . $end
255+
);
256+
}
257+
258+
$targets = self::$doubleArrowTargets + Collections::$closedScopes;
259+
260+
$doubleArrow = ($start - 1);
261+
++$end;
262+
do {
263+
$doubleArrow = $phpcsFile->findNext(
264+
$targets,
265+
($doubleArrow + 1),
266+
$end
267+
);
268+
269+
if ($doubleArrow === false) {
270+
break;
271+
}
272+
273+
if ($tokens[$doubleArrow]['code'] === \T_DOUBLE_ARROW) {
274+
return $doubleArrow;
275+
}
276+
277+
// Skip over closed scopes which may contain foreach structures or generators.
278+
if (isset(Collections::$closedScopes[$tokens[$doubleArrow]['code']]) === true
279+
&& isset($tokens[$doubleArrow]['scope_closer']) === true
280+
) {
281+
$doubleArrow = $tokens[$doubleArrow]['scope_closer'];
282+
continue;
283+
}
284+
285+
// Start of nested long/short array.
286+
break;
287+
} while ($doubleArrow < $end);
288+
289+
return false;
290+
}
220291
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/* testGetDoubleArrowPtr */
4+
$array = [
5+
/* testValueNoArrow */
6+
'value no arrow',
7+
8+
/* testArrowNumericIndex */
9+
123 => 'arrow numeric index',
10+
11+
/* testArrowStringIndex */
12+
'foo' => 'arrow string index',
13+
14+
/* testArrowMultiTokenIndex */
15+
'concat' . 'index' => 'arrow multi token index',
16+
17+
/* testNoArrowValueShortArray */
18+
[
19+
'value only' => 'arrow belongs to value',
20+
],
21+
22+
/* testNoArrowValueLongArray */
23+
array(
24+
'value only' => 'arrow belongs to value',
25+
),
26+
27+
/* testNoArrowValueNestedArrays */
28+
array(
29+
[
30+
array(
31+
['key' => 'arrow belongs to nested array'],
32+
),
33+
],
34+
),
35+
36+
/* testNoArrowValueClosure */
37+
function() {
38+
echo 'closure as value arrow belongs to value';
39+
return array( $a => $b );
40+
},
41+
42+
/* testArrowValueShortArray */
43+
'index and value short array' => [
44+
'index and value' => '',
45+
],
46+
47+
/* testArrowValueLongArray */
48+
'index and value long array' => array(
49+
'index and value' => '',
50+
),
51+
52+
/* testArrowValueClosure */
53+
'index and value closure' => function() {
54+
echo 'closure as value arrow belongs to value';
55+
return array( $a => $b );
56+
},
57+
58+
/* testNoArrowValueAnonClassForeach */
59+
new class($iterable) {
60+
public function __construct($iterable) {
61+
$return = 0;
62+
foreach ($iterable as $key => $value) {
63+
$return = $key * $value;
64+
}
65+
return $return;
66+
}
67+
},
68+
69+
/* testNoArrowValueClosureYieldWithKey */
70+
function() { yield 'k' => $x },
71+
72+
/* testArrowKeyClosureYieldWithKey */
73+
function() { yield 'k' => $x }() => 'value',
74+
];
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
use PHPCSUtils\Utils\PassedParameters;
16+
17+
/**
18+
* Tests for the \PHPCSUtils\Utils\Arrays::getDoubleArrowPtr() method.
19+
*
20+
* @covers \PHPCSUtils\Utils\Arrays::getDoubleArrowPtr
21+
*
22+
* @group arrays
23+
*
24+
* @since 1.0.0
25+
*/
26+
class GetDoubleArrowPtrTest extends UtilityMethodTestCase
27+
{
28+
29+
/**
30+
* Cache for the parsed parameters array.
31+
*
32+
* @var array <string> => <int>
33+
*/
34+
private static $parameters = [];
35+
36+
/**
37+
* Set up the parameters cache for the tests.
38+
*
39+
* Retrieves the parameters array only once and caches it as it won't change
40+
* between the tests anyway.
41+
*
42+
* @before
43+
*
44+
* @return void
45+
*/
46+
protected function setUpCache()
47+
{
48+
if (empty(self::$parameters) === true) {
49+
$target = $this->getTargetToken('/* testGetDoubleArrowPtr */', [\T_OPEN_SHORT_ARRAY]);
50+
$parameters = PassedParameters::getParameters(self::$phpcsFile, $target);
51+
52+
foreach ($parameters as $index => $values) {
53+
\preg_match('`^(/\* test[^*]+ \*/)`', $values['raw'], $matches);
54+
if (empty($matches[1]) === false) {
55+
self::$parameters[$matches[1]] = $values;
56+
}
57+
}
58+
}
59+
}
60+
61+
/**
62+
* Test receiving an expected exception when an invalid start position is passed.
63+
*
64+
* @return void
65+
*/
66+
public function testInvalidStartPositionException()
67+
{
68+
$this->expectPhpcsException(
69+
'Invalid start and/or end position passed to getDoubleArrowPtr(). Received: $start -10, $end 10'
70+
);
71+
72+
Arrays::getDoubleArrowPtr(self::$phpcsFile, -10, 10);
73+
}
74+
75+
/**
76+
* Test receiving an expected exception when an invalid end position is passed.
77+
*
78+
* @return void
79+
*/
80+
public function testInvalidEndPositionException()
81+
{
82+
$this->expectPhpcsException(
83+
'Invalid start and/or end position passed to getDoubleArrowPtr(). Received: $start 0, $end 100000'
84+
);
85+
86+
Arrays::getDoubleArrowPtr(self::$phpcsFile, 0, 100000);
87+
}
88+
89+
/**
90+
* Test receiving an expected exception when the start position is after the end position.
91+
*
92+
* @return void
93+
*/
94+
public function testInvalidStartEndPositionException()
95+
{
96+
$this->expectPhpcsException(
97+
'Invalid start and/or end position passed to getDoubleArrowPtr(). Received: $start 10, $end 5'
98+
);
99+
100+
Arrays::getDoubleArrowPtr(self::$phpcsFile, 10, 5);
101+
}
102+
103+
/**
104+
* Test retrieving the position of the double arrow for an array parameter.
105+
*
106+
* @dataProvider dataGetDoubleArrowPtr
107+
*
108+
* @param string $testMarker The comment which is part of the target array item in the test file.
109+
* @param array $expected The expected function call result.
110+
*
111+
* @return void
112+
*/
113+
public function testGetDoubleArrowPtr($testMarker, $expected)
114+
{
115+
if (isset(self::$parameters[$testMarker]) === false) {
116+
$this->fail('Test case not found for ' . $testMarker);
117+
}
118+
119+
$start = self::$parameters[$testMarker]['start'];
120+
$end = self::$parameters[$testMarker]['end'];
121+
122+
// Change double arrow position from offset to exact position.
123+
if ($expected !== false) {
124+
$expected += $start;
125+
}
126+
127+
$result = Arrays::getDoubleArrowPtr(self::$phpcsFile, $start, $end);
128+
$this->assertSame($expected, $result);
129+
}
130+
131+
/**
132+
* Data provider.
133+
*
134+
* The double arrow positions are provided as offsets from the $start stackPtr.
135+
*
136+
* @see testGetDoubleArrowPtr()
137+
*
138+
* @return array
139+
*/
140+
public function dataGetDoubleArrowPtr()
141+
{
142+
return [
143+
'test-no-arrow' => [
144+
'/* testValueNoArrow */',
145+
false,
146+
],
147+
'test-arrow-numeric-index' => [
148+
'/* testArrowNumericIndex */',
149+
8,
150+
],
151+
'test-arrow-string-index' => [
152+
'/* testArrowStringIndex */',
153+
8,
154+
],
155+
'test-arrow-multi-token-index' => [
156+
'/* testArrowMultiTokenIndex */',
157+
12,
158+
],
159+
'test-no-arrow-value-short-array' => [
160+
'/* testNoArrowValueShortArray */',
161+
false,
162+
],
163+
'test-no-arrow-value-long-array' => [
164+
'/* testNoArrowValueLongArray */',
165+
false,
166+
],
167+
'test-no-arrow-value-nested-arrays' => [
168+
'/* testNoArrowValueNestedArrays */',
169+
false,
170+
],
171+
'test-no-arrow-value-closure' => [
172+
'/* testNoArrowValueClosure */',
173+
false,
174+
],
175+
'test-arrow-value-short-array' => [
176+
'/* testArrowValueShortArray */',
177+
8,
178+
],
179+
'test-arrow-value-long-array' => [
180+
'/* testArrowValueLongArray */',
181+
8,
182+
],
183+
'test-arrow-value-closure' => [
184+
'/* testArrowValueClosure */',
185+
8,
186+
],
187+
'test-no-arrow-value-anon-class-with-foreach' => [
188+
'/* testNoArrowValueAnonClassForeach */',
189+
false,
190+
],
191+
'test-no-arrow-value-closure-with-keyed-yield' => [
192+
'/* testNoArrowValueClosureYieldWithKey */',
193+
false,
194+
],
195+
'test-arrow-key-closure-with-keyed-yield' => [
196+
'/* testArrowKeyClosureYieldWithKey */',
197+
24,
198+
],
199+
];
200+
}
201+
}

0 commit comments

Comments
 (0)