@ttsc/lint 0.10.2 → 0.11.0-dev.20260518
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 +172 -33
- package/lib/defineConfig.d.ts +55 -0
- package/lib/defineConfig.js +39 -0
- package/lib/defineConfig.js.map +1 -0
- package/lib/index.d.ts +25 -1
- package/lib/index.js +622 -2
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPlugin.d.ts +53 -0
- package/lib/structures/ITtscLintPlugin.js +3 -0
- package/lib/structures/ITtscLintPlugin.js.map +1 -0
- package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
- package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
- package/lib/structures/ITtscLintPluginMeta.js +3 -0
- package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
- package/lib/structures/PluginRuleNames.d.ts +14 -0
- package/lib/structures/PluginRuleNames.js +3 -0
- package/lib/structures/PluginRuleNames.js.map +1 -0
- package/lib/structures/TtscLintConfig.d.ts +24 -8
- package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
- package/lib/structures/TtscLintConfigEntry.js +3 -0
- package/lib/structures/TtscLintConfigEntry.js.map +1 -0
- package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
- package/lib/structures/TtscLintFormatConfig.js +3 -0
- package/lib/structures/TtscLintFormatConfig.js.map +1 -0
- package/lib/structures/TtscLintPlugins.d.ts +9 -0
- package/lib/structures/TtscLintPlugins.js +3 -0
- package/lib/structures/TtscLintPlugins.js.map +1 -0
- package/lib/structures/TtscLintRule.d.ts +1 -1
- package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
- package/lib/structures/TtscLintRuleEntry.js +3 -0
- package/lib/structures/TtscLintRuleEntry.js.map +1 -0
- package/lib/structures/TtscLintRuleMap.d.ts +44 -0
- package/lib/structures/TtscLintRuleMap.js +3 -0
- package/lib/structures/TtscLintRuleMap.js.map +1 -0
- package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
- package/lib/structures/TtscLintRuleOptions.js +3 -0
- package/lib/structures/TtscLintRuleOptions.js.map +1 -0
- package/lib/structures/defaultFormat.d.ts +31 -0
- package/lib/structures/defaultFormat.js +35 -0
- package/lib/structures/defaultFormat.js.map +1 -0
- package/lib/structures/index.d.ts +10 -0
- package/lib/structures/index.js +10 -0
- package/lib/structures/index.js.map +1 -1
- package/{plugin → linthost}/ast_helpers.go +92 -1
- package/{plugin → linthost}/compile.go +1 -1
- package/{plugin → linthost}/config.go +583 -66
- package/linthost/config_format.go +360 -0
- package/linthost/contrib_adapter.go +134 -0
- package/{plugin → linthost}/directives.go +1 -1
- package/linthost/dispatch.go +65 -0
- package/{plugin → linthost}/engine.go +156 -11
- package/{plugin → linthost}/eslint_runtime.go +76 -2
- package/linthost/fix.go +249 -0
- package/linthost/format.go +107 -0
- package/{plugin → linthost}/host.go +1 -1
- package/linthost/print_dispatch.go +118 -0
- package/linthost/print_doc.go +168 -0
- package/linthost/print_engine.go +343 -0
- package/linthost/print_nodes_array.go +46 -0
- package/linthost/print_nodes_call.go +180 -0
- package/linthost/print_nodes_imports.go +171 -0
- package/linthost/print_nodes_list.go +64 -0
- package/linthost/print_nodes_object.go +51 -0
- package/{plugin → linthost}/rules_arrays.go +1 -1
- package/{plugin → linthost}/rules_console.go +1 -1
- package/{plugin → linthost}/rules_debugger.go +1 -1
- package/{plugin → linthost}/rules_dupes.go +1 -1
- package/{plugin → linthost}/rules_empty.go +1 -1
- package/linthost/rules_escape.go +238 -0
- package/{plugin → linthost}/rules_eval.go +1 -1
- package/{plugin → linthost}/rules_finally.go +1 -1
- package/linthost/rules_format_jsdoc.go +230 -0
- package/linthost/rules_format_print_width.go +332 -0
- package/linthost/rules_format_quotes.go +219 -0
- package/linthost/rules_format_semi.go +190 -0
- package/linthost/rules_format_sort_imports.go +462 -0
- package/linthost/rules_format_trailing_comma.go +460 -0
- package/{plugin → linthost}/rules_gap.go +101 -6
- package/linthost/rules_imports.go +101 -0
- package/{plugin → linthost}/rules_logic.go +55 -3
- package/{plugin → linthost}/rules_loops.go +1 -1
- package/{plugin → linthost}/rules_misc.go +1 -1
- package/linthost/rules_params.go +64 -0
- package/{plugin → linthost}/rules_problems.go +10 -2
- package/linthost/rules_promise.go +111 -0
- package/{plugin → linthost}/rules_protos.go +1 -1
- package/{plugin → linthost}/rules_self.go +1 -1
- package/{plugin → linthost}/rules_strings.go +1 -1
- package/{plugin → linthost}/rules_suggestions.go +51 -9
- package/{plugin → linthost}/rules_throw.go +1 -1
- package/{plugin → linthost}/rules_ts.go +12 -2
- package/{plugin → linthost}/rules_ts_extra.go +90 -4
- package/{plugin → linthost}/rules_var.go +36 -5
- package/package.json +8 -6
- package/plugin/main.go +14 -27
- package/rule/astutil/astutil.go +148 -0
- package/rule/rule.go +291 -0
- package/src/defineConfig.ts +69 -0
- package/src/index.ts +807 -3
- package/src/structures/ITtscLintPlugin.ts +56 -0
- package/src/structures/ITtscLintPluginConfig.ts +86 -5
- package/src/structures/ITtscLintPluginMeta.ts +14 -0
- package/src/structures/PluginRuleNames.ts +19 -0
- package/src/structures/TtscLintConfig.ts +27 -8
- package/src/structures/TtscLintConfigEntry.ts +50 -0
- package/src/structures/TtscLintFormatConfig.ts +169 -0
- package/src/structures/TtscLintPlugins.ts +10 -0
- package/src/structures/TtscLintRule.ts +14 -1
- package/src/structures/TtscLintRuleEntry.ts +28 -0
- package/src/structures/TtscLintRuleMap.ts +52 -0
- package/src/structures/TtscLintRuleOptions.ts +162 -0
- package/src/structures/defaultFormat.ts +33 -0
- package/src/structures/index.ts +10 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package linthost
|
|
2
2
|
|
|
3
3
|
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
4
|
|
|
@@ -119,12 +119,64 @@ func (eqeqeq) Check(ctx *Context, node *shimast.Node) {
|
|
|
119
119
|
}
|
|
120
120
|
switch expr.OperatorToken.Kind {
|
|
121
121
|
case shimast.KindEqualsEqualsToken:
|
|
122
|
-
ctx
|
|
122
|
+
reportEqeqeq(ctx, expr.OperatorToken, expr, "Expected '===' and instead saw '=='.", "===")
|
|
123
123
|
case shimast.KindExclamationEqualsToken:
|
|
124
|
-
ctx
|
|
124
|
+
reportEqeqeq(ctx, expr.OperatorToken, expr, "Expected '!==' and instead saw '!='.", "!==")
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
func reportEqeqeq(ctx *Context, operator *shimast.Node, expr *shimast.BinaryExpression, message, replacement string) {
|
|
129
|
+
pos, end := tokenRange(ctx.File, operator)
|
|
130
|
+
if pos < 0 {
|
|
131
|
+
pos, end = operator.Pos(), operator.End()
|
|
132
|
+
}
|
|
133
|
+
if isEqeqeqAutoFixSafe(expr) {
|
|
134
|
+
ctx.ReportRangeFix(
|
|
135
|
+
pos,
|
|
136
|
+
end,
|
|
137
|
+
message,
|
|
138
|
+
TextEdit{Pos: pos, End: end, Text: replacement},
|
|
139
|
+
)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
ctx.ReportRange(pos, end, message)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func isEqeqeqAutoFixSafe(expr *shimast.BinaryExpression) bool {
|
|
146
|
+
if expr == nil {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
left := stripParens(expr.Left)
|
|
150
|
+
right := stripParens(expr.Right)
|
|
151
|
+
if left == nil || right == nil {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
if left.Kind == shimast.KindTypeOfExpression || right.Kind == shimast.KindTypeOfExpression {
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
leftKind := comparableLiteralKind(left)
|
|
158
|
+
return leftKind != "" && leftKind == comparableLiteralKind(right)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func comparableLiteralKind(node *shimast.Node) string {
|
|
162
|
+
if node == nil {
|
|
163
|
+
return ""
|
|
164
|
+
}
|
|
165
|
+
switch node.Kind {
|
|
166
|
+
case shimast.KindStringLiteral:
|
|
167
|
+
return "string"
|
|
168
|
+
case shimast.KindNumericLiteral:
|
|
169
|
+
return "number"
|
|
170
|
+
case shimast.KindBigIntLiteral:
|
|
171
|
+
return "bigint"
|
|
172
|
+
case shimast.KindTrueKeyword, shimast.KindFalseKeyword:
|
|
173
|
+
return "boolean"
|
|
174
|
+
case shimast.KindNullKeyword:
|
|
175
|
+
return "object"
|
|
176
|
+
}
|
|
177
|
+
return ""
|
|
178
|
+
}
|
|
179
|
+
|
|
128
180
|
// use-isnan: `x === NaN` is always false. Use `Number.isNaN(x)`.
|
|
129
181
|
// https://eslint.org/docs/latest/rules/use-isnan
|
|
130
182
|
type useIsnan struct{}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
|
+
|
|
5
|
+
// default-param-last: forbid parameters with default initializers that
|
|
6
|
+
// precede non-default parameters. The combination forces every caller
|
|
7
|
+
// to spell out `undefined` for the defaulted parameter — which defeats
|
|
8
|
+
// the purpose of the default. ESLint canonical:
|
|
9
|
+
// https://eslint.org/docs/latest/rules/default-param-last
|
|
10
|
+
type defaultParamLast struct{}
|
|
11
|
+
|
|
12
|
+
func (defaultParamLast) Name() string { return "default-param-last" }
|
|
13
|
+
func (defaultParamLast) Visits() []shimast.Kind {
|
|
14
|
+
return []shimast.Kind{
|
|
15
|
+
shimast.KindFunctionDeclaration,
|
|
16
|
+
shimast.KindFunctionExpression,
|
|
17
|
+
shimast.KindArrowFunction,
|
|
18
|
+
shimast.KindMethodDeclaration,
|
|
19
|
+
shimast.KindConstructor,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
func (defaultParamLast) Check(ctx *Context, node *shimast.Node) {
|
|
23
|
+
params := node.Parameters()
|
|
24
|
+
if len(params) == 0 {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
// Walk right-to-left; once we see a parameter without an Initializer
|
|
28
|
+
// (and not a rest parameter), every earlier parameter that DOES have
|
|
29
|
+
// an Initializer is misordered.
|
|
30
|
+
sawNonDefaultAfter := false
|
|
31
|
+
for i := len(params) - 1; i >= 0; i-- {
|
|
32
|
+
p := params[i]
|
|
33
|
+
if p == nil {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
decl := p.AsParameterDeclaration()
|
|
37
|
+
if decl == nil {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if decl.DotDotDotToken != nil {
|
|
41
|
+
// Rest parameters are always last by grammar; they don't
|
|
42
|
+
// participate in the default-position check.
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
// typescript-eslint canonical treats both default-initialized and
|
|
46
|
+
// optional (`a?: T`) parameters as default-like for ordering: both
|
|
47
|
+
// permit callers to elide the argument, so a non-optional / non-
|
|
48
|
+
// default parameter after either one forces the caller to spell
|
|
49
|
+
// `undefined`. The round-1 implementation only checked Initializer;
|
|
50
|
+
// round 2 adds the QuestionToken branch.
|
|
51
|
+
isDefaultLike := decl.Initializer != nil || decl.QuestionToken != nil
|
|
52
|
+
if !isDefaultLike {
|
|
53
|
+
sawNonDefaultAfter = true
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
if sawNonDefaultAfter {
|
|
57
|
+
ctx.Report(p, "Default parameters should be last.")
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func init() {
|
|
63
|
+
Register(defaultParamLast{})
|
|
64
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// fast and predictable. Rules that require scope binding or
|
|
6
6
|
// flow-sensitive analysis are intentionally not implemented here —
|
|
7
7
|
// those are upstream's job.
|
|
8
|
-
package
|
|
8
|
+
package linthost
|
|
9
9
|
|
|
10
10
|
import (
|
|
11
11
|
"strings"
|
|
@@ -549,6 +549,14 @@ func (noObjCalls) Check(ctx *Context, node *shimast.Node) {
|
|
|
549
549
|
// hasAsyncModifier returns whether a function-like node carries the
|
|
550
550
|
// `async` keyword. Used by no-async-promise-executor.
|
|
551
551
|
func hasAsyncModifier(node *shimast.Node) bool {
|
|
552
|
+
return hasModifier(node, shimast.KindAsyncKeyword)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// hasModifier returns whether a node's modifier list contains a token of
|
|
556
|
+
// the given kind. Generic over modifier kinds (async, static, abstract,
|
|
557
|
+
// override, public/private/protected, readonly) so individual rules don't
|
|
558
|
+
// have to re-implement the loop.
|
|
559
|
+
func hasModifier(node *shimast.Node, kind shimast.Kind) bool {
|
|
552
560
|
if node == nil {
|
|
553
561
|
return false
|
|
554
562
|
}
|
|
@@ -557,7 +565,7 @@ func hasAsyncModifier(node *shimast.Node) bool {
|
|
|
557
565
|
return false
|
|
558
566
|
}
|
|
559
567
|
for _, m := range mods.Nodes {
|
|
560
|
-
if m != nil && m.Kind ==
|
|
568
|
+
if m != nil && m.Kind == kind {
|
|
561
569
|
return true
|
|
562
570
|
}
|
|
563
571
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// await-thenable: `await x` where `x` is not a Promise and not a
|
|
9
|
+
// thenable is always a no-op. The runtime resolves `await 42` to `42`
|
|
10
|
+
// after one microtask hop — almost never the intent. typescript-eslint
|
|
11
|
+
// recommended-type-checked:
|
|
12
|
+
// https://typescript-eslint.io/rules/await-thenable/
|
|
13
|
+
//
|
|
14
|
+
// This is the first rule in the corpus to consult `ctx.Checker`. The
|
|
15
|
+
// shim's `Checker` is a type alias for tsgo's `*innerchecker.Checker`,
|
|
16
|
+
// so every exported method (`GetTypeAtLocation`, `GetPromisedTypeOfPromise`,
|
|
17
|
+
// `GetPropertyOfType`, `GetSignaturesOfType`) is callable directly with
|
|
18
|
+
// no shim addition.
|
|
19
|
+
type awaitThenable struct{}
|
|
20
|
+
|
|
21
|
+
func (awaitThenable) Name() string { return "await-thenable" }
|
|
22
|
+
func (awaitThenable) Visits() []shimast.Kind {
|
|
23
|
+
return []shimast.Kind{shimast.KindAwaitExpression}
|
|
24
|
+
}
|
|
25
|
+
func (awaitThenable) Check(ctx *Context, node *shimast.Node) {
|
|
26
|
+
if ctx.Checker == nil {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
expr := node.AsAwaitExpression()
|
|
30
|
+
if expr == nil || expr.Expression == nil {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
operandType := ctx.Checker.GetTypeAtLocation(expr.Expression)
|
|
34
|
+
if operandType == nil {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if isAwaitable(ctx.Checker, operandType) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
message := "Unexpected `await` of a non-Promise (non-thenable) value."
|
|
41
|
+
// Fix: drop the `await ` keyword and the following whitespace by
|
|
42
|
+
// replacing [node.Pos(), expr.Expression.Pos()) with empty text.
|
|
43
|
+
// `node.Pos()` may include leading trivia; use tokenRange to anchor
|
|
44
|
+
// the start at the actual `await` token.
|
|
45
|
+
startPos, _ := tokenRange(ctx.File, node)
|
|
46
|
+
operandStart := expr.Expression.Pos()
|
|
47
|
+
if startPos < 0 || operandStart <= startPos {
|
|
48
|
+
ctx.Report(node, message)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
ctx.ReportFix(
|
|
52
|
+
node,
|
|
53
|
+
message,
|
|
54
|
+
TextEdit{Pos: startPos, End: operandStart, Text: ""},
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// isAwaitable reports whether `t` is safe to `await`. A type is awaitable
|
|
59
|
+
// when it is `any` / `unknown` / `never` (out of scope for strictness),
|
|
60
|
+
// when it is a Promise, when it is a thenable (has a callable `then`),
|
|
61
|
+
// or — for union/intersection types — when ANY constituent satisfies one
|
|
62
|
+
// of the above. The union case is the round-2 repair: `GetPromisedTypeOfPromise`
|
|
63
|
+
// returns nil on `Promise<X> | number` because the outer type is not a
|
|
64
|
+
// reference to globalPromise, and `GetPropertyOfType` filters `then` as
|
|
65
|
+
// a partial member, so without iterating constituents the rule would
|
|
66
|
+
// fire on legitimate code.
|
|
67
|
+
func isAwaitable(checker *shimchecker.Checker, t *shimchecker.Type) bool {
|
|
68
|
+
if checker == nil || t == nil {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
flags := t.Flags()
|
|
72
|
+
if flags&shimchecker.TypeFlagsAny != 0 ||
|
|
73
|
+
flags&shimchecker.TypeFlagsUnknown != 0 ||
|
|
74
|
+
flags&shimchecker.TypeFlagsNever != 0 {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
if flags&(shimchecker.TypeFlagsUnion|shimchecker.TypeFlagsIntersection) != 0 {
|
|
78
|
+
for _, part := range t.Types() {
|
|
79
|
+
if part == nil {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if isAwaitable(checker, part) {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
if checker.GetPromisedTypeOfPromise(t) != nil {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
return isThenableType(checker, t)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func isThenableType(checker *shimchecker.Checker, t *shimchecker.Type) bool {
|
|
95
|
+
if checker == nil || t == nil {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
prop := checker.GetPropertyOfType(t, "then")
|
|
99
|
+
if prop == nil {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
propType := checker.GetTypeOfSymbol(prop)
|
|
103
|
+
if propType == nil {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
return len(checker.GetSignaturesOfType(propType, shimchecker.SignatureKindCall)) > 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func init() {
|
|
110
|
+
Register(awaitThenable{})
|
|
111
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Bulk implementation of ESLint's "Suggestions" category — rules that
|
|
2
2
|
// don't catch outright bugs but flag stylistic or maintainability
|
|
3
3
|
// patterns. AST-only, no scope analysis.
|
|
4
|
-
package
|
|
4
|
+
package linthost
|
|
5
5
|
|
|
6
6
|
import (
|
|
7
7
|
"strings"
|
|
@@ -891,28 +891,61 @@ func (noUselessRename) Check(ctx *Context, node *shimast.Node) {
|
|
|
891
891
|
if spec == nil || spec.PropertyName == nil {
|
|
892
892
|
return
|
|
893
893
|
}
|
|
894
|
-
if
|
|
895
|
-
ctx.
|
|
894
|
+
if sameIdentifierRename(spec.PropertyName, spec.Name()) {
|
|
895
|
+
reportUselessRenameFix(ctx, node, spec.PropertyName, spec.Name(), "Import { x as x } is redundant.")
|
|
896
896
|
}
|
|
897
897
|
case shimast.KindExportSpecifier:
|
|
898
898
|
spec := node.AsExportSpecifier()
|
|
899
899
|
if spec == nil || spec.PropertyName == nil {
|
|
900
900
|
return
|
|
901
901
|
}
|
|
902
|
-
if
|
|
903
|
-
ctx.
|
|
902
|
+
if sameIdentifierRename(spec.PropertyName, spec.Name()) {
|
|
903
|
+
reportUselessRenameFix(ctx, node, spec.PropertyName, spec.Name(), "Export { x as x } is redundant.")
|
|
904
904
|
}
|
|
905
905
|
case shimast.KindBindingElement:
|
|
906
906
|
el := node.AsBindingElement()
|
|
907
907
|
if el == nil || el.PropertyName == nil {
|
|
908
908
|
return
|
|
909
909
|
}
|
|
910
|
-
if
|
|
911
|
-
ctx.
|
|
910
|
+
if sameIdentifierRename(el.PropertyName, el.Name()) {
|
|
911
|
+
reportUselessRenameFix(ctx, node, el.PropertyName, el.Name(), "Destructuring rename to the same name is redundant.")
|
|
912
912
|
}
|
|
913
913
|
}
|
|
914
914
|
}
|
|
915
915
|
|
|
916
|
+
// sameIdentifierRename gates the rename-tail collapse on both sides being
|
|
917
|
+
// real Identifier nodes that lex to the same text. Without the kind guard
|
|
918
|
+
// a `{ "foo" as "bar" }` pair would compare via `identifierText` ➜ `""`
|
|
919
|
+
// on both sides, collapse to `"" == ""`, and the fix would delete
|
|
920
|
+
// ` as "bar"` — rebinding the local symbol. Both sides must be
|
|
921
|
+
// KindIdentifier (the redundant rename shape ESLint targets).
|
|
922
|
+
func sameIdentifierRename(propertyName, name *shimast.Node) bool {
|
|
923
|
+
if propertyName == nil || name == nil {
|
|
924
|
+
return false
|
|
925
|
+
}
|
|
926
|
+
if propertyName.Kind != shimast.KindIdentifier || name.Kind != shimast.KindIdentifier {
|
|
927
|
+
return false
|
|
928
|
+
}
|
|
929
|
+
return identifierText(propertyName) == identifierText(name)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// reportUselessRenameFix deletes the rename tail (`<separator> name`) so
|
|
933
|
+
// `{ x as x }` collapses to `{ x }` and `{ x: x }` to `{ x }`. The
|
|
934
|
+
// separator (` as ` for import/export, `: ` for binding) lives between
|
|
935
|
+
// the propertyName's end and the name's start; deleting up to name's End
|
|
936
|
+
// removes it without touching surrounding tokens.
|
|
937
|
+
func reportUselessRenameFix(ctx *Context, node, propertyName, name *shimast.Node, message string) {
|
|
938
|
+
if propertyName == nil || name == nil {
|
|
939
|
+
ctx.Report(node, message)
|
|
940
|
+
return
|
|
941
|
+
}
|
|
942
|
+
ctx.ReportFix(
|
|
943
|
+
node,
|
|
944
|
+
message,
|
|
945
|
+
TextEdit{Pos: propertyName.End(), End: name.End(), Text: ""},
|
|
946
|
+
)
|
|
947
|
+
}
|
|
948
|
+
|
|
916
949
|
// object-shorthand: `{ x: x }` → `{ x }`.
|
|
917
950
|
type objectShorthand struct{}
|
|
918
951
|
|
|
@@ -928,9 +961,18 @@ func (objectShorthand) Check(ctx *Context, node *shimast.Node) {
|
|
|
928
961
|
if keyName == "" || valueName == "" {
|
|
929
962
|
return
|
|
930
963
|
}
|
|
931
|
-
if keyName
|
|
932
|
-
|
|
964
|
+
if keyName != valueName {
|
|
965
|
+
return
|
|
933
966
|
}
|
|
967
|
+
// Delete `: <value>` so `{ x: x }` becomes `{ x }`. The range starts
|
|
968
|
+
// at the end of the property name and ends at the end of the
|
|
969
|
+
// initializer; any whitespace between `:` and the value is part of
|
|
970
|
+
// that range.
|
|
971
|
+
ctx.ReportFix(
|
|
972
|
+
node,
|
|
973
|
+
"Expected property shorthand.",
|
|
974
|
+
TextEdit{Pos: prop.Name().End(), End: prop.Initializer.End(), Text: ""},
|
|
975
|
+
)
|
|
934
976
|
}
|
|
935
977
|
|
|
936
978
|
// operator-assignment: `x = x + 1` → `x += 1`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package linthost
|
|
2
2
|
|
|
3
3
|
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
4
|
|
|
@@ -189,7 +189,17 @@ func (preferAsConst) Check(ctx *Context, node *shimast.Node) {
|
|
|
189
189
|
if !literalsMatchSourceText(ctx.File, expr, literalType.Literal) {
|
|
190
190
|
return
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
message := "Expected `as const` instead of `as` literal type."
|
|
193
|
+
pos, end := tokenRange(ctx.File, typeNode)
|
|
194
|
+
if pos < 0 {
|
|
195
|
+
ctx.Report(node, message)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
ctx.ReportFix(
|
|
199
|
+
node,
|
|
200
|
+
message,
|
|
201
|
+
TextEdit{Pos: pos, End: end, Text: "const"},
|
|
202
|
+
)
|
|
193
203
|
}
|
|
194
204
|
|
|
195
205
|
func literalsMatchSourceText(file *shimast.SourceFile, lhs, rhs *shimast.Node) bool {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// match the rules `eslint-plugin-typescript`'s recommended preset relies
|
|
4
4
|
// on most heavily — the rest of that plugin's catalog is type-aware and
|
|
5
5
|
// out of scope for v0.
|
|
6
|
-
package
|
|
6
|
+
package linthost
|
|
7
7
|
|
|
8
8
|
import (
|
|
9
9
|
"strings"
|
|
@@ -77,6 +77,66 @@ func (noDuplicateEnumValues) Check(ctx *Context, node *shimast.Node) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// no-mixed-enums: forbid enums that mix numeric and string member shapes.
|
|
81
|
+
// A single enum with `{ A, B = "two" }` produces broken reverse mappings
|
|
82
|
+
// and surprising assignability. typescript-eslint recommended:
|
|
83
|
+
// https://typescript-eslint.io/rules/no-mixed-enums/
|
|
84
|
+
type noMixedEnums struct{}
|
|
85
|
+
|
|
86
|
+
func (noMixedEnums) Name() string { return "no-mixed-enums" }
|
|
87
|
+
func (noMixedEnums) Visits() []shimast.Kind {
|
|
88
|
+
return []shimast.Kind{shimast.KindEnumDeclaration}
|
|
89
|
+
}
|
|
90
|
+
func (noMixedEnums) Check(ctx *Context, node *shimast.Node) {
|
|
91
|
+
decl := node.AsEnumDeclaration()
|
|
92
|
+
if decl == nil || decl.Members == nil {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
// Classify each member as numeric or string. Implicit members
|
|
96
|
+
// (no initializer) count as numeric — that is what tsgo materializes.
|
|
97
|
+
// The rule fires when both categories appear in the same enum body.
|
|
98
|
+
var hasNumeric, hasString bool
|
|
99
|
+
type classification struct {
|
|
100
|
+
member *shimast.Node
|
|
101
|
+
isStr bool
|
|
102
|
+
}
|
|
103
|
+
members := make([]classification, 0, len(decl.Members.Nodes))
|
|
104
|
+
for _, member := range decl.Members.Nodes {
|
|
105
|
+
if member == nil {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
em := member.AsEnumMember()
|
|
109
|
+
if em == nil {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
isStr := false
|
|
113
|
+
if em.Initializer != nil {
|
|
114
|
+
switch em.Initializer.Kind {
|
|
115
|
+
case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
|
|
116
|
+
isStr = true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if isStr {
|
|
120
|
+
hasString = true
|
|
121
|
+
} else {
|
|
122
|
+
hasNumeric = true
|
|
123
|
+
}
|
|
124
|
+
members = append(members, classification{member: member, isStr: isStr})
|
|
125
|
+
}
|
|
126
|
+
if !(hasNumeric && hasString) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
// Report each member whose category disagrees with the first member's
|
|
130
|
+
// category. ESLint's rule pins to the *second* observed category; we
|
|
131
|
+
// do the same by treating the first member as authoritative.
|
|
132
|
+
firstIsStr := members[0].isStr
|
|
133
|
+
for _, m := range members[1:] {
|
|
134
|
+
if m.isStr != firstIsStr {
|
|
135
|
+
ctx.Report(m.member, "Mixing string and number enum values is not allowed.")
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
80
140
|
// no-extra-non-null-assertion: `a!!` / `a!?.b` collapses two assertions.
|
|
81
141
|
type noExtraNonNullAssertion struct{}
|
|
82
142
|
|
|
@@ -89,9 +149,24 @@ func (noExtraNonNullAssertion) Check(ctx *Context, node *shimast.Node) {
|
|
|
89
149
|
if parent == nil {
|
|
90
150
|
return
|
|
91
151
|
}
|
|
92
|
-
if parent.Kind
|
|
93
|
-
|
|
152
|
+
if parent.Kind != shimast.KindNonNullExpression {
|
|
153
|
+
return
|
|
94
154
|
}
|
|
155
|
+
message := "Forbidden extra non-null assertion."
|
|
156
|
+
// The outer NonNullExpression spans `a!!`; the redundant `!` is the
|
|
157
|
+
// last byte before the outer's End. Deleting [parent.End()-1, parent.End())
|
|
158
|
+
// collapses `a!!` to `a!` while preserving any source already after the
|
|
159
|
+
// outer expression.
|
|
160
|
+
pos := parent.End() - 1
|
|
161
|
+
if pos < 0 || pos >= len(ctx.File.Text()) || ctx.File.Text()[pos] != '!' {
|
|
162
|
+
ctx.Report(node, message)
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
ctx.ReportFix(
|
|
166
|
+
node,
|
|
167
|
+
message,
|
|
168
|
+
TextEdit{Pos: pos, End: pos + 1, Text: ""},
|
|
169
|
+
)
|
|
95
170
|
}
|
|
96
171
|
|
|
97
172
|
// no-non-null-asserted-optional-chain: `foo?.bar!` — the chain produces
|
|
@@ -314,7 +389,17 @@ func (preferNamespaceKeyword) Check(ctx *Context, node *shimast.Node) {
|
|
|
314
389
|
if decl.Keyword != shimast.KindModuleKeyword {
|
|
315
390
|
return
|
|
316
391
|
}
|
|
317
|
-
|
|
392
|
+
message := "Use 'namespace' instead of 'module' to declare custom TypeScript modules."
|
|
393
|
+
start := keywordStart(ctx.File, node, "module")
|
|
394
|
+
if start < 0 {
|
|
395
|
+
ctx.Report(node, message)
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
ctx.ReportFix(
|
|
399
|
+
node,
|
|
400
|
+
message,
|
|
401
|
+
TextEdit{Pos: start, End: start + len("module"), Text: "namespace"},
|
|
402
|
+
)
|
|
318
403
|
}
|
|
319
404
|
|
|
320
405
|
// triple-slash-reference: `/// <reference path="..." />` directives.
|
|
@@ -658,6 +743,7 @@ var _ = struct{}{}
|
|
|
658
743
|
func init() {
|
|
659
744
|
Register(noConfusingNonNullAssertion{})
|
|
660
745
|
Register(noDuplicateEnumValues{})
|
|
746
|
+
Register(noMixedEnums{})
|
|
661
747
|
Register(noExtraNonNullAssertion{})
|
|
662
748
|
Register(noNonNullAssertedOptionalChain{})
|
|
663
749
|
Register(noMisusedNew{})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package linthost
|
|
2
2
|
|
|
3
3
|
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
4
|
|
|
@@ -14,6 +14,16 @@ func (noVar) Check(ctx *Context, node *shimast.Node) {
|
|
|
14
14
|
return
|
|
15
15
|
}
|
|
16
16
|
if shimast.IsVar(stmt.DeclarationList) {
|
|
17
|
+
start := keywordStart(ctx.File, stmt.DeclarationList, "var")
|
|
18
|
+
if start >= 0 {
|
|
19
|
+
ctx.ReportRangeFix(
|
|
20
|
+
start,
|
|
21
|
+
start+len("var"),
|
|
22
|
+
"Unexpected var, use let or const instead.",
|
|
23
|
+
TextEdit{Pos: start, End: start + len("var"), Text: "let"},
|
|
24
|
+
)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
17
27
|
ctx.Report(node, "Unexpected var, use let or const instead.")
|
|
18
28
|
}
|
|
19
29
|
}
|
|
@@ -29,8 +39,9 @@ func (preferConst) Name() string { return "prefer-const" }
|
|
|
29
39
|
func (preferConst) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
|
|
30
40
|
func (preferConst) Check(ctx *Context, node *shimast.Node) {
|
|
31
41
|
type candidate struct {
|
|
32
|
-
name
|
|
33
|
-
node
|
|
42
|
+
name string
|
|
43
|
+
node *shimast.Node
|
|
44
|
+
listNode *shimast.Node
|
|
34
45
|
}
|
|
35
46
|
var candidates []candidate
|
|
36
47
|
assigned := map[string]bool{}
|
|
@@ -53,7 +64,7 @@ func (preferConst) Check(ctx *Context, node *shimast.Node) {
|
|
|
53
64
|
if !isConstEligibleLetDeclaration(child, decl) {
|
|
54
65
|
return
|
|
55
66
|
}
|
|
56
|
-
candidates = append(candidates, candidate{name: name, node: child})
|
|
67
|
+
candidates = append(candidates, candidate{name: name, node: child, listNode: listNode})
|
|
57
68
|
case shimast.KindBinaryExpression:
|
|
58
69
|
expr := child.AsBinaryExpression()
|
|
59
70
|
if expr == nil || expr.OperatorToken == nil || !isAssignmentOperator(expr.OperatorToken.Kind) {
|
|
@@ -87,11 +98,31 @@ func (preferConst) Check(ctx *Context, node *shimast.Node) {
|
|
|
87
98
|
|
|
88
99
|
for _, c := range candidates {
|
|
89
100
|
if !assigned[c.name] {
|
|
90
|
-
|
|
101
|
+
start := -1
|
|
102
|
+
if isSingleDeclarationList(c.listNode) {
|
|
103
|
+
start = keywordStart(ctx.File, c.listNode, "let")
|
|
104
|
+
}
|
|
105
|
+
if start >= 0 {
|
|
106
|
+
ctx.ReportFix(
|
|
107
|
+
c.node,
|
|
108
|
+
"Use const instead of let.",
|
|
109
|
+
TextEdit{Pos: start, End: start + len("let"), Text: "const"},
|
|
110
|
+
)
|
|
111
|
+
} else {
|
|
112
|
+
ctx.Report(c.node, "Use const instead of let.")
|
|
113
|
+
}
|
|
91
114
|
}
|
|
92
115
|
}
|
|
93
116
|
}
|
|
94
117
|
|
|
118
|
+
func isSingleDeclarationList(node *shimast.Node) bool {
|
|
119
|
+
if node == nil {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
list := node.AsVariableDeclarationList()
|
|
123
|
+
return list != nil && list.Declarations != nil && len(list.Declarations.Nodes) == 1
|
|
124
|
+
}
|
|
125
|
+
|
|
95
126
|
func isConstEligibleLetDeclaration(node *shimast.Node, decl *shimast.VariableDeclaration) bool {
|
|
96
127
|
if decl.Initializer != nil {
|
|
97
128
|
if node.Parent != nil && node.Parent.Parent != nil && node.Parent.Parent.Kind == shimast.KindForStatement {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-dev.20260518",
|
|
4
4
|
"description": "Reference ttsc plugin: ESLint-style lint rules hosted in the same Program/Checker as the type-check pass.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -25,16 +25,18 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"README.md",
|
|
28
|
-
"lib",
|
|
29
|
-
"src",
|
|
30
28
|
"go.mod",
|
|
31
|
-
"
|
|
29
|
+
"lib",
|
|
30
|
+
"linthost",
|
|
31
|
+
"plugin",
|
|
32
|
+
"rule",
|
|
33
|
+
"src"
|
|
32
34
|
],
|
|
33
35
|
"devDependencies": {
|
|
34
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
36
|
+
"@typescript/native-preview": "7.0.0-dev.20260515.1",
|
|
35
37
|
"@types/node": "^25.3.0",
|
|
36
38
|
"rimraf": "^6.1.2",
|
|
37
|
-
"ttsc": "0.
|
|
39
|
+
"ttsc": "0.11.0-dev.20260518"
|
|
38
40
|
},
|
|
39
41
|
"repository": {
|
|
40
42
|
"type": "git",
|