Skip to content

Commit bdf1e50

Browse files
authored
Fix scope closer detection in arrow function detection (#298)
* Add test for ternary in arrow function * Do not treat closing paren as a scope closer for arrow func * Add more tests * Add detection for function parameters in arrow function detection * Stop using null coalesce * Fix formatting * Add test for array in args * Support scope closer from phpcs if we could not find one * Add test for simple arrow function in ternary * Change parsing for end token to track pairs * Remove function call inside for loop condition * Add test for arrow function return type * Support arrow functions with return types * Remove accidental artifact
1 parent 4cca2aa commit bdf1e50

File tree

3 files changed

+123
-10
lines changed

3 files changed

+123
-10
lines changed

Tests/VariableAnalysisSniff/ArrowFunctionTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public function testArrowFunctions()
3030
67,
3131
71,
3232
87,
33+
102,
34+
112,
3335
];
3436
$this->assertSame($expectedWarnings, $lines);
3537
}
@@ -60,6 +62,8 @@ public function testArrowFunctionsWithoutUnusedBeforeUsed()
6062
67,
6163
71,
6264
87,
65+
102,
66+
112,
6367
];
6468
$this->assertSame($expectedWarnings, $lines);
6569
}

Tests/VariableAnalysisSniff/fixtures/ArrowFunctionFixture.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,38 @@ function arrowFunctionAsExpressionInArgumentWithArray() {
9292
$type = do_something(fn($array, $needle) => $array[2] === $needle);
9393
echo $type;
9494
}
95+
96+
function arrowFunctionAsExpressionInArgumentWithInnerCall() {
97+
$type = do_something(fn(Thing $func) => $func->call() ? $func : null);
98+
echo $type;
99+
}
100+
101+
function arrowFunctionAsExpressionInArgumentWithInnerCallAndUndefinedAfterTernary() {
102+
$type = do_something(fn(Thing $func) => $func->call() ? $func : $foo); // undefined variable $foo
103+
echo $type;
104+
}
105+
106+
function arrowFunctionAsExpressionInArgumentWithInnerCallAndArgs() {
107+
$type = do_something(fn(Thing $func) => $func->call(1,2) ? $func : null);
108+
echo $type;
109+
}
110+
111+
function arrowFunctionAsExpressionWithUndefinedAfterComma() {
112+
$type = do_something(fn(Thing $func, $bar) => $func->call(1,2) ? $bar : null, $bar); // undefined variable $bar
113+
echo $type;
114+
}
115+
116+
function arrowFunctionAsExpressionInArgumentWithInnerArrayAndArgs() {
117+
$type = do_something(fn(Thing $func) => $func->call([1,2]) ? $func : null);
118+
echo $type;
119+
}
120+
121+
function arrowFunctionAsExpressionInArgumentWithSimpleTernary() {
122+
$type = do_something(fn(Thing $func) => $func ? $func : null);
123+
echo $type;
124+
}
125+
126+
function arrowFunctionWithReturnType() {
127+
$type = do_something(fn(string $func): string => $func ? $func : '');
128+
echo $type;
129+
}

VariableAnalysis/Lib/Helpers.php

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -638,26 +638,100 @@ public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
638638
}
639639
// Find the associated close parenthesis
640640
$closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
641-
// Make sure the next token is a fat arrow
641+
// Make sure the next token is a fat arrow or a return type
642642
$fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
643643
if (! is_int($fatArrowIndex)) {
644644
return null;
645645
}
646-
if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
646+
if (
647+
$tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW &&
648+
$tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW' &&
649+
$tokens[$fatArrowIndex]['code'] !== T_COLON
650+
) {
647651
return null;
648652
}
653+
649654
// Find the scope closer
650-
$endScopeTokens = [
651-
T_COMMA,
652-
T_SEMICOLON,
653-
T_CLOSE_PARENTHESIS,
654-
T_CLOSE_CURLY_BRACKET,
655-
T_CLOSE_SHORT_ARRAY,
656-
];
657-
$scopeCloserIndex = $phpcsFile->findNext($endScopeTokens, $fatArrowIndex + 1);
655+
$scopeCloserIndex = null;
656+
$foundCurlyPairs = 0;
657+
$foundArrayPairs = 0;
658+
$foundParenPairs = 0;
659+
$arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1;
660+
$lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
661+
for ($index = $arrowBodyStart; $index < $lastToken; $index++) {
662+
$token = $tokens[$index];
663+
if (empty($token['code'])) {
664+
$scopeCloserIndex = $index;
665+
break;
666+
}
667+
668+
// A line break is always a closer.
669+
if ($token['line'] !== $tokens[$stackPtr]['line']) {
670+
$scopeCloserIndex = $index;
671+
break;
672+
}
673+
$code = $token['code'];
674+
675+
// A semicolon is always a closer.
676+
if ($code === T_SEMICOLON) {
677+
$scopeCloserIndex = $index;
678+
break;
679+
}
680+
681+
// Track pair opening tokens.
682+
if ($code === T_OPEN_CURLY_BRACKET) {
683+
$foundCurlyPairs += 1;
684+
continue;
685+
}
686+
if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
687+
$foundArrayPairs += 1;
688+
continue;
689+
}
690+
if ($code === T_OPEN_PARENTHESIS) {
691+
$foundParenPairs += 1;
692+
continue;
693+
}
694+
695+
// A pair closing is only an arrow func closer if there was no matching opening token.
696+
if ($code === T_CLOSE_CURLY_BRACKET) {
697+
if ($foundCurlyPairs === 0) {
698+
$scopeCloserIndex = $index;
699+
break;
700+
}
701+
$foundCurlyPairs -= 1;
702+
continue;
703+
}
704+
if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
705+
if ($foundArrayPairs === 0) {
706+
$scopeCloserIndex = $index;
707+
break;
708+
}
709+
$foundArrayPairs -= 1;
710+
continue;
711+
}
712+
if ($code === T_CLOSE_PARENTHESIS) {
713+
if ($foundParenPairs === 0) {
714+
$scopeCloserIndex = $index;
715+
break;
716+
}
717+
$foundParenPairs -= 1;
718+
continue;
719+
}
720+
721+
// A comma is a closer only if we are not inside an opening token.
722+
if ($code === T_COMMA) {
723+
if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) {
724+
$scopeCloserIndex = $index;
725+
break;
726+
}
727+
continue;
728+
}
729+
}
730+
658731
if (! is_int($scopeCloserIndex)) {
659732
return null;
660733
}
734+
661735
return [
662736
'scope_opener' => $stackPtr,
663737
'scope_closer' => $scopeCloserIndex,

0 commit comments

Comments
 (0)