class CognitiveSniff
Cognitive Complexity
@link https://www.sonarsource.com/docs/CognitiveComplexity.pdf
Hierarchy
- class \SlevomatCodingStandard\Sniffs\Complexity\CognitiveSniff implements \PHP_CodeSniffer\Sniffs\Sniff
Expanded class hierarchy of CognitiveSniff
File
-
vendor/
slevomat/ coding-standard/ SlevomatCodingStandard/ Sniffs/ Complexity/ CognitiveSniff.php, line 41
Namespace
SlevomatCodingStandard\Sniffs\ComplexityView source
class CognitiveSniff implements Sniff {
public const CODE_COMPLEXITY = 'ComplexityTooHigh';
/**
* B1. Increments
*
* Boolean operators are handled separately due to their chain logic.
*/
private const INCREMENTS = [
T_CATCH => T_CATCH,
T_DO => T_DO,
T_ELSE => T_ELSE,
T_ELSEIF => T_ELSEIF,
T_FOR => T_FOR,
T_FOREACH => T_FOREACH,
T_IF => T_IF,
T_SWITCH => T_SWITCH,
T_WHILE => T_WHILE,
];
private const BOOLEAN_OPERATORS = [
T_BOOLEAN_AND => T_BOOLEAN_AND,
T_BOOLEAN_OR => T_BOOLEAN_OR,
];
private const OPERATOR_CHAIN_BREAKS = [
T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS,
T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS,
T_SEMICOLON => T_SEMICOLON,
T_INLINE_THEN => T_INLINE_THEN,
T_INLINE_ELSE => T_INLINE_ELSE,
];
/**
* B3. Nesting increments
*/
private const NESTING_INCREMENTS = [
T_CLOSURE => T_CLOSURE,
// increments, but does not receive
T_ELSEIF => T_ELSEIF,
// increments, but does not receive
T_ELSE => T_ELSE,
T_IF => T_IF,
T_INLINE_THEN => T_INLINE_THEN,
T_SWITCH => T_SWITCH,
T_FOR => T_FOR,
T_FOREACH => T_FOREACH,
T_WHILE => T_WHILE,
T_DO => T_DO,
T_CATCH => T_CATCH,
];
/**
* B1. Increments
*/
private const BREAKING_TOKENS = [
T_CONTINUE => T_CONTINUE,
T_GOTO => T_GOTO,
T_BREAK => T_BREAK,
];
/**
* @deprecated
* @var ?int maximum allowed complexity
*/
public $maxComplexity = null;
/** @var int complexity which will raise warning */
public $warningThreshold = 6;
/** @var int complexity which will raise error */
public $errorThreshold = 6;
/** @var int */
private $cognitiveComplexity = 0;
/** @var int|string */
private $lastBooleanOperator = 0;
/** @var File */
private $phpcsFile;
/**
* @return array<int, (int|string)>
*/
public function register() : array {
return [
T_CLOSURE,
T_FUNCTION,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $stackPtr
*/
public function process(File $phpcsFile, $stackPtr) : void {
$this->phpcsFile = $phpcsFile;
if ($phpcsFile->getCondition($stackPtr, T_FUNCTION) !== false) {
return;
}
if ($this->maxComplexity !== null) {
// maxComplexity is deprecated... if set use it
$this->warningThreshold = $this->maxComplexity + 1;
$this->errorThreshold = $this->maxComplexity + 1;
}
$cognitiveComplexity = $this->computeForFunctionFromTokensAndPosition($stackPtr);
if ($cognitiveComplexity < $this->warningThreshold) {
return;
}
$name = $phpcsFile->getDeclarationName($stackPtr);
$errorParameters = [
'Cognitive complexity for "%s" is %d but has to be less than or equal to %d.',
$stackPtr,
self::CODE_COMPLEXITY,
[
$name,
$cognitiveComplexity,
$this->warningThreshold - 1,
],
];
$cognitiveComplexity >= $this->errorThreshold ? $phpcsFile->addError(...$errorParameters) : $phpcsFile->addWarning(...$errorParameters);
}
public function computeForFunctionFromTokensAndPosition(int $position) : int {
if (FunctionHelper::isAbstract($this->phpcsFile, $position)) {
return 0;
}
$tokens = $this->phpcsFile
->getTokens();
// Detect start and end of this function definition
$functionStartPosition = $tokens[$position]['scope_opener'];
$functionEndPosition = $tokens[$position]['scope_closer'];
$this->lastBooleanOperator = 0;
$this->cognitiveComplexity = 0;
/*
Keep track of parser's level stack
We push to this stak whenever we encounter a Tokens::$scopeOpeners
*/
$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::$scopeOpeners, 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)) {
continue;
}
$this->cognitiveComplexity++;
$addNestingIncrement = isset(self::NESTING_INCREMENTS[$currentToken['code']]) && in_array($currentToken['code'], [
T_ELSEIF,
T_ELSE,
], true) === false;
if (!$addNestingIncrement) {
continue;
}
$measuredNestingLevel = count(array_filter($levelStack, static function (array $token) {
return in_array($token['code'], self::NESTING_INCREMENTS, true);
}));
if ($isNestingToken) {
$measuredNestingLevel--;
}
// B3. Nesting increment
if ($measuredNestingLevel > 0) {
$this->cognitiveComplexity += $measuredNestingLevel;
}
}
return $this->cognitiveComplexity;
}
/**
* Keep track of consecutive matching boolean operators, that don't receive increment.
*
* @param array{code:int|string} $token
*/
private function resolveBooleanOperatorChain(array $token) : void {
$code = $token['code'];
// Whenever we cross anything that interrupts possible condition we reset chain.
if ($this->lastBooleanOperator > 0 && isset(self::OPERATOR_CHAIN_BREAKS[$code])) {
$this->lastBooleanOperator = 0;
return;
}
if (isset(self::BOOLEAN_OPERATORS[$code]) === false) {
return;
}
// If we match last operator, there is no increment added for current one.
if ($this->lastBooleanOperator === $code) {
return;
}
$this->cognitiveComplexity++;
$this->lastBooleanOperator = $code;
}
/**
* @param array{code:int|string} $token
* @param array<int, array<string, array<int, int|string>|int|string>> $tokens
*/
private function isIncrementingToken(array $token, array $tokens, int $position) : bool {
$code = $token['code'];
if (isset(self::INCREMENTS[$code])) {
return true;
}
// B1. ternary operator
if ($code === T_INLINE_THEN) {
return true;
}
// B1. goto LABEL, break LABEL, continue LABEL
if (isset(self::BREAKING_TOKENS[$code])) {
$nextToken = $this->phpcsFile
->findNext(Tokens::$emptyTokens, $position + 1, null, true);
if ($nextToken === false || $tokens[$nextToken]['code'] !== T_SEMICOLON) {
return true;
}
}
return false;
}
}
Members
Title Sort descending | Deprecated | Modifiers | Object type | Summary | Overriden Title |
---|---|---|---|---|---|
CognitiveSniff::$cognitiveComplexity | private | property | @var int | ||
CognitiveSniff::$errorThreshold | public | property | @var int complexity which will raise error | ||
CognitiveSniff::$lastBooleanOperator | private | property | @var int|string | ||
CognitiveSniff::$maxComplexity | Deprecated | public | property | * | |
CognitiveSniff::$phpcsFile | private | property | @var File | ||
CognitiveSniff::$warningThreshold | public | property | @var int complexity which will raise warning | ||
CognitiveSniff::BOOLEAN_OPERATORS | private | constant | |||
CognitiveSniff::BREAKING_TOKENS | private | constant | * B1. Increments | ||
CognitiveSniff::CODE_COMPLEXITY | public | constant | |||
CognitiveSniff::computeForFunctionFromTokensAndPosition | public | function | |||
CognitiveSniff::INCREMENTS | private | constant | * B1. Increments * * Boolean operators are handled separately due to their chain logic. |
||
CognitiveSniff::isIncrementingToken | private | function | * | ||
CognitiveSniff::NESTING_INCREMENTS | private | constant | * B3. Nesting increments | ||
CognitiveSniff::OPERATOR_CHAIN_BREAKS | private | constant | |||
CognitiveSniff::process | public | function | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * |
Overrides Sniff::process | |
CognitiveSniff::register | public | function | * | Overrides Sniff::register | |
CognitiveSniff::resolveBooleanOperatorChain | private | function | * Keep track of consecutive matching boolean operators, that don't receive increment. * * |