Skip to content

Commit 2d17453

Browse files
authored
feat: Implement modified cyclomatic complexity (#18896)
* 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
1 parent c1a2725 commit 2d17453

3 files changed

Lines changed: 76 additions & 11 deletions

File tree

docs/src/rules/complexity.md

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,20 +148,67 @@ function foo() { // this function has complexity = 1
148148
149149
## Options
150150
151-
Optionally, you may specify a `max` object property:
151+
This rule has a number or object option:
152152
153-
```json
154-
"complexity": ["error", 2]
155-
```
153+
* `"max"` (default: `20`) enforces a maximum complexity
154+
155+
* `"variant": "classic" | "modified"` (default: `"classic"`) cyclomatic complexity variant to use
156156
157-
is equivalent to
157+
### max
158+
159+
Customize the threshold with the `max` property.
158160
159161
```json
160162
"complexity": ["error", { "max": 2 }]
161163
```
162164
163165
**Deprecated:** the object property `maximum` is deprecated. Please use the property `max` instead.
164166
167+
Or use the shorthand syntax:
168+
169+
```json
170+
"complexity": ["error", 2]
171+
```
172+
173+
### variant
174+
175+
Cyclomatic complexity variant to use:
176+
177+
* `"classic"` (default) - Classic McCabe cyclomatic complexity
178+
* `"modified"` - Modified cyclomatic complexity
179+
180+
_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.
181+
182+
Examples of **correct** code for this rule with the `{ "max": 3, "variant": "modified" }` option:
183+
184+
::: correct
185+
186+
```js
187+
/*eslint complexity: ["error", {"max": 3, "variant": "modified"}]*/
188+
189+
function a(x) { // initial modified complexity is 1
190+
switch (x) { // switch statement increases modified complexity by 1
191+
case 1:
192+
1;
193+
break;
194+
case 2:
195+
2;
196+
break;
197+
case 3:
198+
if (x === 'foo') { // if block increases modified complexity by 1
199+
3;
200+
}
201+
break;
202+
default:
203+
4;
204+
}
205+
}
206+
```
207+
208+
:::
209+
210+
The classic cyclomatic complexity of the above function is `5`, but the modified cyclomatic complexity is only `3`.
211+
165212
## When Not To Use It
166213
167214
If you can't determine an appropriate complexity limit for your code, then it's best to disable this rule.

lib/rules/complexity.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ module.exports = {
4545
max: {
4646
type: "integer",
4747
minimum: 0
48+
},
49+
variant: {
50+
enum: ["classic", "modified"]
4851
}
4952
},
5053
additionalProperties: false
@@ -61,16 +64,22 @@ module.exports = {
6164
create(context) {
6265
const option = context.options[0];
6366
let THRESHOLD = 20;
67+
let VARIANT = "classic";
68+
69+
if (typeof option === "object") {
70+
if (Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max")) {
71+
THRESHOLD = option.maximum || option.max;
72+
}
6473

65-
if (
66-
typeof option === "object" &&
67-
(Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max"))
68-
) {
69-
THRESHOLD = option.maximum || option.max;
74+
if (Object.hasOwn(option, "variant")) {
75+
VARIANT = option.variant;
76+
}
7077
} else if (typeof option === "number") {
7178
THRESHOLD = option;
7279
}
7380

81+
const IS_MODIFIED_COMPLEXITY = VARIANT === "modified";
82+
7483
//--------------------------------------------------------------------------
7584
// Helpers
7685
//--------------------------------------------------------------------------
@@ -112,7 +121,8 @@ module.exports = {
112121
AssignmentPattern: increaseComplexity,
113122

114123
// Avoid `default`
115-
"SwitchCase[test]": increaseComplexity,
124+
"SwitchCase[test]": () => IS_MODIFIED_COMPLEXITY || increaseComplexity(),
125+
SwitchStatement: () => IS_MODIFIED_COMPLEXITY && increaseComplexity(),
116126

117127
// Logical assignment operators have short-circuiting behavior
118128
AssignmentExpression(node) {

tests/lib/rules/complexity.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ ruleTester.run("complexity", rule, {
8989
{ code: "if (foo) { bar(); }", options: [3] },
9090
{ code: "var a = (x) => {do {'foo';} while (true)}", options: [2], languageOptions: { ecmaVersion: 6 } },
9191

92+
// modified complexity
93+
{ code: "function a(x) {switch(x){case 1: 1; break; case 2: 2; break; default: 3;}}", options: [{ max: 2, variant: "modified" }] },
94+
{ code: "function a(x) {switch(x){case 1: 1; break; case 2: 2; break; default: if(x == 'foo') {5;};}}", options: [{ max: 3, variant: "modified" }] },
95+
9296
// class fields
9397
{ code: "function foo() { class C { x = a || b; y = c || d; } }", options: [2], languageOptions: { ecmaVersion: 2022 } },
9498
{ code: "function foo() { class C { static x = a || b; static y = c || d; } }", options: [2], languageOptions: { ecmaVersion: 2022 } },
@@ -185,6 +189,10 @@ ruleTester.run("complexity", rule, {
185189
errors: [makeError("Function 'test'", 21, 20)]
186190
},
187191

192+
// modified complexity
193+
{ 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)] },
194+
{ 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)] },
195+
188196
// class fields
189197
{
190198
code: "function foo () { a || b; class C { x; } c || d; }",

0 commit comments

Comments
 (0)