Skip to content

Commit ef80e53

Browse files
committed
Added match expression support for findStart/EndOfStatement
This also adds unit tests for findStartOfStatement and fixes some edge cases where passing the last token in an expression return the same token back again
1 parent 6e0df17 commit ef80e53

File tree

6 files changed

+914
-11
lines changed

6 files changed

+914
-11
lines changed

package.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
6161
-- This will have no impact on custom sniffs unless they are specifically looking at the value of the T_FN_ARROW constant
6262
-- If sniffs are just using constant to find arrow functions, they will continue to work without modification
6363
-- Thanks to Juliette Reinders Folmer for the patch
64+
- File::findStartOfStatement() now works correctly when passed the last token in a statement
6465
- File::getMethodParameters() now supports PHP 8.0 constructor property promotion
6566
-- Returned method params now include a "property_visibility" and "visibility_token" index if property promotion is detected
6667
-- Thanks to Juliette Reinders Folmer for the patch

src/Files/File.php

Lines changed: 117 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2283,27 +2283,103 @@ public function findNext(
22832283
*/
22842284
public function findStartOfStatement($start, $ignore=null)
22852285
{
2286-
$endTokens = Util\Tokens::$blockOpeners;
2286+
$startTokens = Util\Tokens::$blockOpeners;
2287+
$startTokens[T_OPEN_SHORT_ARRAY] = true;
2288+
$startTokens[T_OPEN_TAG] = true;
22872289

2288-
$endTokens[T_COLON] = true;
2289-
$endTokens[T_COMMA] = true;
2290-
$endTokens[T_DOUBLE_ARROW] = true;
2291-
$endTokens[T_SEMICOLON] = true;
2292-
$endTokens[T_OPEN_TAG] = true;
2293-
$endTokens[T_CLOSE_TAG] = true;
2294-
$endTokens[T_OPEN_SHORT_ARRAY] = true;
2290+
$endTokens = [
2291+
T_CLOSE_TAG => true,
2292+
T_COLON => true,
2293+
T_COMMA => true,
2294+
T_DOUBLE_ARROW => true,
2295+
T_MATCH_ARROW => true,
2296+
T_SEMICOLON => true,
2297+
];
22952298

22962299
if ($ignore !== null) {
22972300
$ignore = (array) $ignore;
22982301
foreach ($ignore as $code) {
2299-
unset($endTokens[$code]);
2302+
if (isset($startTokens[$code]) === true) {
2303+
unset($startTokens[$code]);
2304+
}
2305+
2306+
if (isset($endTokens[$code]) === true) {
2307+
unset($endTokens[$code]);
2308+
}
23002309
}
23012310
}
23022311

2312+
// If the start token is inside the case part of a match expression,
2313+
// find the start of the condition. If it's in the statement part, find
2314+
// the token that comes after the match arrow.
2315+
$matchExpression = $this->getCondition($start, T_MATCH);
2316+
if ($matchExpression !== false) {
2317+
for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
2318+
if ($prevMatch !== $start
2319+
&& ($this->tokens[$prevMatch]['code'] === T_MATCH_ARROW
2320+
|| $this->tokens[$prevMatch]['code'] === T_COMMA)
2321+
) {
2322+
break;
2323+
}
2324+
2325+
// Skip nested statements.
2326+
if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
2327+
&& $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
2328+
) {
2329+
$prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
2330+
} else if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
2331+
&& $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
2332+
) {
2333+
$prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
2334+
}
2335+
}
2336+
2337+
if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
2338+
// We're before the arrow in the first case.
2339+
$next = $this->findNext(Util\Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
2340+
if ($next === false) {
2341+
return $start;
2342+
}
2343+
2344+
return $next;
2345+
}
2346+
2347+
if ($this->tokens[$prevMatch]['code'] === T_COMMA) {
2348+
// We're before the arrow, but not in the first case.
2349+
$prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($prevMatch - 1), $this->tokens[$matchExpression]['scope_opener']);
2350+
if ($prevMatchArrow === false) {
2351+
// We're before the arrow in the first case.
2352+
$next = $this->findNext(Util\Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
2353+
return $next;
2354+
}
2355+
2356+
$nextComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1));
2357+
$next = $this->findNext(Util\Tokens::$emptyTokens, ($nextComma + 1), null, true);
2358+
return $next;
2359+
}
2360+
}//end if
2361+
23032362
$lastNotEmpty = $start;
23042363

