@ttsc/lint 0.10.1 → 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
@@ -0,0 +1,219 @@
1
+ package linthost
2
+
3
+ import (
4
+ "strings"
5
+
6
+ shimast "github.com/microsoft/typescript-go/shim/ast"
7
+ )
8
+
9
+ // format/quotes normalizes string-literal quote style. Mirrors
10
+ // prettier's `singleQuote` option:
11
+ //
12
+ // - `prefer: "double"` (default) converts single-quoted literals to
13
+ // double-quoted when the escape cost is equal or better.
14
+ // - `prefer: "single"` does the reverse.
15
+ //
16
+ // In both directions the escape-cost tie-breaker holds: if conversion
17
+ // would strictly increase the escape count, the literal is left alone.
18
+ //
19
+ // JSX attribute initializers (`<div className="foo" />`) are skipped on
20
+ // purpose. Prettier exposes a separate `jsxSingleQuote` option for that
21
+ // surface and never rewrites JSX attributes via `singleQuote`; the
22
+ // JSX-grammar-canonical form is double quotes and rewriting to single
23
+ // quotes corrupts working code. Template literals, no-substitution
24
+ // template literals, and JSX text nodes use distinct AST kinds and are
25
+ // also intentionally out of scope.
26
+ type formatQuotes struct{}
27
+
28
+ type formatQuotesOptions struct {
29
+ Prefer string `json:"prefer"`
30
+ }
31
+
32
+ func (formatQuotes) Name() string { return "format/quotes" }
33
+ func (formatQuotes) IsFormat() bool { return true }
34
+
35
+ func (formatQuotes) Visits() []shimast.Kind {
36
+ return []shimast.Kind{shimast.KindStringLiteral}
37
+ }
38
+
39
+ func (formatQuotes) Check(ctx *Context, node *shimast.Node) {
40
+ if ctx == nil || ctx.File == nil || node == nil {
41
+ return
42
+ }
43
+ // JSX attribute initializers parse as plain StringLiteral but are
44
+ // grammatically required to use double quotes in the standard JSX
45
+ // form. Prettier mirrors that and never touches them via singleQuote.
46
+ if parent := node.Parent; parent != nil && parent.Kind == shimast.KindJsxAttribute {
47
+ return
48
+ }
49
+ var opts formatQuotesOptions
50
+ _ = ctx.DecodeOptions(&opts)
51
+ preferSingle := opts.Prefer == "single"
52
+
53
+ pos, end := tokenRange(ctx.File, node)
54
+ if pos < 0 || end-pos < 2 {
55
+ return
56
+ }
57
+ src := ctx.File.Text()
58
+ raw := src[pos:end]
59
+ if preferSingle {
60
+ if raw[0] != '"' || raw[len(raw)-1] != '"' {
61
+ return
62
+ }
63
+ converted, ok := convertDoubleQuotedToSingle(raw[1 : len(raw)-1])
64
+ if !ok || converted == raw {
65
+ return
66
+ }
67
+ ctx.ReportRangeFix(
68
+ pos,
69
+ end,
70
+ "Strings must use single quotes.",
71
+ TextEdit{Pos: pos, End: end, Text: converted},
72
+ )
73
+ return
74
+ }
75
+ if raw[0] != '\'' || raw[len(raw)-1] != '\'' {
76
+ return
77
+ }
78
+ converted, ok := convertSingleQuotedToDouble(raw[1 : len(raw)-1])
79
+ if !ok || converted == raw {
80
+ return
81
+ }
82
+ ctx.ReportRangeFix(
83
+ pos,
84
+ end,
85
+ "Strings must use double quotes.",
86
+ TextEdit{Pos: pos, End: end, Text: converted},
87
+ )
88
+ }
89
+
90
+ // convertDoubleQuotedToSingle walks the inner text of a double-quoted
91
+ // literal and returns the single-quoted equivalent, plus an `ok`
92
+ // boolean. `ok=false` means converting would require strictly more
93
+ // escapes than the source (prettier's tie-breaker rule), so the
94
+ // caller should leave the literal alone.
95
+ //
96
+ // Conversion is escape-aware:
97
+ // - `\"` becomes a bare `"` (no longer needs escaping).
98
+ // - Bare `'` becomes `\'` (now must be escaped).
99
+ // - Every other escape sequence (`\n`, `\\`, `\u{…}`) survives intact.
100
+ func convertDoubleQuotedToSingle(inner string) (string, bool) {
101
+ escapedDouble, unescapedSingle := countDoubleEscapes(inner)
102
+ if unescapedSingle > escapedDouble {
103
+ return "", false
104
+ }
105
+ var b strings.Builder
106
+ b.Grow(len(inner) + 2)
107
+ b.WriteByte('\'')
108
+ for i := 0; i < len(inner); {
109
+ if inner[i] == '\\' && i+1 < len(inner) {
110
+ if inner[i+1] == '"' {
111
+ b.WriteByte('"')
112
+ i += 2
113
+ continue
114
+ }
115
+ b.WriteByte(inner[i])
116
+ b.WriteByte(inner[i+1])
117
+ i += 2
118
+ continue
119
+ }
120
+ if inner[i] == '\'' {
121
+ b.WriteByte('\\')
122
+ b.WriteByte('\'')
123
+ i++
124
+ continue
125
+ }
126
+ b.WriteByte(inner[i])
127
+ i++
128
+ }
129
+ b.WriteByte('\'')
130
+ return b.String(), true
131
+ }
132
+
133
+ // countDoubleEscapes returns the number of `\"` sequences and bare `'`
134
+ // bytes inside a double-quoted literal's text. Pairs with
135
+ // countSingleEscapes.
136
+ func countDoubleEscapes(inner string) (escapedDouble, unescapedSingle int) {
137
+ for i := 0; i < len(inner); {
138
+ if inner[i] == '\\' && i+1 < len(inner) {
139
+ if inner[i+1] == '"' {
140
+ escapedDouble++
141
+ }
142
+ i += 2
143
+ continue
144
+ }
145
+ if inner[i] == '\'' {
146
+ unescapedSingle++
147
+ }
148
+ i++
149
+ }
150
+ return escapedDouble, unescapedSingle
151
+ }
152
+
153
+ // convertSingleQuotedToDouble walks the inner text of a single-quoted
154
+ // literal and returns the double-quoted equivalent, plus an `ok`
155
+ // boolean. `ok=false` means converting would require strictly more
156
+ // escapes than the source (prettier's tie-breaker rule), so the
157
+ // caller should leave the literal alone.
158
+ //
159
+ // Conversion is escape-aware:
160
+ // - `\'` becomes a bare `'` (no longer needs escaping).
161
+ // - Bare `"` becomes `\"` (now must be escaped).
162
+ // - Every other escape sequence (`\n`, `\\`, `\u{…}`) survives intact.
163
+ func convertSingleQuotedToDouble(inner string) (string, bool) {
164
+ escapedSingle, unescapedDouble := countSingleEscapes(inner)
165
+ if unescapedDouble > escapedSingle {
166
+ return "", false
167
+ }
168
+ var b strings.Builder
169
+ b.Grow(len(inner) + 2)
170
+ b.WriteByte('"')
171
+ for i := 0; i < len(inner); {
172
+ if inner[i] == '\\' && i+1 < len(inner) {
173
+ if inner[i+1] == '\'' {
174
+ b.WriteByte('\'')
175
+ i += 2
176
+ continue
177
+ }
178
+ b.WriteByte(inner[i])
179
+ b.WriteByte(inner[i+1])
180
+ i += 2
181
+ continue
182
+ }
183
+ if inner[i] == '"' {
184
+ b.WriteByte('\\')
185
+ b.WriteByte('"')
186
+ i++
187
+ continue
188
+ }
189
+ b.WriteByte(inner[i])
190
+ i++
191
+ }
192
+ b.WriteByte('"')
193
+ return b.String(), true
194
+ }
195
+
196
+ // countSingleEscapes returns the number of `\'` sequences and bare `"`
197
+ // bytes inside a single-quoted literal's text. Pairs with
198
+ // countDoubleEscapes; the names describe what they count (the quote kind
199
+ // that's been escape-prefixed).
200
+ func countSingleEscapes(inner string) (escapedSingle, unescapedDouble int) {
201
+ for i := 0; i < len(inner); {
202
+ if inner[i] == '\\' && i+1 < len(inner) {
203
+ if inner[i+1] == '\'' {
204
+ escapedSingle++
205
+ }
206
+ i += 2
207
+ continue
208
+ }
209
+ if inner[i] == '"' {
210
+ unescapedDouble++
211
+ }
212
+ i++
213
+ }
214
+ return escapedSingle, unescapedDouble
215
+ }
216
+
217
+ func init() {
218
+ Register(formatQuotes{})
219
+ }
@@ -0,0 +1,190 @@
1
+ package linthost
2
+
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ )
6
+
7
+ // format/semi controls trailing-semicolon style on ASI statements.
8
+ // Mirrors prettier's `semi` option:
9
+ //
10
+ // - `prefer: "always"` (default) inserts a missing terminator.
11
+ // - `prefer: "never"` strips a trailing terminator from the same
12
+ // statement kinds.
13
+ //
14
+ // The rule scans only statement kinds where TypeScript inserts an
15
+ // optional semicolon. Body-shaped declarations (functions, classes,
16
+ // namespaces, enums) and control-flow statements (if/for/while/try)
17
+ // are out of scope because they parse correctly without a terminator.
18
+ type formatSemi struct{}
19
+
20
+ // formatSemiOptions is the Go mirror of `TtscLintRuleOptions.Semi`. The
21
+ // JSON tag matches the TypeScript field name so users get the same key
22
+ // in both layers.
23
+ type formatSemiOptions struct {
24
+ Prefer string `json:"prefer"`
25
+ }
26
+
27
+ func (formatSemi) Name() string { return "format/semi" }
28
+ func (formatSemi) IsFormat() bool { return true }
29
+
30
+ func (formatSemi) Visits() []shimast.Kind {
31
+ return []shimast.Kind{
32
+ shimast.KindVariableStatement,
33
+ shimast.KindExpressionStatement,
34
+ shimast.KindReturnStatement,
35
+ shimast.KindThrowStatement,
36
+ shimast.KindBreakStatement,
37
+ shimast.KindContinueStatement,
38
+ shimast.KindDoStatement,
39
+ shimast.KindDebuggerStatement,
40
+ shimast.KindImportDeclaration,
41
+ shimast.KindImportEqualsDeclaration,
42
+ shimast.KindExportDeclaration,
43
+ shimast.KindExportAssignment,
44
+ shimast.KindPropertyDeclaration,
45
+ shimast.KindTypeAliasDeclaration,
46
+ }
47
+ }
48
+
49
+ func (formatSemi) Check(ctx *Context, node *shimast.Node) {
50
+ if ctx == nil || ctx.File == nil || node == nil {
51
+ return
52
+ }
53
+ var opts formatSemiOptions
54
+ _ = ctx.DecodeOptions(&opts)
55
+ preferNever := opts.Prefer == "never"
56
+
57
+ src := ctx.File.Text()
58
+ end := node.End()
59
+ if end <= 0 || end > len(src) {
60
+ return
61
+ }
62
+ hasSemi := src[end-1] == ';'
63
+ if preferNever {
64
+ if !hasSemi {
65
+ return
66
+ }
67
+ if !preferNeverSafeKind(node.Kind) {
68
+ // Dropping the `;` after a class field or a type alias can
69
+ // change parse — e.g. `class A { x: number; [k](): void {} }`
70
+ // would reparse `[k]` as a computed index access on `number`.
71
+ // Keep the terminator on those kinds even in prefer:"never"
72
+ // mode.
73
+ return
74
+ }
75
+ if nextStatementHasASIHazard(src, end) {
76
+ // Stripping `;` here would let ASI fail. Prettier defends with a
77
+ // leading-`;` on the next statement; this rule conservatively
78
+ // refuses to strip rather than synthesizing an edit on a node
79
+ // it didn't visit.
80
+ return
81
+ }
82
+ pos := end - 1
83
+ if pos < 0 {
84
+ pos = 0
85
+ }
86
+ ctx.ReportRangeFix(
87
+ pos,
88
+ end,
89
+ "Unexpected trailing semicolon.",
90
+ TextEdit{Pos: end - 1, End: end, Text: ""},
91
+ )
92
+ return
93
+ }
94
+ if hasSemi {
95
+ return
96
+ }
97
+ // Diagnostic anchors on the last character of the statement so the
98
+ // banner underlines "the place a semicolon should follow". The fix
99
+ // itself is a zero-width insertion at node.End() — keeping the edit
100
+ // disjoint from any other rule's edits on the same statement.
101
+ pos := end - 1
102
+ if pos < 0 {
103
+ pos = 0
104
+ }
105
+ ctx.ReportRangeFix(
106
+ pos,
107
+ end,
108
+ "Missing semicolon.",
109
+ TextEdit{Pos: end, End: end, Text: ";"},
110
+ )
111
+ }
112
+
113
+ // nextStatementHasASIHazard reports whether the next non-trivia byte
114
+ // after `end` starts a token that would re-associate with the prior
115
+ // expression if the trailing `;` is removed. Prettier handles this by
116
+ // inserting a defensive leading `;` on the next line; this rule's
117
+ // fixer is single-node, so the safer move is to keep the explicit
118
+ // terminator.
119
+ //
120
+ // Hazard tokens per the ASI spec:
121
+ //
122
+ // - `[` — bracket access continues an expression
123
+ // - `(` — call expression continues
124
+ // - “ ` “ — tagged template literal continues
125
+ // - `+`, `-`, `*`, `/` — binary operator continues
126
+ // - `,` — comma operator continues
127
+ func nextStatementHasASIHazard(src string, end int) bool {
128
+ for i := end; i < len(src); i++ {
129
+ c := src[i]
130
+ if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
131
+ continue
132
+ }
133
+ if c == '/' && i+1 < len(src) {
134
+ if src[i+1] == '/' {
135
+ for i < len(src) && src[i] != '\n' {
136
+ i++
137
+ }
138
+ continue
139
+ }
140
+ if src[i+1] == '*' {
141
+ i += 2
142
+ for i+1 < len(src) && !(src[i] == '*' && src[i+1] == '/') {
143
+ i++
144
+ }
145
+ if i+1 < len(src) {
146
+ i++ // step past '*/'
147
+ }
148
+ continue
149
+ }
150
+ // bare `/` starts a regex literal or division — hazard.
151
+ return true
152
+ }
153
+ switch c {
154
+ case '[', '(', '`', '+', '-', '*', ',':
155
+ return true
156
+ }
157
+ return false
158
+ }
159
+ return false
160
+ }
161
+
162
+ // preferNeverSafeKind reports whether stripping the trailing semicolon
163
+ // is safe for `kind`. Statement kinds end at a line break or `}` in
164
+ // practice; declaration-style kinds (PropertyDeclaration,
165
+ // TypeAliasDeclaration) live next to other class/module members where
166
+ // the explicit terminator disambiguates the next token. The
167
+ // prefer:"never" branch refuses to touch those.
168
+ func preferNeverSafeKind(kind shimast.Kind) bool {
169
+ switch kind {
170
+ case
171
+ shimast.KindVariableStatement,
172
+ shimast.KindExpressionStatement,
173
+ shimast.KindReturnStatement,
174
+ shimast.KindThrowStatement,
175
+ shimast.KindBreakStatement,
176
+ shimast.KindContinueStatement,
177
+ shimast.KindDoStatement,
178
+ shimast.KindDebuggerStatement,
179
+ shimast.KindImportDeclaration,
180
+ shimast.KindImportEqualsDeclaration,
181
+ shimast.KindExportDeclaration,
182
+ shimast.KindExportAssignment:
183
+ return true
184
+ }
185
+ return false
186
+ }
187
+
188
+ func init() {
189
+ Register(formatSemi{})
190
+ }