From 88a39520716bdd11f8647e47c57bd8bf91bc7148 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 10 Sep 2021 12:59:17 +0200 Subject: [PATCH] Update: support class fields in the `complexity` rule (refs #14857) (#14957) * Update: support class fields in the `complexity` rule (refs #14857) * Use code path analysis * Remove an extra empty line --- docs/rules/complexity.md | 29 +++++ lib/rules/complexity.js | 102 +++++++-------- tests/lib/rules/complexity.js | 236 ++++++++++++++++++++++++++++++++++ 3 files changed, 312 insertions(+), 55 deletions(-) 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..b833aafc0f7 100644 --- a/lib/rules/complexity.js +++ b/lib/rules/complexity.js @@ -74,60 +74,16 @@ 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]++; } //-------------------------------------------------------------------------- @@ -135,13 +91,14 @@ module.exports = { //-------------------------------------------------------------------------- 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, @@ -149,14 +106,49 @@ module.exports = { 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 + } + }); + } } }; 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)] } ]