Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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" &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This came in handy! 🎉

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very! This could have been done in other ways, but it wouldn't look as nice and simple - it would be always difficult to trace what's happening when the initializer is a 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