@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,654 @@
|
|
|
1
|
+
// Bulk implementation of @typescript-eslint rules that work off the
|
|
2
|
+
// AST alone (no checker, no scope analysis). The set is curated to
|
|
3
|
+
// match the rules `eslint-plugin-typescript`'s recommended preset relies
|
|
4
|
+
// on most heavily — the rest of that plugin's catalog is type-aware and
|
|
5
|
+
// out of scope for v0.
|
|
6
|
+
package lint
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// no-confusing-non-null-assertion: `a! == b` reads ambiguously.
|
|
15
|
+
type noConfusingNonNullAssertion struct{}
|
|
16
|
+
|
|
17
|
+
func (noConfusingNonNullAssertion) Name() string { return "no-confusing-non-null-assertion" }
|
|
18
|
+
func (noConfusingNonNullAssertion) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBinaryExpression} }
|
|
19
|
+
func (noConfusingNonNullAssertion) Check(ctx *Context, node *shimast.Node) {
|
|
20
|
+
expr := node.AsBinaryExpression()
|
|
21
|
+
if expr == nil || expr.OperatorToken == nil || expr.Left == nil {
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
switch expr.OperatorToken.Kind {
|
|
25
|
+
case shimast.KindEqualsEqualsToken,
|
|
26
|
+
shimast.KindEqualsEqualsEqualsToken,
|
|
27
|
+
shimast.KindExclamationEqualsToken,
|
|
28
|
+
shimast.KindExclamationEqualsEqualsToken,
|
|
29
|
+
shimast.KindEqualsToken:
|
|
30
|
+
default:
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if expr.Left.Kind == shimast.KindNonNullExpression {
|
|
34
|
+
ctx.Report(node, "Confusing combination of non-null assertion and equality.")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// no-duplicate-enum-values: `enum E { A = 1, B = 1 }` — duplicate values
|
|
39
|
+
// silently collapse.
|
|
40
|
+
type noDuplicateEnumValues struct{}
|
|
41
|
+
|
|
42
|
+
func (noDuplicateEnumValues) Name() string { return "no-duplicate-enum-values" }
|
|
43
|
+
func (noDuplicateEnumValues) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindEnumDeclaration} }
|
|
44
|
+
func (noDuplicateEnumValues) Check(ctx *Context, node *shimast.Node) {
|
|
45
|
+
decl := node.AsEnumDeclaration()
|
|
46
|
+
if decl == nil || decl.Members == nil {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
seen := map[string]bool{}
|
|
50
|
+
for _, member := range decl.Members.Nodes {
|
|
51
|
+
if member == nil {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
em := member.AsEnumMember()
|
|
55
|
+
if em == nil || em.Initializer == nil {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
init := em.Initializer
|
|
59
|
+
var key string
|
|
60
|
+
switch init.Kind {
|
|
61
|
+
case shimast.KindNumericLiteral, shimast.KindBigIntLiteral:
|
|
62
|
+
key = "n:" + numericLiteralText(init)
|
|
63
|
+
case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
|
|
64
|
+
key = "s:" + stringLiteralText(init)
|
|
65
|
+
default:
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if seen[key] {
|
|
69
|
+
ctx.Report(member, "Duplicate enum member value.")
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
seen[key] = true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// no-extra-non-null-assertion: `a!!` / `a!?.b` collapses two assertions.
|
|
77
|
+
type noExtraNonNullAssertion struct{}
|
|
78
|
+
|
|
79
|
+
func (noExtraNonNullAssertion) Name() string { return "no-extra-non-null-assertion" }
|
|
80
|
+
func (noExtraNonNullAssertion) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindNonNullExpression} }
|
|
81
|
+
func (noExtraNonNullAssertion) Check(ctx *Context, node *shimast.Node) {
|
|
82
|
+
parent := node.Parent
|
|
83
|
+
if parent == nil {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if parent.Kind == shimast.KindNonNullExpression {
|
|
87
|
+
ctx.Report(node, "Forbidden extra non-null assertion.")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// no-non-null-asserted-optional-chain: `foo?.bar!` — the chain produces
|
|
92
|
+
// undefined; asserting non-null on the whole chain defeats the chain.
|
|
93
|
+
type noNonNullAssertedOptionalChain struct{}
|
|
94
|
+
|
|
95
|
+
func (noNonNullAssertedOptionalChain) Name() string { return "no-non-null-asserted-optional-chain" }
|
|
96
|
+
func (noNonNullAssertedOptionalChain) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindNonNullExpression} }
|
|
97
|
+
func (noNonNullAssertedOptionalChain) Check(ctx *Context, node *shimast.Node) {
|
|
98
|
+
inner := node.AsNonNullExpression()
|
|
99
|
+
if inner == nil || inner.Expression == nil {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if containsOptionalChain(inner.Expression) {
|
|
103
|
+
ctx.Report(node, "Optional chain expressions can return undefined; non-null assertion bypasses that check.")
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func containsOptionalChain(node *shimast.Node) bool {
|
|
108
|
+
if node == nil {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
switch node.Kind {
|
|
112
|
+
case shimast.KindPropertyAccessExpression:
|
|
113
|
+
access := node.AsPropertyAccessExpression()
|
|
114
|
+
if access != nil && access.QuestionDotToken != nil {
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
if access != nil {
|
|
118
|
+
return containsOptionalChain(access.Expression)
|
|
119
|
+
}
|
|
120
|
+
case shimast.KindElementAccessExpression:
|
|
121
|
+
access := node.AsElementAccessExpression()
|
|
122
|
+
if access != nil && access.QuestionDotToken != nil {
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
if access != nil {
|
|
126
|
+
return containsOptionalChain(access.Expression)
|
|
127
|
+
}
|
|
128
|
+
case shimast.KindCallExpression:
|
|
129
|
+
call := node.AsCallExpression()
|
|
130
|
+
if call != nil && call.QuestionDotToken != nil {
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
if call != nil {
|
|
134
|
+
return containsOptionalChain(call.Expression)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// no-misused-new: declaring a `new` signature on a non-class interface
|
|
141
|
+
// or a `constructor` method on an interface — these don't do what
|
|
142
|
+
// authors expect.
|
|
143
|
+
type noMisusedNew struct{}
|
|
144
|
+
|
|
145
|
+
func (noMisusedNew) Name() string { return "no-misused-new" }
|
|
146
|
+
func (noMisusedNew) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindInterfaceDeclaration, shimast.KindTypeAliasDeclaration} }
|
|
147
|
+
func (noMisusedNew) Check(ctx *Context, node *shimast.Node) {
|
|
148
|
+
if node.Kind != shimast.KindInterfaceDeclaration {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
decl := node.AsInterfaceDeclaration()
|
|
152
|
+
if decl == nil || decl.Members == nil {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
for _, member := range decl.Members.Nodes {
|
|
156
|
+
if member == nil {
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
switch member.Kind {
|
|
160
|
+
case shimast.KindConstructor:
|
|
161
|
+
ctx.Report(member, "Interfaces cannot have constructors. Use a class instead.")
|
|
162
|
+
case shimast.KindMethodSignature:
|
|
163
|
+
ms := member.AsMethodSignatureDeclaration()
|
|
164
|
+
if ms != nil && identifierText(ms.Name()) == "constructor" {
|
|
165
|
+
ctx.Report(member, "Interfaces cannot have constructors. Use a class instead.")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// prefer-enum-initializers: every enum member should have an explicit
|
|
172
|
+
// initializer (avoids order-dependent values).
|
|
173
|
+
type preferEnumInitializers struct{}
|
|
174
|
+
|
|
175
|
+
func (preferEnumInitializers) Name() string { return "prefer-enum-initializers" }
|
|
176
|
+
func (preferEnumInitializers) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindEnumDeclaration} }
|
|
177
|
+
func (preferEnumInitializers) Check(ctx *Context, node *shimast.Node) {
|
|
178
|
+
decl := node.AsEnumDeclaration()
|
|
179
|
+
if decl == nil || decl.Members == nil {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
for _, member := range decl.Members.Nodes {
|
|
183
|
+
if member == nil {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
em := member.AsEnumMember()
|
|
187
|
+
if em != nil && em.Initializer == nil {
|
|
188
|
+
ctx.Report(member, "Enum member should have an explicit initializer.")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// prefer-for-of: `for (let i = 0; i < arr.length; i++) { use(arr[i]) }`
|
|
194
|
+
// can usually be replaced with `for (const x of arr) { use(x); }`.
|
|
195
|
+
type preferForOf struct{}
|
|
196
|
+
|
|
197
|
+
func (preferForOf) Name() string { return "prefer-for-of" }
|
|
198
|
+
func (preferForOf) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindForStatement} }
|
|
199
|
+
func (preferForOf) Check(ctx *Context, node *shimast.Node) {
|
|
200
|
+
loop := node.AsForStatement()
|
|
201
|
+
if loop == nil || loop.Initializer == nil || loop.Condition == nil || loop.Incrementor == nil {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
// Initializer: `let i = 0` (single declarator with name `i`).
|
|
205
|
+
init := loop.Initializer
|
|
206
|
+
if init.Kind != shimast.KindVariableDeclarationList {
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
list := init.AsVariableDeclarationList()
|
|
210
|
+
if list == nil || list.Declarations == nil || len(list.Declarations.Nodes) != 1 {
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
decl := list.Declarations.Nodes[0].AsVariableDeclaration()
|
|
214
|
+
if decl == nil {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
counter := identifierText(decl.Name())
|
|
218
|
+
if counter == "" {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if numericLiteralText(decl.Initializer) != "0" {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
// Condition: `i < <something>.length`.
|
|
225
|
+
cond := loop.Condition.AsBinaryExpression()
|
|
226
|
+
if cond == nil || cond.OperatorToken == nil {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
if cond.OperatorToken.Kind != shimast.KindLessThanToken {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
if identifierText(cond.Left) != counter {
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
if cond.Right == nil || cond.Right.Kind != shimast.KindPropertyAccessExpression {
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
rightAccess := cond.Right.AsPropertyAccessExpression()
|
|
239
|
+
if rightAccess == nil || identifierText(rightAccess.Name()) != "length" {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
// Incrementor: `i++` or `++i`.
|
|
243
|
+
if !isCounterIncrement(loop.Incrementor, counter) {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
ctx.Report(node, "Prefer a 'for-of' loop instead of a 'for' loop with this simple iteration.")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func isCounterIncrement(node *shimast.Node, counter string) bool {
|
|
250
|
+
switch node.Kind {
|
|
251
|
+
case shimast.KindPostfixUnaryExpression:
|
|
252
|
+
post := node.AsPostfixUnaryExpression()
|
|
253
|
+
return post != nil && post.Operator == shimast.KindPlusPlusToken && identifierText(post.Operand) == counter
|
|
254
|
+
case shimast.KindPrefixUnaryExpression:
|
|
255
|
+
pre := node.AsPrefixUnaryExpression()
|
|
256
|
+
return pre != nil && pre.Operator == shimast.KindPlusPlusToken && identifierText(pre.Operand) == counter
|
|
257
|
+
}
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// prefer-function-type: a single-call-signature interface or type alias
|
|
262
|
+
// is more readably written as a function type.
|
|
263
|
+
//
|
|
264
|
+
// interface F { (x: number): string } -> type F = (x: number) => string
|
|
265
|
+
type preferFunctionType struct{}
|
|
266
|
+
|
|
267
|
+
func (preferFunctionType) Name() string { return "prefer-function-type" }
|
|
268
|
+
func (preferFunctionType) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindInterfaceDeclaration} }
|
|
269
|
+
func (preferFunctionType) Check(ctx *Context, node *shimast.Node) {
|
|
270
|
+
decl := node.AsInterfaceDeclaration()
|
|
271
|
+
if decl == nil || decl.Members == nil || len(decl.Members.Nodes) != 1 {
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
if decl.HeritageClauses != nil && len(decl.HeritageClauses.Nodes) > 0 {
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
member := decl.Members.Nodes[0]
|
|
278
|
+
if member == nil || member.Kind != shimast.KindCallSignature {
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
ctx.Report(node, "Interface only has a call signature; use 'type' alias and function type instead.")
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// prefer-namespace-keyword: `module Foo {}` (TS namespace via `module`
|
|
285
|
+
// keyword) → `namespace Foo {}`.
|
|
286
|
+
type preferNamespaceKeyword struct{}
|
|
287
|
+
|
|
288
|
+
func (preferNamespaceKeyword) Name() string { return "prefer-namespace-keyword" }
|
|
289
|
+
func (preferNamespaceKeyword) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindModuleDeclaration} }
|
|
290
|
+
func (preferNamespaceKeyword) Check(ctx *Context, node *shimast.Node) {
|
|
291
|
+
decl := node.AsModuleDeclaration()
|
|
292
|
+
if decl == nil || decl.Name() == nil {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if decl.Name().Kind == shimast.KindStringLiteral {
|
|
296
|
+
return // ambient module: `declare module "fs" {}` is fine
|
|
297
|
+
}
|
|
298
|
+
if decl.Keyword != shimast.KindModuleKeyword {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
ctx.Report(node, "Use 'namespace' instead of 'module' to declare custom TypeScript modules.")
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// triple-slash-reference: `/// <reference path="..." />` directives.
|
|
305
|
+
// Discouraged in modern code in favor of `import`.
|
|
306
|
+
type tripleSlashReference struct{}
|
|
307
|
+
|
|
308
|
+
func (tripleSlashReference) Name() string { return "triple-slash-reference" }
|
|
309
|
+
func (tripleSlashReference) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
|
|
310
|
+
func (tripleSlashReference) Check(ctx *Context, node *shimast.Node) {
|
|
311
|
+
if ctx.File == nil {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
for _, ref := range ctx.File.ReferencedFiles {
|
|
315
|
+
ctx.ReportRange(ref.Pos(), ref.End(), "Do not use triple slash references for "+ref.FileName+".")
|
|
316
|
+
}
|
|
317
|
+
for _, ref := range ctx.File.TypeReferenceDirectives {
|
|
318
|
+
ctx.ReportRange(ref.Pos(), ref.End(), "Do not use triple slash references for "+ref.FileName+".")
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// no-array-delete: `delete arr[0]` leaves a sparse hole. Use `splice`.
|
|
323
|
+
type noArrayDelete struct{}
|
|
324
|
+
|
|
325
|
+
func (noArrayDelete) Name() string { return "no-array-delete" }
|
|
326
|
+
func (noArrayDelete) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindDeleteExpression} }
|
|
327
|
+
func (noArrayDelete) Check(ctx *Context, node *shimast.Node) {
|
|
328
|
+
del := node.AsDeleteExpression()
|
|
329
|
+
if del == nil || del.Expression == nil {
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
if del.Expression.Kind != shimast.KindElementAccessExpression {
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
access := del.Expression.AsElementAccessExpression()
|
|
336
|
+
if access == nil || access.ArgumentExpression == nil {
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
// Numeric subscript ⇒ likely-array delete. (Object delete via
|
|
340
|
+
// numeric key is rare.)
|
|
341
|
+
switch access.ArgumentExpression.Kind {
|
|
342
|
+
case shimast.KindNumericLiteral, shimast.KindIdentifier:
|
|
343
|
+
ctx.Report(node, "Using delete with an array expression is unsafe.")
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// consistent-type-imports: `import { Foo } from "./types"` where Foo is
|
|
348
|
+
// only used as a type → `import type { Foo } from "./types"`. We
|
|
349
|
+
// approximate by flagging every `import type` candidate where the
|
|
350
|
+
// specifier appears in a type-only context inside the file.
|
|
351
|
+
//
|
|
352
|
+
// We use a heuristic: if every reference to an imported name occurs
|
|
353
|
+
// only inside a TypeReferenceNode, flag the import. Falls short on
|
|
354
|
+
// unanalyzable shapes (re-exports, `typeof X`) but matches the most
|
|
355
|
+
// common case.
|
|
356
|
+
type consistentTypeImports struct{}
|
|
357
|
+
|
|
358
|
+
func (consistentTypeImports) Name() string { return "consistent-type-imports" }
|
|
359
|
+
func (consistentTypeImports) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindImportDeclaration} }
|
|
360
|
+
func (consistentTypeImports) Check(ctx *Context, node *shimast.Node) {
|
|
361
|
+
decl := node.AsImportDeclaration()
|
|
362
|
+
if decl == nil || decl.ImportClause == nil {
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
clause := decl.ImportClause.AsImportClause()
|
|
366
|
+
if clause == nil {
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
if clause.PhaseModifier == shimast.KindTypeKeyword {
|
|
370
|
+
return // already `import type`.
|
|
371
|
+
}
|
|
372
|
+
if clause.NamedBindings == nil || clause.NamedBindings.Kind != shimast.KindNamedImports {
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
named := clause.NamedBindings.AsNamedImports()
|
|
376
|
+
if named == nil || named.Elements == nil {
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
names := []string{}
|
|
380
|
+
for _, spec := range named.Elements.Nodes {
|
|
381
|
+
if spec == nil {
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
s := spec.AsImportSpecifier()
|
|
385
|
+
if s == nil || s.IsTypeOnly {
|
|
386
|
+
continue
|
|
387
|
+
}
|
|
388
|
+
if name := identifierText(s.Name()); name != "" {
|
|
389
|
+
names = append(names, name)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if len(names) == 0 {
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
if !allUsesAreTypeOnly(ctx.File.AsNode(), names) {
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
ctx.Report(node, "All imports in the declaration are only used as types. Use `import type`.")
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
func allUsesAreTypeOnly(root *shimast.Node, names []string) bool {
|
|
402
|
+
want := map[string]bool{}
|
|
403
|
+
for _, n := range names {
|
|
404
|
+
want[n] = true
|
|
405
|
+
}
|
|
406
|
+
allOk := true
|
|
407
|
+
var visit func(n *shimast.Node, inType bool)
|
|
408
|
+
visit = func(n *shimast.Node, inType bool) {
|
|
409
|
+
if n == nil || !allOk {
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
typeContext := inType
|
|
413
|
+
switch n.Kind {
|
|
414
|
+
case shimast.KindTypeReference,
|
|
415
|
+
shimast.KindTypeAliasDeclaration,
|
|
416
|
+
shimast.KindInterfaceDeclaration,
|
|
417
|
+
shimast.KindTypeQuery,
|
|
418
|
+
shimast.KindTypeOperator,
|
|
419
|
+
shimast.KindTypeLiteral:
|
|
420
|
+
typeContext = true
|
|
421
|
+
case shimast.KindIdentifier:
|
|
422
|
+
if !typeContext && want[identifierText(n)] {
|
|
423
|
+
allOk = false
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
case shimast.KindImportDeclaration:
|
|
427
|
+
return // don't descend into other imports
|
|
428
|
+
}
|
|
429
|
+
n.ForEachChild(func(c *shimast.Node) bool {
|
|
430
|
+
visit(c, typeContext)
|
|
431
|
+
return false
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
visit(root, false)
|
|
435
|
+
return allOk
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// no-empty-object-type: `interface Foo {}` / `type Foo = {}`. ESLint
|
|
439
|
+
// flags empty types because they're equivalent to `unknown` (everything
|
|
440
|
+
// satisfies `{}`).
|
|
441
|
+
type noEmptyObjectType struct{}
|
|
442
|
+
|
|
443
|
+
func (noEmptyObjectType) Name() string { return "no-empty-object-type" }
|
|
444
|
+
func (noEmptyObjectType) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindTypeLiteral} }
|
|
445
|
+
func (noEmptyObjectType) Check(ctx *Context, node *shimast.Node) {
|
|
446
|
+
lit := node.AsTypeLiteralNode()
|
|
447
|
+
if lit == nil || lit.Members == nil {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
if len(lit.Members.Nodes) == 0 {
|
|
451
|
+
ctx.Report(node, "The `{}` type is generally not what's intended; consider `Record<string, unknown>` or `unknown`.")
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// array-type: `Array<T>` vs `T[]`. ESLint default mode prefers `T[]`.
|
|
456
|
+
type arrayType struct{}
|
|
457
|
+
|
|
458
|
+
func (arrayType) Name() string { return "array-type" }
|
|
459
|
+
func (arrayType) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindTypeReference} }
|
|
460
|
+
func (arrayType) Check(ctx *Context, node *shimast.Node) {
|
|
461
|
+
ref := node.AsTypeReferenceNode()
|
|
462
|
+
if ref == nil || ref.TypeName == nil {
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
name := identifierText(ref.TypeName)
|
|
466
|
+
if name != "Array" && name != "ReadonlyArray" {
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
if ref.TypeArguments == nil || len(ref.TypeArguments.Nodes) != 1 {
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
if name == "Array" {
|
|
473
|
+
ctx.Report(node, "Use 'T[]' instead of 'Array<T>'.")
|
|
474
|
+
} else {
|
|
475
|
+
ctx.Report(node, "Use 'readonly T[]' instead of 'ReadonlyArray<T>'.")
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// consistent-indexed-object-style: `{ [key: string]: T }` vs
|
|
480
|
+
// `Record<string, T>`. ESLint default prefers `Record`.
|
|
481
|
+
type consistentIndexedObjectStyle struct{}
|
|
482
|
+
|
|
483
|
+
func (consistentIndexedObjectStyle) Name() string { return "consistent-indexed-object-style" }
|
|
484
|
+
func (consistentIndexedObjectStyle) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindTypeLiteral} }
|
|
485
|
+
func (consistentIndexedObjectStyle) Check(ctx *Context, node *shimast.Node) {
|
|
486
|
+
lit := node.AsTypeLiteralNode()
|
|
487
|
+
if lit == nil || lit.Members == nil || len(lit.Members.Nodes) != 1 {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
member := lit.Members.Nodes[0]
|
|
491
|
+
if member == nil || member.Kind != shimast.KindIndexSignature {
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
ctx.Report(node, "An index signature is preferred to be a Record type.")
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// no-explicit-any-rest-parameter — keeping this distinct from
|
|
498
|
+
// no-explicit-any: rest parameters typed `...args: any[]` are common
|
|
499
|
+
// enough that users want to allow them; this rule lets them ban that
|
|
500
|
+
// shape specifically.
|
|
501
|
+
//
|
|
502
|
+
// (Skipped: too narrow / overlaps with no-explicit-any.)
|
|
503
|
+
|
|
504
|
+
// ban-tslint-comment: `// tslint:disable`. tslint is dead; comments
|
|
505
|
+
// referencing it should be cleaned up.
|
|
506
|
+
type banTslintComment struct{}
|
|
507
|
+
|
|
508
|
+
func (banTslintComment) Name() string { return "ban-tslint-comment" }
|
|
509
|
+
func (banTslintComment) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
|
|
510
|
+
func (banTslintComment) Check(ctx *Context, node *shimast.Node) {
|
|
511
|
+
if ctx.File == nil {
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
text := ctx.File.Text()
|
|
515
|
+
for i := 0; i < len(text)-2; i++ {
|
|
516
|
+
if text[i] == '/' && text[i+1] == '/' {
|
|
517
|
+
// Find end of line.
|
|
518
|
+
end := i
|
|
519
|
+
for end < len(text) && text[end] != '\n' {
|
|
520
|
+
end++
|
|
521
|
+
}
|
|
522
|
+
line := text[i:end]
|
|
523
|
+
if strings.Contains(line, "tslint:") {
|
|
524
|
+
ctx.ReportRange(i, end, "tslint comment detected.")
|
|
525
|
+
}
|
|
526
|
+
i = end
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// adjacent-overload-signatures: function/method overloads must be
|
|
532
|
+
// declared next to each other. ESLint catches the visual confusion
|
|
533
|
+
// when overloads are interleaved with other members.
|
|
534
|
+
type adjacentOverloadSignatures struct{}
|
|
535
|
+
|
|
536
|
+
func (adjacentOverloadSignatures) Name() string { return "adjacent-overload-signatures" }
|
|
537
|
+
func (adjacentOverloadSignatures) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindInterfaceDeclaration, shimast.KindTypeLiteral, shimast.KindClassDeclaration, shimast.KindClassExpression, shimast.KindModuleBlock, shimast.KindSourceFile} }
|
|
538
|
+
func (adjacentOverloadSignatures) Check(ctx *Context, node *shimast.Node) {
|
|
539
|
+
members := containerMembers(node)
|
|
540
|
+
if len(members) == 0 {
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
type entry struct {
|
|
544
|
+
index int
|
|
545
|
+
name string
|
|
546
|
+
kind shimast.Kind
|
|
547
|
+
}
|
|
548
|
+
seen := []entry{}
|
|
549
|
+
for i, m := range members {
|
|
550
|
+
name, kind, ok := overloadName(m)
|
|
551
|
+
if !ok {
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
for _, prev := range seen {
|
|
555
|
+
if prev.name == name && prev.kind == kind && prev.index < i-1 {
|
|
556
|
+
// Check there isn't already a same-name entry adjacent.
|
|
557
|
+
if i > 0 {
|
|
558
|
+
prevName, prevKind, _ := overloadName(members[i-1])
|
|
559
|
+
if prevName == name && prevKind == kind {
|
|
560
|
+
break
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
ctx.Report(m, "All "+name+" signatures should be adjacent.")
|
|
564
|
+
break
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
seen = append(seen, entry{index: i, name: name, kind: kind})
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
func containerMembers(node *shimast.Node) []*shimast.Node {
|
|
572
|
+
switch node.Kind {
|
|
573
|
+
case shimast.KindInterfaceDeclaration:
|
|
574
|
+
decl := node.AsInterfaceDeclaration()
|
|
575
|
+
if decl != nil && decl.Members != nil {
|
|
576
|
+
return decl.Members.Nodes
|
|
577
|
+
}
|
|
578
|
+
case shimast.KindTypeLiteral:
|
|
579
|
+
lit := node.AsTypeLiteralNode()
|
|
580
|
+
if lit != nil && lit.Members != nil {
|
|
581
|
+
return lit.Members.Nodes
|
|
582
|
+
}
|
|
583
|
+
case shimast.KindClassDeclaration:
|
|
584
|
+
decl := node.AsClassDeclaration()
|
|
585
|
+
if decl != nil && decl.Members != nil {
|
|
586
|
+
return decl.Members.Nodes
|
|
587
|
+
}
|
|
588
|
+
case shimast.KindClassExpression:
|
|
589
|
+
decl := node.AsClassExpression()
|
|
590
|
+
if decl != nil && decl.Members != nil {
|
|
591
|
+
return decl.Members.Nodes
|
|
592
|
+
}
|
|
593
|
+
case shimast.KindModuleBlock:
|
|
594
|
+
mb := node.AsModuleBlock()
|
|
595
|
+
if mb != nil && mb.Statements != nil {
|
|
596
|
+
return mb.Statements.Nodes
|
|
597
|
+
}
|
|
598
|
+
case shimast.KindSourceFile:
|
|
599
|
+
f := node.AsSourceFile()
|
|
600
|
+
if f != nil && f.Statements != nil {
|
|
601
|
+
return f.Statements.Nodes
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return nil
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
func overloadName(m *shimast.Node) (string, shimast.Kind, bool) {
|
|
608
|
+
if m == nil {
|
|
609
|
+
return "", 0, false
|
|
610
|
+
}
|
|
611
|
+
switch m.Kind {
|
|
612
|
+
case shimast.KindMethodSignature:
|
|
613
|
+
ms := m.AsMethodSignatureDeclaration()
|
|
614
|
+
if ms != nil {
|
|
615
|
+
return identifierText(ms.Name()), m.Kind, true
|
|
616
|
+
}
|
|
617
|
+
case shimast.KindMethodDeclaration:
|
|
618
|
+
md := m.AsMethodDeclaration()
|
|
619
|
+
if md != nil {
|
|
620
|
+
return identifierText(md.Name()), m.Kind, true
|
|
621
|
+
}
|
|
622
|
+
case shimast.KindFunctionDeclaration:
|
|
623
|
+
fd := m.AsFunctionDeclaration()
|
|
624
|
+
if fd != nil {
|
|
625
|
+
return identifierText(fd.Name()), m.Kind, true
|
|
626
|
+
}
|
|
627
|
+
case shimast.KindCallSignature, shimast.KindConstructSignature:
|
|
628
|
+
return "(" + m.Kind.String() + ")", m.Kind, true
|
|
629
|
+
}
|
|
630
|
+
return "", 0, false
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// no-this-alias-helper: shared helpers for ts rules above.
|
|
634
|
+
var _ = struct{}{}
|
|
635
|
+
|
|
636
|
+
func init() {
|
|
637
|
+
Register(noConfusingNonNullAssertion{})
|
|
638
|
+
Register(noDuplicateEnumValues{})
|
|
639
|
+
Register(noExtraNonNullAssertion{})
|
|
640
|
+
Register(noNonNullAssertedOptionalChain{})
|
|
641
|
+
Register(noMisusedNew{})
|
|
642
|
+
Register(preferEnumInitializers{})
|
|
643
|
+
Register(preferForOf{})
|
|
644
|
+
Register(preferFunctionType{})
|
|
645
|
+
Register(preferNamespaceKeyword{})
|
|
646
|
+
Register(tripleSlashReference{})
|
|
647
|
+
Register(noArrayDelete{})
|
|
648
|
+
Register(consistentTypeImports{})
|
|
649
|
+
Register(noEmptyObjectType{})
|
|
650
|
+
Register(arrayType{})
|
|
651
|
+
Register(consistentIndexedObjectStyle{})
|
|
652
|
+
Register(banTslintComment{})
|
|
653
|
+
Register(adjacentOverloadSignatures{})
|
|
654
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package lint
|
|
2
|
+
|
|
3
|
+
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
|
+
|
|
5
|
+
// no-var: ban `var` declarations. ESLint canonical:
|
|
6
|
+
// https://eslint.org/docs/latest/rules/no-var
|
|
7
|
+
type noVar struct{}
|
|
8
|
+
|
|
9
|
+
func (noVar) Name() string { return "no-var" }
|
|
10
|
+
func (noVar) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindVariableStatement} }
|
|
11
|
+
func (noVar) Check(ctx *Context, node *shimast.Node) {
|
|
12
|
+
stmt := node.AsVariableStatement()
|
|
13
|
+
if stmt == nil || stmt.DeclarationList == nil {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
if shimast.IsVar(stmt.DeclarationList) {
|
|
17
|
+
ctx.Report(node, "Unexpected var, use let or const instead.")
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// no-undef-init: forbid `let x = undefined` and `var x = undefined`.
|
|
22
|
+
// ESLint canonical: https://eslint.org/docs/latest/rules/no-undef-init
|
|
23
|
+
type noUndefInit struct{}
|
|
24
|
+
|
|
25
|
+
func (noUndefInit) Name() string { return "no-undef-init" }
|
|
26
|
+
func (noUndefInit) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindVariableDeclaration} }
|
|
27
|
+
func (noUndefInit) Check(ctx *Context, node *shimast.Node) {
|
|
28
|
+
decl := node.AsVariableDeclaration()
|
|
29
|
+
if decl == nil || decl.Initializer == nil {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
if identifierText(decl.Initializer) == "undefined" {
|
|
33
|
+
ctx.Report(decl.Initializer, "It's not necessary to initialize \"undefined\".")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func init() {
|
|
38
|
+
Register(noVar{})
|
|
39
|
+
Register(noUndefInit{})
|
|
40
|
+
}
|