@ttsc/lint 0.10.2 → 0.11.0-dev.20260517

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.
Files changed (113) hide show
  1. package/README.md +172 -33
  2. package/lib/defineConfig.d.ts +55 -0
  3. package/lib/defineConfig.js +39 -0
  4. package/lib/defineConfig.js.map +1 -0
  5. package/lib/index.d.ts +25 -1
  6. package/lib/index.js +622 -2
  7. package/lib/index.js.map +1 -1
  8. package/lib/structures/ITtscLintPlugin.d.ts +53 -0
  9. package/lib/structures/ITtscLintPlugin.js +3 -0
  10. package/lib/structures/ITtscLintPlugin.js.map +1 -0
  11. package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
  12. package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
  13. package/lib/structures/ITtscLintPluginMeta.js +3 -0
  14. package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
  15. package/lib/structures/PluginRuleNames.d.ts +14 -0
  16. package/lib/structures/PluginRuleNames.js +3 -0
  17. package/lib/structures/PluginRuleNames.js.map +1 -0
  18. package/lib/structures/TtscLintConfig.d.ts +24 -8
  19. package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
  20. package/lib/structures/TtscLintConfigEntry.js +3 -0
  21. package/lib/structures/TtscLintConfigEntry.js.map +1 -0
  22. package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
  23. package/lib/structures/TtscLintFormatConfig.js +3 -0
  24. package/lib/structures/TtscLintFormatConfig.js.map +1 -0
  25. package/lib/structures/TtscLintPlugins.d.ts +9 -0
  26. package/lib/structures/TtscLintPlugins.js +3 -0
  27. package/lib/structures/TtscLintPlugins.js.map +1 -0
  28. package/lib/structures/TtscLintRule.d.ts +1 -1
  29. package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
  30. package/lib/structures/TtscLintRuleEntry.js +3 -0
  31. package/lib/structures/TtscLintRuleEntry.js.map +1 -0
  32. package/lib/structures/TtscLintRuleMap.d.ts +44 -0
  33. package/lib/structures/TtscLintRuleMap.js +3 -0
  34. package/lib/structures/TtscLintRuleMap.js.map +1 -0
  35. package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
  36. package/lib/structures/TtscLintRuleOptions.js +3 -0
  37. package/lib/structures/TtscLintRuleOptions.js.map +1 -0
  38. package/lib/structures/defaultFormat.d.ts +31 -0
  39. package/lib/structures/defaultFormat.js +35 -0
  40. package/lib/structures/defaultFormat.js.map +1 -0
  41. package/lib/structures/index.d.ts +10 -0
  42. package/lib/structures/index.js +10 -0
  43. package/lib/structures/index.js.map +1 -1
  44. package/{plugin → linthost}/ast_helpers.go +92 -1
  45. package/{plugin → linthost}/compile.go +1 -1
  46. package/{plugin → linthost}/config.go +583 -66
  47. package/linthost/config_format.go +360 -0
  48. package/linthost/contrib_adapter.go +134 -0
  49. package/{plugin → linthost}/directives.go +1 -1
  50. package/linthost/dispatch.go +62 -0
  51. package/{plugin → linthost}/engine.go +156 -11
  52. package/{plugin → linthost}/eslint_runtime.go +76 -2
  53. package/linthost/fix.go +249 -0
  54. package/linthost/format.go +107 -0
  55. package/{plugin → linthost}/host.go +1 -1
  56. package/linthost/print_dispatch.go +118 -0
  57. package/linthost/print_doc.go +168 -0
  58. package/linthost/print_engine.go +343 -0
  59. package/linthost/print_nodes_array.go +46 -0
  60. package/linthost/print_nodes_call.go +180 -0
  61. package/linthost/print_nodes_imports.go +171 -0
  62. package/linthost/print_nodes_list.go +64 -0
  63. package/linthost/print_nodes_object.go +51 -0
  64. package/{plugin → linthost}/rules_arrays.go +1 -1
  65. package/{plugin → linthost}/rules_console.go +1 -1
  66. package/{plugin → linthost}/rules_debugger.go +1 -1
  67. package/{plugin → linthost}/rules_dupes.go +1 -1
  68. package/{plugin → linthost}/rules_empty.go +1 -1
  69. package/linthost/rules_escape.go +238 -0
  70. package/{plugin → linthost}/rules_eval.go +1 -1
  71. package/{plugin → linthost}/rules_finally.go +1 -1
  72. package/linthost/rules_format_jsdoc.go +230 -0
  73. package/linthost/rules_format_print_width.go +332 -0
  74. package/linthost/rules_format_quotes.go +219 -0
  75. package/linthost/rules_format_semi.go +190 -0
  76. package/linthost/rules_format_sort_imports.go +462 -0
  77. package/linthost/rules_format_trailing_comma.go +460 -0
  78. package/{plugin → linthost}/rules_gap.go +101 -6
  79. package/linthost/rules_imports.go +101 -0
  80. package/{plugin → linthost}/rules_logic.go +55 -3
  81. package/{plugin → linthost}/rules_loops.go +1 -1
  82. package/{plugin → linthost}/rules_misc.go +1 -1
  83. package/linthost/rules_params.go +64 -0
  84. package/{plugin → linthost}/rules_problems.go +10 -2
  85. package/linthost/rules_promise.go +111 -0
  86. package/{plugin → linthost}/rules_protos.go +1 -1
  87. package/{plugin → linthost}/rules_self.go +1 -1
  88. package/{plugin → linthost}/rules_strings.go +1 -1
  89. package/{plugin → linthost}/rules_suggestions.go +51 -9
  90. package/{plugin → linthost}/rules_throw.go +1 -1
  91. package/{plugin → linthost}/rules_ts.go +12 -2
  92. package/{plugin → linthost}/rules_ts_extra.go +90 -4
  93. package/{plugin → linthost}/rules_var.go +36 -5
  94. package/package.json +8 -6
  95. package/plugin/main.go +14 -27
  96. package/rule/astutil/astutil.go +148 -0
  97. package/rule/rule.go +291 -0
  98. package/src/defineConfig.ts +69 -0
  99. package/src/index.ts +807 -3
  100. package/src/structures/ITtscLintPlugin.ts +56 -0
  101. package/src/structures/ITtscLintPluginConfig.ts +86 -5
  102. package/src/structures/ITtscLintPluginMeta.ts +14 -0
  103. package/src/structures/PluginRuleNames.ts +19 -0
  104. package/src/structures/TtscLintConfig.ts +27 -8
  105. package/src/structures/TtscLintConfigEntry.ts +50 -0
  106. package/src/structures/TtscLintFormatConfig.ts +169 -0
  107. package/src/structures/TtscLintPlugins.ts +10 -0
  108. package/src/structures/TtscLintRule.ts +14 -1
  109. package/src/structures/TtscLintRuleEntry.ts +28 -0
  110. package/src/structures/TtscLintRuleMap.ts +52 -0
  111. package/src/structures/TtscLintRuleOptions.ts +162 -0
  112. package/src/structures/defaultFormat.ts +33 -0
  113. package/src/structures/index.ts +10 -0
