diff --git a/docs/rules/complexity.md b/docs/rules/complexity.md index a57b4366c1c..f6413d6c5dd 100644 --- a/docs/rules/complexity.md +++ b/docs/rules/complexity.md @@ -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: diff --git a/lib/rules/complexity.js b/lib/rules/complexity.js index e3e300386a6..ebf24e10adf 100644 --- a/lib/rules/complexity.js +++ b/lib/rules/complexity.js @@ -88,19 +88,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 + } }); } } @@ -142,6 +155,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, diff --git a/tests/lib/rules/complexity.js b/tests/lib/rules/complexity.js index 2caa8b83005..badde8a715a 100644 --- a/tests/lib/rules/complexity.js +++ b/tests/lib/rules/complexity.js @@ -89,6 +89,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 }] } ], @@ -133,6 +148,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)] } ]