From 41b4101deebc66b13f6f2d6edb967aa297d19078 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 2 Dec 2024 10:57:51 -0500 Subject: [PATCH 1/3] Do not search for nested arrow func outside of arrow func --- VariableAnalysis/Lib/Helpers.php | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/VariableAnalysis/Lib/Helpers.php b/VariableAnalysis/Lib/Helpers.php index 73164e1..b1be1b2 100644 --- a/VariableAnalysis/Lib/Helpers.php +++ b/VariableAnalysis/Lib/Helpers.php @@ -640,6 +640,9 @@ public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $st */ public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr) { + if (! self::isTokenInsideArrowFunction($phpcsFile, $stackPtr)) { + return null; + } $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr); if (! is_int($arrowFunctionIndex)) { return null; @@ -657,6 +660,38 @@ public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPt } /** + * Move back from the stackPtr to the start of the enclosing scope until we + * find a 'fn' token that starts an arrow function, returning true if we find + * one. + * + * @param File $phpcsFile + * @param int $stackPtr + * + * @return bool + */ + private static function isTokenInsideArrowFunction(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr); + for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) { + $token = $tokens[$index]; + if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) { + return true; + } + } + return false; + } + + /** + * Move back from the stackPtr to the start of the enclosing scope until we + * find a 'fn' token that starts an arrow function, returning the index of + * that token. Returns null if we are not inside an arrow function. + * + * NOTE: This is used to find arrow function scope but is not fast because it + * needs to identify nested arrow functions also. Please use + * `isTokenInsideArrowFunction()` instead if you just want to know if we are + * inside an arrow function. + * * @param File $phpcsFile * @param int $stackPtr * From e005959a5522daeba1cafb518172171c362124aa Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 2 Dec 2024 11:29:54 -0500 Subject: [PATCH 2/3] Recursively search for containing arrow function --- VariableAnalysis/Lib/Helpers.php | 71 ++++++++++---------------------- 1 file changed, 22 insertions(+), 49 deletions(-) diff --git a/VariableAnalysis/Lib/Helpers.php b/VariableAnalysis/Lib/Helpers.php index b1be1b2..501f7ed 100644 --- a/VariableAnalysis/Lib/Helpers.php +++ b/VariableAnalysis/Lib/Helpers.php @@ -428,7 +428,8 @@ public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = $token = $tokens[$stackPtr]; $varName = isset($varName) ? $varName : self::normalizeVarName($token['content']); - $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr); + $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr); + $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex); $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex); if ($isTokenInsideArrowFunctionBody) { // Get the list of variables defined by the arrow function @@ -635,15 +636,13 @@ public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $st /** * @param File $phpcsFile * @param int $stackPtr + * @param int $enclosingScopeIndex * * @return ?int */ - public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr) + public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex) { - if (! self::isTokenInsideArrowFunction($phpcsFile, $stackPtr)) { - return null; - } - $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr); + $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex); if (! is_int($arrowFunctionIndex)) { return null; } @@ -651,72 +650,46 @@ public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPt if (! $arrowFunctionInfo) { return null; } - $arrowFunctionScopeStart = $arrowFunctionInfo['scope_opener']; - $arrowFunctionScopeEnd = $arrowFunctionInfo['scope_closer']; - if ($stackPtr > $arrowFunctionScopeStart && $stackPtr < $arrowFunctionScopeEnd) { + + // We found the closest arrow function before this token. If the token is + // within the scope of that arrow function, then return it. + if ($stackPtr > $arrowFunctionInfo['scope_opener'] && $stackPtr < $arrowFunctionInfo['scope_closer']) { return $arrowFunctionIndex; } - return null; - } - /** - * Move back from the stackPtr to the start of the enclosing scope until we - * find a 'fn' token that starts an arrow function, returning true if we find - * one. - * - * @param File $phpcsFile - * @param int $stackPtr - * - * @return bool - */ - private static function isTokenInsideArrowFunction(File $phpcsFile, $stackPtr) - { - $tokens = $phpcsFile->getTokens(); - $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr); - for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) { - $token = $tokens[$index]; - if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) { - return true; - } + // If the token is after the scope of the closest arrow function, we may + // still be inside the scope of a nested arrow function, so we need to + // search further back until we are certain there are no more arrow + // functions. + if ($stackPtr > $arrowFunctionInfo['scope_closer']) { + return self::getContainingArrowFunctionIndex($phpcsFile, $arrowFunctionIndex, $enclosingScopeIndex); } - return false; + + return null; } /** * Move back from the stackPtr to the start of the enclosing scope until we * find a 'fn' token that starts an arrow function, returning the index of - * that token. Returns null if we are not inside an arrow function. + * that token. Returns null if there are no arrow functions before stackPtr. * - * NOTE: This is used to find arrow function scope but is not fast because it - * needs to identify nested arrow functions also. Please use - * `isTokenInsideArrowFunction()` instead if you just want to know if we are - * inside an arrow function. + * Note that this does not guarantee that stackPtr is inside the arrow + * function scope we find! * * @param File $phpcsFile * @param int $stackPtr + * @param int $enclosingScopeIndex * * @return ?int */ - private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr) + private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex) { $tokens = $phpcsFile->getTokens(); - $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr); for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) { $token = $tokens[$index]; if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) { return $index; } - // If we find a token that would close an arrow function scope before we - // find a token that would open an arrow function scope, then we've found - // a nested arrow function and we should ignore it, move back before THAT - // arrow function's scope, and continue to search. - $arrowFunctionStartIndex = $phpcsFile->findPrevious([T_FN], $index, $enclosingScopeIndex); - if (is_int($arrowFunctionStartIndex)) { - $openClose = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionStartIndex); - if ($openClose && $openClose['scope_closer'] === $index) { - $index = $openClose['scope_opener']; - } - } } return null; } From 49ef96a6f3db0015bf6299fc79886be0322cfcb8 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 2 Dec 2024 11:34:11 -0500 Subject: [PATCH 3/3] Guard for no enclosing scope --- VariableAnalysis/Lib/Helpers.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/VariableAnalysis/Lib/Helpers.php b/VariableAnalysis/Lib/Helpers.php index 501f7ed..9b779aa 100644 --- a/VariableAnalysis/Lib/Helpers.php +++ b/VariableAnalysis/Lib/Helpers.php @@ -429,17 +429,19 @@ public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = $varName = isset($varName) ? $varName : self::normalizeVarName($token['content']); $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr); - $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex); - $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex); - if ($isTokenInsideArrowFunctionBody) { - // Get the list of variables defined by the arrow function - // If this matches any of them, the scope is the arrow function, - // otherwise, it uses the enclosing scope. - if ($arrowFunctionIndex) { - $variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex); - self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames); - if (in_array($varName, $variableNames, true)) { - return $arrowFunctionIndex; + if ($enclosingScopeIndex) { + $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex); + $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex); + if ($isTokenInsideArrowFunctionBody) { + // Get the list of variables defined by the arrow function + // If this matches any of them, the scope is the arrow function, + // otherwise, it uses the enclosing scope. + if ($arrowFunctionIndex) { + $variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex); + self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames); + if (in_array($varName, $variableNames, true)) { + return $arrowFunctionIndex; + } } } }