Skip to content
This repository was archived by the owner on Oct 3, 2024. It is now read-only.

Commit 078cb7d

Browse files
cognitive-complexity: provide secondary issue locations (#104)
1 parent 1b2a721 commit 078cb7d

File tree

3 files changed

+117
-46
lines changed

3 files changed

+117
-46
lines changed

src/rules/cognitive-complexity.ts

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@
2222
import { Rule } from "eslint";
2323
import * as estree from "estree";
2424
import { getParent, isIfStatement, isLogicalExpression } from "../utils/nodes";
25-
import { getMainFunctionTokenLocation } from "../utils/locations";
25+
import {
26+
getMainFunctionTokenLocation,
27+
getFirstToken,
28+
getFirstTokenAfter,
29+
report,
30+
IssueLocation,
31+
issueLocation,
32+
} from "../utils/locations";
2633

27-
const MESSAGE =
28-
"Refactor this function to reduce its Cognitive Complexity from {{complexity}} to the {{threshold}} allowed.";
2934
const DEFAULT_THRESHOLD = 15;
3035

3136
type LoopStatement =
@@ -39,16 +44,22 @@ type OptionalLocation = estree.SourceLocation | null | undefined;
3944

4045
const rule: Rule.RuleModule = {
4146
meta: {
42-
schema: [{ type: "integer", minimum: 0 }],
47+
schema: [
48+
{ type: "integer", minimum: 0 },
49+
{
50+
// internal parameter
51+
enum: ["sonar-runtime"],
52+
},
53+
],
4354
},
4455
create(context: Rule.RuleContext) {
4556
const threshold: number = context.options[0] !== undefined ? context.options[0] : DEFAULT_THRESHOLD;
4657

4758
/** Complexity of the current function if it is *not* considered nested to the first level function */
48-
let complexityIfNotNested = 0;
59+
let complexityIfNotNested: ComplexityPoint[] = [];
4960

5061
/** Complexity of the current function if it is considered nested to the first level function */
51-
let complexityIfNested = 0;
62+
let complexityIfNested: ComplexityPoint[] = [];
5263

5364
/** Current nesting level (number of enclosing control flow statements and functions) */
5465
let nesting = 0;
@@ -57,7 +68,7 @@ const rule: Rule.RuleModule = {
5768
let topLevelHasStructuralComplexity = false;
5869

5970
/** Own (not including nested functions) complexity of the current top function */
60-
let topLevelOwnComplexity = 0;
71+
let topLevelOwnComplexity: ComplexityPoint[] = [];
6172

6273
/** Nodes that should increase nesting level */
6374
const nestingNodes: Set<estree.Node> = new Set();
@@ -71,8 +82,8 @@ const rule: Rule.RuleModule = {
7182
let secondLevelFunctions: Array<{
7283
node: estree.Function;
7384
parent: estree.Node | undefined;
74-
complexityIfThisSecondaryIsTopLevel: number;
75-
complexityIfNested: number;
85+
complexityIfThisSecondaryIsTopLevel: ComplexityPoint[];
86+
complexityIfNested: ComplexityPoint[];
7687
loc: OptionalLocation;
7788
}> = [];
7889

@@ -138,12 +149,12 @@ const rule: Rule.RuleModule = {
138149
if (enclosingFunctions.length === 0) {
139150
// top level function
140151
topLevelHasStructuralComplexity = false;
141-
topLevelOwnComplexity = 0;
152+
topLevelOwnComplexity = [];
142153
secondLevelFunctions = [];
143154
} else if (enclosingFunctions.length === 1) {
144155
// second level function
145-
complexityIfNotNested = 0;
146-
complexityIfNested = 0;
156+
complexityIfNotNested = [];
157+
complexityIfNested = [];
147158
} else {
148159
nesting++;
149160
nestingNodes.add(node);
@@ -158,7 +169,7 @@ const rule: Rule.RuleModule = {
158169
if (topLevelHasStructuralComplexity) {
159170
let totalComplexity = topLevelOwnComplexity;
160171
secondLevelFunctions.forEach(secondLevelFunction => {
161-
totalComplexity += secondLevelFunction.complexityIfNested;
172+
totalComplexity = totalComplexity.concat(secondLevelFunction.complexityIfNested);
162173
});
163174
checkFunction(totalComplexity, getMainFunctionTokenLocation(node, getParent(context), context));
164175
} else {
@@ -187,11 +198,12 @@ const rule: Rule.RuleModule = {
187198

188199
function visitIfStatement(ifStatement: estree.IfStatement) {
189200
const parent = getParent(context);
201+
const { loc: ifLoc } = getFirstToken(ifStatement, context);
190202
// if the current `if` statement is `else if`, do not count it in structural complexity
191203
if (isIfStatement(parent) && parent.alternate === ifStatement) {
192-
addComplexity();
204+
addComplexity(ifLoc);
193205
} else {
194-
addStructuralComplexity();
206+
addStructuralComplexity(ifLoc);
195207
}
196208

197209
// always increase nesting level inside `then` statement
@@ -202,35 +214,37 @@ const rule: Rule.RuleModule = {
202214
// - add +1 complexity
203215
if (ifStatement.alternate && !isIfStatement(ifStatement.alternate)) {
204216
nestingNodes.add(ifStatement.alternate);
205-
addComplexity();
217+
const elseTokenLoc = getFirstTokenAfter(ifStatement.consequent, context)!.loc;
218+
addComplexity(elseTokenLoc);
206219
}
207220
}
208221

209222
function visitLoop(loop: LoopStatement) {
210-
addStructuralComplexity();
223+
addStructuralComplexity(getFirstToken(loop, context).loc);
211224
nestingNodes.add(loop.body);
212225
}
213226

214227
function visitSwitchStatement(switchStatement: estree.SwitchStatement) {
215-
addStructuralComplexity();
228+
addStructuralComplexity(getFirstToken(switchStatement, context).loc);
216229
for (const switchCase of switchStatement.cases) {
217230
nestingNodes.add(switchCase);
218231
}
219232
}
220233

221234
function visitContinueOrBreakStatement(statement: estree.ContinueStatement | estree.BreakStatement) {
222235
if (statement.label) {
223-
addComplexity();
236+
addComplexity(getFirstToken(statement, context).loc);
224237
}
225238
}
226239

227240
function visitCatchClause(catchClause: estree.CatchClause) {
228-
addStructuralComplexity();
241+
addStructuralComplexity(getFirstToken(catchClause, context).loc);
229242
nestingNodes.add(catchClause.body);
230243
}
231244

232245
function visitConditionalExpression(conditionalExpression: estree.ConditionalExpression) {
233-
addStructuralComplexity();
246+
const questionTokenLoc = getFirstTokenAfter(conditionalExpression.test, context)!.loc;
247+
addStructuralComplexity(questionTokenLoc);
234248
nestingNodes.add(conditionalExpression.consequent);
235249
nestingNodes.add(conditionalExpression.alternate);
236250
}
@@ -242,7 +256,8 @@ const rule: Rule.RuleModule = {
242256
let previous: estree.LogicalExpression | undefined;
243257
for (const current of flattenedLogicalExpressions) {
244258
if (!previous || previous.operator !== current.operator) {
245-
addComplexity();
259+
const operatorTokenLoc = getFirstTokenAfter(logicalExpression.left, context)!.loc;
260+
addComplexity(operatorTokenLoc);
246261
}
247262
previous = current;
248263
}
@@ -257,40 +272,58 @@ const rule: Rule.RuleModule = {
257272
return [];
258273
}
259274

260-
function addStructuralComplexity() {
275+
function addStructuralComplexity(location: estree.SourceLocation) {
261276
const added = nesting + 1;
277+
const complexityPoint = { complexity: added, location };
262278
if (enclosingFunctions.length === 1) {
263279
// top level function
264280
topLevelHasStructuralComplexity = true;
265-
topLevelOwnComplexity += added;
281+
topLevelOwnComplexity.push(complexityPoint);
266282
} else {
267283
// second+ level function
268-
complexityIfNested += added + 1;
269-
complexityIfNotNested += added;
284+
complexityIfNested.push({ complexity: added + 1, location });
285+
complexityIfNotNested.push(complexityPoint);
270286
}
271287
}
272288

273-
function addComplexity() {
289+
function addComplexity(location: estree.SourceLocation) {
290+
const complexityPoint = { complexity: 1, location };
274291
if (enclosingFunctions.length === 1) {
275292
// top level function
276-
topLevelOwnComplexity++;
293+
topLevelOwnComplexity.push(complexityPoint);
277294
} else {
278295
// second+ level function
279-
complexityIfNested++;
280-
complexityIfNotNested++;
296+
complexityIfNested.push(complexityPoint);
297+
complexityIfNotNested.push(complexityPoint);
281298
}
282299
}
283300

284-
function checkFunction(complexity: number, loc: estree.SourceLocation) {
285-
if (complexity > threshold) {
286-
context.report({
287-
message: MESSAGE,
288-
data: { complexity: String(complexity), threshold: String(threshold) },
289-
loc,
301+
function checkFunction(complexity: ComplexityPoint[] = [], loc: estree.SourceLocation) {
302+
const complexityAmount = complexity.reduce((acc, cur) => acc + cur.complexity, 0);
303+
if (complexityAmount > threshold) {
304+
const secondaryLocations: IssueLocation[] = complexity.map(complexityPoint => {
305+
const { complexity, location } = complexityPoint;
306+
const message = complexity === 1 ? "+1" : `+${complexity} (incl. ${complexity - 1} for nesting)`;
307+
return issueLocation(location, undefined, message);
290308
});
309+
310+
report(
311+
context,
312+
{
313+
message: `Refactor this function to reduce its Cognitive Complexity from ${complexityAmount} to the ${threshold} allowed.`,
314+
loc,
315+
},
316+
secondaryLocations,
317+
complexityAmount - threshold,
318+
);
291319
}
292320
}
293321
},
294322
};
295323

296324
export = rule;
325+
326+
type ComplexityPoint = {
327+
complexity: number;
328+
location: estree.SourceLocation;
329+
};

src/utils/locations.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* along with this program; if not, write to the Free Software Foundation,
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
20-
import { Rule } from "eslint";
20+
import { Rule, AST } from "eslint";
2121
import * as estree from "estree";
2222

2323
export interface IssueLocation {
@@ -121,3 +121,11 @@ function getTokenByValue(node: estree.Node, value: string, context: Rule.RuleCon
121121
.getTokens(node)
122122
.find(token => token.value === value);
123123
}
124+
125+
export function getFirstTokenAfter(node: estree.Node, context: Rule.RuleContext): AST.Token | null {
126+
return context.getSourceCode().getTokenAfter(node);
127+
}
128+
129+
export function getFirstToken(node: estree.Node, context: Rule.RuleContext): AST.Token {
130+
return context.getSourceCode().getTokens(node)[0];
131+
}

tests/rules/cognitive-complexity.test.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,46 @@ ruleTester.run("cognitive-complexity", rule, {
199199
},
200200
{
201201
code: `
202-
function nested_try_catch() {
203-
try {
204-
if (condition) { // +1 (nesting level +1)
205-
try {}
206-
catch (someError) {} // +2
207-
}
208-
} finally {}
202+
function check_secondaries() {
203+
if (condition) { // +1 "if"
204+
if (condition) {} else {} // +2 "if", +1 "else"
205+
try {}
206+
catch (someError) {} // +2 "catch"
207+
} else { // +1
208+
}
209+
210+
foo:
211+
while (cond) { // +1 "while"
212+
break foo; // +1 "break"
213+
}
214+
215+
a ? 1 : 2; // +1 "?"
216+
217+
switch (a) {} // +1 "switch"
218+
219+
return foo(a && b) && c; // +1 "&&", +1 "&&"
209220
}`,
210-
options: [0],
211-
errors: [message(3)],
221+
options: [0, "sonar-runtime"],
222+
errors: [
223+
JSON.stringify({
224+
secondaryLocations: [
225+
{ line: 3, column: 8, endLine: 3, endColumn: 10, message: "+1" }, // if
226+
{ line: 7, column: 10, endLine: 7, endColumn: 14, message: "+1" }, // else
227+
{ line: 4, column: 10, endLine: 4, endColumn: 12, message: "+2 (incl. 1 for nesting)" }, // if
228+
{ line: 4, column: 28, endLine: 4, endColumn: 32, message: "+1" }, // else
229+
{ line: 6, column: 10, endLine: 6, endColumn: 15, message: "+2 (incl. 1 for nesting)" }, // catch
230+
{ line: 11, column: 8, endLine: 11, endColumn: 13, message: "+1" }, // while
231+
{ line: 12, column: 10, endLine: 12, endColumn: 15, message: "+1" }, // break
232+
{ line: 15, column: 10, endLine: 15, endColumn: 11, message: "+1" }, // ?
233+
{ line: 17, column: 8, endLine: 17, endColumn: 14, message: "+1" }, // switch
234+
{ line: 19, column: 27, endLine: 19, endColumn: 29, message: "+1" }, // &&
235+
{ line: 19, column: 21, endLine: 19, endColumn: 23, message: "+1" }, // &&
236+
],
237+
...message(13),
238+
239+
cost: 13,
240+
}),
241+
],
212242
},
213243

214244
// expressions

0 commit comments

Comments
 (0)