Skip to content

Commit

Permalink
Update: support class fields in the complexity rule (refs #14857) (#…
Browse files Browse the repository at this point in the history
…14957)

* Update: support class fields in the `complexity` rule (refs #14857)

* Use code path analysis

* Remove an extra empty line
  • Loading branch information
mdjermanovic committed Sep 10, 2021
1 parent 9bd3d87 commit 88a3952
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 55 deletions.
29 changes: 29 additions & 0 deletions docs/rules/complexity.md
Expand Up @@ -57,6 +57,35 @@ function b() {
}
```

Class field initializers are implicit functions. Therefore, their complexity is calculated separately for each initializer, and it doesn't contribute to the complexity of the enclosing code.

Examples of additional **incorrect** code for a maximum of 2:

```js
/*eslint complexity: ["error", 2]*/

class C {
x = a || b || c; // this initializer has complexity = 3
}
```

Examples of additional **correct** code for a maximum of 2:

```js
/*eslint complexity: ["error", 2]*/

function foo() { // this function has complexity = 1
class C {
x = a + b; // this initializer has complexity = 1
y = c || d; // this initializer has complexity = 2
z = e && f; // this initializer has complexity = 2

static p = g || h; // this initializer has complexity = 2
static q = i ? j : k; // this initializer has complexity = 2
}
}
```

## Options

Optionally, you may specify a `max` object property:
Expand Down
102 changes: 47 additions & 55 deletions lib/rules/complexity.js
Expand Up @@ -74,89 +74,81 @@ module.exports = {
// Helpers
//--------------------------------------------------------------------------

// Using a stack to store complexity (handling nested functions)
const fns = [];
// Using a stack to store complexity per code path
const complexities = [];

/**
* When parsing a new function, store it in our function stack
* @returns {void}
* @private
*/
function startFunction() {
fns.push(1);
}

/**
* Evaluate the node at the end of function
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function endFunction(node) {
const name = upperCaseFirst(astUtils.getFunctionNameWithKind(node));
const complexity = fns.pop();

if (complexity > THRESHOLD) {
context.report({
node,
messageId: "complex",
data: { name, complexity, max: THRESHOLD }
});
}
}

/**
* Increase the complexity of the function in context
* Increase the complexity of the code path in context
* @returns {void}
* @private
*/
function increaseComplexity() {
if (fns.length) {
fns[fns.length - 1]++;
}
}

/**
* Increase the switch complexity in context
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function increaseSwitchComplexity(node) {

// Avoiding `default`
if (node.test) {
increaseComplexity();
}
complexities[complexities.length - 1]++;
}

//--------------------------------------------------------------------------
// Public API
//--------------------------------------------------------------------------

return {
FunctionDeclaration: startFunction,
FunctionExpression: startFunction,
ArrowFunctionExpression: startFunction,
"FunctionDeclaration:exit": endFunction,
"FunctionExpression:exit": endFunction,
"ArrowFunctionExpression:exit": endFunction,

onCodePathStart() {

// The initial complexity is 1, representing one execution path in the CodePath
complexities.push(1);
},

// Each branching in the code adds 1 to the complexity
CatchClause: increaseComplexity,
ConditionalExpression: increaseComplexity,
LogicalExpression: increaseComplexity,
ForStatement: increaseComplexity,
ForInStatement: increaseComplexity,
ForOfStatement: increaseComplexity,
IfStatement: increaseComplexity,
SwitchCase: increaseSwitchComplexity,
WhileStatement: increaseComplexity,
DoWhileStatement: increaseComplexity,

// Avoid `default`
"SwitchCase[test]": increaseComplexity,

// Logical assignment operators have short-circuiting behavior
AssignmentExpression(node) {
if (astUtils.isLogicalAssignmentOperator(node.operator)) {
increaseComplexity();
}
},

onCodePathEnd(codePath, node) {
const complexity = complexities.pop();

/*
* This rule only evaluates complexity of functions, so "program" is excluded.
* Class field initializers are implicit functions. Therefore, they shouldn't contribute
* to the enclosing function's complexity, but their own complexity should be evaluated.
*/
if (
codePath.origin !== "function" &&
codePath.origin !== "class-field-initializer"
) {
return;
}

if (complexity > THRESHOLD) {
const name = codePath.origin === "class-field-initializer"
? "class field initializer"
: astUtils.getFunctionNameWithKind(node);

context.report({
node,
messageId: "complex",
data: {
name: upperCaseFirst(name),
complexity,
max: THRESHOLD
}
});
}
}
};

Expand Down

0 comments on commit 88a3952

Please sign in to comment.