Skip to content

Commit

Permalink
Added match expression support for findStart/EndOfStatement
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gsherwood committed Mar 17, 2021
1 parent 6e0df17 commit ef80e53
Show file tree
Hide file tree
Showing 6 changed files with 914 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
-- This will have no impact on custom sniffs unless they are specifically looking at the value of the T_FN_ARROW constant
-- If sniffs are just using constant to find arrow functions, they will continue to work without modification
-- Thanks to Juliette Reinders Folmer for the patch
- File::findStartOfStatement() now works correctly when passed the last token in a statement
- File::getMethodParameters() now supports PHP 8.0 constructor property promotion
-- Returned method params now include a "property_visibility" and "visibility_token" index if property promotion is detected
-- Thanks to Juliette Reinders Folmer for the patch
Expand Down
128 changes: 117 additions & 11 deletions src/Files/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -2283,27 +2283,103 @@ public function findNext(
*/
public function findStartOfStatement($start, $ignore=null)
{
$endTokens = Util\Tokens::$blockOpeners;
$startTokens = Util\Tokens::$blockOpeners;
$startTokens[T_OPEN_SHORT_ARRAY] = true;
$startTokens[T_OPEN_TAG] = true;

$endTokens[T_COLON] = true;
$endTokens[T_COMMA] = true;
$endTokens[T_DOUBLE_ARROW] = true;
$endTokens[T_SEMICOLON] = true;
$endTokens[T_OPEN_TAG] = true;
$endTokens[T_CLOSE_TAG] = true;
$endTokens[T_OPEN_SHORT_ARRAY] = true;
$endTokens = [
T_CLOSE_TAG => true,
T_COLON => true,
T_COMMA => true,
T_DOUBLE_ARROW => true,
T_MATCH_ARROW => true,
T_SEMICOLON => true,
];

if ($ignore !== null) {
$ignore = (array) $ignore;
foreach ($ignore as $code) {
unset($endTokens[$code]);
if (isset($startTokens[$code]) === true) {
unset($startTokens[$code]);
}

if (isset($endTokens[$code]) === true) {
unset($endTokens[$code]);
}
}
}

// If the start token is inside the case part of a match expression,
// find the start of the condition. If it's in the statement part, find
// the token that comes after the match arrow.
$matchExpression = $this->getCondition($start, T_MATCH);
if ($matchExpression !== false) {
for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
if ($prevMatch !== $start
&& ($this->tokens[$prevMatch]['code'] === T_MATCH_ARROW
|| $this->tokens[$prevMatch]['code'] === T_COMMA)
) {
break;
}

// Skip nested statements.
if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
&& $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
) {
$prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
} else if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
&& $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
) {
$prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
}
}

if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
// We're before the arrow in the first case.
$next = $this->findNext(Util\Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
if ($next === false) {
return $start;
}

return $next;
}

if ($this->tokens[$prevMatch]['code'] === T_COMMA) {
// We're before the arrow, but not in the first case.
$prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($prevMatch - 1), $this->tokens[$matchExpression]['scope_opener']);
if ($prevMatchArrow === false) {
// We're before the arrow in the first case.
$next = $this->findNext(Util\Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
return $next;
}

$nextComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1));
$next = $this->findNext(Util\Tokens::$emptyTokens, ($nextComma + 1), null, true);
return $next;
}
}//end if

$lastNotEmpty = $start;

// If we are starting at a token that ends a scope block, skip to
// the start and continue from there.
// If we are starting at a token that ends a statement, skip this
// token so we find the true start of the statement.
while (isset($endTokens[$this->tokens[$start]['code']]) === true
|| (isset($this->tokens[$start]['scope_condition']) === true
&& $start === $this->tokens[$start]['scope_closer'])
) {
if (isset($this->tokens[$start]['scope_condition']) === true) {
$start = $this->tokens[$start]['scope_condition'];
} else {
$start--;
}
}

for ($i = $start; $i >= 0; $i--) {
if (isset($endTokens[$this->tokens[$i]['code']]) === true) {
if (isset($startTokens[$this->tokens[$i]['code']]) === true
|| isset($endTokens[$this->tokens[$i]['code']]) === true
) {
// Found the end of the previous statement.
return $lastNotEmpty;
}
Expand Down Expand Up @@ -2332,7 +2408,12 @@ public function findStartOfStatement($start, $ignore=null)
&& $i === $this->tokens[$i]['parenthesis_closer']
) {
$i = $this->tokens[$i]['parenthesis_opener'];
}
} else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
$start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
if ($start !== false) {
$i = $start;
}
}//end if

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

