@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,177 @@
1
+ import rule from "./elseIfDuplicates.js";
2
+ import { ruleTester } from "./ruleTester.js";
3
+
4
+ ruleTester.describe(rule, {
5
+ invalid: [
6
+ {
7
+ code: `
8
+ if (isSomething(value)) {
9
+ doFirst();
10
+ } else if (isSomething(value)) {
11
+ doSecond();
12
+ }
13
+ `,
14
+ snapshot: `
15
+ if (isSomething(value)) {
16
+ doFirst();
17
+ } else if (isSomething(value)) {
18
+ ~~~~~~~~~~~~~~~~~~
19
+ This condition is identical to a previous condition in the if-else-if chain.
20
+ doSecond();
21
+ }
22
+ `,
23
+ },
24
+ {
25
+ code: `
26
+ if (isSomething /* ... */ (value)) {
27
+ doFirst();
28
+ } else if (isSomething(value /* ... */)) {
29
+ doSecond();
30
+ }
31
+ `,
32
+ snapshot: `
33
+ if (isSomething /* ... */ (value)) {
34
+ doFirst();
35
+ } else if (isSomething(value /* ... */)) {
36
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
37
+ This condition is identical to a previous condition in the if-else-if chain.
38
+ doSecond();
39
+ }
40
+ `,
41
+ },
42
+ {
43
+ code: `
44
+ if (first) {
45
+ handleFirst();
46
+ } else if (second) {
47
+ handleSecond();
48
+ } else if (third && fourth) {
49
+ handleThird();
50
+ } else if (third && fourth) {
51
+ handleFourth();
52
+ }
53
+ `,
54
+ snapshot: `
55
+ if (first) {
56
+ handleFirst();
57
+ } else if (second) {
58
+ handleSecond();
59
+ } else if (third && fourth) {
60
+ handleThird();
61
+ } else if (third && fourth) {
62
+ ~~~~~~~~~~~~~~~
63
+ This condition is identical to a previous condition in the if-else-if chain.
64
+ handleFourth();
65
+ }
66
+ `,
67
+ },
68
+ {
69
+ code: `
70
+ if (count === 1) {
71
+ handleOne();
72
+ } else if (count === 2) {
73
+ handleTwo();
74
+ } else if (count === 3) {
75
+ handleThree();
76
+ } else if (count === 2) {
77
+ handleFour();
78
+ } else if (count === 5) {
79
+ handleFive();
80
+ }
81
+ `,
82
+ snapshot: `
83
+ if (count === 1) {
84
+ handleOne();
85
+ } else if (count === 2) {
86
+ handleTwo();
87
+ } else if (count === 3) {
88
+ handleThree();
89
+ } else if (count === 2) {
90
+ ~~~~~~~~~~~
91
+ This condition is identical to a previous condition in the if-else-if chain.
92
+ handleFour();
93
+ } else if (count === 5) {
94
+ handleFive();
95
+ }
96
+ `,
97
+ },
98
+ {
99
+ code: `
100
+ if (value === "first") {
101
+ process();
102
+ } else if (value === "second") {
103
+ process();
104
+ } else if (value === "first") {
105
+ process();
106
+ }
107
+ `,
108
+ snapshot: `
109
+ if (value === "first") {
110
+ process();
111
+ } else if (value === "second") {
112
+ process();
113
+ } else if (value === "first") {
114
+ ~~~~~~~~~~~~~~~~~
115
+ This condition is identical to a previous condition in the if-else-if chain.
116
+ process();
117
+ }
118
+ `,
119
+ },
120
+ {
121
+ code: `
122
+ if (isValid && isActive) {
123
+ execute();
124
+ } else if (isPending) {
125
+ wait();
126
+ } else if (isValid && isActive) {
127
+ execute();
128
+ }
129
+ `,
130
+ snapshot: `
131
+ if (isValid && isActive) {
132
+ execute();
133
+ } else if (isPending) {
134
+ wait();
135
+ } else if (isValid && isActive) {
136
+ ~~~~~~~~~~~~~~~~~~~
137
+ This condition is identical to a previous condition in the if-else-if chain.
138
+ execute();
139
+ }
140
+ `,
141
+ },
142
+ {
143
+ code: `
144
+ if (value) {
145
+ first();
146
+ } else if (value) {
147
+ second();
148
+ } else if (value) {
149
+ third();
150
+ }
151
+ `,
152
+ snapshot: `
153
+ if (value) {
154
+ first();
155
+ } else if (value) {
156
+ ~~~~~
157
+ This condition is identical to a previous condition in the if-else-if chain.
158
+ second();
159
+ } else if (value) {
160
+ ~~~~~
161
+ This condition is identical to a previous condition in the if-else-if chain.
162
+ third();
163
+ }
164
+ `,
165
+ },
166
+ ],
167
+ valid: [
168
+ `if (isSomething(value)) { doFirst(); } else if (isSomethingElse(value)) { doSecond(); }`,
169
+ `if (first) { handleFirst(); } else if (second) { handleSecond(); } else if (third && fourth) { handleThird(); } else if (third && fifth) { handleFourth(); }`,
170
+ `if (count === 1) { handleOne(); } else if (count === 2) { handleTwo(); } else if (count === 3) { handleThree(); } else if (count === 4) { handleFour(); }`,
171
+ `if (value === "first") { process(); } else if (value === "second") { process(); } else if (value === "third") { process(); }`,
172
+ `if (isValid) { execute(); } else if (isPending) { wait(); } else if (isInvalid) { reject(); }`,
173
+ `if (value) { process(); }`,
174
+ `if (first) { doFirst(); } else { doSecond(); }`,
175
+ `if (first) { doFirst(); } else if (second) { doSecond(); } else { doDefault(); }`,
176
+ ],
177
+ });
@@ -0,0 +1,69 @@
1
+ import * as ts from "typescript";
2
+
3
+ import { getTSNodeRange } from "../getTSNodeRange.js";
4
+ import { typescriptLanguage } from "../language.js";
5
+ import { hasSameTokens } from "../utils/hasSameTokens.js";
6
+
7
+ export default typescriptLanguage.createRule({
8
+ about: {
9
+ description:
10
+ "Reports duplicate conditions in if-else-if chains that make code unreachable.",
11
+ id: "elseIfDuplicates",
12
+ preset: "logical",
13
+ },
14
+ messages: {
15
+ duplicateCondition: {
16
+ primary:
17
+ "This condition is identical to a previous condition in the if-else-if chain.",
18
+ secondary: [
19
+ "When an if-else-if chain has duplicate conditions, the duplicate branch will never execute because the earlier identical condition will always be evaluated first.",
20
+ "This typically indicates a copy-paste error or logic mistake.",
21
+ ],
22
+ suggestions: [
23
+ "Verify the condition logic is correct and update to check for the intended condition.",
24
+ ],
25
+ },
26
+ },
27
+ setup(context) {
28
+ function checkIfStatement(node: ts.IfStatement) {
29
+ const seen: ts.Expression[] = [];
30
+ let current: ts.IfStatement = node;
31
+
32
+ while (true) {
33
+ if (
34
+ seen.some((previous) =>
35
+ hasSameTokens(previous, current.expression, context.sourceFile),
36
+ )
37
+ ) {
38
+ context.report({
39
+ message: "duplicateCondition",
40
+ range: getTSNodeRange(current.expression, context.sourceFile),
41
+ });
42
+ }
43
+
44
+ if (
45
+ !current.elseStatement ||
46
+ !ts.isIfStatement(current.elseStatement)
47
+ ) {
48
+ break;
49
+ }
50
+
51
+ seen.push(current.expression);
52
+ current = current.elseStatement;
53
+ }
54
+ }
55
+
56
+ return {
57
+ visitors: {
58
+ IfStatement: (node) => {
59
+ if (
60
+ !ts.isIfStatement(node.parent) ||
61
+ node.parent.elseStatement !== node
62
+ ) {
63
+ checkIfStatement(node);
64
+ }
65
+ },
66
+ },
67
+ };
68
+ },
69
+ });
@@ -0,0 +1,136 @@
1
+ import rule from "./emptyBlocks.js";
2
+ import { ruleTester } from "./ruleTester.js";
3
+
4
+ ruleTester.describe(rule, {
5
+ invalid: [
6
+ {
7
+ code: `
8
+ if (condition) {}
9
+ `,
10
+ snapshot: `
11
+ if (condition) {}
12
+ ~~
13
+ Empty block statements should be removed or contain code.
14
+ `,
15
+ },
16
+ {
17
+ code: `
18
+ if (condition) {
19
+ } else {}
20
+ `,
21
+ snapshot: `
22
+ if (condition) {
23
+ ~
24
+ Empty block statements should be removed or contain code.
25
+ } else {}
26
+ ~
27
+ ~~
28
+ Empty block statements should be removed or contain code.
29
+ `,
30
+ },
31
+ {
32
+ code: `
33
+ while (condition) {}
34
+ `,
35
+ snapshot: `
36
+ while (condition) {}
37
+ ~~
38
+ Empty block statements should be removed or contain code.
39
+ `,
40
+ },
41
+ {
42
+ code: `
43
+ for (let i = 0; i < 10; i++) {}
44
+ `,
45
+ snapshot: `
46
+ for (let i = 0; i < 10; i++) {}
47
+ ~~
48
+ Empty block statements should be removed or contain code.
49
+ `,
50
+ },
51
+ {
52
+ code: `
53
+ switch (value) {}
54
+ `,
55
+ snapshot: `
56
+ switch (value) {}
57
+ ~~
58
+ Empty block statements should be removed or contain code.
59
+ `,
60
+ },
61
+ {
62
+ code: `
63
+ do {} while (condition);
64
+ `,
65
+ snapshot: `
66
+ do {} while (condition);
67
+ ~~
68
+ Empty block statements should be removed or contain code.
69
+ `,
70
+ },
71
+ {
72
+ code: `
73
+ if (x) {
74
+ doSomething();
75
+ } else if (y) {}
76
+ `,
77
+ snapshot: `
78
+ if (x) {
79
+ doSomething();
80
+ } else if (y) {}
81
+ ~~
82
+ Empty block statements should be removed or contain code.
83
+ `,
84
+ },
85
+ {
86
+ code: `
87
+ try {
88
+ doSomething();
89
+ } finally {}
90
+ `,
91
+ snapshot: `
92
+ try {
93
+ doSomething();
94
+ } finally {}
95
+ ~~
96
+ Empty block statements should be removed or contain code.
97
+ `,
98
+ },
99
+ ],
100
+ valid: [
101
+ `if (condition) { doSomething(); }`,
102
+ `while (condition) { doSomething(); }`,
103
+ `for (let i = 0; i < 10; i++) { doSomething(); }`,
104
+ `switch (value) { case 1: break; }`,
105
+ `do { doSomething(); } while (condition);`,
106
+ `function test() {}`,
107
+ `const fn = function() {};`,
108
+ `const arrow = () => {};`,
109
+ `class MyClass { method() {} }`,
110
+ `class MyClass { constructor() {} }`,
111
+ `class MyClass { get value() {} }`,
112
+ `class MyClass { set value(v) {} }`,
113
+ `
114
+ if (condition) {
115
+ // Intentionally empty
116
+ }
117
+ `,
118
+ `
119
+ while (condition) {
120
+ /* Do nothing */
121
+ }
122
+ `,
123
+ `
124
+ try {
125
+ doSomething();
126
+ } catch (error) {}
127
+ `,
128
+ `
129
+ try {
130
+ doSomething();
131
+ } catch (error) {
132
+ // Ignore errors
133
+ }
134
+ `,
135
+ ],
136
+ });
@@ -0,0 +1,73 @@
1
+ import * as ts from "typescript";
2
+
3
+ import { getTSNodeRange } from "../getTSNodeRange.js";
4
+ import { typescriptLanguage } from "../language.js";
5
+
6
+ const allowedParents = new Set([
7
+ ts.SyntaxKind.ArrowFunction,
8
+ ts.SyntaxKind.CatchClause,
9
+ ts.SyntaxKind.Constructor,
10
+ ts.SyntaxKind.FunctionDeclaration,
11
+ ts.SyntaxKind.FunctionExpression,
12
+ ts.SyntaxKind.GetAccessor,
13
+ ts.SyntaxKind.MethodDeclaration,
14
+ ts.SyntaxKind.SetAccessor,
15
+ ]);
16
+
17
+ export default typescriptLanguage.createRule({
18
+ about: {
19
+ description: "Reports empty block statements that should contain code.",
20
+ id: "emptyBlocks",
21
+ preset: "stylistic",
22
+ },
23
+ messages: {
24
+ emptyBlock: {
25
+ primary: "Empty block statements should be removed or contain code.",
26
+ secondary: [
27
+ "Empty blocks can indicate incomplete code or areas where logic was removed but the block structure was left behind.",
28
+ "They can also reduce code readability by cluttering the codebase with unnecessary braces.",
29
+ ],
30
+ suggestions: [
31
+ "Add a comment explaining why the block is intentionally empty, or remove the empty block entirely.",
32
+ ],
33
+ },
34
+ },
35
+ setup(context) {
36
+ function hasComments(block: ts.Block): boolean {
37
+ const fullText = context.sourceFile.getFullText();
38
+
39
+ const openBrace = block.getStart(context.sourceFile);
40
+ const closeBrace = block.getEnd();
41
+ const innerText = fullText.substring(openBrace + 1, closeBrace - 1);
42
+
43
+ // Check if there are any non-whitespace characters (which would be comments)
44
+ // since we already know there are no statements
45
+ return /\S+/.test(innerText.trim());
46
+ }
47
+
48
+ function isEmptyBlock(block: ts.Block): boolean {
49
+ return block.statements.length === 0 && !hasComments(block);
50
+ }
51
+
52
+ return {
53
+ visitors: {
54
+ Block: (node) => {
55
+ if (!allowedParents.has(node.parent.kind) && isEmptyBlock(node)) {
56
+ context.report({
57
+ message: "emptyBlock",
58
+ range: getTSNodeRange(node, context.sourceFile),
59
+ });
60
+ }
61
+ },
62
+ CaseBlock: (node) => {
63
+ if (node.clauses.length === 0) {
64
+ context.report({
65
+ message: "emptyBlock",
66
+ range: getTSNodeRange(node, context.sourceFile),
67
+ });
68
+ }
69
+ },
70
+ },
71
+ };
72
+ },
73
+ });
@@ -0,0 +1,100 @@
1
+ import rule from "./forDirections.js";
2
+ import { ruleTester } from "./ruleTester.js";
3
+
4
+ ruleTester.describe(rule, {
5
+ invalid: [
6
+ {
7
+ code: `
8
+ for (let i = 0; i < 10; i--) {}
9
+ `,
10
+ snapshot: `
11
+ for (let i = 0; i < 10; i--) {}
12
+ ~~~
13
+ The update moves the counter in the wrong direction for this loop condition.
14
+ `,
15
+ },
16
+ {
17
+ code: `
18
+ for (let index = 0; index < 10; index--) {}
19
+ `,
20
+ snapshot: `
21
+ for (let index = 0; index < 10; index--) {}
22
+ ~~~~~~~
23
+ The update moves the counter in the wrong direction for this loop condition.
24
+ `,
25
+ },
26
+ {
27
+ code: `
28
+ for (let index = 10; index >= 0; index++) {}
29
+ `,
30
+ snapshot: `
31
+ for (let index = 10; index >= 0; index++) {}
32
+ ~~~~~~~
33
+ The update moves the counter in the wrong direction for this loop condition.
34
+ `,
35
+ },
36
+ {
37
+ code: `
38
+ for (let index = 0; index > 10; index++) {}
39
+ `,
40
+ snapshot: `
41
+ for (let index = 0; index > 10; index++) {}
42
+ ~~~~~~~
43
+ The update moves the counter in the wrong direction for this loop condition.
44
+ `,
45
+ },
46
+ {
47
+ code: `
48
+ for (let index = 0; 10 > index; index--) {}
49
+ `,
50
+ snapshot: `
51
+ for (let index = 0; 10 > index; index--) {}
52
+ ~~~~~~~
53
+ The update moves the counter in the wrong direction for this loop condition.
54
+ `,
55
+ },
56
+ {
57
+ code: `
58
+ for (let index = 0; index < 10; index -= 1) {}
59
+ `,
60
+ snapshot: `
61
+ for (let index = 0; index < 10; index -= 1) {}
62
+ ~~~~~~~~~~
63
+ The update moves the counter in the wrong direction for this loop condition.
64
+ `,
65
+ },
66
+ {
67
+ code: `
68
+ for (let index = 10; index > 0; index += 1) {}
69
+ `,
70
+ snapshot: `
71
+ for (let index = 10; index > 0; index += 1) {}
72
+ ~~~~~~~~~~
73
+ The update moves the counter in the wrong direction for this loop condition.
74
+ `,
75
+ },
76
+ {
77
+ code: `
78
+ for (let index = 0; index < 10; index += -1) {}
79
+ `,
80
+ snapshot: `
81
+ for (let index = 0; index < 10; index += -1) {}
82
+ ~~~~~~~~~~~
83
+ The update moves the counter in the wrong direction for this loop condition.
84
+ `,
85
+ },
86
+ ],
87
+ valid: [
88
+ `for (let i = 0; i < 10; i++) { }`,
89
+ `for (let index = 0; index < 10; index++) { }`,
90
+ `for (let index = 10; index >= 0; index--) { }`,
91
+ `for (let index = 0; 10 > index; index++) { }`,
92
+ `for (let index = 10; 0 < index; index--) { }`,
93
+ `for (let index = 10; index >= 0; index += step) { }`,
94
+ `for (let index = 0; index <= 10; index -= 0) { }`,
95
+ `for (let index = 0; index < 10; index += 1) { }`,
96
+ `for (let index = 10; index > 0; index -= 1) { }`,
97
+ `for (let index = 0; index < 10; index += 2) { }`,
98
+ `for (let index = 10; index > 0; index -= 2) { }`,
99
+ ],
100
+ });
@@ -0,0 +1,163 @@
1
+ import * as ts from "typescript";
2
+
3
+ import { getTSNodeRange } from "../getTSNodeRange.js";
4
+ import { typescriptLanguage } from "../language.js";
5
+
6
+ function getConditionDirection(condition: ts.Expression, counterName: string) {
7
+ if (!ts.isBinaryExpression(condition)) {
8
+ return undefined;
9
+ }
10
+
11
+ const leftName = getCounterName(condition.left);
12
+ const rightName = getCounterName(condition.right);
13
+
14
+ const isCounterLeft = leftName === counterName;
15
+ const isCounterRight = rightName === counterName;
16
+
17
+ if (!isCounterLeft && !isCounterRight) {
18
+ return undefined;
19
+ }
20
+
21
+ const { operatorToken } = condition;
22
+
23
+ if (isCounterLeft) {
24
+ if (
25
+ operatorToken.kind === ts.SyntaxKind.LessThanToken ||
26
+ operatorToken.kind === ts.SyntaxKind.LessThanEqualsToken
27
+ ) {
28
+ return 1;
29
+ }
30
+ if (
31
+ operatorToken.kind === ts.SyntaxKind.GreaterThanToken ||
32
+ operatorToken.kind === ts.SyntaxKind.GreaterThanEqualsToken
33
+ ) {
34
+ return -1;
35
+ }
36
+ } else {
37
+ if (
38
+ operatorToken.kind === ts.SyntaxKind.LessThanToken ||
39
+ operatorToken.kind === ts.SyntaxKind.LessThanEqualsToken
40
+ ) {
41
+ return -1;
42
+ }
43
+ if (
44
+ operatorToken.kind === ts.SyntaxKind.GreaterThanToken ||
45
+ operatorToken.kind === ts.SyntaxKind.GreaterThanEqualsToken
46
+ ) {
47
+ return 1;
48
+ }
49
+ }
50
+
51
+ return undefined;
52
+ }
53
+
54
+ function getCounterName(node: ts.Expression) {
55
+ return ts.isIdentifier(node) ? node.text : undefined;
56
+ }
57
+
58
+ function getUpdateDirection(update: ts.Expression) {
59
+ if (
60
+ ts.isPostfixUnaryExpression(update) ||
61
+ ts.isPrefixUnaryExpression(update)
62
+ ) {
63
+ switch (update.operator) {
64
+ case ts.SyntaxKind.MinusMinusToken:
65
+ return -1;
66
+ case ts.SyntaxKind.PlusPlusToken:
67
+ return 1;
68
+ default:
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ if (ts.isBinaryExpression(update)) {
74
+ const { operatorToken, right } = update;
75
+
76
+ if (
77
+ operatorToken.kind === ts.SyntaxKind.PlusEqualsToken ||
78
+ operatorToken.kind === ts.SyntaxKind.MinusEqualsToken
79
+ ) {
80
+ if (ts.isNumericLiteral(right)) {
81
+ const value = Number(right.text);
82
+ if (operatorToken.kind === ts.SyntaxKind.PlusEqualsToken) {
83
+ return value > 0 ? 1 : value < 0 ? -1 : 0;
84
+ }
85
+ return value > 0 ? -1 : value < 0 ? 1 : 0;
86
+ }
87
+
88
+ if (
89
+ ts.isPrefixUnaryExpression(right) &&
90
+ right.operator === ts.SyntaxKind.MinusToken &&
91
+ ts.isNumericLiteral(right.operand)
92
+ ) {
93
+ const value = Number(right.operand.text);
94
+ if (operatorToken.kind === ts.SyntaxKind.PlusEqualsToken) {
95
+ return value > 0 ? -1 : 1;
96
+ }
97
+ return value > 0 ? 1 : -1;
98
+ }
99
+ }
100
+ }
101
+
102
+ return undefined;
103
+ }
104
+
105
+ export default typescriptLanguage.createRule({
106
+ about: {
107
+ description:
108
+ "Reports for loops with counter variables that move in the wrong direction.",
109
+ id: "forDirections",
110
+ preset: "stylistic",
111
+ },
112
+ messages: {
113
+ wrongDirection: {
114
+ primary:
115
+ "The update moves the counter in the wrong direction for this loop condition.",
116
+ secondary: [
117
+ "A for loop with a counter that moves in the wrong direction relative to its stop condition will run infinitely or never execute.",
118
+ "When the counter increases but the condition expects it to decrease (or vice versa), the loop logic is incorrect.",
119
+ ],
120
+ suggestions: [
121
+ "Verify the loop counter update direction matches the condition.",
122
+ ],
123
+ },
124
+ },
125
+ setup(context) {
126
+ return {
127
+ visitors: {
128
+ ForStatement: (node) => {
129
+ if (
130
+ !node.condition ||
131
+ !node.incrementor ||
132
+ !node.initializer ||
133
+ !ts.isVariableDeclarationList(node.initializer)
134
+ ) {
135
+ return;
136
+ }
137
+
138
+ const declaration = node.initializer.declarations.at(0);
139
+ if (!declaration || !ts.isIdentifier(declaration.name)) {
140
+ return;
141
+ }
142
+
143
+ const updateDirection = getUpdateDirection(node.incrementor);
144
+ if (!updateDirection) {
145
+ return;
146
+ }
147
+
148
+ const conditionDirection = getConditionDirection(
149
+ node.condition,
150
+ declaration.name.text,
151
+ );
152
+
153
+ if (updateDirection !== conditionDirection) {
154
+ context.report({
155
+ message: "wrongDirection",
156
+ range: getTSNodeRange(node.incrementor, context.sourceFile),
157
+ });
158
+ }
159
+ },
160
+ },
161
+ };
162
+ },
163
+ });