@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.
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/go-plugin/go.mod +38 -0
- package/go-plugin/lint/ast_helpers.go +170 -0
- package/go-plugin/lint/compile.go +393 -0
- package/go-plugin/lint/config.go +134 -0
- package/go-plugin/lint/engine.go +259 -0
- package/go-plugin/lint/host.go +190 -0
- package/go-plugin/lint/rules_arrays.go +78 -0
- package/go-plugin/lint/rules_console.go +37 -0
- package/go-plugin/lint/rules_debugger.go +29 -0
- package/go-plugin/lint/rules_dupes.go +179 -0
- package/go-plugin/lint/rules_empty.go +110 -0
- package/go-plugin/lint/rules_eval.go +57 -0
- package/go-plugin/lint/rules_finally.go +123 -0
- package/go-plugin/lint/rules_logic.go +368 -0
- package/go-plugin/lint/rules_loops.go +107 -0
- package/go-plugin/lint/rules_misc.go +69 -0
- package/go-plugin/lint/rules_problems.go +571 -0
- package/go-plugin/lint/rules_protos.go +42 -0
- package/go-plugin/lint/rules_self.go +89 -0
- package/go-plugin/lint/rules_strings.go +123 -0
- package/go-plugin/lint/rules_suggestions.go +1236 -0
- package/go-plugin/lint/rules_throw.go +31 -0
- package/go-plugin/lint/rules_ts.go +250 -0
- package/go-plugin/lint/rules_ts_extra.go +654 -0
- package/go-plugin/lint/rules_var.go +40 -0
- package/go-plugin/main.go +50 -0
- package/index.cjs +28 -0
- package/package.json +41 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
// Bulk implementation of ESLint's "Possible Problems" category.
|
|
2
|
+
//
|
|
3
|
+
// Each rule keeps to pure-AST checks (no scope analysis, no checker
|
|
4
|
+
// queries beyond what's already in the rule's own walk) so they remain
|
|
5
|
+
// fast and predictable. Rules that require scope binding or
|
|
6
|
+
// flow-sensitive analysis are intentionally not implemented here —
|
|
7
|
+
// those are upstream's job.
|
|
8
|
+
package lint
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"strings"
|
|
12
|
+
|
|
13
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// no-dupe-else-if: `if (a) {} else if (a) {}` — the second branch is
|
|
17
|
+
// unreachable.
|
|
18
|
+
type noDupeElseIf struct{}
|
|
19
|
+
|
|
20
|
+
func (noDupeElseIf) Name() string { return "no-dupe-else-if" }
|
|
21
|
+
func (noDupeElseIf) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindIfStatement} }
|
|
22
|
+
func (noDupeElseIf) Check(ctx *Context, node *shimast.Node) {
|
|
23
|
+
// Only fire on the *outermost* if; the recursion below scans the
|
|
24
|
+
// chain once.
|
|
25
|
+
if parent := node.Parent; parent != nil {
|
|
26
|
+
if parent.Kind == shimast.KindIfStatement {
|
|
27
|
+
outer := parent.AsIfStatement()
|
|
28
|
+
if outer != nil && outer.ElseStatement == node {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
seen := map[string]bool{}
|
|
34
|
+
cur := node
|
|
35
|
+
for cur != nil && cur.Kind == shimast.KindIfStatement {
|
|
36
|
+
stmt := cur.AsIfStatement()
|
|
37
|
+
if stmt == nil || stmt.Expression == nil {
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
key := nodeText(ctx.File, stmt.Expression)
|
|
41
|
+
if key != "" {
|
|
42
|
+
if seen[key] {
|
|
43
|
+
ctx.Report(stmt.Expression, "This branch can never execute. Its condition is a duplicate of an earlier branch.")
|
|
44
|
+
} else {
|
|
45
|
+
seen[key] = true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
cur = stmt.ElseStatement
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// no-ex-assign: `try { } catch (e) { e = 1; }` — reassigning the catch
|
|
53
|
+
// binding silently throws away the error.
|
|
54
|
+
type noExAssign struct{}
|
|
55
|
+
|
|
56
|
+
func (noExAssign) Name() string { return "no-ex-assign" }
|
|
57
|
+
func (noExAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindCatchClause} }
|
|
58
|
+
func (noExAssign) Check(ctx *Context, node *shimast.Node) {
|
|
59
|
+
clause := node.AsCatchClause()
|
|
60
|
+
if clause == nil || clause.VariableDeclaration == nil || clause.Block == nil {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
binding := clause.VariableDeclaration.AsVariableDeclaration()
|
|
64
|
+
if binding == nil {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
name := identifierText(binding.Name())
|
|
68
|
+
if name == "" {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
walkAssignments(clause.Block, name, func(target *shimast.Node) {
|
|
72
|
+
ctx.Report(target, "Do not assign to the exception parameter.")
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// walkAssignments invokes `report` on every `<name> = ...` shape inside
|
|
77
|
+
// `root`. Used by no-ex-assign and friends.
|
|
78
|
+
func walkAssignments(root *shimast.Node, name string, report func(*shimast.Node)) {
|
|
79
|
+
if root == nil {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
root.ForEachChild(func(child *shimast.Node) bool {
|
|
83
|
+
if child == nil {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
if child.Kind == shimast.KindBinaryExpression {
|
|
87
|
+
expr := child.AsBinaryExpression()
|
|
88
|
+
if expr != nil && expr.OperatorToken != nil && isAssignmentOperator(expr.OperatorToken.Kind) {
|
|
89
|
+
if identifierText(expr.Left) == name {
|
|
90
|
+
report(expr.Left)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
walkAssignments(child, name, report)
|
|
95
|
+
return false
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// no-empty-character-class: `/[]/` matches nothing.
|
|
100
|
+
type noEmptyCharacterClass struct{}
|
|
101
|
+
|
|
102
|
+
func (noEmptyCharacterClass) Name() string { return "no-empty-character-class" }
|
|
103
|
+
func (noEmptyCharacterClass) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindRegularExpressionLiteral} }
|
|
104
|
+
func (noEmptyCharacterClass) Check(ctx *Context, node *shimast.Node) {
|
|
105
|
+
src := nodeText(ctx.File, node)
|
|
106
|
+
if hasEmptyCharClass(src) {
|
|
107
|
+
ctx.Report(node, "Empty class.")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func hasEmptyCharClass(src string) bool {
|
|
112
|
+
// Walk the regex literal source manually, respecting escapes.
|
|
113
|
+
for i := 0; i < len(src); i++ {
|
|
114
|
+
switch src[i] {
|
|
115
|
+
case '\\':
|
|
116
|
+
i++ // skip escape
|
|
117
|
+
case '[':
|
|
118
|
+
j := i + 1
|
|
119
|
+
if j < len(src) && src[j] == '^' {
|
|
120
|
+
j++
|
|
121
|
+
}
|
|
122
|
+
if j < len(src) && src[j] == ']' {
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// no-misleading-character-class: `/[👍]/` — surrogate pairs in regex.
|
|
131
|
+
type noMisleadingCharacterClass struct{}
|
|
132
|
+
|
|
133
|
+
func (noMisleadingCharacterClass) Name() string { return "no-misleading-character-class" }
|
|
134
|
+
func (noMisleadingCharacterClass) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindRegularExpressionLiteral} }
|
|
135
|
+
func (noMisleadingCharacterClass) Check(ctx *Context, node *shimast.Node) {
|
|
136
|
+
src := nodeText(ctx.File, node)
|
|
137
|
+
if regexHasSurrogatePair(src) {
|
|
138
|
+
ctx.Report(node, "Unexpected surrogate pair in character class. Use the 'u' flag.")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func regexHasSurrogatePair(src string) bool {
|
|
143
|
+
// Strip the trailing flags so we don't misread the `u` flag — it
|
|
144
|
+
// suppresses this rule.
|
|
145
|
+
end := strings.LastIndex(src, "/")
|
|
146
|
+
if end < 0 {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
flags := src[end+1:]
|
|
150
|
+
if strings.ContainsRune(flags, 'u') {
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
body := src[:end]
|
|
154
|
+
in := false
|
|
155
|
+
for _, r := range body {
|
|
156
|
+
switch r {
|
|
157
|
+
case '[':
|
|
158
|
+
in = true
|
|
159
|
+
case ']':
|
|
160
|
+
in = false
|
|
161
|
+
}
|
|
162
|
+
if in && r >= 0x10000 {
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// no-loss-of-precision: `9007199254740993` — integer literal larger than
|
|
170
|
+
// `Number.MAX_SAFE_INTEGER`. We read the *source* form (not the parser's
|
|
171
|
+
// normalized .Text, which has already lost precision) and decide off
|
|
172
|
+
// that.
|
|
173
|
+
type noLossOfPrecision struct{}
|
|
174
|
+
|
|
175
|
+
func (noLossOfPrecision) Name() string { return "no-loss-of-precision" }
|
|
176
|
+
func (noLossOfPrecision) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindNumericLiteral} }
|
|
177
|
+
func (noLossOfPrecision) Check(ctx *Context, node *shimast.Node) {
|
|
178
|
+
source := strings.TrimSpace(nodeText(ctx.File, node))
|
|
179
|
+
if source == "" {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if numericLiteralLosesPrecision(source) {
|
|
183
|
+
ctx.Report(node, "This number literal will lose precision at runtime.")
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func numericLiteralLosesPrecision(text string) bool {
|
|
188
|
+
// Strip underscore separators, exponents, decimal/hex/oct/binary
|
|
189
|
+
// markers — for the simple-base-10 integer case the round-trip
|
|
190
|
+
// parse → format check is sufficient.
|
|
191
|
+
clean := strings.ReplaceAll(text, "_", "")
|
|
192
|
+
if strings.ContainsAny(clean, "eE.xXoObB") {
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
if len(clean) < 16 {
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
// Trim leading zeros for comparison.
|
|
199
|
+
trimmed := strings.TrimLeft(clean, "0")
|
|
200
|
+
if trimmed == "" {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
// 2^53 = 9007199254740992; anything larger as an integer literal loses precision.
|
|
204
|
+
const maxSafe = "9007199254740992"
|
|
205
|
+
if len(trimmed) < len(maxSafe) {
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
if len(trimmed) > len(maxSafe) {
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
return trimmed > maxSafe
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// no-class-assign: assigning to a class declaration's name.
|
|
215
|
+
type noClassAssign struct{}
|
|
216
|
+
|
|
217
|
+
func (noClassAssign) Name() string { return "no-class-assign" }
|
|
218
|
+
func (noClassAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindClassDeclaration} }
|
|
219
|
+
func (noClassAssign) Check(ctx *Context, node *shimast.Node) {
|
|
220
|
+
decl := node.AsClassDeclaration()
|
|
221
|
+
if decl == nil || decl.Name() == nil {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
name := identifierText(decl.Name())
|
|
225
|
+
if name == "" {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
walkAssignments(ctx.File.AsNode(), name, func(target *shimast.Node) {
|
|
229
|
+
ctx.Report(target, "'"+name+"' is a class.")
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// no-func-assign: same idea, but for function declarations.
|
|
234
|
+
type noFuncAssign struct{}
|
|
235
|
+
|
|
236
|
+
func (noFuncAssign) Name() string { return "no-func-assign" }
|
|
237
|
+
func (noFuncAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindFunctionDeclaration} }
|
|
238
|
+
func (noFuncAssign) Check(ctx *Context, node *shimast.Node) {
|
|
239
|
+
decl := node.AsFunctionDeclaration()
|
|
240
|
+
if decl == nil || decl.Name() == nil {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
name := identifierText(decl.Name())
|
|
244
|
+
if name == "" {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
walkAssignments(ctx.File.AsNode(), name, func(target *shimast.Node) {
|
|
248
|
+
ctx.Report(target, "'"+name+"' is a function.")
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// no-prototype-builtins: `obj.hasOwnProperty(x)` — should be
|
|
253
|
+
// `Object.prototype.hasOwnProperty.call(obj, x)` or `Object.hasOwn`.
|
|
254
|
+
type noPrototypeBuiltins struct{}
|
|
255
|
+
|
|
256
|
+
func (noPrototypeBuiltins) Name() string { return "no-prototype-builtins" }
|
|
257
|
+
func (noPrototypeBuiltins) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindCallExpression} }
|
|
258
|
+
func (noPrototypeBuiltins) Check(ctx *Context, node *shimast.Node) {
|
|
259
|
+
call := node.AsCallExpression()
|
|
260
|
+
if call == nil || call.Expression == nil {
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
if call.Expression.Kind != shimast.KindPropertyAccessExpression {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
access := call.Expression.AsPropertyAccessExpression()
|
|
267
|
+
if access == nil {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
method := identifierText(access.Name())
|
|
271
|
+
switch method {
|
|
272
|
+
case "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable":
|
|
273
|
+
ctx.Report(node, "Do not access Object.prototype method '"+method+"' from target object.")
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// no-async-promise-executor: `new Promise(async (resolve) => {...})`.
|
|
278
|
+
type noAsyncPromiseExecutor struct{}
|
|
279
|
+
|
|
280
|
+
func (noAsyncPromiseExecutor) Name() string { return "no-async-promise-executor" }
|
|
281
|
+
func (noAsyncPromiseExecutor) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindNewExpression} }
|
|
282
|
+
func (noAsyncPromiseExecutor) Check(ctx *Context, node *shimast.Node) {
|
|
283
|
+
ne := node.AsNewExpression()
|
|
284
|
+
if ne == nil || identifierText(ne.Expression) != "Promise" {
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
if ne.Arguments == nil || len(ne.Arguments.Nodes) == 0 {
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
executor := ne.Arguments.Nodes[0]
|
|
291
|
+
if executor == nil {
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
if !isFunctionLikeKind(executor) {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
if hasAsyncModifier(executor) {
|
|
298
|
+
ctx.Report(executor, "Promise executor functions should not be async.")
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// no-promise-executor-return: `new Promise(() => 1)` — the value is
|
|
303
|
+
// thrown away.
|
|
304
|
+
type noPromiseExecutorReturn struct{}
|
|
305
|
+
|
|
306
|
+
func (noPromiseExecutorReturn) Name() string { return "no-promise-executor-return" }
|
|
307
|
+
func (noPromiseExecutorReturn) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindNewExpression} }
|
|
308
|
+
func (noPromiseExecutorReturn) Check(ctx *Context, node *shimast.Node) {
|
|
309
|
+
ne := node.AsNewExpression()
|
|
310
|
+
if ne == nil || identifierText(ne.Expression) != "Promise" {
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
if ne.Arguments == nil || len(ne.Arguments.Nodes) == 0 {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
executor := ne.Arguments.Nodes[0]
|
|
317
|
+
if executor == nil || executor.Kind != shimast.KindArrowFunction {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
arrow := executor.AsArrowFunction()
|
|
321
|
+
if arrow == nil || arrow.Body == nil {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
// Concise arrow body returns the value implicitly.
|
|
325
|
+
if arrow.Body.Kind != shimast.KindBlock {
|
|
326
|
+
ctx.Report(arrow.Body, "Return values from promise executor functions cannot be read.")
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// no-control-regex: `/\x00/` — control characters in regex are usually
|
|
331
|
+
// the result of accidentally typing the escape rather than the printable
|
|
332
|
+
// counterpart.
|
|
333
|
+
type noControlRegex struct{}
|
|
334
|
+
|
|
335
|
+
func (noControlRegex) Name() string { return "no-control-regex" }
|
|
336
|
+
func (noControlRegex) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindRegularExpressionLiteral} }
|
|
337
|
+
func (noControlRegex) Check(ctx *Context, node *shimast.Node) {
|
|
338
|
+
src := nodeText(ctx.File, node)
|
|
339
|
+
if regexContainsControl(src) {
|
|
340
|
+
ctx.Report(node, "Unexpected control character(s) in regular expression.")
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
func regexContainsControl(src string) bool {
|
|
345
|
+
for i := 0; i < len(src); i++ {
|
|
346
|
+
c := src[i]
|
|
347
|
+
if c == '\\' && i+1 < len(src) {
|
|
348
|
+
next := src[i+1]
|
|
349
|
+
if next == 'x' && i+3 < len(src) {
|
|
350
|
+
value := hexDigit(src[i+2])*16 + hexDigit(src[i+3])
|
|
351
|
+
if value >= 0 && value < 0x20 {
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
i += 3
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
if next == 'u' && i+5 < len(src) {
|
|
358
|
+
value := hexDigit(src[i+2])*4096 + hexDigit(src[i+3])*256 + hexDigit(src[i+4])*16 + hexDigit(src[i+5])
|
|
359
|
+
if value >= 0 && value < 0x20 {
|
|
360
|
+
return true
|
|
361
|
+
}
|
|
362
|
+
i += 5
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
i++
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
if c < 0x20 && c != '\t' && c != '\n' && c != '\r' {
|
|
369
|
+
return true
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func hexDigit(b byte) int {
|
|
376
|
+
switch {
|
|
377
|
+
case b >= '0' && b <= '9':
|
|
378
|
+
return int(b - '0')
|
|
379
|
+
case b >= 'a' && b <= 'f':
|
|
380
|
+
return int(b-'a') + 10
|
|
381
|
+
case b >= 'A' && b <= 'F':
|
|
382
|
+
return int(b-'A') + 10
|
|
383
|
+
}
|
|
384
|
+
return -1
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// no-irregular-whitespace: zero-width spaces, NBSP, etc. The TS parser
|
|
388
|
+
// accepts them but copy-paste into source is almost always a mistake.
|
|
389
|
+
type noIrregularWhitespace struct{}
|
|
390
|
+
|
|
391
|
+
func (noIrregularWhitespace) Name() string { return "no-irregular-whitespace" }
|
|
392
|
+
func (noIrregularWhitespace) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
|
|
393
|
+
func (noIrregularWhitespace) Check(ctx *Context, node *shimast.Node) {
|
|
394
|
+
if ctx.File == nil {
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
text := ctx.File.Text()
|
|
398
|
+
for i, r := range text {
|
|
399
|
+
if isIrregularWhitespace(r) {
|
|
400
|
+
ctx.ReportRange(i, i+len(string(r)), "Irregular whitespace not allowed.")
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func isIrregularWhitespace(r rune) bool {
|
|
406
|
+
switch r {
|
|
407
|
+
case '\v', '\f',
|
|
408
|
+
0x00A0, 0x1680,
|
|
409
|
+
0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005,
|
|
410
|
+
0x2006, 0x2007, 0x2008, 0x2009, 0x200A,
|
|
411
|
+
0x200B, 0x202F, 0x205F,
|
|
412
|
+
0x3000,
|
|
413
|
+
0x2028, 0x2029,
|
|
414
|
+
0xFEFF:
|
|
415
|
+
return true
|
|
416
|
+
}
|
|
417
|
+
return false
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// no-fallthrough: `switch` cases that fall through to the next label
|
|
421
|
+
// without an explicit `break` / `return` / `throw` / `continue`.
|
|
422
|
+
type noFallthrough struct{}
|
|
423
|
+
|
|
424
|
+
func (noFallthrough) Name() string { return "no-fallthrough" }
|
|
425
|
+
func (noFallthrough) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSwitchStatement} }
|
|
426
|
+
func (noFallthrough) Check(ctx *Context, node *shimast.Node) {
|
|
427
|
+
sw := node.AsSwitchStatement()
|
|
428
|
+
if sw == nil || sw.CaseBlock == nil {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
block := sw.CaseBlock.AsCaseBlock()
|
|
432
|
+
if block == nil || block.Clauses == nil {
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
clauses := block.Clauses.Nodes
|
|
436
|
+
for i := 0; i+1 < len(clauses); i++ {
|
|
437
|
+
clause := clauses[i].AsCaseOrDefaultClause()
|
|
438
|
+
if clause == nil || clause.Statements == nil {
|
|
439
|
+
continue
|
|
440
|
+
}
|
|
441
|
+
stmts := clause.Statements.Nodes
|
|
442
|
+
if len(stmts) == 0 {
|
|
443
|
+
continue // empty case is intentional, never a fallthrough.
|
|
444
|
+
}
|
|
445
|
+
if !isTerminating(stmts[len(stmts)-1]) {
|
|
446
|
+
ctx.Report(clauses[i+1], "Expected a 'break' statement before this case.")
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
func isTerminating(stmt *shimast.Node) bool {
|
|
452
|
+
if stmt == nil {
|
|
453
|
+
return false
|
|
454
|
+
}
|
|
455
|
+
switch stmt.Kind {
|
|
456
|
+
case shimast.KindBreakStatement,
|
|
457
|
+
shimast.KindContinueStatement,
|
|
458
|
+
shimast.KindReturnStatement,
|
|
459
|
+
shimast.KindThrowStatement:
|
|
460
|
+
return true
|
|
461
|
+
case shimast.KindBlock:
|
|
462
|
+
block := stmt.AsBlock()
|
|
463
|
+
if block == nil || block.Statements == nil {
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
nodes := block.Statements.Nodes
|
|
467
|
+
if len(nodes) == 0 {
|
|
468
|
+
return false
|
|
469
|
+
}
|
|
470
|
+
return isTerminating(nodes[len(nodes)-1])
|
|
471
|
+
}
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// no-inner-declarations: `function foo() { if (x) { function bar() {} } }`
|
|
476
|
+
// — inner function declarations are hoisted differently in strict mode
|
|
477
|
+
// vs sloppy and are confusing.
|
|
478
|
+
type noInnerDeclarations struct{}
|
|
479
|
+
|
|
480
|
+
func (noInnerDeclarations) Name() string { return "no-inner-declarations" }
|
|
481
|
+
func (noInnerDeclarations) Visits() []shimast.Kind {
|
|
482
|
+
return []shimast.Kind{shimast.KindFunctionDeclaration, shimast.KindVariableStatement}
|
|
483
|
+
}
|
|
484
|
+
func (noInnerDeclarations) Check(ctx *Context, node *shimast.Node) {
|
|
485
|
+
if node.Kind == shimast.KindVariableStatement {
|
|
486
|
+
stmt := node.AsVariableStatement()
|
|
487
|
+
if stmt == nil || stmt.DeclarationList == nil {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
// Only `var` is hoisted oddly.
|
|
491
|
+
if !shimast.IsVar(stmt.DeclarationList) {
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
parent := node.Parent
|
|
496
|
+
if parent == nil {
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
switch parent.Kind {
|
|
500
|
+
case shimast.KindSourceFile, shimast.KindModuleBlock:
|
|
501
|
+
return
|
|
502
|
+
case shimast.KindBlock:
|
|
503
|
+
grand := parent.Parent
|
|
504
|
+
if grand == nil {
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
if isFunctionLikeKind(grand) {
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
what := "function"
|
|
512
|
+
if node.Kind == shimast.KindVariableStatement {
|
|
513
|
+
what = "variable"
|
|
514
|
+
}
|
|
515
|
+
ctx.Report(node, "Move "+what+" declaration to the function scope.")
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// no-obj-calls: `Math()`, `JSON()` — these globals are objects, not
|
|
519
|
+
// callables. ESLint catches a small list.
|
|
520
|
+
type noObjCalls struct{}
|
|
521
|
+
|
|
522
|
+
func (noObjCalls) Name() string { return "no-obj-calls" }
|
|
523
|
+
func (noObjCalls) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindCallExpression, shimast.KindNewExpression} }
|
|
524
|
+
func (noObjCalls) Check(ctx *Context, node *shimast.Node) {
|
|
525
|
+
var callee *shimast.Node
|
|
526
|
+
if node.Kind == shimast.KindCallExpression {
|
|
527
|
+
callee = node.AsCallExpression().Expression
|
|
528
|
+
} else {
|
|
529
|
+
callee = node.AsNewExpression().Expression
|
|
530
|
+
}
|
|
531
|
+
switch identifierText(callee) {
|
|
532
|
+
case "Math", "JSON", "Reflect", "Atomics", "Intl":
|
|
533
|
+
ctx.Report(node, "'"+identifierText(callee)+"' is not a function.")
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// hasAsyncModifier returns whether a function-like node carries the
|
|
538
|
+
// `async` keyword. Used by no-async-promise-executor.
|
|
539
|
+
func hasAsyncModifier(node *shimast.Node) bool {
|
|
540
|
+
if node == nil {
|
|
541
|
+
return false
|
|
542
|
+
}
|
|
543
|
+
mods := node.Modifiers()
|
|
544
|
+
if mods == nil {
|
|
545
|
+
return false
|
|
546
|
+
}
|
|
547
|
+
for _, m := range mods.Nodes {
|
|
548
|
+
if m != nil && m.Kind == shimast.KindAsyncKeyword {
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return false
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
func init() {
|
|
556
|
+
Register(noDupeElseIf{})
|
|
557
|
+
Register(noExAssign{})
|
|
558
|
+
Register(noEmptyCharacterClass{})
|
|
559
|
+
Register(noMisleadingCharacterClass{})
|
|
560
|
+
Register(noLossOfPrecision{})
|
|
561
|
+
Register(noClassAssign{})
|
|
562
|
+
Register(noFuncAssign{})
|
|
563
|
+
Register(noPrototypeBuiltins{})
|
|
564
|
+
Register(noAsyncPromiseExecutor{})
|
|
565
|
+
Register(noPromiseExecutorReturn{})
|
|
566
|
+
Register(noControlRegex{})
|
|
567
|
+
Register(noIrregularWhitespace{})
|
|
568
|
+
Register(noFallthrough{})
|
|
569
|
+
Register(noInnerDeclarations{})
|
|
570
|
+
Register(noObjCalls{})
|
|
571
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package lint
|
|
2
|
+
|
|
3
|
+
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
|
+
|
|
5
|
+
// no-iterator: `obj.__iterator__` is a non-standard SpiderMonkey-era
|
|
6
|
+
// access. Use `Symbol.iterator` instead.
|
|
7
|
+
// https://eslint.org/docs/latest/rules/no-iterator
|
|
8
|
+
type noIterator struct{}
|
|
9
|
+
|
|
10
|
+
func (noIterator) Name() string { return "no-iterator" }
|
|
11
|
+
func (noIterator) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindPropertyAccessExpression} }
|
|
12
|
+
func (noIterator) Check(ctx *Context, node *shimast.Node) {
|
|
13
|
+
access := node.AsPropertyAccessExpression()
|
|
14
|
+
if access == nil {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
if identifierText(access.Name()) == "__iterator__" {
|
|
18
|
+
ctx.Report(node, "Reserved name '__iterator__'.")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// no-proto: `obj.__proto__` access is legacy. Use `Object.getPrototypeOf`
|
|
23
|
+
// / `Object.setPrototypeOf`.
|
|
24
|
+
// https://eslint.org/docs/latest/rules/no-proto
|
|
25
|
+
type noProto struct{}
|
|
26
|
+
|
|
27
|
+
func (noProto) Name() string { return "no-proto" }
|
|
28
|
+
func (noProto) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindPropertyAccessExpression} }
|
|
29
|
+
func (noProto) Check(ctx *Context, node *shimast.Node) {
|
|
30
|
+
access := node.AsPropertyAccessExpression()
|
|
31
|
+
if access == nil {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
if identifierText(access.Name()) == "__proto__" {
|
|
35
|
+
ctx.Report(node, "The '__proto__' property is deprecated.")
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func init() {
|
|
40
|
+
Register(noIterator{})
|
|
41
|
+
Register(noProto{})
|
|
42
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package lint
|
|
2
|
+
|
|
3
|
+
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
|
+
|
|
5
|
+
// no-self-assign: detect `x = x` / `obj.foo = obj.foo`. Limited to the
|
|
6
|
+
// cheap textual identity match — sufficient for the canonical cases and
|
|
7
|
+
// matches the `no-self-assign` ESLint behavior on simple identifiers.
|
|
8
|
+
// https://eslint.org/docs/latest/rules/no-self-assign
|
|
9
|
+
type noSelfAssign struct{}
|
|
10
|
+
|
|
11
|
+
func (noSelfAssign) Name() string { return "no-self-assign" }
|
|
12
|
+
func (noSelfAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBinaryExpression} }
|
|
13
|
+
func (noSelfAssign) Check(ctx *Context, node *shimast.Node) {
|
|
14
|
+
expr := node.AsBinaryExpression()
|
|
15
|
+
if expr == nil || expr.OperatorToken == nil {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
if expr.OperatorToken.Kind != shimast.KindEqualsToken {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
left := stripParens(expr.Left)
|
|
22
|
+
right := stripParens(expr.Right)
|
|
23
|
+
if left == nil || right == nil {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
if !isAssignableLeftHand(left) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
if nodeText(ctx.File, left) == nodeText(ctx.File, right) {
|
|
30
|
+
ctx.Report(node, "Self-assignment of a variable.")
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func isAssignableLeftHand(node *shimast.Node) bool {
|
|
35
|
+
if node == nil {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
switch node.Kind {
|
|
39
|
+
case shimast.KindIdentifier, shimast.KindPropertyAccessExpression, shimast.KindElementAccessExpression:
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// no-self-compare: `x === x`, `x !== x`, etc. Useful for catching typos
|
|
46
|
+
// where the developer meant to compare against a different value.
|
|
47
|
+
// https://eslint.org/docs/latest/rules/no-self-compare
|
|
48
|
+
type noSelfCompare struct{}
|
|
49
|
+
|
|
50
|
+
func (noSelfCompare) Name() string { return "no-self-compare" }
|
|
51
|
+
func (noSelfCompare) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBinaryExpression} }
|
|
52
|
+
func (noSelfCompare) Check(ctx *Context, node *shimast.Node) {
|
|
53
|
+
expr := node.AsBinaryExpression()
|
|
54
|
+
if expr == nil || expr.OperatorToken == nil {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if !isComparisonOperator(expr.OperatorToken.Kind) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
left := stripParens(expr.Left)
|
|
61
|
+
right := stripParens(expr.Right)
|
|
62
|
+
if left == nil || right == nil {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if nodeText(ctx.File, left) == nodeText(ctx.File, right) {
|
|
66
|
+
ctx.Report(node, "Comparing to itself is potentially pointless.")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func isComparisonOperator(kind shimast.Kind) bool {
|
|
71
|
+
switch kind {
|
|
72
|
+
case
|
|
73
|
+
shimast.KindEqualsEqualsToken,
|
|
74
|
+
shimast.KindEqualsEqualsEqualsToken,
|
|
75
|
+
shimast.KindExclamationEqualsToken,
|
|
76
|
+
shimast.KindExclamationEqualsEqualsToken,
|
|
77
|
+
shimast.KindLessThanToken,
|
|
78
|
+
shimast.KindGreaterThanToken,
|
|
79
|
+
shimast.KindLessThanEqualsToken,
|
|
80
|
+
shimast.KindGreaterThanEqualsToken:
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func init() {
|
|
87
|
+
Register(noSelfAssign{})
|
|
88
|
+
Register(noSelfCompare{})
|
|
89
|
+
}
|