@ttsc/lint 0.5.0 → 0.6.0-dev.20250501
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/README.md +66 -8
- package/go.mod +1 -1
- package/lib/index.d.ts +10 -0
- package/lib/index.js +40 -0
- package/lib/index.js.map +1 -0
- package/lib/structures/ITtscLintConfig.d.ts +5 -0
- package/lib/structures/ITtscLintConfig.js +3 -0
- package/lib/structures/ITtscLintConfig.js.map +1 -0
- package/lib/structures/ITtscLintRule.d.ts +1 -0
- package/lib/structures/ITtscLintRule.js +3 -0
- package/lib/structures/ITtscLintRule.js.map +1 -0
- package/lib/structures/ITtscLintSeverity.d.ts +1 -0
- package/lib/structures/ITtscLintSeverity.js +3 -0
- package/lib/structures/ITtscLintSeverity.js.map +1 -0
- package/package.json +18 -9
- package/plugin/ast_helpers.go +56 -0
- package/plugin/compile.go +4 -4
- package/plugin/config.go +359 -5
- package/plugin/rules_gap.go +341 -0
- package/plugin/rules_logic.go +14 -1
- package/plugin/rules_var.go +87 -0
- package/src/index.ts +35 -0
- package/src/structures/ITtscLintConfig.ts +6 -0
- package/src/structures/ITtscLintRule.ts +131 -0
- package/src/structures/ITtscLintSeverity.ts +1 -0
- package/tsconfig.json +10 -0
- package/src/index.cjs +0 -29
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
|
+
|
|
5
|
+
// no-empty-static-block: `class C { static {} }` has no effect.
|
|
6
|
+
// ESLint recommended: https://eslint.org/docs/latest/rules/no-empty-static-block
|
|
7
|
+
type noEmptyStaticBlock struct{}
|
|
8
|
+
|
|
9
|
+
func (noEmptyStaticBlock) Name() string { return "no-empty-static-block" }
|
|
10
|
+
func (noEmptyStaticBlock) Visits() []shimast.Kind {
|
|
11
|
+
return []shimast.Kind{shimast.KindClassStaticBlockDeclaration}
|
|
12
|
+
}
|
|
13
|
+
func (noEmptyStaticBlock) Check(ctx *Context, node *shimast.Node) {
|
|
14
|
+
block := node.AsClassStaticBlockDeclaration()
|
|
15
|
+
if block == nil || block.Body == nil {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
body := block.Body.AsBlock()
|
|
19
|
+
if body == nil || body.Statements == nil || len(body.Statements.Nodes) == 0 {
|
|
20
|
+
ctx.Report(node, "Unexpected empty static block.")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// no-setter-return: setters must not return a value.
|
|
25
|
+
// ESLint recommended: https://eslint.org/docs/latest/rules/no-setter-return
|
|
26
|
+
type noSetterReturn struct{}
|
|
27
|
+
|
|
28
|
+
func (noSetterReturn) Name() string { return "no-setter-return" }
|
|
29
|
+
func (noSetterReturn) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindReturnStatement} }
|
|
30
|
+
func (noSetterReturn) Check(ctx *Context, node *shimast.Node) {
|
|
31
|
+
ret := node.AsReturnStatement()
|
|
32
|
+
if ret == nil || ret.Expression == nil || !isInsideDirectSetter(node) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
ctx.Report(node, "Setter should not return a value.")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func isInsideDirectSetter(node *shimast.Node) bool {
|
|
39
|
+
for p := node.Parent; p != nil; p = p.Parent {
|
|
40
|
+
if p.Kind == shimast.KindSetAccessor {
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
if isFunctionLikeKind(p) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// no-unused-labels: a label is useful only when a break/continue targets it.
|
|
51
|
+
// ESLint recommended: https://eslint.org/docs/latest/rules/no-unused-labels
|
|
52
|
+
type noUnusedLabels struct{}
|
|
53
|
+
|
|
54
|
+
func (noUnusedLabels) Name() string { return "no-unused-labels" }
|
|
55
|
+
func (noUnusedLabels) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindLabeledStatement} }
|
|
56
|
+
func (noUnusedLabels) Check(ctx *Context, node *shimast.Node) {
|
|
57
|
+
stmt := node.AsLabeledStatement()
|
|
58
|
+
if stmt == nil || stmt.Label == nil {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
label := identifierText(stmt.Label)
|
|
62
|
+
if label == "" {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
used := false
|
|
66
|
+
walkDescendants(stmt.Statement, func(child *shimast.Node) {
|
|
67
|
+
if used || child == nil {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
switch child.Kind {
|
|
71
|
+
case shimast.KindBreakStatement:
|
|
72
|
+
br := child.AsBreakStatement()
|
|
73
|
+
used = br != nil && identifierText(br.Label) == label
|
|
74
|
+
case shimast.KindContinueStatement:
|
|
75
|
+
cont := child.AsContinueStatement()
|
|
76
|
+
used = cont != nil && identifierText(cont.Label) == label
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
if !used {
|
|
80
|
+
ctx.Report(stmt.Label, "Label '"+label+"' is defined but never used.")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// no-dynamic-delete: avoid deleting properties through dynamic keys.
|
|
85
|
+
// typescript-eslint strict: https://typescript-eslint.io/rules/no-dynamic-delete/
|
|
86
|
+
type noDynamicDelete struct{}
|
|
87
|
+
|
|
88
|
+
func (noDynamicDelete) Name() string { return "no-dynamic-delete" }
|
|
89
|
+
func (noDynamicDelete) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindDeleteExpression} }
|
|
90
|
+
func (noDynamicDelete) Check(ctx *Context, node *shimast.Node) {
|
|
91
|
+
del := node.AsDeleteExpression()
|
|
92
|
+
if del == nil || del.Expression == nil || del.Expression.Kind != shimast.KindElementAccessExpression {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
access := del.Expression.AsElementAccessExpression()
|
|
96
|
+
if access == nil || isStaticPropertyKey(access.ArgumentExpression) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
ctx.Report(node, "Do not delete dynamically computed property keys.")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func isStaticPropertyKey(node *shimast.Node) bool {
|
|
103
|
+
node = stripParens(node)
|
|
104
|
+
if node == nil {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
switch node.Kind {
|
|
108
|
+
case shimast.KindStringLiteral,
|
|
109
|
+
shimast.KindNoSubstitutionTemplateLiteral,
|
|
110
|
+
shimast.KindNumericLiteral:
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// no-non-null-asserted-nullish-coalescing: `foo! ?? bar` is contradictory.
|
|
117
|
+
// typescript-eslint strict: https://typescript-eslint.io/rules/no-non-null-asserted-nullish-coalescing/
|
|
118
|
+
type noNonNullAssertedNullishCoalescing struct{}
|
|
119
|
+
|
|
120
|
+
func (noNonNullAssertedNullishCoalescing) Name() string {
|
|
121
|
+
return "no-non-null-asserted-nullish-coalescing"
|
|
122
|
+
}
|
|
123
|
+
func (noNonNullAssertedNullishCoalescing) Visits() []shimast.Kind {
|
|
124
|
+
return []shimast.Kind{shimast.KindBinaryExpression}
|
|
125
|
+
}
|
|
126
|
+
func (noNonNullAssertedNullishCoalescing) Check(ctx *Context, node *shimast.Node) {
|
|
127
|
+
expr := node.AsBinaryExpression()
|
|
128
|
+
if expr == nil || expr.OperatorToken == nil || expr.OperatorToken.Kind != shimast.KindQuestionQuestionToken {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
if left := stripParens(expr.Left); left != nil && left.Kind == shimast.KindNonNullExpression {
|
|
132
|
+
ctx.Report(left, "Nullish coalescing is unnecessary after a non-null assertion.")
|
|
133
|
+
}
|
|
134
|
+
if right := stripParens(expr.Right); right != nil && right.Kind == shimast.KindNonNullExpression {
|
|
135
|
+
ctx.Report(right, "Nullish coalescing is unnecessary before a non-null assertion.")
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// no-unnecessary-type-constraint: `<T extends any>` / `<T extends unknown>`.
|
|
140
|
+
// typescript-eslint recommended: https://typescript-eslint.io/rules/no-unnecessary-type-constraint/
|
|
141
|
+
type noUnnecessaryTypeConstraint struct{}
|
|
142
|
+
|
|
143
|
+
func (noUnnecessaryTypeConstraint) Name() string { return "no-unnecessary-type-constraint" }
|
|
144
|
+
func (noUnnecessaryTypeConstraint) Visits() []shimast.Kind {
|
|
145
|
+
return []shimast.Kind{shimast.KindTypeParameter}
|
|
146
|
+
}
|
|
147
|
+
func (noUnnecessaryTypeConstraint) Check(ctx *Context, node *shimast.Node) {
|
|
148
|
+
param := node.AsTypeParameterDeclaration()
|
|
149
|
+
if param == nil || param.Constraint == nil {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
if param.Constraint.Kind == shimast.KindAnyKeyword || param.Constraint.Kind == shimast.KindUnknownKeyword {
|
|
153
|
+
ctx.Report(param.Constraint, "Constraining a type parameter to any or unknown is unnecessary.")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// no-unsafe-function-type: `Function` accepts any callable shape.
|
|
158
|
+
// typescript-eslint recommended: https://typescript-eslint.io/rules/no-unsafe-function-type/
|
|
159
|
+
type noUnsafeFunctionType struct{}
|
|
160
|
+
|
|
161
|
+
func (noUnsafeFunctionType) Name() string { return "no-unsafe-function-type" }
|
|
162
|
+
func (noUnsafeFunctionType) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindTypeReference} }
|
|
163
|
+
func (noUnsafeFunctionType) Check(ctx *Context, node *shimast.Node) {
|
|
164
|
+
ref := node.AsTypeReferenceNode()
|
|
165
|
+
if ref == nil || identifierText(ref.TypeName) != "Function" {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
ctx.Report(node, "The Function type is unsafe. Use a specific function type instead.")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// no-wrapper-object-types: prefer primitive type keywords over boxed object
|
|
172
|
+
// type names such as `String` and `Boolean`.
|
|
173
|
+
// typescript-eslint recommended: https://typescript-eslint.io/rules/no-wrapper-object-types/
|
|
174
|
+
type noWrapperObjectTypes struct{}
|
|
175
|
+
|
|
176
|
+
func (noWrapperObjectTypes) Name() string { return "no-wrapper-object-types" }
|
|
177
|
+
func (noWrapperObjectTypes) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindTypeReference} }
|
|
178
|
+
func (noWrapperObjectTypes) Check(ctx *Context, node *shimast.Node) {
|
|
179
|
+
ref := node.AsTypeReferenceNode()
|
|
180
|
+
if ref == nil {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
switch identifierText(ref.TypeName) {
|
|
184
|
+
case "String", "Number", "Boolean", "Symbol", "BigInt", "Object":
|
|
185
|
+
ctx.Report(node, "Use primitive type keywords instead of wrapper object types.")
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// no-useless-constructor: an empty constructor with no parameters is noise.
|
|
190
|
+
// typescript-eslint strict: https://typescript-eslint.io/rules/no-useless-constructor/
|
|
191
|
+
type noUselessConstructor struct{}
|
|
192
|
+
|
|
193
|
+
func (noUselessConstructor) Name() string { return "no-useless-constructor" }
|
|
194
|
+
func (noUselessConstructor) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindConstructor} }
|
|
195
|
+
func (noUselessConstructor) Check(ctx *Context, node *shimast.Node) {
|
|
196
|
+
ctor := node.AsConstructorDeclaration()
|
|
197
|
+
if ctor == nil || ctor.Body == nil {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
if len(node.Parameters()) != 0 {
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
body := ctor.Body.AsBlock()
|
|
204
|
+
if body == nil || body.Statements == nil || len(body.Statements.Nodes) == 0 {
|
|
205
|
+
ctx.Report(node, "Useless empty constructor.")
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// prefer-literal-enum-member: computed enum members are harder to inspect.
|
|
210
|
+
// typescript-eslint strict: https://typescript-eslint.io/rules/prefer-literal-enum-member/
|
|
211
|
+
type preferLiteralEnumMember struct{}
|
|
212
|
+
|
|
213
|
+
func (preferLiteralEnumMember) Name() string { return "prefer-literal-enum-member" }
|
|
214
|
+
func (preferLiteralEnumMember) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindEnumMember} }
|
|
215
|
+
func (preferLiteralEnumMember) Check(ctx *Context, node *shimast.Node) {
|
|
216
|
+
member := node.AsEnumMember()
|
|
217
|
+
if member == nil || member.Initializer == nil || isLiteralLike(member.Initializer) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
ctx.Report(member.Initializer, "Enum member initializer should be a literal value.")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// consistent-type-assertions: prefer `value as Type` over `<Type>value`.
|
|
224
|
+
// typescript-eslint stylistic: https://typescript-eslint.io/rules/consistent-type-assertions/
|
|
225
|
+
type consistentTypeAssertions struct{}
|
|
226
|
+
|
|
227
|
+
func (consistentTypeAssertions) Name() string { return "consistent-type-assertions" }
|
|
228
|
+
func (consistentTypeAssertions) Visits() []shimast.Kind {
|
|
229
|
+
return []shimast.Kind{shimast.KindTypeAssertionExpression}
|
|
230
|
+
}
|
|
231
|
+
func (consistentTypeAssertions) Check(ctx *Context, node *shimast.Node) {
|
|
232
|
+
ctx.Report(node, "Use `as` type assertions instead of angle-bracket assertions.")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// consistent-type-definitions: prefer interfaces for object-shaped public
|
|
236
|
+
// contracts. This mirrors typescript-eslint's default option.
|
|
237
|
+
// typescript-eslint stylistic: https://typescript-eslint.io/rules/consistent-type-definitions/
|
|
238
|
+
type consistentTypeDefinitions struct{}
|
|
239
|
+
|
|
240
|
+
func (consistentTypeDefinitions) Name() string { return "consistent-type-definitions" }
|
|
241
|
+
func (consistentTypeDefinitions) Visits() []shimast.Kind {
|
|
242
|
+
return []shimast.Kind{shimast.KindTypeAliasDeclaration}
|
|
243
|
+
}
|
|
244
|
+
func (consistentTypeDefinitions) Check(ctx *Context, node *shimast.Node) {
|
|
245
|
+
alias := node.AsTypeAliasDeclaration()
|
|
246
|
+
if alias == nil || alias.Type == nil || alias.Type.Kind != shimast.KindTypeLiteral {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
ctx.Report(node, "Use an interface instead of a type literal alias.")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// dot-notation: `obj["prop"]` should be `obj.prop` when the key is a valid
|
|
253
|
+
// identifier.
|
|
254
|
+
// ESLint canonical: https://eslint.org/docs/latest/rules/dot-notation
|
|
255
|
+
type dotNotation struct{}
|
|
256
|
+
|
|
257
|
+
func (dotNotation) Name() string { return "dot-notation" }
|
|
258
|
+
func (dotNotation) Visits() []shimast.Kind {
|
|
259
|
+
return []shimast.Kind{shimast.KindElementAccessExpression}
|
|
260
|
+
}
|
|
261
|
+
func (dotNotation) Check(ctx *Context, node *shimast.Node) {
|
|
262
|
+
access := node.AsElementAccessExpression()
|
|
263
|
+
if access == nil {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
key := stringLiteralText(access.ArgumentExpression)
|
|
267
|
+
if key == "" || !isSimpleIdentifierName(key) {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
ctx.Report(node, "Use dot notation instead of a string literal property access.")
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
func isSimpleIdentifierName(value string) bool {
|
|
274
|
+
if value == "" {
|
|
275
|
+
return false
|
|
276
|
+
}
|
|
277
|
+
for i, r := range value {
|
|
278
|
+
if r == '_' || r == '$' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' {
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
if i > 0 && r >= '0' && r <= '9' {
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
return true
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// no-unsafe-declaration-merging: class/interface merging hides runtime vs type
|
|
290
|
+
// surface differences.
|
|
291
|
+
// typescript-eslint recommended: https://typescript-eslint.io/rules/no-unsafe-declaration-merging/
|
|
292
|
+
type noUnsafeDeclarationMerging struct{}
|
|
293
|
+
|
|
294
|
+
func (noUnsafeDeclarationMerging) Name() string { return "no-unsafe-declaration-merging" }
|
|
295
|
+
func (noUnsafeDeclarationMerging) Visits() []shimast.Kind {
|
|
296
|
+
return []shimast.Kind{shimast.KindSourceFile}
|
|
297
|
+
}
|
|
298
|
+
func (noUnsafeDeclarationMerging) Check(ctx *Context, node *shimast.Node) {
|
|
299
|
+
classes := map[string]bool{}
|
|
300
|
+
var interfaces []*shimast.Node
|
|
301
|
+
walkDescendants(node, func(child *shimast.Node) {
|
|
302
|
+
switch child.Kind {
|
|
303
|
+
case shimast.KindClassDeclaration:
|
|
304
|
+
decl := child.AsClassDeclaration()
|
|
305
|
+
if decl != nil {
|
|
306
|
+
if name := identifierText(decl.Name()); name != "" {
|
|
307
|
+
classes[name] = true
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
case shimast.KindInterfaceDeclaration:
|
|
311
|
+
interfaces = append(interfaces, child)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
for _, ifaceNode := range interfaces {
|
|
315
|
+
decl := ifaceNode.AsInterfaceDeclaration()
|
|
316
|
+
if decl == nil {
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
name := identifierText(decl.Name())
|
|
320
|
+
if name != "" && classes[name] {
|
|
321
|
+
ctx.Report(ifaceNode, "Unsafe declaration merging between class and interface '"+name+"'.")
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func init() {
|
|
327
|
+
Register(noEmptyStaticBlock{})
|
|
328
|
+
Register(noSetterReturn{})
|
|
329
|
+
Register(noUnusedLabels{})
|
|
330
|
+
Register(noDynamicDelete{})
|
|
331
|
+
Register(noNonNullAssertedNullishCoalescing{})
|
|
332
|
+
Register(noUnnecessaryTypeConstraint{})
|
|
333
|
+
Register(noUnsafeDeclarationMerging{})
|
|
334
|
+
Register(noUnsafeFunctionType{})
|
|
335
|
+
Register(noWrapperObjectTypes{})
|
|
336
|
+
Register(noUselessConstructor{})
|
|
337
|
+
Register(preferLiteralEnumMember{})
|
|
338
|
+
Register(consistentTypeAssertions{})
|
|
339
|
+
Register(consistentTypeDefinitions{})
|
|
340
|
+
Register(dotNotation{})
|
|
341
|
+
}
|
package/plugin/rules_logic.go
CHANGED
|
@@ -277,7 +277,20 @@ func isAssignmentOperator(kind shimast.Kind) bool {
|
|
|
277
277
|
switch kind {
|
|
278
278
|
case shimast.KindEqualsToken,
|
|
279
279
|
shimast.KindPlusEqualsToken,
|
|
280
|
-
shimast.KindMinusEqualsToken
|
|
280
|
+
shimast.KindMinusEqualsToken,
|
|
281
|
+
shimast.KindAsteriskEqualsToken,
|
|
282
|
+
shimast.KindAsteriskAsteriskEqualsToken,
|
|
283
|
+
shimast.KindSlashEqualsToken,
|
|
284
|
+
shimast.KindPercentEqualsToken,
|
|
285
|
+
shimast.KindLessThanLessThanEqualsToken,
|
|
286
|
+
shimast.KindGreaterThanGreaterThanEqualsToken,
|
|
287
|
+
shimast.KindGreaterThanGreaterThanGreaterThanEqualsToken,
|
|
288
|
+
shimast.KindAmpersandEqualsToken,
|
|
289
|
+
shimast.KindBarEqualsToken,
|
|
290
|
+
shimast.KindCaretEqualsToken,
|
|
291
|
+
shimast.KindAmpersandAmpersandEqualsToken,
|
|
292
|
+
shimast.KindBarBarEqualsToken,
|
|
293
|
+
shimast.KindQuestionQuestionEqualsToken:
|
|
281
294
|
return true
|
|
282
295
|
}
|
|
283
296
|
return false
|
package/plugin/rules_var.go
CHANGED
|
@@ -18,6 +18,92 @@ func (noVar) Check(ctx *Context, node *shimast.Node) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// prefer-const: flag `let` declarations whose binding is never reassigned.
|
|
22
|
+
// This follows ESLint's core rule for the common AST-local cases. It is
|
|
23
|
+
// intentionally conservative: destructuring and declaration-only `let`
|
|
24
|
+
// variables are skipped until the lint host grows full scope/data-flow state.
|
|
25
|
+
// ESLint canonical: https://eslint.org/docs/latest/rules/prefer-const
|
|
26
|
+
type preferConst struct{}
|
|
27
|
+
|
|
28
|
+
func (preferConst) Name() string { return "prefer-const" }
|
|
29
|
+
func (preferConst) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
|
|
30
|
+
func (preferConst) Check(ctx *Context, node *shimast.Node) {
|
|
31
|
+
type candidate struct {
|
|
32
|
+
name string
|
|
33
|
+
node *shimast.Node
|
|
34
|
+
}
|
|
35
|
+
var candidates []candidate
|
|
36
|
+
assigned := map[string]bool{}
|
|
37
|
+
|
|
38
|
+
walkDescendants(node, func(child *shimast.Node) {
|
|
39
|
+
switch child.Kind {
|
|
40
|
+
case shimast.KindVariableDeclaration:
|
|
41
|
+
decl := child.AsVariableDeclaration()
|
|
42
|
+
if decl == nil || child.Parent == nil || child.Parent.Kind != shimast.KindVariableDeclarationList {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
listNode := child.Parent
|
|
46
|
+
if !shimast.IsLet(listNode) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
name := identifierText(decl.Name())
|
|
50
|
+
if name == "" {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if !isConstEligibleLetDeclaration(child, decl) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
candidates = append(candidates, candidate{name: name, node: child})
|
|
57
|
+
case shimast.KindBinaryExpression:
|
|
58
|
+
expr := child.AsBinaryExpression()
|
|
59
|
+
if expr == nil || expr.OperatorToken == nil || !isAssignmentOperator(expr.OperatorToken.Kind) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
for _, name := range bindingIdentifierNames(expr.Left) {
|
|
63
|
+
assigned[name] = true
|
|
64
|
+
}
|
|
65
|
+
case shimast.KindPrefixUnaryExpression:
|
|
66
|
+
expr := child.AsPrefixUnaryExpression()
|
|
67
|
+
if expr == nil {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if expr.Operator == shimast.KindPlusPlusToken || expr.Operator == shimast.KindMinusMinusToken {
|
|
71
|
+
if name := identifierText(expr.Operand); name != "" {
|
|
72
|
+
assigned[name] = true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
case shimast.KindPostfixUnaryExpression:
|
|
76
|
+
expr := child.AsPostfixUnaryExpression()
|
|
77
|
+
if expr == nil {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
if expr.Operator == shimast.KindPlusPlusToken || expr.Operator == shimast.KindMinusMinusToken {
|
|
81
|
+
if name := identifierText(expr.Operand); name != "" {
|
|
82
|
+
assigned[name] = true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
for _, c := range candidates {
|
|
89
|
+
if !assigned[c.name] {
|
|
90
|
+
ctx.Report(c.node, "Use const instead of let.")
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func isConstEligibleLetDeclaration(node *shimast.Node, decl *shimast.VariableDeclaration) bool {
|
|
96
|
+
if decl.Initializer != nil {
|
|
97
|
+
if node.Parent != nil && node.Parent.Parent != nil && node.Parent.Parent.Kind == shimast.KindForStatement {
|
|
98
|
+
list := node.Parent.AsVariableDeclarationList()
|
|
99
|
+
return list == nil || list.Declarations == nil || len(list.Declarations.Nodes) == 1
|
|
100
|
+
}
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
return node.Parent != nil && node.Parent.Parent != nil &&
|
|
104
|
+
(node.Parent.Parent.Kind == shimast.KindForInStatement || node.Parent.Parent.Kind == shimast.KindForOfStatement)
|
|
105
|
+
}
|
|
106
|
+
|
|
21
107
|
// no-undef-init: forbid `let x = undefined` and `var x = undefined`.
|
|
22
108
|
// ESLint canonical: https://eslint.org/docs/latest/rules/no-undef-init
|
|
23
109
|
type noUndefInit struct{}
|
|
@@ -36,5 +122,6 @@ func (noUndefInit) Check(ctx *Context, node *shimast.Node) {
|
|
|
36
122
|
|
|
37
123
|
func init() {
|
|
38
124
|
Register(noVar{})
|
|
125
|
+
Register(preferConst{})
|
|
39
126
|
Register(noUndefInit{})
|
|
40
127
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type {
|
|
3
|
+
ITtscProjectPluginConfig,
|
|
4
|
+
ITtscPlugin,
|
|
5
|
+
ITtscPluginFactoryContext,
|
|
6
|
+
} from "ttsc";
|
|
7
|
+
|
|
8
|
+
import type { ITtscLintConfig } from "./structures/ITtscLintConfig";
|
|
9
|
+
|
|
10
|
+
export * from "./structures/ITtscLintConfig";
|
|
11
|
+
export * from "./structures/ITtscLintRule";
|
|
12
|
+
export * from "./structures/ITtscLintSeverity";
|
|
13
|
+
|
|
14
|
+
export type ITtscLintPluginConfig = ITtscProjectPluginConfig & {
|
|
15
|
+
config?: string | ITtscLintConfig;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function createTtscPlugin(
|
|
19
|
+
_context: ITtscPluginFactoryContext<ITtscLintPluginConfig>,
|
|
20
|
+
): ITtscPlugin {
|
|
21
|
+
return {
|
|
22
|
+
name: "@ttsc/lint",
|
|
23
|
+
native: {
|
|
24
|
+
mode: "ttsc-lint",
|
|
25
|
+
source: {
|
|
26
|
+
dir: path.resolve(__dirname, ".."),
|
|
27
|
+
entry: "./plugin",
|
|
28
|
+
},
|
|
29
|
+
contractVersion: 1,
|
|
30
|
+
capabilities: ["check"],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default createTtscPlugin;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export type ITtscLintRule =
|
|
2
|
+
| "adjacent-overload-signatures"
|
|
3
|
+
| "array-type"
|
|
4
|
+
| "ban-ts-comment"
|
|
5
|
+
| "ban-tslint-comment"
|
|
6
|
+
| "consistent-indexed-object-style"
|
|
7
|
+
| "consistent-type-assertions"
|
|
8
|
+
| "consistent-type-definitions"
|
|
9
|
+
| "consistent-type-imports"
|
|
10
|
+
| "dot-notation"
|
|
11
|
+
| "eqeqeq"
|
|
12
|
+
| "for-direction"
|
|
13
|
+
| "no-alert"
|
|
14
|
+
| "no-array-constructor"
|
|
15
|
+
| "no-array-delete"
|
|
16
|
+
| "no-async-promise-executor"
|
|
17
|
+
| "no-bitwise"
|
|
18
|
+
| "no-caller"
|
|
19
|
+
| "no-case-declarations"
|
|
20
|
+
| "no-class-assign"
|
|
21
|
+
| "no-compare-neg-zero"
|
|
22
|
+
| "no-cond-assign"
|
|
23
|
+
| "no-confusing-non-null-assertion"
|
|
24
|
+
| "no-console"
|
|
25
|
+
| "no-constant-condition"
|
|
26
|
+
| "no-continue"
|
|
27
|
+
| "no-control-regex"
|
|
28
|
+
| "no-debugger"
|
|
29
|
+
| "no-delete-var"
|
|
30
|
+
| "no-dupe-args"
|
|
31
|
+
| "no-dupe-else-if"
|
|
32
|
+
| "no-dupe-keys"
|
|
33
|
+
| "no-duplicate-case"
|
|
34
|
+
| "no-duplicate-enum-values"
|
|
35
|
+
| "no-dynamic-delete"
|
|
36
|
+
| "no-empty"
|
|
37
|
+
| "no-empty-character-class"
|
|
38
|
+
| "no-empty-function"
|
|
39
|
+
| "no-empty-interface"
|
|
40
|
+
| "no-empty-object-type"
|
|
41
|
+
| "no-empty-pattern"
|
|
42
|
+
| "no-empty-static-block"
|
|
43
|
+
| "no-eq-null"
|
|
44
|
+
| "no-eval"
|
|
45
|
+
| "no-ex-assign"
|
|
46
|
+
| "no-explicit-any"
|
|
47
|
+
| "no-extra-bind"
|
|
48
|
+
| "no-extra-boolean-cast"
|
|
49
|
+
| "no-extra-non-null-assertion"
|
|
50
|
+
| "no-fallthrough"
|
|
51
|
+
| "no-func-assign"
|
|
52
|
+
| "no-inferrable-types"
|
|
53
|
+
| "no-inner-declarations"
|
|
54
|
+
| "no-irregular-whitespace"
|
|
55
|
+
| "no-iterator"
|
|
56
|
+
| "no-labels"
|
|
57
|
+
| "no-lone-blocks"
|
|
58
|
+
| "no-lonely-if"
|
|
59
|
+
| "no-loss-of-precision"
|
|
60
|
+
| "no-misleading-character-class"
|
|
61
|
+
| "no-misused-new"
|
|
62
|
+
| "no-multi-assign"
|
|
63
|
+
| "no-multi-str"
|
|
64
|
+
| "no-namespace"
|
|
65
|
+
| "no-negated-condition"
|
|
66
|
+
| "no-nested-ternary"
|
|
67
|
+
| "no-new"
|
|
68
|
+
| "no-new-func"
|
|
69
|
+
| "no-new-wrappers"
|
|
70
|
+
| "no-non-null-asserted-nullish-coalescing"
|
|
71
|
+
| "no-non-null-asserted-optional-chain"
|
|
72
|
+
| "no-non-null-assertion"
|
|
73
|
+
| "no-obj-calls"
|
|
74
|
+
| "no-object-constructor"
|
|
75
|
+
| "no-octal"
|
|
76
|
+
| "no-octal-escape"
|
|
77
|
+
| "no-plusplus"
|
|
78
|
+
| "no-promise-executor-return"
|
|
79
|
+
| "no-proto"
|
|
80
|
+
| "no-prototype-builtins"
|
|
81
|
+
| "no-regex-spaces"
|
|
82
|
+
| "no-require-imports"
|
|
83
|
+
| "no-return-assign"
|
|
84
|
+
| "no-script-url"
|
|
85
|
+
| "no-self-assign"
|
|
86
|
+
| "no-self-compare"
|
|
87
|
+
| "no-sequences"
|
|
88
|
+
| "no-setter-return"
|
|
89
|
+
| "no-shadow-restricted-names"
|
|
90
|
+
| "no-sparse-arrays"
|
|
91
|
+
| "no-template-curly-in-string"
|
|
92
|
+
| "no-this-alias"
|
|
93
|
+
| "no-throw-literal"
|
|
94
|
+
| "no-undef-init"
|
|
95
|
+
| "no-undefined"
|
|
96
|
+
| "no-unnecessary-type-constraint"
|
|
97
|
+
| "no-unneeded-ternary"
|
|
98
|
+
| "no-unsafe-declaration-merging"
|
|
99
|
+
| "no-unsafe-finally"
|
|
100
|
+
| "no-unsafe-function-type"
|
|
101
|
+
| "no-unsafe-negation"
|
|
102
|
+
| "no-unused-expressions"
|
|
103
|
+
| "no-unused-labels"
|
|
104
|
+
| "no-useless-call"
|
|
105
|
+
| "no-useless-catch"
|
|
106
|
+
| "no-useless-computed-key"
|
|
107
|
+
| "no-useless-concat"
|
|
108
|
+
| "no-useless-constructor"
|
|
109
|
+
| "no-useless-rename"
|
|
110
|
+
| "no-var"
|
|
111
|
+
| "no-with"
|
|
112
|
+
| "no-wrapper-object-types"
|
|
113
|
+
| "object-shorthand"
|
|
114
|
+
| "operator-assignment"
|
|
115
|
+
| "prefer-as-const"
|
|
116
|
+
| "prefer-const"
|
|
117
|
+
| "prefer-enum-initializers"
|
|
118
|
+
| "prefer-exponentiation-operator"
|
|
119
|
+
| "prefer-for-of"
|
|
120
|
+
| "prefer-function-type"
|
|
121
|
+
| "prefer-literal-enum-member"
|
|
122
|
+
| "prefer-namespace-keyword"
|
|
123
|
+
| "prefer-spread"
|
|
124
|
+
| "prefer-template"
|
|
125
|
+
| "radix"
|
|
126
|
+
| "require-yield"
|
|
127
|
+
| "triple-slash-reference"
|
|
128
|
+
| "use-isnan"
|
|
129
|
+
| "valid-typeof"
|
|
130
|
+
| "vars-on-top"
|
|
131
|
+
| "yoda";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ITtscLintSeverity = "off" | "warning" | "warn" | "error" | 0 | 1 | 2;
|
package/tsconfig.json
ADDED
package/src/index.cjs
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const path = require("node:path");
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* `@ttsc/lint` plugin descriptor.
|
|
8
|
-
*
|
|
9
|
-
* The `rules` field on the tsconfig plugin entry flows straight through to
|
|
10
|
-
* the native binary via `--plugins-json`. Documented severity values are
|
|
11
|
-
* `"off"`, `"warning"`, or `"error"`.
|
|
12
|
-
*
|
|
13
|
-
* @param {Record<string, unknown>} _config
|
|
14
|
-
* @returns {{ name: string; native: { mode: string; source: { dir: string; entry: string }; contractVersion: 1; capabilities: string[] } }}
|
|
15
|
-
*/
|
|
16
|
-
module.exports = function createTtscLint(_config) {
|
|
17
|
-
return {
|
|
18
|
-
name: "@ttsc/lint",
|
|
19
|
-
native: {
|
|
20
|
-
mode: "ttsc-lint",
|
|
21
|
-
source: {
|
|
22
|
-
dir: path.resolve(__dirname, ".."),
|
|
23
|
-
entry: "./plugin",
|
|
24
|
-
},
|
|
25
|
-
contractVersion: 1,
|
|
26
|
-
capabilities: ["check"],
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
};
|