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

Commit f757f28

Browse files
authored
Improve S3776: Exclude complexity of JSX short-circuits (#377)
1 parent 034ecc7 commit f757f28

File tree

4 files changed

+235
-57
lines changed

4 files changed

+235
-57
lines changed

ruling/snapshots/cognitive-complexity

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,6 @@ src/reveal.js/plugin/zoom-js/zoom.js: 34
472472
src/socket.io/lib/index.js: 69,143
473473
src/spectrum/admin/src/helpers/utils.js: 52
474474
src/spectrum/admin/src/views/communities/components/search/index.js: 86
475-
src/spectrum/admin/src/views/dashboard/components/coreMetrics.js: 114
476475
src/spectrum/admin/src/views/users/components/search/index.js: 92
477476
src/spectrum/api/authentication.js: 274
478477
src/spectrum/api/models/community.js: 168,330
@@ -503,16 +502,13 @@ src/spectrum/shared/graphql/queries/thread/getThreadMessageConnection.js: 78
503502
src/spectrum/src/components/avatar/hoverProfile.js: 44
504503
src/spectrum/src/components/chatInput/index.js: 152
505504
src/spectrum/src/components/composer/index.js: 131
506-
src/spectrum/src/components/draftjs-editor/index.js: 177
507505
src/spectrum/src/components/globals/index.js: 334
508506
src/spectrum/src/components/listItems/index.js: 129
509507
src/spectrum/src/components/modals/ChangeChannelModal/channelSelector.js: 22
510508
src/spectrum/src/components/modals/DeleteDoubleCheckModal/index.js: 74
511-
src/spectrum/src/components/profile/channel.js: 95
512509
src/spectrum/src/components/profile/community.js: 60
513510
src/spectrum/src/components/profile/user.js: 68
514511
src/spectrum/src/components/threadComposer/components/composer.js: 120,215,346
515-
src/spectrum/src/components/threadFeed/index.js: 195
516512
src/spectrum/src/helpers/utils.js: 96
517513
src/spectrum/src/registerServiceWorker.js: 24
518514
src/spectrum/src/views/channel/index.js: 159
@@ -524,20 +520,15 @@ src/spectrum/src/views/communityMembers/components/communityMembers.js: 141
524520
src/spectrum/src/views/communityMembers/components/importSlack.js: 117
525521
src/spectrum/src/views/dashboard/components/sidebarChannels.js: 50
526522
src/spectrum/src/views/dashboard/components/threadFeed.js: 93,213
527-
src/spectrum/src/views/dashboard/index.js: 98
528523
src/spectrum/src/views/directMessages/containers/newThread.js: 201
529-
src/spectrum/src/views/directMessages/index.js: 83
530524
src/spectrum/src/views/explore/components/search.js: 115
531525
src/spectrum/src/views/explore/view.js: 109
532526
src/spectrum/src/views/navbar/components/notificationsTab.js: 51,234
533527
src/spectrum/src/views/navbar/index.js: 75
534-
src/spectrum/src/views/newCommunity/index.js: 173
535528
src/spectrum/src/views/notifications/components/sortAndGroupNotificationMessages.js: 3
536529
src/spectrum/src/views/thread/components/actionBar.js: 209
537-
src/spectrum/src/views/thread/components/messages.js: 142
538530
src/spectrum/src/views/thread/index.js: 223,286
539531
src/spectrum/src/views/user/components/communityList.js: 23
540-
src/spectrum/src/views/user/index.js: 97
541532
src/three.js/editor/js/Loader.js: 12,520
542533
src/three.js/editor/js/Menubar.Edit.js: 125
543534
src/three.js/editor/js/Script.js: 72,136

src/rules/cognitive-complexity.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,18 @@
1919
*/
2020
// https://sonarsource.github.io/rspec/#/rspec/S3776
2121

22-
import type { TSESTree, TSESLint } from '@typescript-eslint/experimental-utils';
22+
import type { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
2323
import { isArrowFunctionExpression, isIfStatement, isLogicalExpression } from '../utils/nodes';
2424
import {
25-
getMainFunctionTokenLocation,
2625
getFirstToken,
2726
getFirstTokenAfter,
28-
report,
27+
getMainFunctionTokenLocation,
2928
IssueLocation,
3029
issueLocation,
30+
report,
3131
} from '../utils/locations';
3232
import docsUrl from '../utils/docs-url';
33+
import { getJsxShortCircuitNodes } from '../utils/jsx';
3334

3435
const DEFAULT_THRESHOLD = 15;
3536

@@ -339,13 +340,19 @@ const rule: TSESLint.RuleModule<string, (number | 'metric' | 'sonar-runtime')[]>
339340
}
340341

341342
function visitLogicalExpression(logicalExpression: TSESTree.LogicalExpression) {
343+
const jsxShortCircuitNodes = getJsxShortCircuitNodes(logicalExpression);
344+
if (jsxShortCircuitNodes != null) {
345+
jsxShortCircuitNodes.forEach(node => consideredLogicalExpressions.add(node));
346+
return;
347+
}
348+
342349
if (!consideredLogicalExpressions.has(logicalExpression)) {
343350
const flattenedLogicalExpressions = flattenLogicalExpression(logicalExpression);
344351

345352
let previous: TSESTree.LogicalExpression | undefined;
346353
for (const current of flattenedLogicalExpressions) {
347354
if (!previous || previous.operator !== current.operator) {
348-
const operatorTokenLoc = getFirstTokenAfter(logicalExpression.left, context)!.loc;
355+
const operatorTokenLoc = getFirstTokenAfter(current.left, context)!.loc;
349356
addComplexity(operatorTokenLoc);
350357
}
351358
previous = current;

src/utils/jsx.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* eslint-plugin-sonarjs
3+
* Copyright (C) 2018-2021 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import type { TSESTree } from '@typescript-eslint/experimental-utils';
22+
23+
export function getJsxShortCircuitNodes(logicalExpression: TSESTree.LogicalExpression) {
24+
if (logicalExpression.parent?.type !== 'JSXExpressionContainer') {
25+
return null;
26+
} else {
27+
return flattenJsxShortCircuitNodes(logicalExpression, logicalExpression);
28+
}
29+
}
30+
31+
function flattenJsxShortCircuitNodes(
32+
root: TSESTree.LogicalExpression,
33+
node: TSESTree.Node,
34+
): TSESTree.LogicalExpression[] | null {
35+
if (
36+
node.type === 'ConditionalExpression' ||
37+
(node.type === 'LogicalExpression' && node.operator !== root.operator)
38+
) {
39+
return null;
40+
} else if (node.type !== 'LogicalExpression') {
41+
return [];
42+
} else {
43+
const leftNodes = flattenJsxShortCircuitNodes(root, node.left);
44+
const rightNodes = flattenJsxShortCircuitNodes(root, node.right);
45+
if (leftNodes == null || rightNodes == null) {
46+
return null;
47+
}
48+
return [...leftNodes, node, ...rightNodes];
49+
}
50+
}

tests/rules/cognitive-complexity.test.ts

Lines changed: 174 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,79 @@
1919
*/
2020
import { TSESLint } from '@typescript-eslint/experimental-utils';
2121
import { ruleTester } from '../rule-tester';
22+
import { IssueLocation } from '../../src/utils/locations';
2223
import rule = require('../../src/rules/cognitive-complexity');
2324

2425
ruleTester.run('cognitive-complexity', rule, {
25-
valid: [{ code: `function zero_complexity() {}`, options: [0] }],
26+
valid: [
27+
{ code: `function zero_complexity() {}`, options: [0] },
28+
{
29+
code: `
30+
function Component(obj) {
31+
return (
32+
<span>{ obj.title?.text }</span>
33+
);
34+
}`,
35+
parserOptions: { ecmaFeatures: { jsx: true } },
36+
options: [0],
37+
},
38+
{
39+
code: `
40+
function Component(obj) {
41+
return (
42+
<>
43+
{ obj.isFriendly && <strong>Welcome</strong> }
44+
</>
45+
);
46+
}`,
47+
parserOptions: { ecmaFeatures: { jsx: true } },
48+
options: [0],
49+
},
50+
{
51+
code: `
52+
function Component(obj) {
53+
return (
54+
<>
55+
{ obj.isFriendly && obj.isLoggedIn && <strong>Welcome</strong> }
56+
</>
57+
);
58+
}`,
59+
parserOptions: { ecmaFeatures: { jsx: true } },
60+
options: [0],
61+
},
62+
{
63+
code: `
64+
function Component(obj) {
65+
return (
66+
<>
67+
{ obj.x && obj.y && obj.z && <strong>Welcome</strong> }
68+
</>
69+
);
70+
}`,
71+
parserOptions: { ecmaFeatures: { jsx: true } },
72+
options: [0],
73+
},
74+
{
75+
code: `
76+
function Component(obj) {
77+
return (
78+
<span title={ obj.title || obj.disclaimer }>Text</span>
79+
);
80+
}`,
81+
parserOptions: { ecmaFeatures: { jsx: true } },
82+
options: [0],
83+
},
84+
{
85+
code: `
86+
function Component(obj) {
87+
return (
88+
<button type="button" disabled={ obj.user?.isBot ?? obj.isDemo }>Logout</button>
89+
);
90+
}`,
91+
parserOptions: { ecmaFeatures: { jsx: true } },
92+
options: [0],
93+
},
94+
],
2695
invalid: [
2796
// if
2897
{
@@ -196,8 +265,8 @@ ruleTester.run('cognitive-complexity', rule, {
196265
options: [0],
197266
errors: [message(2)],
198267
},
199-
{
200-
code: `
268+
testCaseWithSonarRuntime(
269+
`
201270
function check_secondaries() {
202271
if (condition) { // +1 "if"
203272
if (condition) {} else {} // +2 "if", +1 "else"
@@ -217,51 +286,46 @@ ruleTester.run('cognitive-complexity', rule, {
217286
218287
return foo(a && b) && c; // +1 "&&", +1 "&&"
219288
}`,
220-
options: [0, 'sonar-runtime'],
221-
errors: [
289+
[
290+
{ line: 3, column: 8, endLine: 3, endColumn: 10, message: '+1' }, // if
291+
{ line: 7, column: 10, endLine: 7, endColumn: 14, message: '+1' }, // else
222292
{
223-
messageId: 'sonarRuntime',
224-
data: {
225-
complexityAmount: 13,
226-
threshold: 0,
227-
sonarRuntimeData: JSON.stringify({
228-
secondaryLocations: [
229-
{ line: 3, column: 8, endLine: 3, endColumn: 10, message: '+1' }, // if
230-
{ line: 7, column: 10, endLine: 7, endColumn: 14, message: '+1' }, // else
231-
{
232-
line: 4,
233-
column: 10,
234-
endLine: 4,
235-
endColumn: 12,
236-
message: '+2 (incl. 1 for nesting)',
237-
}, // if
238-
{ line: 4, column: 28, endLine: 4, endColumn: 32, message: '+1' }, // else
239-
{
240-
line: 6,
241-
column: 10,
242-
endLine: 6,
243-
endColumn: 15,
244-
message: '+2 (incl. 1 for nesting)',
245-
}, // catch
246-
{ line: 11, column: 8, endLine: 11, endColumn: 13, message: '+1' }, // while
247-
{ line: 12, column: 10, endLine: 12, endColumn: 15, message: '+1' }, // break
248-
{ line: 15, column: 10, endLine: 15, endColumn: 11, message: '+1' }, // ?
249-
{ line: 17, column: 8, endLine: 17, endColumn: 14, message: '+1' }, // switch
250-
{ line: 19, column: 27, endLine: 19, endColumn: 29, message: '+1' }, // &&
251-
{ line: 19, column: 21, endLine: 19, endColumn: 23, message: '+1' }, // &&
252-
],
253-
message:
254-
'Refactor this function to reduce its Cognitive Complexity from 13 to the 0 allowed.',
255-
cost: 13,
256-
}),
257-
...message(13),
258-
cost: 13,
259-
},
260-
},
293+
line: 4,
294+
column: 10,
295+
endLine: 4,
296+
endColumn: 12,
297+
message: '+2 (incl. 1 for nesting)',
298+
}, // if
299+
{ line: 4, column: 28, endLine: 4, endColumn: 32, message: '+1' }, // else
300+
{
301+
line: 6,
302+
column: 10,
303+
endLine: 6,
304+
endColumn: 15,
305+
message: '+2 (incl. 1 for nesting)',
306+
}, // catch
307+
{ line: 11, column: 8, endLine: 11, endColumn: 13, message: '+1' }, // while
308+
{ line: 12, column: 10, endLine: 12, endColumn: 15, message: '+1' }, // break
309+
{ line: 15, column: 10, endLine: 15, endColumn: 11, message: '+1' }, // ?
310+
{ line: 17, column: 8, endLine: 17, endColumn: 14, message: '+1' }, // switch
311+
{ line: 19, column: 27, endLine: 19, endColumn: 29, message: '+1' }, // &&
312+
{ line: 19, column: 21, endLine: 19, endColumn: 23, message: '+1' }, // &&
261313
],
262-
},
314+
13,
315+
),
263316

264317
// expressions
318+
testCaseWithSonarRuntime(
319+
`
320+
function and_or_locations() {
321+
foo(1 && 2 || 3 && 4);
322+
}`,
323+
[
324+
{ line: 3, column: 14, endLine: 3, endColumn: 16, message: '+1' }, // &&
325+
{ line: 3, column: 19, endLine: 3, endColumn: 21, message: '+1' }, // ||
326+
{ line: 3, column: 24, endLine: 3, endColumn: 26, message: '+1' }, // &&
327+
],
328+
),
265329
{
266330
code: `
267331
function and_or() {
@@ -516,6 +580,48 @@ ruleTester.run('cognitive-complexity', rule, {
516580
options: [0],
517581
errors: [message(1, { line: 2 }), message(1, { line: 3 })],
518582
},
583+
testCaseWithSonarRuntime(
584+
`
585+
function Component(obj) {
586+
return (
587+
<>
588+
<span title={ obj.user?.name ?? (obj.isDemo ? 'demo' : 'none') }>Text</span>
589+
</>
590+
);
591+
}`,
592+
[
593+
{ line: 5, column: 41, endLine: 5, endColumn: 43, message: '+1' }, // ??
594+
{ line: 5, column: 56, endLine: 5, endColumn: 57, message: '+1' }, // ?:
595+
],
596+
),
597+
testCaseWithSonarRuntime(
598+
`
599+
function Component(obj) {
600+
return (
601+
<>
602+
{ obj.isUser && (obj.name || obj.surname) }
603+
</>
604+
);
605+
}`,
606+
[
607+
{ line: 5, column: 25, endLine: 5, endColumn: 27, message: '+1' }, // &&
608+
{ line: 5, column: 38, endLine: 5, endColumn: 40, message: '+1' }, // ||
609+
],
610+
),
611+
testCaseWithSonarRuntime(
612+
`
613+
function Component(obj) {
614+
return (
615+
<>
616+
{ obj.isUser && (obj.isDemo ? <strong>Demo</strong> : <em>None</em>) }
617+
</>
618+
);
619+
}`,
620+
[
621+
{ line: 5, column: 25, endLine: 5, endColumn: 27, message: '+1' }, // &&
622+
{ line: 5, column: 40, endLine: 5, endColumn: 41, message: '+1' }, // ||
623+
],
624+
),
519625
],
520626
});
521627

@@ -655,6 +761,30 @@ class TopLevel {
655761
],
656762
});
657763

764+
function testCaseWithSonarRuntime(
765+
code: string,
766+
secondaryLocations: IssueLocation[],
767+
complexity?: number,
768+
): TSESLint.InvalidTestCase<string, (number | 'sonar-runtime')[]> {
769+
const cost = complexity ?? secondaryLocations.length;
770+
const message = `Refactor this function to reduce its Cognitive Complexity from ${cost} to the 0 allowed.`;
771+
const sonarRuntimeData = JSON.stringify({ secondaryLocations, message, cost });
772+
return {
773+
code,
774+
parserOptions: { ecmaFeatures: { jsx: true } },
775+
options: [0, 'sonar-runtime'],
776+
errors: [
777+
{
778+
messageId: 'sonarRuntime',
779+
data: {
780+
threshold: 0,
781+
sonarRuntimeData,
782+
},
783+
},
784+
],
785+
};
786+
}
787+
658788
function message(complexityAmount: number, other: Partial<TSESLint.TestCaseError<string>> = {}) {
659789
return {
660790
messageId: 'refactorFunction',

0 commit comments

Comments
 (0)