@flint.fyi/ts 0.14.3 → 0.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/lib/createTypeScriptFileFromProgram.js +5 -1
  3. package/lib/index.d.ts +3 -0
  4. package/lib/index.js +3 -0
  5. package/lib/language.d.ts +1 -0
  6. package/lib/plugin.d.ts +61 -1
  7. package/lib/plugin.js +30 -0
  8. package/lib/rules/anyReturns.d.ts +6 -0
  9. package/lib/rules/anyReturns.js +187 -0
  10. package/lib/rules/anyReturns.test.d.ts +1 -0
  11. package/lib/rules/anyReturns.test.js +647 -0
  12. package/lib/rules/caseDuplicates.d.ts +6 -0
  13. package/lib/rules/caseDuplicates.js +49 -0
  14. package/lib/rules/caseDuplicates.test.d.ts +1 -0
  15. package/lib/rules/caseDuplicates.test.js +307 -0
  16. package/lib/rules/chainedAssignments.js +2 -1
  17. package/lib/rules/chainedAssignments.test.js +24 -0
  18. package/lib/rules/debuggerStatements.test.js +4 -4
  19. package/lib/rules/elseIfDuplicates.d.ts +6 -0
  20. package/lib/rules/elseIfDuplicates.js +53 -0
  21. package/lib/rules/elseIfDuplicates.test.d.ts +1 -0
  22. package/lib/rules/elseIfDuplicates.test.js +176 -0
  23. package/lib/rules/emptyBlocks.d.ts +6 -0
  24. package/lib/rules/emptyBlocks.js +66 -0
  25. package/lib/rules/emptyBlocks.test.d.ts +1 -0
  26. package/lib/rules/emptyBlocks.test.js +135 -0
  27. package/lib/rules/forDirections.d.ts +6 -0
  28. package/lib/rules/forDirections.js +124 -0
  29. package/lib/rules/forDirections.test.d.ts +1 -0
  30. package/lib/rules/forDirections.test.js +99 -0
  31. package/lib/rules/functionNewCalls.d.ts +6 -0
  32. package/lib/rules/functionNewCalls.js +60 -0
  33. package/lib/rules/functionNewCalls.test.d.ts +1 -0
  34. package/lib/rules/functionNewCalls.test.js +115 -0
  35. package/lib/rules/negativeZeroComparisons.js +4 -22
  36. package/lib/rules/nonOctalDecimalEscapes.d.ts +6 -0
  37. package/lib/rules/nonOctalDecimalEscapes.js +57 -0
  38. package/lib/rules/nonOctalDecimalEscapes.test.d.ts +1 -0
  39. package/lib/rules/nonOctalDecimalEscapes.test.js +69 -0
  40. package/lib/rules/numericLiteralParsing.d.ts +6 -0
  41. package/lib/rules/numericLiteralParsing.js +97 -0
  42. package/lib/rules/numericLiteralParsing.test.d.ts +1 -0
  43. package/lib/rules/numericLiteralParsing.test.js +99 -0
  44. package/lib/rules/octalNumbers.test.js +2 -2
  45. package/lib/rules/selfAssignments.d.ts +6 -0
  46. package/lib/rules/selfAssignments.js +44 -0
  47. package/lib/rules/selfAssignments.test.d.ts +1 -0
  48. package/lib/rules/selfAssignments.test.js +66 -0
  49. package/lib/rules/selfComparisons.d.ts +6 -0
  50. package/lib/rules/selfComparisons.js +41 -0
  51. package/lib/rules/selfComparisons.test.d.ts +1 -0
  52. package/lib/rules/selfComparisons.test.js +297 -0
  53. package/lib/rules/sequences.d.ts +6 -0
  54. package/lib/rules/sequences.js +34 -0
  55. package/lib/rules/sequences.test.d.ts +1 -0
  56. package/lib/rules/sequences.test.js +48 -0
  57. package/lib/rules/shadowedRestrictedNames.d.ts +6 -0
  58. package/lib/rules/shadowedRestrictedNames.js +95 -0
  59. package/lib/rules/shadowedRestrictedNames.test.d.ts +1 -0
  60. package/lib/rules/shadowedRestrictedNames.test.js +147 -0
  61. package/lib/rules/symbolDescriptions.test.js +4 -4
  62. package/lib/rules/typeofComparisons.d.ts +6 -0
  63. package/lib/rules/typeofComparisons.js +78 -0
  64. package/lib/rules/typeofComparisons.test.d.ts +1 -0
  65. package/lib/rules/typeofComparisons.test.js +85 -0
  66. package/lib/rules/unicodeBOMs.test.js +1 -0
  67. package/lib/rules/unnecessaryBlocks.d.ts +6 -0
  68. package/lib/rules/unnecessaryBlocks.js +75 -0
  69. package/lib/rules/unnecessaryBlocks.test.d.ts +1 -0
  70. package/lib/rules/unnecessaryBlocks.test.js +147 -0
  71. package/lib/rules/unnecessaryConcatenation.d.ts +6 -0
  72. package/lib/rules/unnecessaryConcatenation.js +38 -0
  73. package/lib/rules/unnecessaryConcatenation.test.d.ts +1 -0
  74. package/lib/rules/unnecessaryConcatenation.test.js +87 -0
  75. package/lib/rules/utils/discriminateAnyType.d.ts +12 -0
  76. package/lib/rules/utils/discriminateAnyType.js +41 -0
  77. package/lib/rules/utils/getThisExpression.d.ts +2 -0
  78. package/lib/rules/utils/getThisExpression.js +23 -0
  79. package/lib/rules/utils/isUnsafeAssignment.d.ts +13 -0
  80. package/lib/rules/utils/isUnsafeAssignment.js +76 -0
  81. package/lib/rules/utils/operators.d.ts +4 -0
  82. package/lib/rules/utils/operators.js +36 -0
  83. package/lib/rules/voidOperator.test.js +2 -2
  84. package/lib/utils/declarationIncludesGlobal.d.ts +2 -0
  85. package/lib/utils/declarationIncludesGlobal.js +5 -0
  86. package/lib/utils/declarationsIncludeGlobal.js +2 -5
  87. package/lib/utils/getDeclarationsIfGlobal.d.ts +2 -0
  88. package/lib/utils/getDeclarationsIfGlobal.js +7 -0
  89. package/lib/utils/hasSameTokens.d.ts +2 -0
  90. package/lib/utils/hasSameTokens.js +30 -0
  91. package/lib/utils/isGlobalDeclaration.js +2 -3
  92. package/lib/utils/isGlobalDeclarationOfName.d.ts +5 -0
  93. package/lib/utils/isGlobalDeclarationOfName.js +35 -0
  94. package/package.json +3 -3
  95. package/src/createTypeScriptFileFromProgram.ts +6 -1
  96. package/src/index.ts +3 -0
  97. package/src/language.ts +1 -0
  98. package/src/plugin.ts +30 -0
  99. package/src/rules/anyReturns.test.ts +649 -0
  100. package/src/rules/anyReturns.ts +263 -0
  101. package/src/rules/caseDuplicates.test.ts +308 -0
  102. package/src/rules/caseDuplicates.ts +57 -0
  103. package/src/rules/chainedAssignments.test.ts +24 -0
  104. package/src/rules/chainedAssignments.ts +4 -1
  105. package/src/rules/debuggerStatements.test.ts +4 -4
  106. package/src/rules/elseIfDuplicates.test.ts +177 -0
  107. package/src/rules/elseIfDuplicates.ts +69 -0
  108. package/src/rules/emptyBlocks.test.ts +136 -0
  109. package/src/rules/emptyBlocks.ts +73 -0
  110. package/src/rules/forDirections.test.ts +100 -0
  111. package/src/rules/forDirections.ts +163 -0
  112. package/src/rules/functionNewCalls.test.ts +116 -0
  113. package/src/rules/functionNewCalls.ts +75 -0
  114. package/src/rules/negativeZeroComparisons.ts +8 -27
  115. package/src/rules/nonOctalDecimalEscapes.test.ts +70 -0
  116. package/src/rules/nonOctalDecimalEscapes.ts +70 -0
  117. package/src/rules/numericLiteralParsing.test.ts +100 -0
  118. package/src/rules/numericLiteralParsing.ts +116 -0
  119. package/src/rules/octalNumbers.test.ts +2 -2
  120. package/src/rules/selfAssignments.test.ts +67 -0
  121. package/src/rules/selfAssignments.ts +50 -0
  122. package/src/rules/selfComparisons.test.ts +298 -0
  123. package/src/rules/selfComparisons.ts +45 -0
  124. package/src/rules/sequences.test.ts +49 -0
  125. package/src/rules/sequences.ts +37 -0
  126. package/src/rules/shadowedRestrictedNames.test.ts +148 -0
  127. package/src/rules/shadowedRestrictedNames.ts +105 -0
  128. package/src/rules/symbolDescriptions.test.ts +4 -4
  129. package/src/rules/typeofComparisons.test.ts +86 -0
  130. package/src/rules/typeofComparisons.ts +88 -0
  131. package/src/rules/unicodeBOMs.test.ts +1 -0
  132. package/src/rules/unnecessaryBlocks.test.ts +148 -0
  133. package/src/rules/unnecessaryBlocks.ts +89 -0
  134. package/src/rules/unnecessaryConcatenation.test.ts +88 -0
  135. package/src/rules/unnecessaryConcatenation.ts +44 -0
  136. package/src/rules/utils/discriminateAnyType.ts +66 -0
  137. package/src/rules/utils/getThisExpression.ts +24 -0
  138. package/src/rules/utils/isUnsafeAssignment.ts +113 -0
  139. package/src/rules/utils/operators.ts +39 -0
  140. package/src/rules/voidOperator.test.ts +2 -2
  141. package/src/utils/declarationIncludesGlobal.ts +9 -0
  142. package/src/utils/declarationsIncludeGlobal.ts +3 -7
  143. package/src/utils/getDeclarationsIfGlobal.ts +14 -0
  144. package/src/utils/hasSameTokens.ts +45 -0
  145. package/src/utils/isGlobalDeclaration.ts +3 -5
  146. package/src/utils/isGlobalDeclarationOfName.ts +56 -0
  147. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,263 @@
