Skip to content

Commit

Permalink
feat: Implement modified cyclomatic complexity (#18896)
Browse files Browse the repository at this point in the history
* feat: Implement modified cyclomatic complexity

Implements an option to use the modified cyclomatic complexity, where a switch statement
only increases the value by 1 regardless of how many case statements it contains.

Fixes #18885

* Switch to the `variant` config option

* PR feedback
  • Loading branch information
dpashk-figma authored Sep 25, 2024
1 parent c1a2725 commit 2d17453
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 11 deletions.
57 changes: 52 additions & 5 deletions docs/src/rules/complexity.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,20 +148,67 @@ function foo() { // this function has complexity = 1
## Options
Optionally, you may specify a `max` object property:
This rule has a number or object option:
```json
"complexity": ["error", 2]
```
* `"max"` (default: `20`) enforces a maximum complexity
* `"variant": "classic" | "modified"` (default: `"classic"`) cyclomatic complexity variant to use
is equivalent to
### max
Customize the threshold with the `max` property.
```json
"complexity": ["error", { "max": 2 }]
```
**Deprecated:** the object property `maximum` is deprecated. Please use the property `max` instead.
Or use the shorthand syntax:
```json
"complexity": ["error", 2]
```
### variant
Cyclomatic complexity variant to use:
* `"classic"` (default) - Classic McCabe cyclomatic complexity
* `"modified"` - Modified cyclomatic complexity
_Modified cyclomatic complexity_ is the same as the classic cyclomatic complexity, but each `switch` statement only increases the complexity value by `1`, regardless of how many `case` statements it contains.

Examples of **correct** code for this rule with the `{ "max": 3, "variant": "modified" }` option:
::: correct
```js
/*eslint complexity: ["error", {"max": 3, "variant": "modified"}]*/

function a(x) { // initial modified complexity is 1
switch (x) { // switch statement increases modified complexity by 1
case 1:
1;
break;
case 2:
2;
break;
case 3:
if (x === 'foo') { // if block increases modified complexity by 1
3;
}
break;
default:
4;
}
}
```
:::
The classic cyclomatic complexity of the above function is `5`, but the modified cyclomatic complexity is only `3`.
## When Not To Use It
If you can't determine an appropriate complexity limit for your code, then it's best to disable this rule.
22 changes: 16 additions & 6 deletions lib/rules/complexity.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ module.exports = {
max: {
type: "integer",
minimum: 0
},
variant: {
enum: ["classic", "modified"]
}
},
additionalProperties: false
Expand All @@ -61,16 +64,22 @@ module.exports = {
create(context) {
const option = context.options[0];
let THRESHOLD = 20;
let VARIANT = "classic";

if (typeof option === "object") {
if (Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max")) {
THRESHOLD = option.maximum || option.max;
}

if (
typeof option === "object" &&
(Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max"))
) {
THRESHOLD = option.maximum || option.max;
if (Object.hasOwn(option, "variant")) {
VARIANT = option.variant;
}
} else if (typeof option === "number") {
THRESHOLD = option;
}

const IS_MODIFIED_COMPLEXITY = VARIANT === "modified";

//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
Expand Down Expand Up @@ -112,7 +121,8 @@ module.exports = {
AssignmentPattern: increaseComplexity,

// Avoid `default`
"SwitchCase[test]": increaseComplexity,
"SwitchCase[test]": () => IS_MODIFIED_COMPLEXITY || increaseComplexity(),
SwitchStatement: () => IS_MODIFIED_COMPLEXITY && increaseComplexity(),

// Logical assignment operators have short-circuiting behavior
AssignmentExpression(node) {
Expand Down
8 changes: 8 additions & 0 deletions tests/lib/rules/complexity.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ ruleTester.run("complexity", rule, {
{ code: "if (foo) { bar(); }", options: [3] },
{ code: "var a = (x) => {do {'foo';} while (true)}", options: [2], languageOptions: { ecmaVersion: 6 } },

// modified complexity
{ code: "function a(x) {switch(x){case 1: 1; break; case 2: 2; break; default: 3;}}", options: [{ max: 2, variant: "modified" }] },
{ code: "function a(x) {switch(x){case 1: 1; break; case 2: 2; break; default: if(x == 'foo') {5;};}}", options: [{ max: 3, variant: "modified" }] },

// class fields
{ code: "function foo() { class C { x = a || b; y = c || d; } }", options: [2], languageOptions: { ecmaVersion: 2022 } },
{ code: "function foo() { class C { static x = a || b; static y = c || d; } }", options: [2], languageOptions: { ecmaVersion: 2022 } },
Expand Down Expand Up @@ -185,6 +189,10 @@ ruleTester.run("complexity", rule, {
errors: [makeError("Function 'test'", 21, 20)]
},

// modified complexity
{ code: "function a(x) {switch(x){case 1: 1; break; case 2: 2; break; default: 3;}}", options: [{ max: 1, variant: "modified" }], errors: [makeError("Function 'a'", 2, 1)] },
{ code: "function a(x) {switch(x){case 1: 1; break; case 2: 2; break; default: if(x == 'foo') {5;};}}", options: [{ max: 2, variant: "modified" }], errors: [makeError("Function 'a'", 3, 2)] },

// class fields
{
code: "function foo () { a || b; class C { x; } c || d; }",
Expand Down

0 comments on commit 2d17453

Please sign in to comment.