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
  • Loading branch information
mdjermanovic committed Aug 21, 2021
1 parent 3d7d5fb commit b851e2b
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 4 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
35 changes: 31 additions & 4 deletions lib/rules/complexity.js
Expand Up @@ -89,19 +89,32 @@ module.exports = {

/**
* Evaluate the node at the end of function
* @param {ASTNode} node node to evaluate
* @param {ASTNode} node node to evaluate. If it is a `PropertyDefinition` node, its initializer is being evaluated.
* @returns {void}
* @private
*/
function endFunction(node) {
const name = upperCaseFirst(astUtils.getFunctionNameWithKind(node));
const complexity = fns.pop();

if (complexity > THRESHOLD) {
let evaluatedNode, name;

if (node.type === "PropertyDefinition") {
evaluatedNode = node.value;
name = "class field initializer";
} else {
evaluatedNode = node;
name = astUtils.getFunctionNameWithKind(node);
}

context.report({
node,
node: evaluatedNode,
messageId: "complex",
data: { name, complexity, max: THRESHOLD }
data: {
name: upperCaseFirst(name),
complexity,
max: THRESHOLD
}
});
}
}
Expand Down Expand Up @@ -143,6 +156,20 @@ module.exports = {
"FunctionExpression:exit": endFunction,
"ArrowFunctionExpression:exit": endFunction,

/*
* Class field initializers are implicit functions. Therefore, they shouldn't contribute
* to the enclosing function's complexity, but their own complexity should be evaluated.
* We're using `*.key:exit` here in order to make sure that `startFunction()` is called
* before entering the `.value` node, and thus certainly before other listeners
* (e.g., if the initializer is `a || b`, due to a higher selector specificity
* `PropertyDefinition > *.value` would be called after `LogicalExpression`).
* We're passing the `PropertyDefinition` node instead of `PropertyDefinition.value` node
* to `endFunction(node)` in order to disambiguate between evaluating implicit initializer
* functions and "regular" functions, which may be the `.value` itself, e.g., `x = () => {};`.
*/
"PropertyDefinition[value] > *.key:exit": startFunction,
"PropertyDefinition[value]:exit": endFunction,

CatchClause: increaseComplexity,
ConditionalExpression: increaseComplexity,
LogicalExpression: increaseComplexity,
Expand Down
236 changes: 236 additions & 0 deletions tests/lib/rules/complexity.js
Expand Up @@ -85,6 +85,21 @@ ruleTester.run("complexity", rule, {
{ code: "if (foo) { bar(); }", options: [3] },
{ code: "var a = (x) => {do {'foo';} while (true)}", options: [2], parserOptions: { ecmaVersion: 6 } },

// class fields
{ code: "function foo() { class C { x = a || b; y = c || d; } }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "function foo() { class C { static x = a || b; static y = c || d; } }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "function foo() { class C { x = a || b; y = c || d; } e || f; }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "function foo() { a || b; class C { x = c || d; y = e || f; } }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "function foo() { class C { [x || y] = a || b; } }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "class C { x = a || b; y() { c || d; } z = e || f; }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "class C { x() { a || b; } y = c || d; z() { e || f; } }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "class C { x = (() => { a || b }) || (() => { c || d }) }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "class C { x = () => { a || b }; y = () => { c || d } }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "class C { x = a || (() => { b || c }); }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "class C { x = class { y = a || b; z = c || d; }; }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "class C { x = a || class { y = b || c; z = d || e; }; }", options: [2], parserOptions: { ecmaVersion: 2022 } },
{ code: "class C { x; y = a; static z; static q = b; }", options: [1], parserOptions: { ecmaVersion: 2022 } },

// object property options
{ code: "function b(x) {}", options: [{ max: 1 }] }
],
Expand Down Expand Up @@ -129,6 +144,227 @@ ruleTester.run("complexity", rule, {
errors: [makeError("Function 'test'", 21, 20)]
},

// class fields
{
code: "function foo () { a || b; class C { x; } c || d; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Function 'foo'", 3, 2)]
},
{
code: "function foo () { a || b; class C { x = c; } d || e; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Function 'foo'", 3, 2)]
},
{
code: "function foo () { a || b; class C { [x || y]; } }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Function 'foo'", 3, 2)]
},
{
code: "function foo () { a || b; class C { [x || y] = c; } }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Function 'foo'", 3, 2)]
},
{
code: "function foo () { class C { [x || y]; } a || b; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Function 'foo'", 3, 2)]
},
{
code: "function foo () { class C { [x || y] = a; } b || c; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Function 'foo'", 3, 2)]
},
{
code: "function foo () { class C { [x || y]; [z || q]; } }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Function 'foo'", 3, 2)]
},
{
code: "function foo () { class C { [x || y] = a; [z || q] = b; } }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Function 'foo'", 3, 2)]
},
{
code: "function foo () { a || b; class C { x = c || d; } e || f; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Function 'foo'", 3, 2)]
},
{
code: "class C { x(){ a || b; } y = c || d || e; z() { f || g; } }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Class field initializer", 3, 2)]
},
{
code: "class C { x = a || b; y() { c || d || e; } z = f || g; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Method 'y'", 3, 2)]
},
{
code: "class C { x; y() { c || d || e; } z; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Method 'y'", 3, 2)]
},
{
code: "class C { x = a || b; }",
options: [1],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Class field initializer", 2, 1)]
},
{
code: "(class { x = a || b; })",
options: [1],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Class field initializer", 2, 1)]
},
{
code: "class C { static x = a || b; }",
options: [1],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Class field initializer", 2, 1)]
},
{
code: "(class { x = a ? b : c; })",
options: [1],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Class field initializer", 2, 1)]
},
{
code: "class C { x = a || b || c; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Class field initializer", 3, 2)]
},
{
code: "class C { x = a || b; y = b || c || d; z = e || f; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [{
...makeError("Class field initializer", 3, 2),
line: 1,
column: 27,
endLine: 1,
endColumn: 38
}]
},
{
code: "class C { x = a || b || c; y = d || e; z = f || g || h; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [
{
...makeError("Class field initializer", 3, 2),
line: 1,
column: 15,
endLine: 1,
endColumn: 26
},
{
...makeError("Class field initializer", 3, 2),
line: 1,
column: 44,
endLine: 1,
endColumn: 55
}
]
},
{
code: "class C { x = () => a || b || c; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Method 'x'", 3, 2)]
},
{
code: "class C { x = (() => a || b || c) || d; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Arrow function", 3, 2)]
},
{
code: "class C { x = () => a || b || c; y = d || e; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [makeError("Method 'x'", 3, 2)]
},
{
code: "class C { x = () => a || b || c; y = d || e || f; }",
options: [2],
parserOptions: { ecmaVersion: 2022 },
errors: [
makeError("Method 'x'", 3, 2),
{
...makeError("Class field initializer", 3, 2),
line: 1,
column: 38,
endLine: 1,
endColumn: 49
}
]
},
{
code: "class C { x = function () { a || b }; y = function () { c || d }; }",
options: [1],
parserOptions: { ecmaVersion: 2022 },
errors: [
makeError("Method 'x'", 2, 1),
makeError("Method 'y'", 2, 1)
]
},
{
code: "class C { x = class { [y || z]; }; }",
options: [1],
parserOptions: { ecmaVersion: 2022 },
errors: [
{
...makeError("Class field initializer", 2, 1),
line: 1,
column: 15,
endLine: 1,
endColumn: 34
}
]
},
{
code: "class C { x = class { [y || z] = a; }; }",
options: [1],
parserOptions: { ecmaVersion: 2022 },
errors: [
{
...makeError("Class field initializer", 2, 1),
line: 1,
column: 15,
endLine: 1,
endColumn: 38
}
]
},
{
code: "class C { x = class { y = a || b; }; }",
options: [1],
parserOptions: { ecmaVersion: 2022 },
errors: [
{
...makeError("Class field initializer", 2, 1),
line: 1,
column: 27,
endLine: 1,
endColumn: 33
}
]
},

// object property options
{ code: "function a(x) {}", options: [{ max: 0 }], errors: [makeError("Function 'a'", 1, 0)] }
]
Expand Down

0 comments on commit b851e2b

Please sign in to comment.