@@ -1,4 +1,4 @@
1
- package main
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.ReportRange(expr.OperatorToken.Pos(), expr.OperatorToken.End(), "Expected '===' and instead saw '=='.")
122
+ reportEqeqeq(ctx, expr.OperatorToken, expr, "Expected '===' and instead saw '=='.", "===")
123
123
  case shimast.KindExclamationEqualsToken:
124
- ctx.ReportRange(expr.OperatorToken.Pos(), expr.OperatorToken.End(), "Expected '!==' and instead saw '!='.")
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{}
@@ -1,4 +1,4 @@
1
- package main
1
+ package linthost
2
2
 
3
3
  import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
@@ -1,4 +1,4 @@
1
- package main
1
+ package linthost
2
2
 
3
3
  import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
@@ -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 main
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 == shimast.KindAsyncKeyword {
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,4 +1,4 @@
1
- package main
1
+ package linthost
2
2
 
3
3
  import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
@@ -1,4 +1,4 @@
1
- package main
1
+ package linthost
2
2
 
3
3
  import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
@@ -1,4 +1,4 @@
1
- package main
1
+ package linthost
2
2
 
3
3
  import (
4
4
  "strings"
@@ -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 main
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 identifierText(spec.PropertyName) == identifierText(spec.Name()) {
895
- ctx.Report(node, "Import { x as x } is redundant.")
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 identifierText(spec.PropertyName) == identifierText(spec.Name()) {
903
- ctx.Report(node, "Export { x as x } is redundant.")
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 identifierText(el.PropertyName) == identifierText(el.Name()) {
911
- ctx.Report(node, "Destructuring rename to the same name is redundant.")
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 == valueName {
932
- ctx.Report(node, "Expected property shorthand.")
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 main
1
+ package linthost
2
2
 
3
3
  import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
@@ -1,4 +1,4 @@
1
- package main
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
- ctx.Report(node, "Expected `as const` instead of `as` literal type.")
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 main
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 == shimast.KindNonNullExpression {
93
- ctx.Report(node, "Forbidden extra non-null assertion.")
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
- ctx.Report(node, "Use 'namespace' instead of 'module' to declare custom TypeScript modules.")
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 main
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 string
33
- node *shimast.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
- ctx.Report(c.node, "Use const instead of let.")
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.10.2",
3
+ "version": "0.11.0-dev.20260517",
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
- "plugin"
29
+ "lib",
30
+ "linthost",
31
+ "plugin",
32
+ "rule",
33
+ "src"
32
34
  ],
33
35
  "devDependencies": {
34
- "@typescript/native-preview": "7.0.0-dev.20260510.1",
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.10.2"
39
+ "ttsc": "0.11.0-dev.20260517"
38
40
  },
39
41
  "repository": {
40
42
  "type": "git",