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

Commit 4dfd992

Browse files
Modify S1862 (no-identical-conditions): Detect duplicated condition overlaps
1 parent c347dae commit 4dfd992

File tree

3 files changed

+227
-34
lines changed

3 files changed

+227
-34
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
src/react-native/Libraries/vendor/core/Map.js: 582
12
src/spectrum/api/queries/user/contextPermissions.js: 63

src/rules/no-identical-conditions.ts

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,17 @@
2020
// https://sonarsource.github.io/rspec/#/rspec/S1862
2121

2222
import type { TSESTree, TSESLint } from '@typescript-eslint/experimental-utils';
23-
import { isIfStatement } from '../utils/nodes';
2423
import { areEquivalent } from '../utils/equivalence';
2524
import { report, issueLocation } from '../utils/locations';
2625
import docsUrl from '../utils/docs-url';
2726

28-
const duplicatedBranchMessage = 'This branch duplicates the one on line {{line}}';
27+
const duplicatedConditionMessage = 'This condition is covered by the one on line {{line}}';
2928
const duplicatedCaseMessage = 'This case duplicates the one on line {{line}}';
3029

3130
const rule: TSESLint.RuleModule<string, string[]> = {
3231
meta: {
3332
messages: {
34-
duplicatedBranch: duplicatedBranchMessage,
33+
duplicatedCondition: duplicatedConditionMessage,
3534
duplicatedCase: duplicatedCaseMessage,
3635
sonarRuntime: '{{sonarRuntimeData}}',
3736
},
@@ -53,30 +52,38 @@ const rule: TSESLint.RuleModule<string, string[]> = {
5352
const sourceCode = context.getSourceCode();
5453
return {
5554
IfStatement(node: TSESTree.Node) {
56-
const ifStmt = node as TSESTree.IfStatement;
57-
const condition = ifStmt.test;
58-
let statement = ifStmt.alternate;
59-
while (statement) {
60-
if (isIfStatement(statement)) {
61-
if (areEquivalent(condition, statement.test, sourceCode)) {
62-
const line = ifStmt.loc && ifStmt.loc.start.line;
63-
if (line && condition.loc) {
64-
report(
65-
context,
66-
{
67-
messageId: 'duplicatedBranch',
68-
data: {
69-
line,
70-
},
71-
node: statement.test,
72-
},
73-
[issueLocation(condition.loc, condition.loc, 'Original')],
74-
duplicatedBranchMessage,
75-
);
76-
}
77-
}
78-
statement = statement.alternate;
79-
} else {
55+
const { test } = node as TSESTree.IfStatement;
56+
const conditionsToCheck =
57+
test.type === 'LogicalExpression' && test.operator === '&&'
58+
? [test, ...splitByAnd(test)]
59+
: [test];
60+
61+
let current = node;
62+
let operandsToCheck = conditionsToCheck.map(c => splitByOr(c).map(splitByAnd));
63+
while (current.parent?.type === 'IfStatement' && current.parent.alternate === current) {
64+
current = current.parent;
65+
66+
const currentOrOperands = splitByOr(current.test).map(splitByAnd);
67+
operandsToCheck = operandsToCheck.map(orOperands =>
68+
orOperands.filter(
69+
orOperand =>
70+
!currentOrOperands.some(currentOrOperand =>
71+
isSubset(currentOrOperand, orOperand, sourceCode),
72+
),
73+
),
74+
);
75+
76+
if (operandsToCheck.some(orOperands => orOperands.length === 0)) {
77+
report(
78+
context,
79+
{
80+
messageId: 'duplicatedCondition',
81+
data: { line: current.test.loc.start.line },
82+
node: test,
83+
},
84+
[issueLocation(current.test.loc, current.test.loc, 'Covering')],
85+
duplicatedConditionMessage,
86+
);
8087
break;
8188
}
8289
}
@@ -113,4 +120,55 @@ const rule: TSESLint.RuleModule<string, string[]> = {
113120
},
114121
};
115122

123+
const splitByOr = splitByLogicalOperator.bind(null, '||');
124+
const splitByAnd = splitByLogicalOperator.bind(null, '&&');
125+
126+
function splitByLogicalOperator(
127+
operator: '??' | '&&' | '||',
128+
node: TSESTree.Node,
129+
): TSESTree.Node[] {
130+
if (node.type === 'LogicalExpression' && node.operator === operator) {
131+
return [
132+
...splitByLogicalOperator(operator, node.left),
133+
...splitByLogicalOperator(operator, node.right),
134+
];
135+
}
136+
return [node];
137+
}
138+
139+
function isSubset(
140+
first: TSESTree.Node[],
141+
second: TSESTree.Node[],
142+
sourceCode: TSESLint.SourceCode,
143+
): boolean {
144+
return first.every(fst => second.some(snd => isSubsetOf(fst, snd, sourceCode)));
145+
146+
function isSubsetOf(
147+
first: TSESTree.Node,
148+
second: TSESTree.Node,
149+
sourceCode: TSESLint.SourceCode,
150+
): boolean {
151+
if (first.type !== second.type) {
152+
return false;
153+
}
154+
155+
if (first.type === 'LogicalExpression') {
156+
const second1 = second as TSESTree.LogicalExpression;
157+
if (
158+
(first.operator === '||' || first.operator === '&&') &&
159+
first.operator === second1.operator
160+
) {
161+
return (
162+
(isSubsetOf(first.left, second1.left, sourceCode) &&
163+
isSubsetOf(first.right, second1.right, sourceCode)) ||
164+
(isSubsetOf(first.left, second1.right, sourceCode) &&
165+
isSubsetOf(first.right, second1.left, sourceCode))
166+
);
167+
}
168+
}
169+
170+
return areEquivalent(first, second, sourceCode);
171+
}
172+
}
173+
116174
export = rule;

tests/rules/no-identical-conditions.test.ts

Lines changed: 141 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import { ruleTester } from '../rule-tester';
2121
import rule = require('../../src/rules/no-identical-conditions');
2222

23+
const SONAR_RUNTIME = 'sonar-runtime';
24+
2325
ruleTester.run('no-identical-conditions', rule, {
2426
valid: [
2527
{
@@ -28,6 +30,18 @@ ruleTester.run('no-identical-conditions', rule, {
2830
{
2931
code: 'if (a) {} else {}',
3032
},
33+
{
34+
code: 'if (a && b) {} else if (a) {}',
35+
},
36+
{
37+
code: 'if (a && b) {} else if (c && d) {}',
38+
},
39+
{
40+
code: 'if (a || b) {} else if (c || d) {}',
41+
},
42+
{
43+
code: 'if (a ?? b) {} else if (c) {}',
44+
},
3145
{
3246
code: `
3347
switch (a) {
@@ -47,7 +61,7 @@ ruleTester.run('no-identical-conditions', rule, {
4761
`,
4862
errors: [
4963
{
50-
messageId: 'duplicatedBranch',
64+
messageId: 'duplicatedCondition',
5165
data: {
5266
line: 2,
5367
},
@@ -64,7 +78,7 @@ ruleTester.run('no-identical-conditions', rule, {
6478
else if (a) {}
6579
// ^
6680
`,
67-
options: ['sonar-runtime'],
81+
options: [SONAR_RUNTIME],
6882
errors: [
6983
{
7084
messageId: 'sonarRuntime',
@@ -77,10 +91,10 @@ ruleTester.run('no-identical-conditions', rule, {
7791
column: 12,
7892
endLine: 2,
7993
endColumn: 13,
80-
message: 'Original',
94+
message: 'Covering',
8195
},
8296
],
83-
message: 'This branch duplicates the one on line 2',
97+
message: 'This condition is covered by the one on line 2',
8498
}),
8599
},
86100
},
@@ -94,7 +108,7 @@ ruleTester.run('no-identical-conditions', rule, {
94108
`,
95109
errors: [
96110
{
97-
messageId: 'duplicatedBranch',
111+
messageId: 'duplicatedCondition',
98112
data: {
99113
line: 3,
100114
},
@@ -112,7 +126,7 @@ ruleTester.run('no-identical-conditions', rule, {
112126
`,
113127
errors: [
114128
{
115-
messageId: 'duplicatedBranch',
129+
messageId: 'duplicatedCondition',
116130
data: {
117131
line: 2,
118132
},
@@ -122,6 +136,126 @@ ruleTester.run('no-identical-conditions', rule, {
122136
},
123137
],
124138
},
139+
{
140+
code: `
141+
if (a || b) {}
142+
// >^^^^^^
143+
else if (a) {}
144+
// ^`,
145+
options: [SONAR_RUNTIME],
146+
errors: [
147+
{
148+
messageId: 'sonarRuntime',
149+
line: 4,
150+
column: 18,
151+
endLine: 4,
152+
endColumn: 19,
153+
data: {
154+
line: 2,
155+
sonarRuntimeData: JSON.stringify({
156+
secondaryLocations: [
157+
{
158+
line: 2,
159+
column: 12,
160+
endLine: 2,
161+
endColumn: 18,
162+
message: 'Covering',
163+
},
164+
],
165+
message: 'This condition is covered by the one on line 2',
166+
}),
167+
},
168+
},
169+
],
170+
},
171+
{
172+
code: `if (a || b) {} else if (b) {}`,
173+
errors: [{ messageId: 'duplicatedCondition' }],
174+
},
175+
{
176+
code: `if ((a === b && fn(c)) || d) {} else if (a === b && fn(c)) {}`,
177+
errors: [{ messageId: 'duplicatedCondition' }],
178+
},
179+
{
180+
code: `if (a) {} else if (a && b) {}`,
181+
errors: [{ messageId: 'duplicatedCondition' }],
182+
},
183+
{
184+
code: `if (a && b) ; else if (b && a) {}`,
185+
errors: [{ messageId: 'duplicatedCondition' }],
186+
},
187+
{
188+
code: `if (a) {} else if (b) {} else if (c && a || b) {}`,
189+
errors: [{ messageId: 'duplicatedCondition' }],
190+
},
191+
{
192+
code: `if (a) {} else if (b) {} else if (c && (a || b)) {}`,
193+
errors: [{ messageId: 'duplicatedCondition' }],
194+
},
195+
{
196+
code: `if (a) {} else if (b && c) {} else if (d && (a || e && c && b)) {}`,
197+
errors: [{ messageId: 'duplicatedCondition' }],
198+
},
199+
{
200+
code: `if (a || b && c) {} else if (b && c && d) {}`,
201+
errors: [{ messageId: 'duplicatedCondition' }],
202+
},
203+
{
204+
code: `if (a || b) {} else if (b && c) {}`,
205+
errors: [{ messageId: 'duplicatedCondition' }],
206+
},
207+
{
208+
code: `if (a) {} else if (b) {} else if ((a || b) && c) {}`,
209+
errors: [{ messageId: 'duplicatedCondition' }],
210+
},
211+
{
212+
code: `if ((a && (b || c)) || d) {} else if ((c || b) && e && a) {}`,
213+
errors: [{ messageId: 'duplicatedCondition' }],
214+
},
215+
{
216+
code: `if (a && b || b && c) {} else if (a && b && c) {}`,
217+
errors: [{ messageId: 'duplicatedCondition' }],
218+
},
219+
{
220+
code: `if (a) {} else if (b && c) {} else if (d && (c && e && b || a)) {}`,
221+
errors: [{ messageId: 'duplicatedCondition' }],
222+
},
223+
{
224+
code: `if (a || (b && (c || d))) {} else if ((d || c) && b) {}`,
225+
errors: [{ messageId: 'duplicatedCondition' }],
226+
},
227+
{
228+
code: `if (a || b) {} else if ((b || a) && c) {}`,
229+
errors: [{ messageId: 'duplicatedCondition' }],
230+
},
231+
{
232+
code: `if (a || b) {} else if (c) {} else if (d) {} else if (b && (a || c)) {}`,
233+
errors: [{ messageId: 'duplicatedCondition' }],
234+
},
235+
{
236+
code: `if (a || b || c) {} else if (a || (b && d) || (c && e)) {}`,
237+
errors: [{ messageId: 'duplicatedCondition' }],
238+
},
239+
{
240+
code: `if (a || (b || c)) {} else if (a || (b && c)) {}`,
241+
errors: [{ messageId: 'duplicatedCondition' }],
242+
},
243+
{
244+
code: `if (a || b) {} else if (c) {} else if (d) {} else if ((a || c) && (b || d)) {}`,
245+
errors: [{ messageId: 'duplicatedCondition' }],
246+
},
247+
{
248+
code: `if (a) {} else if (b) {} else if (c && (a || d && b)) {}`,
249+
errors: [{ messageId: 'duplicatedCondition' }],
250+
},
251+
{
252+
code: `if (a) {} else if (a || a) {}`,
253+
errors: [{ messageId: 'duplicatedCondition' }],
254+
},
255+
{
256+
code: `if (a) {} else if (a && a) {}`,
257+
errors: [{ messageId: 'duplicatedCondition' }],
258+
},
125259
{
126260
code: `
127261
switch (a) {
@@ -136,7 +270,7 @@ ruleTester.run('no-identical-conditions', rule, {
136270
break;
137271
}
138272
`,
139-
options: ['sonar-runtime'],
273+
options: [SONAR_RUNTIME],
140274
errors: [
141275
{
142276
messageId: 'sonarRuntime',

0 commit comments

Comments
 (0)