2364+
// If we are starting at a token that ends a scope block, skip to
2365+
// the start and continue from there.
2366+
// If we are starting at a token that ends a statement, skip this
2367+
// token so we find the true start of the statement.
2368+
while (isset($endTokens[$this->tokens[$start]['code']]) === true
2369+
|| (isset($this->tokens[$start]['scope_condition']) === true
2370+
&& $start === $this->tokens[$start]['scope_closer'])
2371+
) {
2372+
if (isset($this->tokens[$start]['scope_condition']) === true) {
2373+
$start = $this->tokens[$start]['scope_condition'];
2374+
} else {
2375+
$start--;
2376+
}
2377+
}
2378+
23052379
for ($i = $start; $i >= 0; $i--) {
2306-
if (isset($endTokens[$this->tokens[$i]['code']]) === true) {
2380+
if (isset($startTokens[$this->tokens[$i]['code']]) === true
2381+
|| isset($endTokens[$this->tokens[$i]['code']]) === true
2382+
) {
23072383
// Found the end of the previous statement.
23082384
return $lastNotEmpty;
23092385
}
@@ -2332,7 +2408,12 @@ public function findStartOfStatement($start, $ignore=null)
23322408
&& $i === $this->tokens[$i]['parenthesis_closer']
23332409
) {
23342410
$i = $this->tokens[$i]['parenthesis_opener'];
2335-
}
2411+
} else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
2412+
$start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
2413+
if ($start !== false) {
2414+
$i = $start;
2415+
}
2416+
}//end if
23362417

23372418
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
23382419
$lastNotEmpty = $i;
@@ -2374,6 +2455,31 @@ public function findEndOfStatement($start, $ignore=null)
23742455
}
23752456
}
23762457

