Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 30 additions & 42 deletions SlevomatCodingStandard/Sniffs/Complexity/CognitiveSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
Expand All @@ -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.
*
Expand Down
18 changes: 18 additions & 0 deletions tests/Sniffs/Complexity/CognitiveSniffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions tests/Sniffs/Complexity/data/cognitive/arrowFunctionIncNesting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

function arrowFunction($items)
{
if ($items !== []) { // +1
return array_map(fn ($item) => $item ? 1 : 2, $items); // +3 (nesting = 2)
}

return [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

function closureInCondition($items)
{
if (array_filter($items, function ($item) { return $item; }) !== []) { // +1
foreach ($items as $item) { // +2 (nesting = 1)
echo $item;
}
}
}
14 changes: 14 additions & 0 deletions tests/Sniffs/Complexity/data/cognitive/closureUseIncNesting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

function closureUse($items, $flag)
{
foreach ($items as $item) { // +1
$callback = function () use ($flag) { // +0 (but nesting level is now 2)
if ($flag) { // +3 (nesting = 2)
return true;
}

return false;
};
}
}
Loading