// If the start token is inside the case part of a match expression,
// advance to the match arrow and continue looking for the
// end of the statement from there so that we skip over commas.
$matchExpression = $this->getCondition($start, T_MATCH);
if ($matchExpression !== false) {
$beforeArrow = true;
$prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
if ($prevMatchArrow !== false) {
$prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
if ($prevComma === false) {
// No comma between this token and the last match arrow,
// so this token exists after the arrow and we can continue
// checking as normal.
$beforeArrow = false;
}
}

if ($beforeArrow === true) {
$nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
if ($nextMatchArrow !== false) {
$start = $nextMatchArrow;
}
}
}//end if

$lastNotEmpty = $start;
for ($i = $start; $i < $this->numTokens; $i++) {
if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
Expand Down
50 changes: 50 additions & 0 deletions tests/Core/File/FindEndOfStatementTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,54 @@ $foo = foo(
fn() => [$row[0], $row[3]]
);

$match = match ($a) {
/* testMatchCase */
1 => 'foo',
/* testMatchDefault */
default => 'bar'
};

$match = match ($a) {
/* testMatchMultipleCase */
1, 2, => $a * $b,
/* testMatchDefaultComma */
default, => 'something'
};

match ($pressedKey) {
/* testMatchFunctionCall */
Key::RETURN_ => save($value, $user)
};

$result = match (true) {
/* testMatchFunctionCallArm */
str_contains($text, 'Welcome') || str_contains($text, 'Hello') => 'en',
str_contains($text, 'Bienvenue') || str_contains($text, 'Bonjour') => 'fr',
default => 'pl'
};

/* testMatchClosure */
$result = match ($key) {
1 => function($a, $b) {},
2 => function($b, $c) {},
};

/* testMatchArray */
$result = match ($key) {
1 => [1,2,3],
2 => [1 => one(), 2 => two()],
};

/* testNestedMatch */
$result = match ($key) {
1 => match ($key) {
1 => 'one',
2 => 'two',
},
2 => match ($key) {
1 => 'two',
2 => 'one',
},
};

return 0;
172 changes: 172 additions & 0 deletions tests/Core/File/FindEndOfStatementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,176 @@ public function testArrowFunctionWithArrayAsArgument()
}//end testArrowFunctionWithArrayAsArgument()


/**
* Test simple match expression case.
*
* @return void
*/
public function testMatchCase()
{
$start = $this->getTargetToken('/* testMatchCase */', T_LNUMBER);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 5), $found);

$start = $this->getTargetToken('/* testMatchCase */', T_CONSTANT_ENCAPSED_STRING);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 1), $found);

}//end testMatchCase()


/**
* Test simple match expression default case.
*
* @return void
*/
public function testMatchDefault()
{
$start = $this->getTargetToken('/* testMatchDefault */', T_MATCH_DEFAULT);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 4), $found);

$start = $this->getTargetToken('/* testMatchDefault */', T_CONSTANT_ENCAPSED_STRING);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame($start, $found);

}//end testMatchDefault()


/**
* Test multiple comma-seperated match expression case values.
*
* @return void
*/
public function testMatchMultipleCase()
{
$start = $this->getTargetToken('/* testMatchMultipleCase */', T_LNUMBER);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 13), $found);

}//end testMatchMultipleCase()


/**
* Test match expression default case with trailing comma.
*
* @return void
*/
public function testMatchDefaultComma()
{
$start = $this->getTargetToken('/* testMatchDefaultComma */', T_MATCH_DEFAULT);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 5), $found);

}//end testMatchDefaultComma()


/**
* Test match expression with function call.
*
* @return void
*/
public function testMatchFunctionCall()
{
$start = $this->getTargetToken('/* testMatchFunctionCall */', T_STRING);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 12), $found);

$start += 8;
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 1), $found);

}//end testMatchFunctionCall()


/**
* Test match expression with function call in the arm.
*
* @return void
*/
public function testMatchFunctionCallArm()
{
// Check the first case.
$start = $this->getTargetToken('/* testMatchFunctionCallArm */', T_STRING);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 21), $found);

// Check the second case.
$start += 24;
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 21), $found);

}//end testMatchFunctionCallArm()


/**
* Test match expression with closure.
*
* @return void
*/
public function testMatchClosure()
{
$start = $this->getTargetToken('/* testMatchClosure */', T_LNUMBER);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 14), $found);

$start += 17;
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 14), $found);

}//end testMatchClosure()


/**
* Test match expression with array declaration.
*
* @return void
*/
public function testMatchArray()
{
$start = $this->getTargetToken('/* testMatchArray */', T_LNUMBER);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 11), $found);

$start += 14;
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 22), $found);

}//end testMatchArray()


/**
* Test nested match expressions.
*
* @return void
*/
public function testNestedMatch()
{
$start = $this->getTargetToken('/* testNestedMatch */', T_LNUMBER);
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 30), $found);

$start += 21;
$found = self::$phpcsFile->findEndOfStatement($start);

$this->assertSame(($start + 5), $found);

}//end testNestedMatch()


}//end class
Loading

0 comments on commit ef80e53

Please sign in to comment.