@ttsc/lint 0.5.0-dev.20260429

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.
@@ -0,0 +1,368 @@
1
+ package lint
2
+
3
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
+
5
+ // no-extra-boolean-cast: `if (!!x)`, `if (Boolean(x))`, `Boolean(!!x)` —
6
+ // the conversion is implicit in a boolean context.
7
+ // https://eslint.org/docs/latest/rules/no-extra-boolean-cast
8
+ type noExtraBooleanCast struct{}
9
+
10
+ func (noExtraBooleanCast) Name() string { return "no-extra-boolean-cast" }
11
+ func (noExtraBooleanCast) Visits() []shimast.Kind {
12
+ return []shimast.Kind{shimast.KindCallExpression, shimast.KindPrefixUnaryExpression}
13
+ }
14
+ func (noExtraBooleanCast) Check(ctx *Context, node *shimast.Node) {
15
+ switch node.Kind {
16
+ case shimast.KindCallExpression:
17
+ call := node.AsCallExpression()
18
+ if call == nil {
19
+ return
20
+ }
21
+ if identifierText(call.Expression) != "Boolean" {
22
+ return
23
+ }
24
+ if isInBooleanContext(node) {
25
+ ctx.Report(node, "Redundant Boolean call.")
26
+ }
27
+ case shimast.KindPrefixUnaryExpression:
28
+ outer := node.AsPrefixUnaryExpression()
29
+ if outer == nil || outer.Operator != shimast.KindExclamationToken {
30
+ return
31
+ }
32
+ operand := outer.Operand
33
+ if operand == nil || operand.Kind != shimast.KindPrefixUnaryExpression {
34
+ return
35
+ }
36
+ inner := operand.AsPrefixUnaryExpression()
37
+ if inner == nil || inner.Operator != shimast.KindExclamationToken {
38
+ return
39
+ }
40
+ if isInBooleanContext(node) {
41
+ ctx.Report(node, "Redundant double negation.")
42
+ }
43
+ }
44
+ }
45
+
46
+ // isInBooleanContext walks up the parent chain to determine whether the
47
+ // expression's value is consumed as a boolean (test of an if/while/for/
48
+ // ternary, or operand of `!`).
49
+ func isInBooleanContext(node *shimast.Node) bool {
50
+ parent := node.Parent
51
+ for parent != nil && parent.Kind == shimast.KindParenthesizedExpression {
52
+ parent = parent.Parent
53
+ }
54
+ if parent == nil {
55
+ return false
56
+ }
57
+ switch parent.Kind {
58
+ case shimast.KindIfStatement:
59
+ return parent.AsIfStatement().Expression == skipParents(node)
60
+ case shimast.KindWhileStatement:
61
+ return parent.AsWhileStatement().Expression == skipParents(node)
62
+ case shimast.KindDoStatement:
63
+ return parent.AsDoStatement().Expression == skipParents(node)
64
+ case shimast.KindForStatement:
65
+ return parent.AsForStatement().Condition == skipParents(node)
66
+ case shimast.KindConditionalExpression:
67
+ return parent.AsConditionalExpression().Condition == skipParents(node)
68
+ case shimast.KindPrefixUnaryExpression:
69
+ return parent.AsPrefixUnaryExpression().Operator == shimast.KindExclamationToken
70
+ }
71
+ return false
72
+ }
73
+
74
+ func skipParents(node *shimast.Node) *shimast.Node {
75
+ for node != nil && node.Parent != nil && node.Parent.Kind == shimast.KindParenthesizedExpression {
76
+ node = node.Parent
77
+ }
78
+ return node
79
+ }
80
+
81
+ // no-unsafe-negation: `!a in b` and `!a instanceof b` — the parser
82
+ // applies the negation to `a`, not to the whole comparison.
83
+ // https://eslint.org/docs/latest/rules/no-unsafe-negation
84
+ type noUnsafeNegation struct{}
85
+
86
+ func (noUnsafeNegation) Name() string { return "no-unsafe-negation" }
87
+ func (noUnsafeNegation) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBinaryExpression} }
88
+ func (noUnsafeNegation) Check(ctx *Context, node *shimast.Node) {
89
+ expr := node.AsBinaryExpression()
90
+ if expr == nil || expr.OperatorToken == nil {
91
+ return
92
+ }
93
+ switch expr.OperatorToken.Kind {
94
+ case shimast.KindInKeyword, shimast.KindInstanceOfKeyword:
95
+ default:
96
+ return
97
+ }
98
+ if expr.Left == nil || expr.Left.Kind != shimast.KindPrefixUnaryExpression {
99
+ return
100
+ }
101
+ prefix := expr.Left.AsPrefixUnaryExpression()
102
+ if prefix == nil || prefix.Operator != shimast.KindExclamationToken {
103
+ return
104
+ }
105
+ ctx.Report(node, "Unexpected negating the left operand of a relational operator.")
106
+ }
107
+
108
+ // eqeqeq: enforce `===` / `!==`. Default mode matches ESLint's `always`
109
+ // preset — no exceptions for null comparison.
110
+ // https://eslint.org/docs/latest/rules/eqeqeq
111
+ type eqeqeq struct{}
112
+
113
+ func (eqeqeq) Name() string { return "eqeqeq" }
114
+ func (eqeqeq) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBinaryExpression} }
115
+ func (eqeqeq) Check(ctx *Context, node *shimast.Node) {
116
+ expr := node.AsBinaryExpression()
117
+ if expr == nil || expr.OperatorToken == nil {
118
+ return
119
+ }
120
+ switch expr.OperatorToken.Kind {
121
+ case shimast.KindEqualsEqualsToken:
122
+ ctx.ReportRange(expr.OperatorToken.Pos(), expr.OperatorToken.End(), "Expected '===' and instead saw '=='.")
123
+ case shimast.KindExclamationEqualsToken:
124
+ ctx.ReportRange(expr.OperatorToken.Pos(), expr.OperatorToken.End(), "Expected '!==' and instead saw '!='.")
125
+ }
126
+ }
127
+
128
+ // use-isnan: `x === NaN` is always false. Use `Number.isNaN(x)`.
129
+ // https://eslint.org/docs/latest/rules/use-isnan
130
+ type useIsnan struct{}
131
+
132
+ func (useIsnan) Name() string { return "use-isnan" }
133
+ func (useIsnan) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBinaryExpression} }
134
+ func (useIsnan) Check(ctx *Context, node *shimast.Node) {
135
+ expr := node.AsBinaryExpression()
136
+ if expr == nil || expr.OperatorToken == nil {
137
+ return
138
+ }
139
+ if !isComparisonOperator(expr.OperatorToken.Kind) {
140
+ return
141
+ }
142
+ if identifierText(expr.Left) == "NaN" || identifierText(expr.Right) == "NaN" {
143
+ ctx.Report(node, "Use the isNaN function to compare with NaN.")
144
+ }
145
+ }
146
+
147
+ // valid-typeof: typeof expressions can only be compared to known type
148
+ // strings. Catches `typeof x === "stirng"`.
149
+ // https://eslint.org/docs/latest/rules/valid-typeof
150
+ type validTypeof struct{}
151
+
152
+ func (validTypeof) Name() string { return "valid-typeof" }
153
+ func (validTypeof) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBinaryExpression} }
154
+ func (validTypeof) Check(ctx *Context, node *shimast.Node) {
155
+ expr := node.AsBinaryExpression()
156
+ if expr == nil || expr.OperatorToken == nil {
157
+ return
158
+ }
159
+ if !isComparisonOperator(expr.OperatorToken.Kind) {
160
+ return
161
+ }
162
+ left := stripParens(expr.Left)
163
+ right := stripParens(expr.Right)
164
+ var literal *shimast.Node
165
+ if left != nil && left.Kind == shimast.KindTypeOfExpression {
166
+ literal = right
167
+ } else if right != nil && right.Kind == shimast.KindTypeOfExpression {
168
+ literal = left
169
+ } else {
170
+ return
171
+ }
172
+ if literal == nil {
173
+ return
174
+ }
175
+ value := stringLiteralText(literal)
176
+ if value == "" {
177
+ return
178
+ }
179
+ if !isValidTypeofString(value) {
180
+ ctx.Report(literal, "Invalid typeof comparison value.")
181
+ }
182
+ }
183
+
184
+ func isValidTypeofString(value string) bool {
185
+ switch value {
186
+ case "undefined", "object", "boolean", "number", "string", "function", "symbol", "bigint":
187
+ return true
188
+ }
189
+ return false
190
+ }
191
+
192
+ // no-compare-neg-zero: `x === -0`. Comparison ignores the sign — use
193
+ // `Object.is(x, -0)` if you really mean it.
194
+ // https://eslint.org/docs/latest/rules/no-compare-neg-zero
195
+ type noCompareNegZero struct{}
196
+
197
+ func (noCompareNegZero) Name() string { return "no-compare-neg-zero" }
198
+ func (noCompareNegZero) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBinaryExpression} }
199
+ func (noCompareNegZero) Check(ctx *Context, node *shimast.Node) {
200
+ expr := node.AsBinaryExpression()
201
+ if expr == nil || expr.OperatorToken == nil {
202
+ return
203
+ }
204
+ if !isComparisonOperator(expr.OperatorToken.Kind) {
205
+ return
206
+ }
207
+ if isNegZero(expr.Left) || isNegZero(expr.Right) {
208
+ ctx.Report(node, "Do not use the '-0' literal in comparisons.")
209
+ }
210
+ }
211
+
212
+ func isNegZero(node *shimast.Node) bool {
213
+ if node == nil || node.Kind != shimast.KindPrefixUnaryExpression {
214
+ return false
215
+ }
216
+ prefix := node.AsPrefixUnaryExpression()
217
+ if prefix == nil || prefix.Operator != shimast.KindMinusToken || prefix.Operand == nil {
218
+ return false
219
+ }
220
+ return numericLiteralText(prefix.Operand) == "0"
221
+ }
222
+
223
+ // no-cond-assign: `if (a = b)` is almost always a typo for `if (a == b)`.
224
+ // Default mode matches ESLint's `except-parens` — wrapping in `( )`
225
+ // silences the rule.
226
+ // https://eslint.org/docs/latest/rules/no-cond-assign
227
+ type noCondAssign struct{}
228
+
229
+ func (noCondAssign) Name() string { return "no-cond-assign" }
230
+ func (noCondAssign) Visits() []shimast.Kind {
231
+ return []shimast.Kind{
232
+ shimast.KindIfStatement,
233
+ shimast.KindWhileStatement,
234
+ shimast.KindDoStatement,
235
+ shimast.KindForStatement,
236
+ shimast.KindConditionalExpression,
237
+ }
238
+ }
239
+ func (noCondAssign) Check(ctx *Context, node *shimast.Node) {
240
+ var test *shimast.Node
241
+ switch node.Kind {
242
+ case shimast.KindIfStatement:
243
+ test = node.AsIfStatement().Expression
244
+ case shimast.KindWhileStatement:
245
+ test = node.AsWhileStatement().Expression
246
+ case shimast.KindDoStatement:
247
+ test = node.AsDoStatement().Expression
248
+ case shimast.KindForStatement:
249
+ test = node.AsForStatement().Condition
250
+ case shimast.KindConditionalExpression:
251
+ test = node.AsConditionalExpression().Condition
252
+ }
253
+ if test == nil {
254
+ return
255
+ }
256
+ // `except-parens`: an explicitly parenthesized assignment is OK.
257
+ if test.Kind == shimast.KindParenthesizedExpression {
258
+ return
259
+ }
260
+ if isAssignmentExpression(test) {
261
+ ctx.Report(test, "Expected a conditional expression and instead saw an assignment.")
262
+ }
263
+ }
264
+
265
+ func isAssignmentExpression(node *shimast.Node) bool {
266
+ if node == nil || node.Kind != shimast.KindBinaryExpression {
267
+ return false
268
+ }
269
+ expr := node.AsBinaryExpression()
270
+ if expr == nil || expr.OperatorToken == nil {
271
+ return false
272
+ }
273
+ return isAssignmentOperator(expr.OperatorToken.Kind)
274
+ }
275
+
276
+ func isAssignmentOperator(kind shimast.Kind) bool {
277
+ switch kind {
278
+ case shimast.KindEqualsToken,
279
+ shimast.KindPlusEqualsToken,
280
+ shimast.KindMinusEqualsToken:
281
+ return true
282
+ }
283
+ return false
284
+ }
285
+
286
+ // no-constant-condition: `if (true)`, `while (1)`, `if (literal)`. Often
287
+ // the result of leftover debug code or a typo.
288
+ // https://eslint.org/docs/latest/rules/no-constant-condition
289
+ type noConstantCondition struct{}
290
+
291
+ func (noConstantCondition) Name() string { return "no-constant-condition" }
292
+ func (noConstantCondition) Visits() []shimast.Kind {
293
+ return []shimast.Kind{
294
+ shimast.KindIfStatement,
295
+ shimast.KindWhileStatement,
296
+ shimast.KindDoStatement,
297
+ shimast.KindForStatement,
298
+ shimast.KindConditionalExpression,
299
+ }
300
+ }
301
+ func (noConstantCondition) Check(ctx *Context, node *shimast.Node) {
302
+ var test *shimast.Node
303
+ switch node.Kind {
304
+ case shimast.KindIfStatement:
305
+ test = node.AsIfStatement().Expression
306
+ case shimast.KindWhileStatement:
307
+ test = node.AsWhileStatement().Expression
308
+ case shimast.KindDoStatement:
309
+ test = node.AsDoStatement().Expression
310
+ case shimast.KindForStatement:
311
+ test = node.AsForStatement().Condition
312
+ case shimast.KindConditionalExpression:
313
+ test = node.AsConditionalExpression().Condition
314
+ }
315
+ test = stripParens(test)
316
+ if test == nil {
317
+ return // covers `for (;;)` — omitted condition is idiomatic.
318
+ }
319
+ // Allow `while (true)` since it's a common deliberate idiom.
320
+ if node.Kind == shimast.KindWhileStatement {
321
+ if v, ok := isLiteralBoolean(test); ok && v {
322
+ return
323
+ }
324
+ }
325
+ if isConstantTruthyOrFalsy(test) {
326
+ ctx.Report(test, "Unexpected constant condition.")
327
+ }
328
+ }
329
+
330
+ func isConstantTruthyOrFalsy(node *shimast.Node) bool {
331
+ if node == nil {
332
+ return false
333
+ }
334
+ switch node.Kind {
335
+ case
336
+ shimast.KindNumericLiteral,
337
+ shimast.KindBigIntLiteral,
338
+ shimast.KindStringLiteral,
339
+ shimast.KindNoSubstitutionTemplateLiteral,
340
+ shimast.KindRegularExpressionLiteral,
341
+ shimast.KindTrueKeyword,
342
+ shimast.KindFalseKeyword,
343
+ shimast.KindNullKeyword,
344
+ shimast.KindArrayLiteralExpression,
345
+ shimast.KindObjectLiteralExpression,
346
+ shimast.KindArrowFunction,
347
+ shimast.KindFunctionExpression:
348
+ return true
349
+ case shimast.KindPrefixUnaryExpression:
350
+ prefix := node.AsPrefixUnaryExpression()
351
+ if prefix == nil {
352
+ return false
353
+ }
354
+ return isConstantTruthyOrFalsy(prefix.Operand)
355
+ }
356
+ return false
357
+ }
358
+
359
+ func init() {
360
+ Register(noExtraBooleanCast{})
361
+ Register(noUnsafeNegation{})
362
+ Register(eqeqeq{})
363
+ Register(useIsnan{})
364
+ Register(validTypeof{})
365
+ Register(noCompareNegZero{})
366
+ Register(noCondAssign{})
367
+ Register(noConstantCondition{})
368
+ }
@@ -0,0 +1,107 @@
1
+ package lint
2
+
3
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
+
5
+ // for-direction: `for (var i = 10; i < 20; i--)` will never terminate.
6
+ // https://eslint.org/docs/latest/rules/for-direction
7
+ type forDirection struct{}
8
+
9
+ func (forDirection) Name() string { return "for-direction" }
10
+ func (forDirection) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindForStatement} }
11
+ func (forDirection) Check(ctx *Context, node *shimast.Node) {
12
+ loop := node.AsForStatement()
13
+ if loop == nil || loop.Condition == nil || loop.Incrementor == nil {
14
+ return
15
+ }
16
+ cond := loop.Condition
17
+ if cond.Kind != shimast.KindBinaryExpression {
18
+ return
19
+ }
20
+ bin := cond.AsBinaryExpression()
21
+ if bin == nil || bin.OperatorToken == nil {
22
+ return
23
+ }
24
+ leftName := identifierText(bin.Left)
25
+ rightName := identifierText(bin.Right)
26
+ if leftName == "" && rightName == "" {
27
+ return
28
+ }
29
+ directionFromCondition := 0
30
+ switch bin.OperatorToken.Kind {
31
+ case shimast.KindLessThanToken, shimast.KindLessThanEqualsToken:
32
+ // `i < limit` requires increment
33
+ if leftName != "" {
34
+ directionFromCondition = +1
35
+ } else {
36
+ directionFromCondition = -1
37
+ }
38
+ case shimast.KindGreaterThanToken, shimast.KindGreaterThanEqualsToken:
39
+ if leftName != "" {
40
+ directionFromCondition = -1
41
+ } else {
42
+ directionFromCondition = +1
43
+ }
44
+ default:
45
+ return
46
+ }
47
+ counterName := leftName
48
+ if counterName == "" {
49
+ counterName = rightName
50
+ }
51
+ directionFromIncr := updateDirection(loop.Incrementor, counterName)
52
+ if directionFromIncr == 0 {
53
+ return
54
+ }
55
+ if directionFromIncr*directionFromCondition < 0 {
56
+ ctx.Report(loop.Incrementor, "The update clause in this loop moves the variable in the wrong direction.")
57
+ }
58
+ }
59
+
60
+ func updateDirection(node *shimast.Node, counter string) int {
61
+ if node == nil || counter == "" {
62
+ return 0
63
+ }
64
+ switch node.Kind {
65
+ case shimast.KindPostfixUnaryExpression:
66
+ post := node.AsPostfixUnaryExpression()
67
+ if post == nil || identifierText(post.Operand) != counter {
68
+ return 0
69
+ }
70
+ switch post.Operator {
71
+ case shimast.KindPlusPlusToken:
72
+ return +1
73
+ case shimast.KindMinusMinusToken:
74
+ return -1
75
+ }
76
+ case shimast.KindPrefixUnaryExpression:
77
+ pre := node.AsPrefixUnaryExpression()
78
+ if pre == nil || identifierText(pre.Operand) != counter {
79
+ return 0
80
+ }
81
+ switch pre.Operator {
82
+ case shimast.KindPlusPlusToken:
83
+ return +1
84
+ case shimast.KindMinusMinusToken:
85
+ return -1
86
+ }
87
+ case shimast.KindBinaryExpression:
88
+ bin := node.AsBinaryExpression()
89
+ if bin == nil || bin.OperatorToken == nil {
90
+ return 0
91
+ }
92
+ if identifierText(bin.Left) != counter {
93
+ return 0
94
+ }
95
+ switch bin.OperatorToken.Kind {
96
+ case shimast.KindPlusEqualsToken:
97
+ return +1
98
+ case shimast.KindMinusEqualsToken:
99
+ return -1
100
+ }
101
+ }
102
+ return 0
103
+ }
104
+
105
+ func init() {
106
+ Register(forDirection{})
107
+ }
@@ -0,0 +1,69 @@
1
+ package lint
2
+
3
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
+
5
+ // radix: `parseInt(x)` without an explicit radix can hit the legacy
6
+ // "leading 0 means octal" trap. ESLint default mode requires the radix.
7
+ // https://eslint.org/docs/latest/rules/radix
8
+ type radix struct{}
9
+
10
+ func (radix) Name() string { return "radix" }
11
+ func (radix) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindCallExpression} }
12
+ func (radix) Check(ctx *Context, node *shimast.Node) {
13
+ call := node.AsCallExpression()
14
+ if call == nil {
15
+ return
16
+ }
17
+ name := callCalleeName(call)
18
+ if name != "parseInt" && name != "Number.parseInt" && !isMatchingPropertyAccess(call.Expression, "Number", "parseInt") {
19
+ return
20
+ }
21
+ args := 0
22
+ if call.Arguments != nil {
23
+ args = len(call.Arguments.Nodes)
24
+ }
25
+ if args == 0 {
26
+ return
27
+ }
28
+ if args == 1 {
29
+ ctx.Report(node, "Missing radix parameter.")
30
+ return
31
+ }
32
+ radixArg := call.Arguments.Nodes[1]
33
+ radixArg = stripParens(radixArg)
34
+ if radixArg == nil {
35
+ ctx.Report(node, "Missing radix parameter.")
36
+ return
37
+ }
38
+ if radixArg.Kind == shimast.KindNumericLiteral {
39
+ text := numericLiteralText(radixArg)
40
+ if text == "10" || text == "16" || text == "8" || text == "2" {
41
+ return
42
+ }
43
+ ctx.Report(radixArg, "Invalid radix parameter.")
44
+ return
45
+ }
46
+ }
47
+
48
+ // no-new-wrappers: `new String("")`, `new Number(0)`, `new Boolean(false)`
49
+ // build wrapper objects rarely intended.
50
+ // https://eslint.org/docs/latest/rules/no-new-wrappers
51
+ type noNewWrappers struct{}
52
+
53
+ func (noNewWrappers) Name() string { return "no-new-wrappers" }
54
+ func (noNewWrappers) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindNewExpression} }
55
+ func (noNewWrappers) Check(ctx *Context, node *shimast.Node) {
56
+ ne := node.AsNewExpression()
57
+ if ne == nil {
58
+ return
59
+ }
60
+ switch identifierText(ne.Expression) {
61
+ case "String", "Number", "Boolean":
62
+ ctx.Report(node, "Do not use "+identifierText(ne.Expression)+" as a constructor.")
63
+ }
64
+ }
65
+
66
+ func init() {
67
+ Register(radix{})
68
+ Register(noNewWrappers{})
69
+ }