Skip to content

Commit f681f8f

Browse files
committed
File::findStartOfStatement(): bug fix - don't give tokens within nested parentheses the "match" treatment
The `File::findStartOfStatement()` method has special handling for `match` expressions to find the start of a statement, but that special handling would also kick in when the `$start` token is within a parenthesized expression within the `match`, while, in that case, the special handling is not needed and ends up returning an incorrect "start" pointer, in most cases, even a "start pointer" which is _after_ the token for which the start of statement is requested, which should never be possible. Fixed now by changing the special handling for `match` expressions to only kick in when the `$start` pointer and the `match` pointer are at the same parentheses nesting level. Includes unit tests. Of the tests, the first array item/parameter was not affected by this bug, but subsequent array items/parameters were all affected by this bug.
1 parent 7c6bd67 commit f681f8f

File tree

3 files changed

+191
-3
lines changed

3 files changed

+191
-3
lines changed

src/Files/File.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2438,9 +2438,16 @@ public function findStartOfStatement($start, $ignore=null)
24382438
if (empty($this->tokens[$start]['conditions']) === false) {
24392439
$conditions = $this->tokens[$start]['conditions'];
24402440
$lastConditionOwner = end($conditions);
2441-
2442-
if ($lastConditionOwner === T_MATCH) {
2443-
$matchExpression = key($conditions);
2441+
$matchExpression = key($conditions);
2442+
2443+
if ($lastConditionOwner === T_MATCH
2444+
// Check if the $start token is at the same parentheses nesting level as the match token.
2445+
&& ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === true
2446+
&& empty($this->tokens[$start]['nested_parenthesis']) === true)
2447+
|| ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === false
2448+
&& empty($this->tokens[$start]['nested_parenthesis']) === false)
2449+
&& $this->tokens[$matchExpression]['nested_parenthesis'] === $this->tokens[$start]['nested_parenthesis']))
2450+
) {
24442451
for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
24452452
if ($prevMatch !== $start
24462453
&& ($this->tokens[$prevMatch]['code'] === T_MATCH_ARROW

tests/Core/File/FindStartOfStatementTest.inc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,23 @@ match ($var) {
172172
},
173173
default => false
174174
};
175+
176+
match ($var) {
177+
/* test437NestedLongArrayWithinMatch */
178+
'a' => array( 1, 2.5, $var),
179+
/* test437NestedFunctionCallWithinMatch */
180+
'b' => functionCall( 11, $var, 50.50),
181+
/* test437NestedArrowFunctionWithinMatch */
182+
'c' => fn($p1, /* test437FnSecondParamWithinMatch */ $p2) => $p1 + $p2,
183+
default => false
184+
};
185+
186+
callMe($paramA, match ($var) {
187+
/* test437NestedLongArrayWithinNestedMatch */
188+
'a' => array( 1, 2.5, $var),
189+
/* test437NestedFunctionCallWithinNestedMatch */
190+
'b' => functionCall( 11, $var, 50.50),
191+
/* test437NestedArrowFunctionWithinNestedMatch */
192+
'c' => fn($p1, /* test437FnSecondParamWithinNestedMatch */ $p2) => $p1 + $p2,
193+
default => false
194+
});

tests/Core/File/FindStartOfStatementTest.php

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,4 +719,165 @@ public static function dataFindStartInsideClosedScopeNestedWithinMatch()
719719
}//end dataFindStartInsideClosedScopeNestedWithinMatch()
720720

721721

722+
/**
723+
* Test finding the start of a statement for a token within a set of parentheses within a match expressions.
724+
*
725+
* @param string $testMarker The comment which prefaces the target token in the test file.
726+
* @param int|string $target The token to search for after the test marker.
727+
* @param int|string $expectedTarget Token code of the expected start of statement stack pointer.
728+
*
729+
* @link https://github.com/PHPCSStandards/PHP_CodeSniffer/issues/437
730+
*
731+
* @dataProvider dataFindStartInsideParenthesesNestedWithinMatch
732+
*
733+
* @return void
734+
*/
735+
public function testFindStartInsideParenthesesNestedWithinMatch($testMarker, $target, $expectedTarget)
736+
{
737+
$testToken = $this->getTargetToken($testMarker, $target);
738+
$expected = $this->getTargetToken($testMarker, $expectedTarget);
739+
740+
$found = self::$phpcsFile->findStartOfStatement($testToken);
741+
742+
$this->assertSame($expected, $found);
743+
744+
}//end testFindStartInsideParenthesesNestedWithinMatch()
745+
746+
747+
/**
748+
* Data provider.
749+
*
750+
* @return array<string, array<string, int|string>>
751+
*/
752+
public static function dataFindStartInsideParenthesesNestedWithinMatch()
753+
{
754+
return [
755+
'Array item itself should be start for first array item' => [
756+
'testMarker' => '/* test437NestedLongArrayWithinMatch */',
757+
'targets' => T_LNUMBER,
758+
'expectedTarget' => T_LNUMBER,
759+
],
760+
'Array item itself should be start for second array item' => [
761+
'testMarker' => '/* test437NestedLongArrayWithinMatch */',
762+
'targets' => T_DNUMBER,
763+
'expectedTarget' => T_DNUMBER,
764+
],
765+
'Array item itself should be start for third array item' => [
766+
'testMarker' => '/* test437NestedLongArrayWithinMatch */',
767+
'targets' => T_VARIABLE,
768+
'expectedTarget' => T_VARIABLE,
769+
],
770+
771+
'Parameter itself should be start for first param passed to function call' => [
772+
'testMarker' => '/* test437NestedFunctionCallWithinMatch */',
773+
'targets' => T_LNUMBER,
774+
'expectedTarget' => T_LNUMBER,
775+
],
776+
'Parameter itself should be start for second param passed to function call' => [
777+
'testMarker' => '/* test437NestedFunctionCallWithinMatch */',
778+
'targets' => T_VARIABLE,
779+
'expectedTarget' => T_VARIABLE,
780+
],
781+
'Parameter itself should be start for third param passed to function call' => [
782+
'testMarker' => '/* test437NestedFunctionCallWithinMatch */',
783+
'targets' => T_DNUMBER,
784+
'expectedTarget' => T_DNUMBER,
785+
],
786+
787+
'Parameter itself should be start for first param declared in arrow function' => [
788+
'testMarker' => '/* test437NestedArrowFunctionWithinMatch */',
789+
'targets' => T_VARIABLE,
790+
'expectedTarget' => T_VARIABLE,
791+
],
792+
'Parameter itself should be start for second param declared in arrow function' => [
793+
'testMarker' => '/* test437FnSecondParamWithinMatch */',
794+
'targets' => T_VARIABLE,
795+
'expectedTarget' => T_VARIABLE,
796+
],
797+
];
798+
799+
}//end dataFindStartInsideParenthesesNestedWithinMatch()
800+
801+
802+
/**
803+
* Test finding the start of a statement for a token within a set of parentheses within a match expressions,
804+
* which itself is nested within parentheses.
805+
*
806+
* @param string $testMarker The comment which prefaces the target token in the test file.
807+
* @param int|string $target The token to search for after the test marker.
808+
* @param int|string $expectedTarget Token code of the expected start of statement stack pointer.
809+
*
810+
* @link https://github.com/PHPCSStandards/PHP_CodeSniffer/issues/437
811+
*
812+
* @dataProvider dataFindStartInsideParenthesesNestedWithinNestedMatch
813+
*
814+
* @return void
815+
*/
816+
public function testFindStartInsideParenthesesNestedWithinNestedMatch($testMarker, $target, $expectedTarget)
817+
{
818+
$testToken = $this->getTargetToken($testMarker, $target);
819+
$expected = $this->getTargetToken($testMarker, $expectedTarget);
820+
821+
$found = self::$phpcsFile->findStartOfStatement($testToken);
822+
823+
$this->assertSame($expected, $found);
824+
825+
}//end testFindStartInsideParenthesesNestedWithinNestedMatch()
826+
827+
828+
/**
829+
* Data provider.
830+
*
831+
* @return array<string, array<string, int|string>>
832+
*/
833+
public static function dataFindStartInsideParenthesesNestedWithinNestedMatch()
834+
{
835+
return [
836+
'Array item itself should be start for first array item' => [
837+
'testMarker' => '/* test437NestedLongArrayWithinNestedMatch */',
838+
'targets' => T_LNUMBER,
839+
'expectedTarget' => T_LNUMBER,
840+
],
841+
'Array item itself should be start for second array item' => [
842+
'testMarker' => '/* test437NestedLongArrayWithinNestedMatch */',
843+
'targets' => T_DNUMBER,
844+
'expectedTarget' => T_DNUMBER,
845+
],
846+
'Array item itself should be start for third array item' => [
847+
'testMarker' => '/* test437NestedLongArrayWithinNestedMatch */',
848+
'targets' => T_VARIABLE,
849+
'expectedTarget' => T_VARIABLE,
850+
],
851+
852+
'Parameter itself should be start for first param passed to function call' => [
853+
'testMarker' => '/* test437NestedFunctionCallWithinNestedMatch */',
854+
'targets' => T_LNUMBER,
855+
'expectedTarget' => T_LNUMBER,
856+
],
857+
'Parameter itself should be start for second param passed to function call' => [
858+
'testMarker' => '/* test437NestedFunctionCallWithinNestedMatch */',
859+
'targets' => T_VARIABLE,
860+
'expectedTarget' => T_VARIABLE,
861+
],
862+
'Parameter itself should be start for third param passed to function call' => [
863+
'testMarker' => '/* test437NestedFunctionCallWithinNestedMatch */',
864+
'targets' => T_DNUMBER,
865+
'expectedTarget' => T_DNUMBER,
866+
],
867+
868+
'Parameter itself should be start for first param declared in arrow function' => [
869+
'testMarker' => '/* test437NestedArrowFunctionWithinNestedMatch */',
870+
'targets' => T_VARIABLE,
871+
'expectedTarget' => T_VARIABLE,
872+
],
873+
'Parameter itself should be start for second param declared in arrow function' => [
874+
'testMarker' => '/* test437FnSecondParamWithinNestedMatch */',
875+
'targets' => T_VARIABLE,
876+
'expectedTarget' => T_VARIABLE,
877+
],
878+
];
879+
880+
}//end dataFindStartInsideParenthesesNestedWithinNestedMatch()
881+
882+
722883
}//end class

0 commit comments

Comments
 (0)