@ttsc/lint 0.12.3 → 0.13.0
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/lib/defaultFormat.d.ts +10 -11
- package/lib/defaultFormat.js +10 -11
- package/lib/defaultFormat.js.map +1 -1
- package/lib/index.js +233 -135
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +2 -2
- package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
- package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
- package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
- package/lib/structures/TtscLintRule.d.ts +10 -1
- package/lib/structures/TtscLintRuleMap.d.ts +2 -2
- package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
- package/linthost/ast_helpers.go +80 -11
- package/linthost/compile.go +116 -112
- package/linthost/config.go +647 -687
- package/linthost/config_format.go +272 -247
- package/linthost/contrib_adapter.go +7 -0
- package/linthost/directives.go +116 -0
- package/linthost/dispatch.go +33 -33
- package/linthost/engine.go +156 -43
- package/linthost/fix.go +47 -27
- package/linthost/flags_gen.go +31 -0
- package/linthost/format.go +119 -3
- package/linthost/host.go +115 -5
- package/linthost/print_dispatch.go +128 -28
- package/linthost/print_doc.go +23 -0
- package/linthost/print_engine.go +168 -4
- package/linthost/print_nodes_array.go +17 -7
- package/linthost/print_nodes_call.go +138 -20
- package/linthost/print_nodes_function.go +353 -0
- package/linthost/print_nodes_imports.go +46 -29
- package/linthost/print_nodes_list.go +86 -5
- package/linthost/print_nodes_object.go +56 -11
- package/linthost/rules_arrays.go +5 -2
- package/linthost/rules_debugger.go +3 -2
- package/linthost/rules_dupes.go +7 -4
- package/linthost/rules_empty.go +3 -2
- package/linthost/rules_escape.go +35 -3
- package/linthost/rules_eval.go +3 -0
- package/linthost/rules_finally.go +11 -0
- package/linthost/rules_format_jsdoc.go +7 -0
- package/linthost/rules_format_print_width.go +270 -15
- package/linthost/rules_format_quotes.go +3 -0
- package/linthost/rules_format_sort_imports.go +4 -0
- package/linthost/rules_gap.go +75 -3
- package/linthost/rules_imports.go +5 -7
- package/linthost/rules_logic.go +75 -5
- package/linthost/rules_loops.go +4 -0
- package/linthost/rules_misc.go +4 -2
- package/linthost/rules_params.go +7 -11
- package/linthost/rules_problems.go +89 -22
- package/linthost/rules_promise.go +15 -0
- package/linthost/rules_protos.go +3 -2
- package/linthost/rules_self.go +6 -0
- package/linthost/rules_strings.go +10 -0
- package/linthost/rules_suggestions.go +174 -9
- package/linthost/rules_throw.go +2 -0
- package/linthost/rules_ts.go +13 -0
- package/linthost/rules_ts_extra.go +25 -8
- package/linthost/rules_var.go +15 -2
- package/package.json +3 -3
- package/plugin/main.go +4 -4
- package/rule/astutil/astutil.go +12 -0
- package/rule/rule.go +123 -123
- package/src/defaultFormat.ts +10 -11
- package/src/index.ts +257 -168
- package/src/structures/ITtscLintConfig.ts +2 -2
- package/src/structures/ITtscLintFormatConfig.ts +53 -55
- package/src/structures/ITtscLintPluginConfig.ts +10 -83
- package/src/structures/ITtscLintPluginMeta.ts +9 -1
- package/src/structures/TtscLintRule.ts +10 -1
- package/src/structures/TtscLintRuleMap.ts +17 -18
- package/src/structures/TtscLintRuleOptions.ts +15 -0
- package/linthost/eslint_runtime.go +0 -320
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package linthost
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
4
6
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
7
|
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
6
8
|
)
|
|
@@ -25,16 +27,22 @@ import (
|
|
|
25
27
|
// printer's StartingIndent so continuation lines align under the
|
|
26
28
|
// opening token and fit measurement charges the prefix against
|
|
27
29
|
// the budget.
|
|
28
|
-
// 3. Build the node's Doc via PrintNode
|
|
29
|
-
//
|
|
30
|
+
// 3. Build the node's Doc via PrintNode, which also reports a
|
|
31
|
+
// `covered` flag.
|
|
32
|
+
// 4. Abstain when `covered` is false: the subtree holds a multi-line
|
|
33
|
+
// verbatim node whose frozen columns would not survive a reflow.
|
|
34
|
+
// 5. Render with the configured printWidth / tabWidth / useTabs /
|
|
30
35
|
// endOfLine.
|
|
31
|
-
//
|
|
32
|
-
//
|
|
36
|
+
// 6. Slice the original source bytes for the node's range.
|
|
37
|
+
// 7. If the rendered output differs, emit one TextEdit replacing
|
|
33
38
|
// [start, end) with the new bytes.
|
|
34
39
|
//
|
|
35
40
|
// The "no diff → no edit" invariant is what keeps `ttsc format`
|
|
36
41
|
// idempotent: a second pass renders identical bytes, the comparison
|
|
37
|
-
// short-circuits, and the cascade converges.
|
|
42
|
+
// short-circuits, and the cascade converges. The `covered` abstain in
|
|
43
|
+
// step 4 is the safety floor: `ttsc format` either reflows correctly or
|
|
44
|
+
// leaves the node byte-identical — it never emits a half-reflowed,
|
|
45
|
+
// inconsistently indented shape.
|
|
38
46
|
//
|
|
39
47
|
// The rule is a format-class rule (IsFormat == true) so `ttsc format`
|
|
40
48
|
// applies its edits while `ttsc check` only emits diagnostics for
|
|
@@ -44,11 +52,19 @@ import (
|
|
|
44
52
|
type formatPrintWidth struct{}
|
|
45
53
|
|
|
46
54
|
// formatPrintWidthOptions mirrors `TtscLintRuleOptions.PrintWidth`.
|
|
55
|
+
//
|
|
56
|
+
// TrailingComma reaches this rule because the printer's reflow decides
|
|
57
|
+
// whether to emit a trailing comma on every multi-line list — and that
|
|
58
|
+
// decision must match the user's `format.trailingComma` setting or the
|
|
59
|
+
// reflow oscillates against `format/trailing-comma` on every cascade
|
|
60
|
+
// pass. The config layer mirrors `format.trailingComma` into both
|
|
61
|
+
// rules' option blobs (see `expandFormatBlock` in config_format.go).
|
|
47
62
|
type formatPrintWidthOptions struct {
|
|
48
|
-
PrintWidth
|
|
49
|
-
TabWidth
|
|
50
|
-
UseTabs
|
|
51
|
-
EndOfLine
|
|
63
|
+
PrintWidth *int `json:"printWidth"`
|
|
64
|
+
TabWidth *int `json:"tabWidth"`
|
|
65
|
+
UseTabs *bool `json:"useTabs"`
|
|
66
|
+
EndOfLine *string `json:"endOfLine"`
|
|
67
|
+
TrailingComma *string `json:"trailingComma"`
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
func (formatPrintWidth) Name() string { return "format/print-width" }
|
|
@@ -66,6 +82,29 @@ func (formatPrintWidth) Visits() []shimast.Kind {
|
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
|
|
85
|
+
// Known residual divergence from Prettier 3 (investigated against the
|
|
86
|
+
// nestjs / typeorm / vscode benchmark fixtures, not closed in this
|
|
87
|
+
// pass):
|
|
88
|
+
//
|
|
89
|
+
// - Multi-line `reduce(...)` (or other single-arg method) calls where
|
|
90
|
+
// Prettier 3 keeps the inline form because it fits print-width
|
|
91
|
+
// minus the trailing-suffix budget. The current shrunk-budget
|
|
92
|
+
// re-render still over-breaks some of these; the next slice should
|
|
93
|
+
// measure fitsFirstLine against `pw - col - trailingNonComment`
|
|
94
|
+
// before committing to the broken layout.
|
|
95
|
+
// - Single-line `export type { X } from "long-path"` reexports.
|
|
96
|
+
// Prettier 3 keeps them flat even when the whole declaration
|
|
97
|
+
// overflows; ttsc-lint visits `KindNamedExports` in isolation and
|
|
98
|
+
// breaks the brace clause. A real fix requires teaching the rule
|
|
99
|
+
// about the surrounding ExportDeclaration so the brace clause is
|
|
100
|
+
// measured against the full declaration line, or visiting
|
|
101
|
+
// ExportDeclaration directly so the `from "..."` tail joins the
|
|
102
|
+
// reflow surface.
|
|
103
|
+
//
|
|
104
|
+
// Both cases are tracked benchmark cases that forced
|
|
105
|
+
// `format/print-width: 'off'` on the ttsc-lint branch. They are listed
|
|
106
|
+
// here so a future slice can pick them up without rediscovering the
|
|
107
|
+
// divergence.
|
|
69
108
|
func (formatPrintWidth) Check(ctx *Context, node *shimast.Node) {
|
|
70
109
|
if ctx == nil || ctx.File == nil || node == nil {
|
|
71
110
|
return
|
|
@@ -85,6 +124,9 @@ func (formatPrintWidth) Check(ctx *Context, node *shimast.Node) {
|
|
|
85
124
|
if opts.EndOfLine != nil {
|
|
86
125
|
printOpts.EndOfLine = *opts.EndOfLine
|
|
87
126
|
}
|
|
127
|
+
if opts.TrailingComma != nil {
|
|
128
|
+
printOpts.TrailingComma = *opts.TrailingComma
|
|
129
|
+
}
|
|
88
130
|
|
|
89
131
|
src := ctx.File.Text()
|
|
90
132
|
start := shimscanner.SkipTrivia(src, node.Pos())
|
|
@@ -101,6 +143,16 @@ func (formatPrintWidth) Check(ctx *Context, node *shimast.Node) {
|
|
|
101
143
|
return
|
|
102
144
|
}
|
|
103
145
|
|
|
146
|
+
// Abstain on any node nested inside a template-literal substitution.
|
|
147
|
+
// Prettier renders `${…}` expressions at printWidth:Infinity — it
|
|
148
|
+
// never breaks an interpolation the source wrote on one line — so
|
|
149
|
+
// reflowing a call or literal inside `${…}` would split the template
|
|
150
|
+
// across lines and diverge from Prettier. See the printWidth:Infinity
|
|
151
|
+
// branch in Prettier's printTemplateExpression.
|
|
152
|
+
if hasTemplateSubstitutionAncestor(node) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
104
156
|
// Safety: abstain when the node carries comments outside its
|
|
105
157
|
// children. The per-node printers join child docs with a fresh
|
|
106
158
|
// `, ` separator and have no path for trivia between siblings, so
|
|
@@ -113,29 +165,85 @@ func (formatPrintWidth) Check(ctx *Context, node *shimast.Node) {
|
|
|
113
165
|
}
|
|
114
166
|
|
|
115
167
|
printOpts.StartingColumn = leadingColumn(src, start, printOpts.TabWidth)
|
|
116
|
-
|
|
168
|
+
// A node reflowed on a ternary-arm continuation line (`? expr` or
|
|
169
|
+
// `: expr`) hangs its broken continuation under the arm's expression,
|
|
170
|
+
// two columns past the `?`/`:` marker — not under the marker itself.
|
|
171
|
+
printOpts.BaseIndent = lineLeadingIndent(src, start, printOpts.TabWidth) +
|
|
172
|
+
ternaryArmIndentBonus(src, start)
|
|
173
|
+
|
|
174
|
+
// trailingWidth is the column span of the tokens that stay on the
|
|
175
|
+
// node's last line after `end` — a `;`, a `);`, a `) {`. The reflow
|
|
176
|
+
// replaces only [start, end) and cannot move them, so both the fast
|
|
177
|
+
// path and the layout budget must reserve those columns; otherwise
|
|
178
|
+
// the rule emits a line that overflows by exactly the suffix.
|
|
179
|
+
trailingWidth := trailingLineWidth(src, end, printOpts.TabWidth)
|
|
117
180
|
|
|
118
181
|
// Fast path: if the node's existing single-line bytes already fit
|
|
119
|
-
// the printWidth budget
|
|
120
|
-
//
|
|
121
|
-
// the
|
|
182
|
+
// the printWidth budget — prefix column, node width and trailing
|
|
183
|
+
// suffix all charged — the reflowed output cannot differ from the
|
|
184
|
+
// source (the printer would render the same flat shape). Skip the
|
|
185
|
+
// Doc build + render entirely. This is the common case on
|
|
122
186
|
// well-formatted code — every short call, every short literal —
|
|
123
187
|
// and saves the allocations from PrintNode + Print.
|
|
124
188
|
if !sliceContainsNewline(src, start, end) &&
|
|
125
|
-
printOpts.StartingColumn+(end-start) <= printOpts.PrintWidth {
|
|
189
|
+
printOpts.StartingColumn+(end-start)+trailingWidth <= printOpts.PrintWidth {
|
|
126
190
|
return
|
|
127
191
|
}
|
|
128
192
|
|
|
129
193
|
printCtx := NewPrintContext(ctx.File, printOpts)
|
|
130
|
-
doc,
|
|
194
|
+
doc, covered := PrintNode(printCtx, node)
|
|
131
195
|
if doc.IsNil() {
|
|
132
196
|
return
|
|
133
197
|
}
|
|
198
|
+
// Safety abstain: the printed subtree contains a multi-line verbatim
|
|
199
|
+
// node — one the dispatcher has no printer for. Such a node keeps the
|
|
200
|
+
// source columns its lines were written at, while the reflow
|
|
201
|
+
// re-indents everything around it. Emitting the edit would produce
|
|
202
|
+
// inconsistently indented, corrupt output (a callback header at one
|
|
203
|
+
// indent, its body frozen at another). Abstaining leaves the bytes
|
|
204
|
+
// byte-identical, which is always safe. See the coverage-signal note
|
|
205
|
+
// in print_dispatch.go.
|
|
206
|
+
if !covered {
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
// Render at the full printWidth budget. A reflow that breaks across
|
|
210
|
+
// lines then makes every layout decision — which call argument hugs,
|
|
211
|
+
// where a list explodes — against the true column budget. The
|
|
212
|
+
// un-movable trailing suffix (`;`, `) {`, ` satisfies T`) lands on a
|
|
213
|
+
// short last line; charging it against the whole budget would
|
|
214
|
+
// wrongly penalize the interior lines and over-break the node.
|
|
134
215
|
rendered := Print(doc, printOpts)
|
|
216
|
+
// The one case the suffix genuinely shares the node's line is a
|
|
217
|
+
// reflow that collapses to a single line. When the flat form plus
|
|
218
|
+
// the suffix would overflow, re-render under a budget shrunk by the
|
|
219
|
+
// suffix so the node breaks instead of spilling the suffix past
|
|
220
|
+
// printWidth — the regression that keeps a call flat at exactly
|
|
221
|
+
// printWidth while the trailing `;` runs over.
|
|
222
|
+
if trailingWidth > 0 &&
|
|
223
|
+
!strings.Contains(rendered, "\n") &&
|
|
224
|
+
maxLineWidth(rendered, printOpts.StartingColumn, trailingWidth, printOpts.TabWidth) > printOpts.PrintWidth &&
|
|
225
|
+
printOpts.PrintWidth-trailingWidth >= 1 {
|
|
226
|
+
shrunk := printOpts
|
|
227
|
+
shrunk.PrintWidth -= trailingWidth
|
|
228
|
+
rendered = Print(doc, shrunk)
|
|
229
|
+
}
|
|
135
230
|
original := src[start:end]
|
|
136
231
|
if rendered == original {
|
|
137
232
|
return
|
|
138
233
|
}
|
|
234
|
+
// Safety floor: never emit an edit that makes the widest line wider
|
|
235
|
+
// than it already was. The reflow may be unable to break an
|
|
236
|
+
// un-breakable token run — a long string literal, a verbatim object
|
|
237
|
+
// member — but it must never *worsen* the worst line. That is exactly
|
|
238
|
+
// the regression this guards: `ttsc format` collapsing an
|
|
239
|
+
// already-broken, fitting call into one over-wide line. A reflow that
|
|
240
|
+
// only fixes indentation and leaves a pre-existing over-wide line
|
|
241
|
+
// untouched is still emitted.
|
|
242
|
+
renderedMax := maxLineWidth(rendered, printOpts.StartingColumn, trailingWidth, printOpts.TabWidth)
|
|
243
|
+
originalMax := maxLineWidth(original, printOpts.StartingColumn, trailingWidth, printOpts.TabWidth)
|
|
244
|
+
if renderedMax > printOpts.PrintWidth && renderedMax > originalMax {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
139
247
|
ctx.ReportRangeFix(
|
|
140
248
|
start,
|
|
141
249
|
end,
|
|
@@ -144,6 +252,113 @@ func (formatPrintWidth) Check(ctx *Context, node *shimast.Node) {
|
|
|
144
252
|
)
|
|
145
253
|
}
|
|
146
254
|
|
|
255
|
+
// trailingLineWidth returns the visual column width of src[end:] up to
|
|
256
|
+
// the next newline, with trailing whitespace trimmed. The format/print-
|
|
257
|
+
// width reflow replaces only the node's own byte range, so whatever
|
|
258
|
+
// shares the node's last source line — a statement `;`, a `) {` header
|
|
259
|
+
// tail, a `, nextArg)` continuation — stays put. Charging that width
|
|
260
|
+
// against the budget keeps the rule from emitting a line that overflows
|
|
261
|
+
// by exactly the suffix it could never move.
|
|
262
|
+
//
|
|
263
|
+
// Trailing `//` line comments are excluded from the budget. A line
|
|
264
|
+
// comment runs to the end of the source line by definition, so breaking
|
|
265
|
+
// the reflowed node to make the comment fit cannot help — Prettier 3
|
|
266
|
+
// keeps the node inline and lets the comment trail (see typeorm's
|
|
267
|
+
// `comment.replaceAll(...) // Null bytes' shape that pushed
|
|
268
|
+
// `format/print-width: 'off'` onto the ttsc-lint benchmark branch).
|
|
269
|
+
// Excluding the comment from `trailingLineWidth` matches that
|
|
270
|
+
// behavior: the fast path sees just the un-movable punctuation suffix,
|
|
271
|
+
// and the shrunk-budget re-render does not over-shrink and over-break.
|
|
272
|
+
func trailingLineWidth(src string, end int, tabWidth int) int {
|
|
273
|
+
if end < 0 || end > len(src) {
|
|
274
|
+
return 0
|
|
275
|
+
}
|
|
276
|
+
if tabWidth <= 0 {
|
|
277
|
+
tabWidth = 2
|
|
278
|
+
}
|
|
279
|
+
lineEnd := trailingSuffixEnd(src, end)
|
|
280
|
+
col := 0
|
|
281
|
+
for i := end; i < lineEnd; i++ {
|
|
282
|
+
if src[i] == '\t' {
|
|
283
|
+
col += tabWidth - (col % tabWidth)
|
|
284
|
+
} else {
|
|
285
|
+
col++
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return col
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// trailingSuffixEnd returns the byte offset where the node's un-movable
|
|
292
|
+
// trailing suffix ends on the line that begins at `end`. The walk stops
|
|
293
|
+
// at the first `//` line comment (Prettier-style "free" trailing
|
|
294
|
+
// attachment, see trailingLineWidth) or at the newline, then trims
|
|
295
|
+
// trailing whitespace so a `;<spaces><newline>` tail measures the
|
|
296
|
+
// `;` only. Trailing block comments inside the un-movable suffix span
|
|
297
|
+
// (`} /* note */`) keep their bytes counted; that path is rare and
|
|
298
|
+
// already exercised through the existing block-comment fixture.
|
|
299
|
+
func trailingSuffixEnd(src string, end int) int {
|
|
300
|
+
lineEnd := end
|
|
301
|
+
for lineEnd < len(src) && src[lineEnd] != '\n' {
|
|
302
|
+
if src[lineEnd] == '/' && lineEnd+1 < len(src) {
|
|
303
|
+
next := src[lineEnd+1]
|
|
304
|
+
if next == '/' {
|
|
305
|
+
// `//` line comment — Prettier treats the whole tail as a
|
|
306
|
+
// trailing comment that runs to EOL. Drop it from the suffix
|
|
307
|
+
// budget so the rule does not break the node to chase a
|
|
308
|
+
// comment that cannot be moved or wrapped.
|
|
309
|
+
break
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
lineEnd++
|
|
313
|
+
}
|
|
314
|
+
for lineEnd > end {
|
|
315
|
+
c := src[lineEnd-1]
|
|
316
|
+
if c != ' ' && c != '\t' && c != '\r' {
|
|
317
|
+
break
|
|
318
|
+
}
|
|
319
|
+
lineEnd--
|
|
320
|
+
}
|
|
321
|
+
return lineEnd
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// maxLineWidth returns the widest effective column span among the lines
|
|
325
|
+
// of `text`. The first line is charged `startingColumn` — the prefix
|
|
326
|
+
// already on that source line that the reflow does not re-emit — and
|
|
327
|
+
// the last line is charged `trailingWidth` for the un-movable suffix
|
|
328
|
+
// that follows the node. Tabs count as `tabWidth` columns.
|
|
329
|
+
//
|
|
330
|
+
// The rule compares the rendered output's widest line against the
|
|
331
|
+
// source node's: a reflow that cannot fit an un-breakable token is
|
|
332
|
+
// still allowed through as long as it does not make the worst line any
|
|
333
|
+
// wider than it already was.
|
|
334
|
+
func maxLineWidth(text string, startingColumn, trailingWidth, tabWidth int) int {
|
|
335
|
+
if tabWidth <= 0 {
|
|
336
|
+
tabWidth = 2
|
|
337
|
+
}
|
|
338
|
+
lines := strings.Split(text, "\n")
|
|
339
|
+
widest := 0
|
|
340
|
+
for i, line := range lines {
|
|
341
|
+
width := 0
|
|
342
|
+
for _, r := range line {
|
|
343
|
+
if r == '\t' {
|
|
344
|
+
width += tabWidth - (width % tabWidth)
|
|
345
|
+
} else if r != '\r' {
|
|
346
|
+
width++
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if i == 0 {
|
|
350
|
+
width += startingColumn
|
|
351
|
+
}
|
|
352
|
+
if i == len(lines)-1 {
|
|
353
|
+
width += trailingWidth
|
|
354
|
+
}
|
|
355
|
+
if width > widest {
|
|
356
|
+
widest = width
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return widest
|
|
360
|
+
}
|
|
361
|
+
|
|
147
362
|
// leadingColumn returns the visual column the byte at `pos` occupies on
|
|
148
363
|
// its line. Tabs expand to `tabWidth` columns; other bytes count as 1.
|
|
149
364
|
// The rule uses this to seed the printer's StartingColumn so fit
|
|
@@ -240,6 +455,25 @@ func lineStartOffset(src string, pos int) int {
|
|
|
240
455
|
return pos
|
|
241
456
|
}
|
|
242
457
|
|
|
458
|
+
// ternaryArmIndentBonus returns 2 when the line containing `pos` begins,
|
|
459
|
+
// after its leading whitespace, with a `? ` or `: ` ternary-arm marker,
|
|
460
|
+
// and 0 otherwise. format/print-width adds it to BaseIndent so a node
|
|
461
|
+
// reflowed inside a ternary arm indents its broken continuation under
|
|
462
|
+
// the arm's expression rather than under the `?`/`:` token. In practice
|
|
463
|
+
// only a ternary arm opens a reflow target's line with `? ` / `: `; the
|
|
464
|
+
// two-byte prefix is a heuristic, and a rare false positive only shifts
|
|
465
|
+
// a broken continuation by two columns — it never corrupts bytes.
|
|
466
|
+
func ternaryArmIndentBonus(src string, pos int) int {
|
|
467
|
+
i := lineStartOffset(src, pos)
|
|
468
|
+
for i < len(src) && (src[i] == ' ' || src[i] == '\t') {
|
|
469
|
+
i++
|
|
470
|
+
}
|
|
471
|
+
if i+1 < len(src) && (src[i] == '?' || src[i] == ':') && src[i+1] == ' ' {
|
|
472
|
+
return 2
|
|
473
|
+
}
|
|
474
|
+
return 0
|
|
475
|
+
}
|
|
476
|
+
|
|
243
477
|
// hasReflowAncestor reports whether any ancestor of `node` would also
|
|
244
478
|
// match the format/print-width visitor. The rule uses this to suppress
|
|
245
479
|
// nested fires when an enclosing reflow target already covers the
|
|
@@ -256,6 +490,27 @@ func hasReflowAncestor(node *shimast.Node) bool {
|
|
|
256
490
|
return false
|
|
257
491
|
}
|
|
258
492
|
|
|
493
|
+
// hasTemplateSubstitutionAncestor reports whether `node` sits inside a
|
|
494
|
+
// template-literal substitution (`${…}`). format/print-width abstains
|
|
495
|
+
// on such nodes: Prettier prints template interpolations at infinite
|
|
496
|
+
// printWidth and only keeps a break the source already had, so a reflow
|
|
497
|
+
// of a nested call or literal would split a one-line `${…}` and never
|
|
498
|
+
// match Prettier's output.
|
|
499
|
+
func hasTemplateSubstitutionAncestor(node *shimast.Node) bool {
|
|
500
|
+
if node == nil {
|
|
501
|
+
return false
|
|
502
|
+
}
|
|
503
|
+
for parent := node.Parent; parent != nil; parent = parent.Parent {
|
|
504
|
+
if parent.Kind == shimast.KindTemplateExpression {
|
|
505
|
+
return true
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return false
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// isReflowKind reports whether `k` is one of the node kinds the
|
|
512
|
+
// format/print-width rule visits. Kept in sync with Visits() so
|
|
513
|
+
// hasReflowAncestor does not need to call Visits() at runtime.
|
|
259
514
|
func isReflowKind(k shimast.Kind) bool {
|
|
260
515
|
switch k {
|
|
261
516
|
case shimast.KindObjectLiteralExpression,
|
|
@@ -25,6 +25,9 @@ import (
|
|
|
25
25
|
// also intentionally out of scope.
|
|
26
26
|
type formatQuotes struct{}
|
|
27
27
|
|
|
28
|
+
// formatQuotesOptions mirrors `TtscLintRuleOptions.Quotes`. Prefer accepts
|
|
29
|
+
// `"double"` (the default, enforces double quotes) or `"single"` (enforces
|
|
30
|
+
// single quotes). Any other value is treated as the default.
|
|
28
31
|
type formatQuotesOptions struct {
|
|
29
32
|
Prefer string `json:"prefer"`
|
|
30
33
|
}
|
|
@@ -173,6 +173,8 @@ func loadSortImportsOptions(ctx *Context) resolvedSortImportsOptions {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
// hasThirdPartyGroup reports whether at least one group in the list is the
|
|
177
|
+
// third-party catchall. Used to decide whether a sentinel needs to be appended.
|
|
176
178
|
func hasThirdPartyGroup(groups []sortImportsGroup) bool {
|
|
177
179
|
for _, g := range groups {
|
|
178
180
|
if g.thirdParty {
|
|
@@ -449,6 +451,8 @@ func reportNamedSpecifierSort(ctx *Context, decl *shimast.Node, caseInsensitive
|
|
|
449
451
|
)
|
|
450
452
|
}
|
|
451
453
|
|
|
454
|
+
// joinSortedSpecifiers returns the specifier texts joined with ", " in their
|
|
455
|
+
// already-sorted order. The caller owns the sort; this is a formatting step only.
|
|
452
456
|
func joinSortedSpecifiers(entries []specifierEntry) string {
|
|
453
457
|
texts := make([]string, len(entries))
|
|
454
458
|
for i, e := range entries {
|
package/linthost/rules_gap.go
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
// Miscellaneous AST-only rules that cover the gap between the core ESLint
|
|
2
|
+
// "Possible Problems" list and the @typescript-eslint strict/stylistic
|
|
3
|
+
// presets: empty static blocks, setter returns, unused labels, dynamic
|
|
4
|
+
// delete, nullish-coalescing with non-null assertions, unnecessary type
|
|
5
|
+
// constraints, unsafe built-in types, wrapper object types, useless
|
|
6
|
+
// constructors, non-literal enum members, type-assertion style, type
|
|
7
|
+
// definition style, dot notation, and unsafe declaration merging.
|
|
8
|
+
// Each rule is registered in init() at the bottom of the file.
|
|
1
9
|
package linthost
|
|
2
10
|
|
|
3
|
-
import
|
|
11
|
+
import (
|
|
12
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
13
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
14
|
+
)
|
|
4
15
|
|
|
5
16
|
// no-empty-static-block: `class C { static {} }` has no effect.
|
|
6
17
|
// ESLint recommended: https://eslint.org/docs/latest/rules/no-empty-static-block
|
|
@@ -35,6 +46,9 @@ func (noSetterReturn) Check(ctx *Context, node *shimast.Node) {
|
|
|
35
46
|
ctx.Report(node, "Setter should not return a value.")
|
|
36
47
|
}
|
|
37
48
|
|
|
49
|
+
// isInsideDirectSetter reports whether node is lexically nested inside a
|
|
50
|
+
// SetAccessor without an intervening function boundary. "Direct" means the
|
|
51
|
+
// return value would actually be a setter return, not a nested callback's.
|
|
38
52
|
func isInsideDirectSetter(node *shimast.Node) bool {
|
|
39
53
|
for p := node.Parent; p != nil; p = p.Parent {
|
|
40
54
|
if p.Kind == shimast.KindSetAccessor {
|
|
@@ -99,6 +113,9 @@ func (noDynamicDelete) Check(ctx *Context, node *shimast.Node) {
|
|
|
99
113
|
ctx.Report(node, "Do not delete dynamically computed property keys.")
|
|
100
114
|
}
|
|
101
115
|
|
|
116
|
+
// isStaticPropertyKey reports whether node is a literal key that can
|
|
117
|
+
// appear in a delete expression without triggering no-dynamic-delete:
|
|
118
|
+
// string literals, no-substitution template literals, and numeric literals.
|
|
102
119
|
func isStaticPropertyKey(node *shimast.Node) bool {
|
|
103
120
|
node = stripParens(node)
|
|
104
121
|
if node == nil {
|
|
@@ -263,6 +280,9 @@ func fileShadowsWrapperName(file *shimast.SourceFile, name string) bool {
|
|
|
263
280
|
return false
|
|
264
281
|
}
|
|
265
282
|
|
|
283
|
+
// wrapperPrimitive maps a boxed object type name (e.g. "String") to its
|
|
284
|
+
// primitive keyword equivalent (e.g. "string"). Returns ("", false) for
|
|
285
|
+
// names that are not recognized wrapper types.
|
|
266
286
|
func wrapperPrimitive(name string) (string, bool) {
|
|
267
287
|
switch name {
|
|
268
288
|
case "String":
|
|
@@ -355,16 +375,68 @@ func (dotNotation) Visits() []shimast.Kind {
|
|
|
355
375
|
}
|
|
356
376
|
func (dotNotation) Check(ctx *Context, node *shimast.Node) {
|
|
357
377
|
access := node.AsElementAccessExpression()
|
|
358
|
-
if access == nil {
|
|
378
|
+
if access == nil || access.Expression == nil || access.ArgumentExpression == nil {
|
|
359
379
|
return
|
|
360
380
|
}
|
|
361
381
|
key := stringLiteralText(access.ArgumentExpression)
|
|
362
382
|
if key == "" || !isSimpleIdentifierName(key) {
|
|
363
383
|
return
|
|
364
384
|
}
|
|
365
|
-
|
|
385
|
+
message := "Use dot notation instead of a string literal property access."
|
|
386
|
+
// Conservative: keep reserved words as bracket access. Modern JS accepts
|
|
387
|
+
// `obj.class`/`obj.if`/etc. syntactically but mixing them with member
|
|
388
|
+
// expression syntax is jarring and can confuse minifiers and older runtimes.
|
|
389
|
+
if isReservedWord(key) {
|
|
390
|
+
ctx.Report(node, message)
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
src := ctx.File.Text()
|
|
394
|
+
// Determine where the bracket access begins. With an optional chain
|
|
395
|
+
// (`obj?.["foo"]`) the `?.` token already separates object from access, so
|
|
396
|
+
// we replace the `["foo"]` tail with `foo`. Otherwise we replace the
|
|
397
|
+
// `["foo"]` tail with `.foo`.
|
|
398
|
+
var replaceFrom int
|
|
399
|
+
var replacement string
|
|
400
|
+
if access.QuestionDotToken != nil {
|
|
401
|
+
replaceFrom = shimscanner.SkipTrivia(src, access.QuestionDotToken.End())
|
|
402
|
+
replacement = key
|
|
403
|
+
} else {
|
|
404
|
+
replaceFrom = access.Expression.End()
|
|
405
|
+
replacement = "." + key
|
|
406
|
+
}
|
|
407
|
+
if replaceFrom < 0 || replaceFrom >= node.End() {
|
|
408
|
+
ctx.Report(node, message)
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
ctx.ReportFix(
|
|
412
|
+
node,
|
|
413
|
+
message,
|
|
414
|
+
TextEdit{Pos: replaceFrom, End: node.End(), Text: replacement},
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// isReservedWord reports whether `value` is an ECMAScript reserved word. The
|
|
419
|
+
// `dot-notation` autofix uses this to skip the rewrite even though modern
|
|
420
|
+
// parsers accept reserved-word member names: leaving bracket access matches
|
|
421
|
+
// the conservative branch ESLint takes when `allowKeywords: false`.
|
|
422
|
+
func isReservedWord(value string) bool {
|
|
423
|
+
switch value {
|
|
424
|
+
case "break", "case", "catch", "class", "const", "continue",
|
|
425
|
+
"debugger", "default", "delete", "do", "else", "enum",
|
|
426
|
+
"export", "extends", "false", "finally", "for", "function",
|
|
427
|
+
"if", "implements", "import", "in", "instanceof", "interface",
|
|
428
|
+
"let", "new", "null", "package", "private", "protected",
|
|
429
|
+
"public", "return", "static", "super", "switch", "this",
|
|
430
|
+
"throw", "true", "try", "typeof", "var", "void",
|
|
431
|
+
"while", "with", "yield", "await":
|
|
432
|
+
return true
|
|
433
|
+
}
|
|
434
|
+
return false
|
|
366
435
|
}
|
|
367
436
|
|
|
437
|
+
// isSimpleIdentifierName reports whether value is a valid bare identifier
|
|
438
|
+
// (ASCII letters, digits, underscore, dollar sign; digits not first). Used
|
|
439
|
+
// by dot-notation to decide whether bracket access can become dot access.
|
|
368
440
|
func isSimpleIdentifierName(value string) bool {
|
|
369
441
|
if value == "" {
|
|
370
442
|
return false
|
|
@@ -67,13 +67,11 @@ func (noImportTypeSideEffects) Check(ctx *Context, node *shimast.Node) {
|
|
|
67
67
|
}
|
|
68
68
|
src := ctx.File.Text()
|
|
69
69
|
for _, spec := range named.Elements.Nodes {
|
|
70
|
-
// Locate the `type` keyword token at the head of
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// the source. SkipTrivia honors the lexer's notion of trivia, so
|
|
76
|
-
// the post-skip position is the actual first token byte.
|
|
70
|
+
// Locate the `type` keyword token at the head of each specifier by
|
|
71
|
+
// skipping leading trivia (whitespace + comments). A naive
|
|
72
|
+
// findKeyword scan would risk matching `type` inside a block comment
|
|
73
|
+
// such as `/* type alias */`, corrupting the source. SkipTrivia
|
|
74
|
+
// anchors the scan at the actual first token byte.
|
|
77
75
|
typePos := shimscanner.SkipTrivia(src, spec.Pos())
|
|
78
76
|
if typePos < 0 || typePos+len("type") > len(src) {
|
|
79
77
|
continue
|
package/linthost/rules_logic.go
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
// Correctness and equality rules — a focused set from ESLint's "Possible
|
|
2
|
+
// Problems" and "Suggestions" categories that catch logic errors rather than
|
|
3
|
+
// style issues: redundant boolean casts, unsafe negations, loose equality,
|
|
4
|
+
// NaN comparisons, constant conditions, and assignment-in-condition.
|
|
5
|
+
// AST-only, no scope analysis.
|
|
1
6
|
package linthost
|
|
2
7
|
|
|
3
|
-
import
|
|
8
|
+
import (
|
|
9
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
10
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
11
|
+
)
|
|
4
12
|
|
|
5
13
|
// no-extra-boolean-cast: `if (!!x)`, `if (Boolean(x))`, `Boolean(!!x)` —
|
|
6
14
|
// the conversion is implicit in a boolean context.
|
|
@@ -21,9 +29,43 @@ func (noExtraBooleanCast) Check(ctx *Context, node *shimast.Node) {
|
|
|
21
29
|
if identifierText(call.Expression) != "Boolean" {
|
|
22
30
|
return
|
|
23
31
|
}
|
|
24
|
-
if
|
|
25
|
-
|
|
32
|
+
if call.QuestionDotToken != nil {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
if !isInBooleanContext(node) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
message := "Redundant Boolean call."
|
|
39
|
+
// Only autofix when there is exactly one positional argument: zero
|
|
40
|
+
// arguments produce `undefined → false`, and multi-arg calls signal the
|
|
41
|
+
// author may be passing through an unrelated value as the second slot.
|
|
42
|
+
// Spread arguments hide the runtime shape, so skip those as well.
|
|
43
|
+
if call.Arguments == nil || len(call.Arguments.Nodes) != 1 {
|
|
44
|
+
ctx.Report(node, message)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
arg := call.Arguments.Nodes[0]
|
|
48
|
+
if arg == nil || arg.Kind == shimast.KindSpreadElement {
|
|
49
|
+
ctx.Report(node, message)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
src := ctx.File.Text()
|
|
53
|
+
argStart := shimscanner.SkipTrivia(src, arg.Pos())
|
|
54
|
+
argEnd := arg.End()
|
|
55
|
+
if argStart < 0 || argStart >= argEnd || argEnd > len(src) {
|
|
56
|
+
ctx.Report(node, message)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
editPos := shimscanner.SkipTrivia(src, node.Pos())
|
|
60
|
+
if editPos < 0 || editPos >= node.End() {
|
|
61
|
+
ctx.Report(node, message)
|
|
62
|
+
return
|
|
26
63
|
}
|
|
64
|
+
ctx.ReportFix(
|
|
65
|
+
node,
|
|
66
|
+
message,
|
|
67
|
+
TextEdit{Pos: editPos, End: node.End(), Text: src[argStart:argEnd]},
|
|
68
|
+
)
|
|
27
69
|
case shimast.KindPrefixUnaryExpression:
|
|
28
70
|
outer := node.AsPrefixUnaryExpression()
|
|
29
71
|
if outer == nil || outer.Operator != shimast.KindExclamationToken {
|
|
@@ -37,9 +79,31 @@ func (noExtraBooleanCast) Check(ctx *Context, node *shimast.Node) {
|
|
|
37
79
|
if inner == nil || inner.Operator != shimast.KindExclamationToken {
|
|
38
80
|
return
|
|
39
81
|
}
|
|
40
|
-
if isInBooleanContext(node) {
|
|
41
|
-
|
|
82
|
+
if !isInBooleanContext(node) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
message := "Redundant double negation."
|
|
86
|
+
if inner.Operand == nil {
|
|
87
|
+
ctx.Report(node, message)
|
|
88
|
+
return
|
|
42
89
|
}
|
|
90
|
+
src := ctx.File.Text()
|
|
91
|
+
inStart := shimscanner.SkipTrivia(src, inner.Operand.Pos())
|
|
92
|
+
inEnd := inner.Operand.End()
|
|
93
|
+
if inStart < 0 || inStart >= inEnd || inEnd > len(src) {
|
|
94
|
+
ctx.Report(node, message)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
editPos := shimscanner.SkipTrivia(src, node.Pos())
|
|
98
|
+
if editPos < 0 || editPos >= node.End() {
|
|
99
|
+
ctx.Report(node, message)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
ctx.ReportFix(
|
|
103
|
+
node,
|
|
104
|
+
message,
|
|
105
|
+
TextEdit{Pos: editPos, End: node.End(), Text: src[inStart:inEnd]},
|
|
106
|
+
)
|
|
43
107
|
}
|
|
44
108
|
}
|
|
45
109
|
|
|
@@ -71,6 +135,12 @@ func isInBooleanContext(node *shimast.Node) bool {
|
|
|
71
135
|
return false
|
|
72
136
|
}
|
|
73
137
|
|
|
138
|
+
// skipParents walks up through any wrapping ParenthesizedExpression nodes and
|
|
139
|
+
// returns the outermost parenthesized wrapper. This is the inverse of
|
|
140
|
+
// stripParens: where stripParens descends into the canonical inner expression,
|
|
141
|
+
// skipParents climbs up to the outermost paren so that structural parent
|
|
142
|
+
// comparisons (e.g. "is this expression the test of an if?") resolve against
|
|
143
|
+
// the node the parser actually attached as a child, not the inner form.
|
|
74
144
|
func skipParents(node *shimast.Node) *shimast.Node {
|
|
75
145
|
for node != nil && node.Parent != nil && node.Parent.Kind == shimast.KindParenthesizedExpression {
|
|
76
146
|
node = node.Parent
|
package/linthost/rules_loops.go
CHANGED
|
@@ -57,6 +57,10 @@ func (forDirection) Check(ctx *Context, node *shimast.Node) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// updateDirection returns the direction (+1 for incrementing, -1 for
|
|
61
|
+
// decrementing, 0 for unknown) that the incrementor expression moves the
|
|
62
|
+
// named counter variable. Only simple patterns are recognised: i++, i--,
|
|
63
|
+
// ++i, --i, i += N, and i -= N. Compound or computed expressions return 0.
|
|
60
64
|
func updateDirection(node *shimast.Node, counter string) int {
|
|
61
65
|
if node == nil || counter == "" {
|
|
62
66
|
return 0
|