1
+ import * as tsutils from "ts-api-utils";
2
+ import * as ts from "typescript";
3
+
4
+ import { typescriptLanguage } from "../language.js";
5
+ import { AnyType, discriminateAnyType } from "./utils/discriminateAnyType.js";
6
+ import { getConstrainedTypeAtLocation } from "./utils/getConstrainedType.js";
7
+ import { getThisExpression } from "./utils/getThisExpression.js";
8
+ import { isUnsafeAssignment } from "./utils/isUnsafeAssignment.js";
9
+
10
+ export default typescriptLanguage.createRule({
11
+ about: {
12
+ description: "Reports returning a value with type `any` from a function.",
13
+ id: "anyReturns",
14
+ preset: "logical",
15
+ },
16
+ messages: {
17
+ unsafeReturn: {
18
+ primary: "Unsafe return of a value of type {{ type }}.",
19
+ secondary: [
20
+ "Returning a value of type `any` or a similar unsafe type defeats TypeScript's type safety guarantees.",
21
+ "This can allow unexpected types to propagate through your codebase, potentially causing runtime errors.",
22
+ ],
23
+ suggestions: [
24
+ "Ensure the returned value has a well-defined, specific type.",
25
+ ],
26
+ },
27
+ unsafeReturnAssignment: {
28
+ primary:
29
+ "Unsafe return of type `{{ sender }}` from function with return type `{{ receiver }}`.",
30
+ secondary: [
31
+ "The function's declared return type does not safely accept the value being returned.",
32
+ "This can allow unexpected types to propagate through your codebase, potentially causing runtime errors.",
33
+ ],
34
+ suggestions: [
35
+ "Adjust the return type of the function to match the returned value, if appropriate.",
36
+ "Otherwise, refine the returned value to ensure it matches the expected return type.",
37
+ ],
38
+ },
39
+ unsafeReturnThis: {
40
+ primary:
41
+ "Unsafe return of a value of type `{{ type }}`. `this` is typed as `any`.",
42
+ secondary: [
43
+ "Returning `this` when it is implicitly typed as `any` introduces type-unsafe behavior.",
44
+ "This can allow unexpected types to propagate through your codebase, potentially causing runtime errors.",
45
+ ],
46
+ suggestions: [
47
+ "Enable the `noImplicitThis` compiler option to enforce explicit `this` types.",
48
+ "Add an explicit `this` parameter to the function to clarify its type.",
49
+ ],
50
+ },
51
+ },
52
+ setup(context) {
53
+ const isNoImplicitThis = tsutils.isStrictCompilerOptionEnabled(
54
+ context.program.getCompilerOptions(),
55
+ "noImplicitThis",
56
+ );
57
+
58
+ function checkReturn(returnNode: ts.Node, reportingNode: ts.Node): void {
59
+ const type = context.typeChecker.getTypeAtLocation(returnNode);
60
+
61
+ const anyType = discriminateAnyType(
62
+ type,
63
+ context.typeChecker,
64
+ context.program,
65
+ returnNode,
66
+ );
67
+ const functionNode = ts.findAncestor(
68
+ returnNode,
69
+ // TODO: I believe isFunctionLikeDeclaration was incorrectly marked
70
+ // as deprecated in https://github.com/JoshuaKGoldberg/ts-api-utils/pull/124
71
+ // It says "With TypeScript v5, in favor of typescript's `isFunctionLike`."
72
+ // However, isFunctionLike also checks for signature-like nodes,
73
+ // whereas isFunctionLikeDeclaration checks only for function-like nodes.
74
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
75
+ tsutils.isFunctionLikeDeclaration,
76
+ );
77
+ if (!functionNode) {
78
+ return;
79
+ }
80
+
81
+ // function has an explicit return type, so ensure it's a safe return
82
+ const returnNodeType = getConstrainedTypeAtLocation(
83
+ returnNode,
84
+ context.typeChecker,
85
+ );
86
+
87
+ // function expressions will not have their return type modified based on receiver typing
88
+ // so we have to use the contextual typing in these cases, i.e.
89
+ // const foo1: () => Set<string> = () => new Set<any>();
90
+ // the return type of the arrow function is Set<any> even though the variable is typed as Set<string>
91
+ let functionType =
92
+ ts.isFunctionExpression(functionNode) ||
93
+ ts.isArrowFunction(functionNode)
94
+ ? context.typeChecker.getContextualType(functionNode)
95
+ : context.typeChecker.getTypeAtLocation(functionNode);
96
+ functionType ??= context.typeChecker.getTypeAtLocation(functionNode);
97
+ const callSignatures = tsutils.getCallSignaturesOfType(functionType);
98
+ // If there is an explicit type annotation *and* that type matches the actual
99
+ // function return type, we shouldn't complain (it's intentional, even if unsafe)
100
+ if (functionNode.type) {
101
+ for (const signature of callSignatures) {
102
+ const signatureReturnType = signature.getReturnType();
103
+
104
+ if (
105
+ returnNodeType === signatureReturnType ||
106
+ tsutils.isTypeFlagSet(
107
+ signatureReturnType,
108
+ ts.TypeFlags.Any | ts.TypeFlags.Unknown,
109
+ )
110
+ ) {
111
+ return;
112
+ }
113
+ if (
114
+ tsutils.includesModifier(
115
+ functionNode.modifiers,
116
+ ts.SyntaxKind.AsyncKeyword,
117
+ )
118
+ ) {
119
+ const awaitedSignatureReturnType =
120
+ context.typeChecker.getAwaitedType(signatureReturnType);
121
+
122
+ const awaitedReturnNodeType =
123
+ context.typeChecker.getAwaitedType(returnNodeType);
124
+ if (
125
+ awaitedReturnNodeType === awaitedSignatureReturnType ||
126
+ (awaitedSignatureReturnType &&
127
+ tsutils.isTypeFlagSet(
128
+ awaitedSignatureReturnType,
129
+ ts.TypeFlags.Any | ts.TypeFlags.Unknown,
130
+ ))
131
+ ) {
132
+ return;
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ if (anyType !== AnyType.Safe) {
139
+ // Allow cases when the declared return type of the function is either unknown or unknown[]
140
+ // and the function is returning any or any[].
141
+ for (const signature of callSignatures) {
142
+ const functionReturnType = signature.getReturnType();
143
+ if (
144
+ anyType === AnyType.Any &&
145
+ tsutils.isTypeFlagSet(functionReturnType, ts.TypeFlags.Unknown)
146
+ ) {
147
+ return;
148
+ }
149
+ if (
150
+ anyType === AnyType.AnyArray &&
151
+ context.typeChecker.isArrayType(functionReturnType) &&
152
+ tsutils.isTypeFlagSet(
153
+ context.typeChecker.getTypeArguments(
154
+ functionReturnType as ts.TypeReference,
155
+ )[0],
156
+ ts.TypeFlags.Unknown,
157
+ )
158
+ ) {
159
+ return;
160
+ }
161
+ const awaitedType =
162
+ context.typeChecker.getAwaitedType(functionReturnType);
163
+ if (
164
+ awaitedType &&
165
+ anyType === AnyType.PromiseAny &&
166
+ tsutils.isTypeFlagSet(awaitedType, ts.TypeFlags.Unknown)
167
+ ) {
168
+ return;
169
+ }
170
+ }
171
+
172
+ if (
173
+ anyType === AnyType.PromiseAny &&
174
+ !tsutils.includesModifier(
175
+ functionNode.modifiers,
176
+ ts.SyntaxKind.AsyncKeyword,
177
+ )
178
+ ) {
179
+ return;
180
+ }
181
+
182
+ let message: "unsafeReturn" | "unsafeReturnThis" = "unsafeReturn";
183
+ const isErrorType = tsutils.isIntrinsicErrorType(returnNodeType);
184
+
185
+ if (!isNoImplicitThis) {
186
+ // `return this`
187
+ const thisExpression = getThisExpression(returnNode);
188
+ if (
189
+ thisExpression &&
190
+ tsutils.isTypeFlagSet(
191
+ getConstrainedTypeAtLocation(thisExpression, context.typeChecker),
192
+ ts.TypeFlags.Any,
193
+ )
194
+ ) {
195
+ message = "unsafeReturnThis";
196
+ }
197
+ }
198
+
199
+ // If the function return type was not unknown/unknown[], mark usage as unsafeReturn.
200
+ context.report({
201
+ data: {
202
+ type: isErrorType
203
+ ? "error"
204
+ : anyType === AnyType.Any
205
+ ? "`any`"
206
+ : anyType === AnyType.PromiseAny
207
+ ? "`Promise<any>`"
208
+ : "`any[]`",
209
+ },
210
+ message,
211
+ range: {
212
+ begin: reportingNode.getStart(),
213
+ end: reportingNode.getEnd(),
214
+ },
215
+ });
216
+ return;
217
+ }
218
+
219
+ const signature = functionType.getCallSignatures().at(0);
220
+ if (signature) {
221
+ const functionReturnType = signature.getReturnType();
222
+ const result = isUnsafeAssignment(
223
+ returnNodeType,
224
+ functionReturnType,
225
+ context.typeChecker,
226
+ returnNode,
227
+ );
228
+ if (!result) {
229
+ return;
230
+ }
231
+
232
+ const { receiver, sender } = result;
233
+ context.report({
234
+ data: {
235
+ receiver: context.typeChecker.typeToString(receiver),
236
+ sender: context.typeChecker.typeToString(sender),
237
+ },
238
+ message: "unsafeReturnAssignment",
239
+ range: {
240
+ begin: reportingNode.getStart(),
241
+ end: reportingNode.getEnd(),
242
+ },
243
+ });
244
+ return;
245
+ }
246
+ }
247
+
248
+ return {
249
+ visitors: {
250
+ ArrowFunction: (node) => {
251
+ if (!ts.isBlock(node.body)) {
252
+ checkReturn(node.body, node.body);
253
+ }
254
+ },
255
+ ReturnStatement: (node) => {
256
+ if (node.expression != null) {
257
+ checkReturn(node.expression, node);
258
+ }
259
+ },
260
+ },
261
+ };
262
+ },
263
+ });
@@ -0,0 +1,308 @@
1
+ import rule from "./caseDuplicates.js";
2
+ import { ruleTester } from "./ruleTester.js";
3
+
4
+ ruleTester.describe(rule, {
5
+ invalid: [
6
+ {
7
+ code: `
8
+ switch (value) {
9
+ case 1:
10
+ break;
11
+ case 1:
12
+ break;
13
+ }
14
+ `,
15
+ snapshot: `
16
+ switch (value) {
17
+ case 1:
18
+ break;
19
+ case 1:
20
+ ~~~~~~
21
+ This case duplicates a previous case, so it will never be reached.
22
+ break;
23
+ }
24
+ `,
25
+ },
26
+ {
27
+ code: `
28
+ switch (value) {
29
+ case "a":
30
+ break;
31
+ case "b":
32
+ break;
33
+ case "a":
34
+ break;
35
+ }
36
+ `,
37
+ snapshot: `
38
+ switch (value) {
39
+ case "a":
40
+ break;
41
+ case "b":
42
+ break;
43
+ case "a":
44
+ ~~~~~~~~
45
+ This case duplicates a previous case, so it will never be reached.
46
+ break;
47
+ }
48
+ `,
49
+ },
50
+ {
51
+ code: `
52
+ switch (value) {
53
+ case x + 1:
54
+ break;
55
+ case x + 1:
56
+ break;
57
+ }
58
+ `,
59
+ snapshot: `
60
+ switch (value) {
61
+ case x + 1:
62
+ break;
63
+ case x + 1:
64
+ ~~~~~~~~~~
65
+ This case duplicates a previous case, so it will never be reached.
66
+ break;
67
+ }
68
+ `,
69
+ },
70
+ {
71
+ code: `
72
+ switch (value) {
73
+ case 1:
74
+ break;
75
+ case 2:
76
+ break;
77
+ case 1:
78
+ break;
79
+ }
80
+ `,
81
+ snapshot: `
82
+ switch (value) {
83
+ case 1:
84
+ break;
85
+ case 2:
86
+ break;
87
+ case 1:
88
+ ~~~~~~
89
+ This case duplicates a previous case, so it will never be reached.
90
+ break;
91
+ }
92
+ `,
93
+ },
94
+ {
95
+ code: `
96
+ switch (value) {
97
+ case true:
98
+ break;
99
+ case false:
100
+ break;
101
+ case true:
102
+ break;
103
+ }
104
+ `,
105
+ snapshot: `
106
+ switch (value) {
107
+ case true:
108
+ break;
109
+ case false:
110
+ break;
111
+ case true:
112
+ ~~~~~~~~~
113
+ This case duplicates a previous case, so it will never be reached.
114
+ break;
115
+ }
116
+ `,
117
+ },
118
+ {
119
+ code: `
120
+ switch (value) {
121
+ case obj.property:
122
+ break;
123
+ case obj.property:
124
+ break;
125
+ }
126
+ `,
127
+ snapshot: `
128
+ switch (value) {
129
+ case obj.property:
130
+ break;
131
+ case obj.property:
132
+ ~~~~~~~~~~~~~~~~~
133
+ This case duplicates a previous case, so it will never be reached.
134
+ break;
135
+ }
136
+ `,
137
+ },
138
+ {
139
+ code: `
140
+ const result = (input: number) => {
141
+ switch (input) {
142
+ case 1:
143
+ return "one";
144
+ case 2:
145
+ return "two";
146
+ case 1:
147
+ return "duplicate";
148
+ }
149
+ };
150
+ `,
151
+ snapshot: `
152
+ const result = (input: number) => {
153
+ switch (input) {
154
+ case 1:
155
+ return "one";
156
+ case 2:
157
+ return "two";
158
+ case 1:
159
+ ~~~~~~
160
+ This case duplicates a previous case, so it will never be reached.
161
+ return "duplicate";
162
+ }
163
+ };
164
+ `,
165
+ },
166
+ {
167
+ code: `
168
+ switch (value) {
169
+ case 1: case 1: break;
170
+ }
171
+ `,
172
+ snapshot: `
173
+ switch (value) {
174
+ case 1: case 1: break;
175
+ ~~~~~~
176
+ This case duplicates a previous case, so it will never be reached.
177
+ }
178
+ `,
179
+ },
180
+ {
181
+ code: `
182
+ const condition = true;
183
+ switch (value) {
184
+ case condition ? "a" : "b":
185
+ break;
186
+ case condition ? "a" : "b":
187
+ break;
188
+ }
189
+ `,
190
+ snapshot: `
191
+ const condition = true;
192
+ switch (value) {
193
+ case condition ? "a" : "b":
194
+ break;
195
+ case condition ? "a" : "b":
196
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
197
+ This case duplicates a previous case, so it will never be reached.
198
+ break;
199
+ }
200
+ `,
201
+ },
202
+ {
203
+ code: `
204
+ switch (value) {
205
+ case /* comment */ 1:
206
+ break;
207
+ case 1:
208
+ break;
209
+ }
210
+ `,
211
+ snapshot: `
212
+ switch (value) {
213
+ case /* comment */ 1:
214
+ break;
215
+ case 1:
216
+ ~~~~~~
217
+ This case duplicates a previous case, so it will never be reached.
218
+ break;
219
+ }
220
+ `,
221
+ },
222
+ {
223
+ code: `
224
+ switch (value) {
225
+ case obj.property:
226
+ break;
227
+ case obj /* comment */ .property:
228
+ break;
229
+ }
230
+ `,
231
+ snapshot: `
232
+ switch (value) {
233
+ case obj.property:
234
+ break;
235
+ case obj /* comment */ .property:
236
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
237
+ This case duplicates a previous case, so it will never be reached.
238
+ break;
239
+ }
240
+ `,
241
+ },
242
+ ],
243
+ valid: [
244
+ `switch (value) { case 1: break; case 2: break; }`,
245
+ `switch (value) { case 1: break; case 2: break; case 3: break; }`,
246
+ `switch (value) { case "a": break; case "b": break; }`,
247
+ `switch (value) { case true: break; case false: break; }`,
248
+ `switch (value) { case 1: case 2: break; }`,
249
+ `switch (value) { default: break; }`,
250
+ `
251
+ switch (value) {
252
+ case 1:
253
+ break;
254
+ case 2:
255
+ break;
256
+ }
257
+ `,
258
+ `
259
+ switch (value) {
260
+ case x:
261
+ break;
262
+ case y:
263
+ break;
264
+ }
265
+ `,
266
+ `
267
+ switch (value) {
268
+ case obj.a:
269
+ break;
270
+ case obj.b:
271
+ break;
272
+ }
273
+ `,
274
+ `
275
+ switch (value) {
276
+ case 1:
277
+ console.log("one");
278
+ break;
279
+ case 2:
280
+ console.log("two");
281
+ break;
282
+ default:
283
+ console.log("default");
284
+ break;
285
+ }
286
+ `,
287
+ `
288
+ switch (value) {
289
+ case x + 1:
290
+ break;
291
+ case x + 2:
292
+ break;
293
+ }
294
+ `,
295
+ `
296
+ const result = (input: number) => {
297
+ switch (input) {
298
+ case 1:
299
+ return "one";
300
+ case 2:
301
+ return "two";
302
+ default:
303
+ return "unknown";
304
+ }
305
+ };
306
+ `,
307
+ ],
308
+ });
@@ -0,0 +1,57 @@
1
+ import * as ts from "typescript";
2
+
3
+ import { typescriptLanguage } from "../language.js";
4
+ import { hasSameTokens } from "../utils/hasSameTokens.js";
5
+
6
+ export default typescriptLanguage.createRule({
7
+ about: {
8
+ description:
9
+ "Reports switch statements with duplicate case clause test expressions.",
10
+ id: "caseDuplicates",
11
+ preset: "logical",
12
+ },
13
+ messages: {
14
+ duplicateCase: {
15
+ primary:
16
+ "This case duplicates a previous case, so it will never be reached.",
17
+ secondary: [
18
+ "Having duplicate case clauses in a switch statement is a logic error.",
19
+ "The second case clause will never be reached because the first matching case will always execute first.",
20
+ ],
21
+ suggestions: [
22
+ "Remove the duplicate case clause or modify its test expression to be unique.",
23
+ ],
24
+ },
25
+ },
26
+ setup(context) {
27
+ return {
28
+ visitors: {
29
+ SwitchStatement: (node) => {
30
+ const seenCases: ts.Expression[] = [];
31
+
32
+ for (const clause of node.caseBlock.clauses) {
33
+ if (!ts.isCaseClause(clause)) {
34
+ continue;
35
+ }
36
+
37
+ const isDuplicate = seenCases.some((seenCase) =>
38
+ hasSameTokens(seenCase, clause.expression, context.sourceFile),
39
+ );
40
+
41
+ if (isDuplicate) {
42
+ context.report({
43
+ message: "duplicateCase",
44
+ range: {
45
+ begin: clause.getStart(context.sourceFile),
46
+ end: clause.expression.getEnd(),
47
+ },
48
+ });
49
+ } else {
50
+ seenCases.push(clause.expression);
51
+ }
52
+ }
53
+ },
54
+ },
55
+ };
56
+ },
57
+ });
@@ -234,6 +234,30 @@ class MyClass {
234
234
  second = 0;
235
235
  }
236
236
  }
237
+ `,
238
+ `
239
+ let result = "";
240
+ result += "a" + "b";
241
+ `,
242
+ `
243
+ let result = 0;
244
+ result += 1 + 2;
245
+ `,
246
+ `
247
+ let result = 0;
248
+ result += 1 * 2;
249
+ `,
250
+ `
251
+ let result = 0;
252
+ result += 1 - 2;
253
+ `,
254
+ `
255
+ let result = 0;
256
+ result += 1 / 2;
257
+ `,
258
+ `
259
+ let result = 0;
260
+ result += 1 ** 2;
237
261
  `,
238
262
  ],
239
263
  });
@@ -35,7 +35,10 @@ export default typescriptLanguage.createRule({
35
35
  }
36
36
 
37
37
  const rightSide = unwrapParenthesizedExpression(node.right);
38
- if (!ts.isBinaryExpression(rightSide)) {
38
+ if (
39
+ !ts.isBinaryExpression(rightSide) ||
40
+ !tsutils.isAssignmentKind(rightSide.operatorToken.kind)
41
+ ) {
39
42
  return;
40
43
  }
41
44
 
@@ -30,8 +30,8 @@ function test() {
30
30
  snapshot: `
31
31
  function test() {
32
32
  debugger;
33
- ~~~~~~~~~
34
- Debugger statements should not be used in production code.
33
+ ~~~~~~~~~
34
+ Debugger statements should not be used in production code.
35
35
  }
36
36
  `,
37
37
  suggestions: [
@@ -54,8 +54,8 @@ if (condition) {
54
54
  snapshot: `
55
55
  if (condition) {
56
56
  debugger;
57
- ~~~~~~~~~
58
- Debugger statements should not be used in production code.
57
+ ~~~~~~~~~
58
+ Debugger statements should not be used in production code.
59
59
  }
60
60
  `,
61
61
  suggestions: [