diff --git a/SlevomatCodingStandard/Sniffs/Complexity/CognitiveSniff.php b/SlevomatCodingStandard/Sniffs/Complexity/CognitiveSniff.php index 9fe3a8d17..cd28448fb 100644 --- a/SlevomatCodingStandard/Sniffs/Complexity/CognitiveSniff.php +++ b/SlevomatCodingStandard/Sniffs/Complexity/CognitiveSniff.php @@ -7,10 +7,6 @@ use PHP_CodeSniffer\Util\Tokens; use SlevomatCodingStandard\Helpers\FunctionHelper; use SlevomatCodingStandard\Helpers\TokenHelper; -use function array_filter; -use function array_pop; -use function array_splice; -use function count; use function in_array; use const T_BOOLEAN_AND; use const T_BOOLEAN_OR; @@ -22,6 +18,7 @@ use const T_DO; use const T_ELSE; use const T_ELSEIF; +use const T_FN; use const T_FOR; use const T_FOREACH; use const T_FUNCTION; @@ -188,38 +185,9 @@ public function computeForFunctionFromTokensAndPosition(int $position): int $this->lastBooleanOperator = 0; $this->cognitiveComplexity = 0; - /* - Keep track of parser's level stack - We push to this stak whenever we encounter a Tokens::SCOPE_OPENERS token - */ - $levelStack = []; - /* - We look for changes in token[level] to know when to remove from the stack - however ['level'] only increases when there are tokens inside {} - after pushing to the stack watch for a level change - */ - $levelIncreased = false; - for ($i = $functionStartPosition + 1; $i < $functionEndPosition; $i++) { $currentToken = $tokens[$i]; - $isNestingToken = false; - if (in_array($currentToken['code'], Tokens::SCOPE_OPENERS, true)) { - $isNestingToken = true; - if ($levelIncreased === false && count($levelStack) > 0) { - // parser's level never increased - // caused by empty condition such as `if ($x) { }` - array_pop($levelStack); - } - $levelStack[] = $currentToken; - $levelIncreased = false; - } elseif (isset($tokens[$i - 1]) && $currentToken['level'] < $tokens[$i - 1]['level']) { - $diff = $tokens[$i - 1]['level'] - $currentToken['level']; - array_splice($levelStack, 0 - $diff); - } elseif (isset($tokens[$i - 1]) && $currentToken['level'] > $tokens[$i - 1]['level']) { - $levelIncreased = true; - } - $this->resolveBooleanOperatorChain($currentToken); if (!$this->isIncrementingToken($currentToken, $tokens, $i)) { @@ -233,16 +201,9 @@ public function computeForFunctionFromTokensAndPosition(int $position): int if (!$addNestingIncrement) { continue; } - $measuredNestingLevel = count( - array_filter($levelStack, static fn (array $token) => in_array($token['code'], self::NESTING_INCREMENTS, true)), - ); - if ($isNestingToken) { - $measuredNestingLevel--; - } + // B3. Nesting increment - if ($measuredNestingLevel > 0) { - $this->cognitiveComplexity += $measuredNestingLevel; - } + $this->cognitiveComplexity += $this->getNestingLevel($position, $i); } return $this->cognitiveComplexity; @@ -258,6 +219,33 @@ protected function isPartOfDo(File $phpcsFile, int $whilePointer): bool return $tokens[$pointerAfterParenthesisCloser]['code'] !== T_OPEN_CURLY_BRACKET; } + /** + * Nesting level of a token, relative to the function being measured. + * + * PHPCS records the scope conditions of every token, but arrow functions have no braces + * and are not part of them, so they are counted separately. + */ + private function getNestingLevel(int $functionPointer, int $pointer): int + { + $tokens = $this->phpcsFile->getTokens(); + + $nestingLevel = 0; + foreach ($tokens[$pointer]['conditions'] as $conditionPointer => $conditionCode) { + if ($conditionPointer > $functionPointer && isset(self::NESTING_INCREMENTS[$conditionCode])) { + $nestingLevel++; + } + } + + $searchStartPointer = $tokens[$functionPointer]['scope_opener'] + 1; + foreach (TokenHelper::findNextAll($this->phpcsFile, T_FN, $searchStartPointer, $pointer) as $arrowFunctionPointer) { + if ($tokens[$arrowFunctionPointer]['scope_opener'] < $pointer && $tokens[$arrowFunctionPointer]['scope_closer'] >= $pointer) { + $nestingLevel++; + } + } + + return $nestingLevel; + } + /** * Keep track of consecutive matching boolean operators, that don't receive increment. * diff --git a/tests/Sniffs/Complexity/CognitiveSniffTest.php b/tests/Sniffs/Complexity/CognitiveSniffTest.php index efcc9a69b..3408ab10f 100644 --- a/tests/Sniffs/Complexity/CognitiveSniffTest.php +++ b/tests/Sniffs/Complexity/CognitiveSniffTest.php @@ -21,12 +21,30 @@ public static function dataProviderFiles(): array 'binaryLogicalOperators', 19, ], + [ + __DIR__ . '/data/cognitive/arrowFunctionIncNesting.php', + 3, + 'arrowFunction', + 4, + ], [ __DIR__ . '/data/cognitive/closureIncNesting.php', 3, 'myMethod2', 2, ], + [ + __DIR__ . '/data/cognitive/closureInConditionNesting.php', + 3, + 'closureInCondition', + 3, + ], + [ + __DIR__ . '/data/cognitive/closureUseIncNesting.php', + 3, + 'closureUse', + 4, + ], [ __DIR__ . '/data/cognitive/continue.php', 3, diff --git a/tests/Sniffs/Complexity/data/cognitive/arrowFunctionIncNesting.php b/tests/Sniffs/Complexity/data/cognitive/arrowFunctionIncNesting.php new file mode 100644 index 000000000..3a2fd9f37 --- /dev/null +++ b/tests/Sniffs/Complexity/data/cognitive/arrowFunctionIncNesting.php @@ -0,0 +1,10 @@ + $item ? 1 : 2, $items); // +3 (nesting = 2) + } + + return []; +} diff --git a/tests/Sniffs/Complexity/data/cognitive/closureInConditionNesting.php b/tests/Sniffs/Complexity/data/cognitive/closureInConditionNesting.php new file mode 100644 index 000000000..5ae51a24f --- /dev/null +++ b/tests/Sniffs/Complexity/data/cognitive/closureInConditionNesting.php @@ -0,0 +1,10 @@ +