2458+
// If the start token is inside the case part of a match expression,
2459+
// advance to the match arrow and continue looking for the
2460+
// end of the statement from there so that we skip over commas.
2461+
$matchExpression = $this->getCondition($start, T_MATCH);
2462+
if ($matchExpression !== false) {
2463+
$beforeArrow = true;
2464+
$prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
2465+
if ($prevMatchArrow !== false) {
2466+
$prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
2467+
if ($prevComma === false) {
2468+
// No comma between this token and the last match arrow,
2469+
// so this token exists after the arrow and we can continue
2470+
// checking as normal.
2471+
$beforeArrow = false;
2472+
}
2473+
}
2474+
2475+
if ($beforeArrow === true) {
2476+
$nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
2477+
if ($nextMatchArrow !== false) {
2478+
$start = $nextMatchArrow;
2479+
}
2480+
}
2481+
}//end if
2482+
23772483
$lastNotEmpty = $start;
23782484
for ($i = $start; $i < $this->numTokens; $i++) {
23792485
if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {

tests/Core/File/FindEndOfStatementTest.inc

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,54 @@ $foo = foo(
5252
fn() => [$row[0], $row[3]]
5353
);
5454

55+
$match = match ($a) {
56+
/* testMatchCase */
57+
1 => 'foo',
58+
/* testMatchDefault */
59+
default => 'bar'
60+
};
61+
62+
$match = match ($a) {
63+
/* testMatchMultipleCase */
64+
1, 2, => $a * $b,
65+
/* testMatchDefaultComma */
66+
default, => 'something'
67+
};
68+
69+
match ($pressedKey) {
70+
/* testMatchFunctionCall */
71+
Key::RETURN_ => save($value, $user)
72+
};
73+
74+
$result = match (true) {
75+
/* testMatchFunctionCallArm */
76+
str_contains($text, 'Welcome') || str_contains($text, 'Hello') => 'en',
77+
str_contains($text, 'Bienvenue') || str_contains($text, 'Bonjour') => 'fr',
78+
default => 'pl'
79+
};
80+
81+
/* testMatchClosure */
82+
$result = match ($key) {
83+
1 => function($a, $b) {},
84+
2 => function($b, $c) {},
85+
};
86+
87+
/* testMatchArray */
88+
$result = match ($key) {
89+
1 => [1,2,3],
90+
2 => [1 => one(), 2 => two()],
91+
};
92+
93+
/* testNestedMatch */
94+
$result = match ($key) {
95+
1 => match ($key) {
96+
1 => 'one',
97+
2 => 'two',
98+
},
99+
2 => match ($key) {
100+
1 => 'two',
101+
2 => 'one',
102+
},
103+
};
104+
55105
return 0;

tests/Core/File/FindEndOfStatementTest.php

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,176 @@ public function testArrowFunctionWithArrayAsArgument()
237237
}//end testArrowFunctionWithArrayAsArgument()
238238

239239

240+
/**
241+
* Test simple match expression case.
242+
*
243+
* @return void
244+
*/
245+
public function testMatchCase()
246+
{
247+
$start = $this->getTargetToken('/* testMatchCase */', T_LNUMBER);
248+
$found = self::$phpcsFile->findEndOfStatement($start);
249+
250+
$this->assertSame(($start + 5), $found);
251+
252+
$start = $this->getTargetToken('/* testMatchCase */', T_CONSTANT_ENCAPSED_STRING);
253+
$found = self::$phpcsFile->findEndOfStatement($start);
254+
255+
$this->assertSame(($start + 1), $found);
256+
257+
}//end testMatchCase()
258+
259+
260+
/**
261+
* Test simple match expression default case.
262+
*
263+
* @return void
264+
*/
265+
public function testMatchDefault()
266+
{
267+
$start = $this->getTargetToken('/* testMatchDefault */', T_MATCH_DEFAULT);
268+
$found = self::$phpcsFile->findEndOfStatement($start);
269+
270+
$this->assertSame(($start + 4), $found);
271+
272+
$start = $this->getTargetToken('/* testMatchDefault */', T_CONSTANT_ENCAPSED_STRING);
273+
$found = self::$phpcsFile->findEndOfStatement($start);
274+
275+
$this->assertSame($start, $found);
276+
277+
}//end testMatchDefault()
278+
279+
280+
/**
281+
* Test multiple comma-seperated match expression case values.
282+
*
283+
* @return void
284+
*/
285+
public function testMatchMultipleCase()
286+
{
287+
$start = $this->getTargetToken('/* testMatchMultipleCase */', T_LNUMBER);
288+
$found = self::$phpcsFile->findEndOfStatement($start);
289+
290+
$this->assertSame(($start + 13), $found);
291+
292+
}//end testMatchMultipleCase()
293+
294+
295+
/**
296+
* Test match expression default case with trailing comma.
297+
*
298+
* @return void
299+
*/
300+
public function testMatchDefaultComma()
301+
{
302+
$start = $this->getTargetToken('/* testMatchDefaultComma */', T_MATCH_DEFAULT);
303+
$found = self::$phpcsFile->findEndOfStatement($start);
304+
305+
$this->assertSame(($start + 5), $found);
306+
307+
}//end testMatchDefaultComma()
308+
309+
310+
/**
311+
* Test match expression with function call.
312+
*
313+
* @return void
314+
*/
315+
public function testMatchFunctionCall()
316+
{
317+
$start = $this->getTargetToken('/* testMatchFunctionCall */', T_STRING);
318+
$found = self::$phpcsFile->findEndOfStatement($start);
319+
320+
$this->assertSame(($start + 12), $found);
321+
322+
$start += 8;
323+
$found = self::$phpcsFile->findEndOfStatement($start);
324+
325+
$this->assertSame(($start + 1), $found);
326+
327+
}//end testMatchFunctionCall()
328+
329+
330+
/**
331+
* Test match expression with function call in the arm.
332+
*
333+
* @return void
334+
*/
335+
public function testMatchFunctionCallArm()
336+
{
337+
// Check the first case.
338+
$start = $this->getTargetToken('/* testMatchFunctionCallArm */', T_STRING);
339+
$found = self::$phpcsFile->findEndOfStatement($start);
340+
341+
$this->assertSame(($start + 21), $found);
342+
343+
// Check the second case.
344+
$start += 24;
345+
$found = self::$phpcsFile->findEndOfStatement($start);
346+
347+
$this->assertSame(($start + 21), $found);
348+
349+
}//end testMatchFunctionCallArm()
350+
351+
352+
/**
353+
* Test match expression with closure.
354+
*
355+
* @return void
356+
*/
357+
public function testMatchClosure()
358+
{
359+
$start = $this->getTargetToken('/* testMatchClosure */', T_LNUMBER);
360+
$found = self::$phpcsFile->findEndOfStatement($start);
361+
362+
$this->assertSame(($start + 14), $found);
363+
364+
$start += 17;
365+
$found = self::$phpcsFile->findEndOfStatement($start);
366+
367+
$this->assertSame(($start + 14), $found);
368+
369+
}//end testMatchClosure()
370+
371+
372+
/**
373+
* Test match expression with array declaration.
374+
*
375+
* @return void
376+
*/
377+
public function testMatchArray()
378+
{
379+
$start = $this->getTargetToken('/* testMatchArray */', T_LNUMBER);
380+
$found = self::$phpcsFile->findEndOfStatement($start);
381+
382+
$this->assertSame(($start + 11), $found);
383+
384+
$start += 14;
385+
$found = self::$phpcsFile->findEndOfStatement($start);
386+
387+
$this->assertSame(($start + 22), $found);
388+
389+
}//end testMatchArray()
390+
391+
392+
/**
393+
* Test nested match expressions.
394+
*
395+
* @return void
396+
*/
397+
public function testNestedMatch()
398+
{
399+
$start = $this->getTargetToken('/* testNestedMatch */', T_LNUMBER);
400+
$found = self::$phpcsFile->findEndOfStatement($start);
401+
402+
$this->assertSame(($start + 30), $found);
403+
404+
$start += 21;
405+
$found = self::$phpcsFile->findEndOfStatement($start);
406+
407+
$this->assertSame(($start + 5), $found);
408+
409+
}//end testNestedMatch()
410+
411+
240412
}//end class

0 commit comments

Comments
 (0)