@ttsc/lint 0.12.4 → 0.13.1
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/index.js +225 -135
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
- package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
- package/linthost/ast_helpers.go +68 -16
- package/linthost/compile.go +117 -119
- package/linthost/config.go +518 -707
- package/linthost/config_format.go +16 -4
- package/linthost/contrib_adapter.go +7 -0
- package/linthost/directives.go +44 -0
- package/linthost/engine.go +152 -44
- package/linthost/fix.go +24 -33
- package/linthost/flags_gen.go +33 -0
- package/linthost/format.go +96 -3
- package/linthost/host.go +144 -8
- package/linthost/print_dispatch.go +121 -23
- package/linthost/print_doc.go +19 -0
- package/linthost/print_engine.go +168 -4
- package/linthost/print_nodes_array.go +17 -7
- package/linthost/print_nodes_call.go +129 -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_escape.go +20 -3
- package/linthost/rules_format_print_width.go +267 -15
- package/linthost/rules_gap.go +55 -3
- package/linthost/rules_logic.go +64 -5
- package/linthost/rules_problems.go +65 -21
- package/linthost/rules_promise.go +3 -0
- package/linthost/rules_suggestions.go +160 -5
- package/linthost/rules_var.go +7 -1
- package/package.json +3 -3
- package/src/index.ts +243 -168
- package/src/structures/ITtscLintPluginConfig.ts +10 -83
- package/src/structures/TtscLintRuleOptions.ts +15 -0
- package/linthost/eslint_runtime.go +0 -351
|
@@ -2,6 +2,7 @@ package linthost
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
5
6
|
)
|
|
6
7
|
|
|
7
8
|
// printObjectLiteral renders an ObjectLiteralExpression with width-aware
|
|
@@ -22,30 +23,74 @@ import (
|
|
|
22
23
|
// The flat form uses a single space inside the braces, matching
|
|
23
24
|
// Prettier's `bracketSpacing: true` default. Empty object literals
|
|
24
25
|
// collapse to `{}` with no inner space, matching every formatter.
|
|
25
|
-
|
|
26
|
+
//
|
|
27
|
+
// The second return value is the `covered` flag: see PrintNode. It is
|
|
28
|
+
// the AND of every property's coverage — one multi-line verbatim
|
|
29
|
+
// member taints the whole literal.
|
|
30
|
+
func printObjectLiteral(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
26
31
|
if node == nil {
|
|
27
|
-
return Doc{}
|
|
32
|
+
return Doc{}, true
|
|
28
33
|
}
|
|
29
34
|
obj := node.AsObjectLiteralExpression()
|
|
30
35
|
if obj == nil || obj.Properties == nil {
|
|
31
|
-
return verbatim(ctx, node)
|
|
36
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
32
37
|
}
|
|
33
38
|
items := make([]Doc, 0, len(obj.Properties.Nodes))
|
|
39
|
+
covered := true
|
|
34
40
|
for _, prop := range obj.Properties.Nodes {
|
|
35
41
|
if prop == nil {
|
|
36
42
|
// A nil child entry would render as an empty Doc and surface
|
|
37
43
|
// as `a, , b` in the output. Bail to verbatim so the source
|
|
38
44
|
// bytes round-trip unchanged.
|
|
39
|
-
return verbatim(ctx, node)
|
|
45
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
40
46
|
}
|
|
41
|
-
doc,
|
|
47
|
+
doc, childCovered := PrintNode(ctx, prop)
|
|
48
|
+
covered = covered && childCovered
|
|
42
49
|
items = append(items, doc)
|
|
43
50
|
}
|
|
51
|
+
// objectWrap:"preserve" — keep a non-empty object expanded when the
|
|
52
|
+
// source wrote a newline after `{`. An empty literal has no first
|
|
53
|
+
// property to anchor the check and never preserves.
|
|
54
|
+
forceBreak := false
|
|
55
|
+
if len(obj.Properties.Nodes) > 0 {
|
|
56
|
+
forceBreak = objectHasNewlineAfterBrace(ctx.Source, node, obj.Properties.Nodes[0])
|
|
57
|
+
}
|
|
58
|
+
// AddComma honors `format.trailingComma`: object literals accept
|
|
59
|
+
// trailing commas in ES5 so both "all" and "es5" keep them; only
|
|
60
|
+
// "none" suppresses. Pairs with the call/array branches so the printer
|
|
61
|
+
// never disagrees with the trailing-comma rule on the same setting.
|
|
44
62
|
return printList(ctx, listShape{
|
|
45
|
-
OpenTok:
|
|
46
|
-
CloseTok:
|
|
47
|
-
Items:
|
|
48
|
-
Space:
|
|
49
|
-
AddComma:
|
|
50
|
-
|
|
63
|
+
OpenTok: "{",
|
|
64
|
+
CloseTok: "}",
|
|
65
|
+
Items: items,
|
|
66
|
+
Space: true,
|
|
67
|
+
AddComma: ctx.allowsEs5TrailingComma(),
|
|
68
|
+
ForceBreak: forceBreak,
|
|
69
|
+
}), covered
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// objectHasNewlineAfterBrace reports whether the source places a
|
|
73
|
+
// newline between the object literal's `{` and its first property.
|
|
74
|
+
//
|
|
75
|
+
// Prettier's objectWrap:"preserve" default keeps such an object
|
|
76
|
+
// expanded even when it would fit flat, treating the author's line
|
|
77
|
+
// break as intentional structure. format/print-width mirrors that:
|
|
78
|
+
// without it a deliberately multi-line object that happens to fit
|
|
79
|
+
// would be silently collapsed onto one line, which Prettier never
|
|
80
|
+
// does.
|
|
81
|
+
func objectHasNewlineAfterBrace(src string, node *shimast.Node, firstProp *shimast.Node) bool {
|
|
82
|
+
if node == nil || firstProp == nil {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
brace := shimscanner.SkipTrivia(src, node.Pos())
|
|
86
|
+
propStart := shimscanner.SkipTrivia(src, firstProp.Pos())
|
|
87
|
+
if brace < 0 || propStart <= brace || propStart > len(src) {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
for i := brace; i < propStart; i++ {
|
|
91
|
+
if src[i] == '\n' {
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false
|
|
51
96
|
}
|
package/linthost/rules_escape.go
CHANGED
|
@@ -52,12 +52,12 @@ func (noUselessEscape) Check(ctx *Context, node *shimast.Node) {
|
|
|
52
52
|
// single-char escape whitelist matches ESLint per-context.
|
|
53
53
|
switch node.Kind {
|
|
54
54
|
case shimast.KindStringLiteral:
|
|
55
|
-
reportStringEscapes(ctx, raw, pos, stringValidEscapes)
|
|
55
|
+
reportStringEscapes(ctx, raw, pos, stringValidEscapes, false)
|
|
56
56
|
case shimast.KindNoSubstitutionTemplateLiteral,
|
|
57
57
|
shimast.KindTemplateHead,
|
|
58
58
|
shimast.KindTemplateMiddle,
|
|
59
59
|
shimast.KindTemplateTail:
|
|
60
|
-
reportStringEscapes(ctx, raw, pos, templateValidEscapes)
|
|
60
|
+
reportStringEscapes(ctx, raw, pos, templateValidEscapes, true)
|
|
61
61
|
case shimast.KindRegularExpressionLiteral:
|
|
62
62
|
reportRegexEscapes(ctx, raw, pos)
|
|
63
63
|
}
|
|
@@ -79,7 +79,16 @@ const regexValidEscapes = "^$\\.*+?()[]{}|/-\n\r"
|
|
|
79
79
|
// translate to absolute file positions. The function issues an autofix
|
|
80
80
|
// (delete the backslash) for ASCII escapes; multi-byte sequences are
|
|
81
81
|
// reported without a fix to avoid corrupting UTF-8.
|
|
82
|
-
|
|
82
|
+
//
|
|
83
|
+
// `isTemplate` is true for `NoSubstitutionTemplateLiteral` and
|
|
84
|
+
// `TemplateHead`/`Middle`/`Tail` payloads. Inside a template, `\${` escapes
|
|
85
|
+
// the interpolation opener: stripping the backslash from `\${expr}` would
|
|
86
|
+
// either turn the literal text into an interpolation (corrupting the
|
|
87
|
+
// program) or — when the surrounding template already contains a real
|
|
88
|
+
// `${expr}` — produce TS syntax that no longer parses. The explicit guard
|
|
89
|
+
// here pins that exception so future tightening of `templateValidEscapes`
|
|
90
|
+
// cannot regress the corruption.
|
|
91
|
+
func reportStringEscapes(ctx *Context, raw string, base int, whitelist string, isTemplate bool) {
|
|
83
92
|
if len(raw) < 2 {
|
|
84
93
|
return
|
|
85
94
|
}
|
|
@@ -101,6 +110,14 @@ func reportStringEscapes(ctx *Context, raw string, base int, whitelist string) {
|
|
|
101
110
|
return
|
|
102
111
|
}
|
|
103
112
|
next := raw[i+1]
|
|
113
|
+
// Template-literal exception: `\${` escapes the interpolation
|
|
114
|
+
// opener. Without the backslash the next two bytes would either
|
|
115
|
+
// start an interpolation or trigger a parse error, so the escape is
|
|
116
|
+
// load-bearing even though `\$` looks redundant in isolation.
|
|
117
|
+
if isTemplate && next == '$' && i+2 < len(raw) && raw[i+2] == '{' {
|
|
118
|
+
i++ // consume the `$` so the `{` is not re-examined as a fresh char.
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
104
121
|
if isUselessStringEscape(next, whitelist) {
|
|
105
122
|
// Only emit a fix when both surrounding bytes are plain ASCII so
|
|
106
123
|
// deleting one byte cannot corrupt a multi-byte sequence.
|
|
@@ -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,24 @@ 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
|
+
|
|
259
511
|
// isReflowKind reports whether `k` is one of the node kinds the
|
|
260
512
|
// format/print-width rule visits. Kept in sync with Visits() so
|
|
261
513
|
// hasReflowAncestor does not need to call Visits() at runtime.
|
package/linthost/rules_gap.go
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
// Each rule is registered in init() at the bottom of the file.
|
|
9
9
|
package linthost
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import (
|
|
12
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
13
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
14
|
+
)
|
|
12
15
|
|
|
13
16
|
// no-empty-static-block: `class C { static {} }` has no effect.
|
|
14
17
|
// ESLint recommended: https://eslint.org/docs/latest/rules/no-empty-static-block
|
|
@@ -372,14 +375,63 @@ func (dotNotation) Visits() []shimast.Kind {
|
|
|
372
375
|
}
|
|
373
376
|
func (dotNotation) Check(ctx *Context, node *shimast.Node) {
|
|
374
377
|
access := node.AsElementAccessExpression()
|
|
375
|
-
if access == nil {
|
|
378
|
+
if access == nil || access.Expression == nil || access.ArgumentExpression == nil {
|
|
376
379
|
return
|
|
377
380
|
}
|
|
378
381
|
key := stringLiteralText(access.ArgumentExpression)
|
|
379
382
|
if key == "" || !isSimpleIdentifierName(key) {
|
|
380
383
|
return
|
|
381
384
|
}
|
|
382
|
-
|
|
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
|
|
383
435
|
}
|
|
384
436
|
|
|
385
437
|
// isSimpleIdentifierName reports whether value is a valid bare identifier
|