@ttsc/lint 0.25.0 → 0.26.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/README.md +1 -1
- package/lib/index.js +62 -2
- package/lib/index.js.map +1 -1
- package/lib/structures/rules/ITtscLintFunctionalRuleOptions.d.ts +73 -16
- package/linthost/config_format.go +7 -0
- package/linthost/directives.go +4 -7
- package/linthost/fix.go +6 -4
- package/linthost/rule_codes.json +1 -0
- package/linthost/rules_format_brace_continuation.go +267 -0
- package/linthost/rules_format_clause_join.go +289 -40
- package/linthost/rules_format_parameter_properties.go +9 -21
- package/linthost/rules_format_whitespace.go +5 -1
- package/linthost/rules_functional.go +248 -25
- package/linthost/rules_ts_extra.go +7 -5
- package/package.json +2 -2
- package/rule/rule.go +21 -9
- package/src/index.ts +70 -2
- package/src/structures/rules/ITtscLintFunctionalRuleOptions.ts +73 -16
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
7
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// formatBraceContinuation places a continuation keyword against the clause it
|
|
11
|
+
// continues, mirroring Prettier. The rule is one decision read in two
|
|
12
|
+
// directions, taken from the shape of the preceding clause:
|
|
13
|
+
//
|
|
14
|
+
// - the clause is a block, so the keyword shares its closing brace's line:
|
|
15
|
+
// `} else {`, `} catch (e) {`, `} finally {`, `} while (ready);`
|
|
16
|
+
// - the clause is not a block, so the keyword starts its own line:
|
|
17
|
+
// `if (a) x();` then `else y();`, and `do tick();` then `while (ready);`
|
|
18
|
+
//
|
|
19
|
+
// Both halves belong to one rule because they are the same decision. Splitting
|
|
20
|
+
// them across two owners lets a source satisfy neither, which is what the
|
|
21
|
+
// formatter did before: it never touched this boundary at all, so an
|
|
22
|
+
// Allman-braced file survived `ttsc format` unchanged and a one-line
|
|
23
|
+
// `if (a) x(); else y();` stayed on one line.
|
|
24
|
+
//
|
|
25
|
+
// The rule rewrites only the gap between the preceding clause's last byte and
|
|
26
|
+
// the continuation keyword's first byte. No structural format rule contends for
|
|
27
|
+
// it: `format/clause-join` rewrites the gap AFTER a header token,
|
|
28
|
+
// `format/statement-split` splits statement-list members, and `format/indent`
|
|
29
|
+
// visits statement-list members, closing-brace lines, and member headers.
|
|
30
|
+
// `format/whitespace`'s trailing-whitespace trim does land inside the gap when
|
|
31
|
+
// the clause line ends in spaces; the host drops one of the two findings and the
|
|
32
|
+
// survivor re-fires on the next cascade pass, so the file still converges.
|
|
33
|
+
//
|
|
34
|
+
// A comment in the gap makes the rule abstain rather than relocate it, the same
|
|
35
|
+
// abstention `format/clause-join` and `format/statement-split` apply. Idempotent
|
|
36
|
+
// in both directions: once the keyword sits where it belongs the gap already
|
|
37
|
+
// holds the target text and the rule emits nothing.
|
|
38
|
+
type formatBraceContinuation struct{ optionsRule }
|
|
39
|
+
|
|
40
|
+
// formatBraceContinuationOptions carries only the EOL setting. The push-down
|
|
41
|
+
// direction copies the owning statement's own indent verbatim rather than
|
|
42
|
+
// synthesizing one, so tabWidth and useTabs would decide nothing here and the
|
|
43
|
+
// rule takes the same trimmed option surface `format/whitespace` does. The JSON
|
|
44
|
+
// tag matches the `format` block key the config layer mirrors in (see
|
|
45
|
+
// expandFormatBlock).
|
|
46
|
+
type formatBraceContinuationOptions struct {
|
|
47
|
+
EndOfLine *string `json:"endOfLine"`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func (formatBraceContinuation) Name() string { return "format/brace-continuation" }
|
|
51
|
+
func (formatBraceContinuation) IsFormat() bool { return true }
|
|
52
|
+
|
|
53
|
+
func (formatBraceContinuation) Visits() []shimast.Kind {
|
|
54
|
+
return []shimast.Kind{
|
|
55
|
+
shimast.KindIfStatement,
|
|
56
|
+
shimast.KindTryStatement,
|
|
57
|
+
shimast.KindDoStatement,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// braceContinuation is one keyword placement: the clause that precedes it and
|
|
62
|
+
// the keyword that continues it.
|
|
63
|
+
type braceContinuation struct {
|
|
64
|
+
// previous is the clause the keyword continues. Its `End()` is the left edge
|
|
65
|
+
// of the gap, and whether it is a block decides the direction.
|
|
66
|
+
previous *shimast.Node
|
|
67
|
+
// keyword is the exact lexeme expected at the right edge of the gap. The
|
|
68
|
+
// following node cannot bound the scan: a catch clause and a finally block
|
|
69
|
+
// both start at the preceding clause's end, so their Pos() is the gap's left
|
|
70
|
+
// edge, not its right. Matching the lexeme instead is also what makes a
|
|
71
|
+
// comment in the gap detectable, since the first non-whitespace byte is then
|
|
72
|
+
// the comment rather than the keyword.
|
|
73
|
+
keyword string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func (formatBraceContinuation) Check(ctx *Context, node *shimast.Node) {
|
|
77
|
+
if ctx == nil || ctx.File == nil || node == nil {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
var opts formatBraceContinuationOptions
|
|
81
|
+
_ = ctx.DecodeOptions(&opts)
|
|
82
|
+
eol := "\n"
|
|
83
|
+
if opts.EndOfLine != nil && *opts.EndOfLine == "crlf" {
|
|
84
|
+
eol = "\r\n"
|
|
85
|
+
}
|
|
86
|
+
src := ctx.File.Text()
|
|
87
|
+
for _, continuation := range braceContinuations(node) {
|
|
88
|
+
placeBraceContinuation(ctx, src, node, continuation, eol)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// braceContinuations returns every continuation keyword `node` carries, paired
|
|
93
|
+
// with the clause it continues.
|
|
94
|
+
func braceContinuations(node *shimast.Node) []braceContinuation {
|
|
95
|
+
switch node.Kind {
|
|
96
|
+
case shimast.KindIfStatement:
|
|
97
|
+
stmt := node.AsIfStatement()
|
|
98
|
+
if stmt == nil || stmt.ElseStatement == nil || stmt.ThenStatement == nil {
|
|
99
|
+
return nil
|
|
100
|
+
}
|
|
101
|
+
return []braceContinuation{{previous: stmt.ThenStatement, keyword: "else"}}
|
|
102
|
+
case shimast.KindTryStatement:
|
|
103
|
+
stmt := node.AsTryStatement()
|
|
104
|
+
if stmt == nil || stmt.TryBlock == nil {
|
|
105
|
+
return nil
|
|
106
|
+
}
|
|
107
|
+
out := make([]braceContinuation, 0, 2)
|
|
108
|
+
previous := stmt.TryBlock.AsNode()
|
|
109
|
+
if stmt.CatchClause != nil {
|
|
110
|
+
out = append(out, braceContinuation{previous: previous, keyword: "catch"})
|
|
111
|
+
previous = stmt.CatchClause
|
|
112
|
+
}
|
|
113
|
+
if stmt.FinallyBlock != nil {
|
|
114
|
+
out = append(out, braceContinuation{previous: previous, keyword: "finally"})
|
|
115
|
+
}
|
|
116
|
+
return out
|
|
117
|
+
case shimast.KindDoStatement:
|
|
118
|
+
stmt := node.AsDoStatement()
|
|
119
|
+
if stmt == nil || stmt.Statement == nil || stmt.Expression == nil {
|
|
120
|
+
return nil
|
|
121
|
+
}
|
|
122
|
+
return []braceContinuation{{previous: stmt.Statement, keyword: "while"}}
|
|
123
|
+
}
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func placeBraceContinuation(
|
|
128
|
+
ctx *Context,
|
|
129
|
+
src string,
|
|
130
|
+
node *shimast.Node,
|
|
131
|
+
continuation braceContinuation,
|
|
132
|
+
eol string,
|
|
133
|
+
) {
|
|
134
|
+
previous := continuation.previous
|
|
135
|
+
if previous == nil {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
gapStart := previous.End()
|
|
139
|
+
if gapStart < 0 || gapStart > len(src) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
// The gap is the whitespace run after the preceding clause. The bytes that
|
|
143
|
+
// follow it must be the continuation keyword itself: a comment there is
|
|
144
|
+
// content the rewrite would delete, and it shows up as a first non-whitespace
|
|
145
|
+
// byte that is not the keyword.
|
|
146
|
+
keywordStart := gapStart
|
|
147
|
+
for keywordStart < len(src) && isBraceContinuationGapByte(src[keywordStart]) {
|
|
148
|
+
keywordStart++
|
|
149
|
+
}
|
|
150
|
+
if !braceContinuationKeywordAt(src, keywordStart, continuation.keyword) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
want := " "
|
|
155
|
+
if !braceContinuationEndsInBlock(previous) {
|
|
156
|
+
// A non-block clause pushes the keyword onto its own line, indented to the
|
|
157
|
+
// statement that owns it. The whole gap is the target, so a keyword already
|
|
158
|
+
// on its own line at the wrong column is corrected by the same comparison
|
|
159
|
+
// instead of being ceded to a rule that never visits that line.
|
|
160
|
+
indent, ok := braceContinuationIndent(src, node)
|
|
161
|
+
if !ok {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
want = eol + indent
|
|
165
|
+
}
|
|
166
|
+
if src[gapStart:keywordStart] == want {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
ctx.ReportRangeFix(
|
|
170
|
+
gapStart,
|
|
171
|
+
keywordStart,
|
|
172
|
+
"Continuation keyword should sit against the clause it continues.",
|
|
173
|
+
TextEdit{Pos: gapStart, End: keywordStart, Text: want},
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// braceContinuationIndent returns the leading whitespace of the line the
|
|
178
|
+
// statement starts on, which is the column Prettier gives the pushed-down
|
|
179
|
+
// keyword, and reports whether that column is knowable yet.
|
|
180
|
+
//
|
|
181
|
+
// A statement sharing its line with something else has no column of its own,
|
|
182
|
+
// and nothing would repair a keyword pushed to column zero: `format/indent`
|
|
183
|
+
// visits statement-list members, closing-brace lines, and member headers, and a
|
|
184
|
+
// continuation-keyword line is none of the three. So the rule abstains, lets
|
|
185
|
+
// `format/statement-split` give the statement its own line first, and reads the
|
|
186
|
+
// real column on the next cascade pass. `format/indent` takes the same
|
|
187
|
+
// not-first-on-its-line abstention for the same reason.
|
|
188
|
+
func braceContinuationIndent(src string, node *shimast.Node) (string, bool) {
|
|
189
|
+
start := shimscanner.SkipTrivia(src, node.Pos())
|
|
190
|
+
if start < 0 || start > len(src) {
|
|
191
|
+
return "", false
|
|
192
|
+
}
|
|
193
|
+
lineStart := lineStartOffset(src, start)
|
|
194
|
+
indentEnd := lineStart
|
|
195
|
+
for indentEnd < start && (src[indentEnd] == ' ' || src[indentEnd] == '\t') {
|
|
196
|
+
indentEnd++
|
|
197
|
+
}
|
|
198
|
+
// A label prefix is part of the statement's own line rather than another
|
|
199
|
+
// statement sharing it, and nothing downstream would ever give a labeled body
|
|
200
|
+
// its own line, so refusing here would strand the keyword permanently.
|
|
201
|
+
if !isBraceContinuationLabelPrefix(src[indentEnd:start]) {
|
|
202
|
+
return "", false
|
|
203
|
+
}
|
|
204
|
+
return src[lineStart:indentEnd], true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// isBraceContinuationLabelPrefix reports whether `prefix` is empty or a run of
|
|
208
|
+
// `identifier :` labels, the only thing allowed to precede a statement on the
|
|
209
|
+
// line whose indent the pushed-down keyword adopts.
|
|
210
|
+
func isBraceContinuationLabelPrefix(prefix string) bool {
|
|
211
|
+
for {
|
|
212
|
+
prefix = strings.TrimLeft(prefix, " \t")
|
|
213
|
+
if prefix == "" {
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
name := 0
|
|
217
|
+
for name < len(prefix) && isBraceContinuationIdentifierByte(prefix[name]) {
|
|
218
|
+
name++
|
|
219
|
+
}
|
|
220
|
+
if name == 0 {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
rest := strings.TrimLeft(prefix[name:], " \t")
|
|
224
|
+
if !strings.HasPrefix(rest, ":") {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
prefix = rest[1:]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// braceContinuationEndsInBlock reports whether the keyword shares the preceding
|
|
232
|
+
// clause's last line, which is what decides the direction. The predicate is the
|
|
233
|
+
// clause's kind, not its last byte: a `switch` consequent and an Annex-B
|
|
234
|
+
// `function` consequent both end in `}` and Prettier still pushes `else` onto
|
|
235
|
+
// its own line after them. A catch clause is the one non-block kind that shares
|
|
236
|
+
// its line, because the try printer always spaces `} finally`.
|
|
237
|
+
func braceContinuationEndsInBlock(previous *shimast.Node) bool {
|
|
238
|
+
return previous.Kind == shimast.KindBlock || previous.Kind == shimast.KindCatchClause
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// braceContinuationKeywordAt reports whether `keyword` starts at `offset` as a
|
|
242
|
+
// whole token. The trailing boundary matters: `finally` must not match the
|
|
243
|
+
// `final` prefix of an identifier a comment left exposed.
|
|
244
|
+
func braceContinuationKeywordAt(src string, offset int, keyword string) bool {
|
|
245
|
+
end := offset + len(keyword)
|
|
246
|
+
if offset < 0 || end > len(src) || src[offset:end] != keyword {
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
return end == len(src) || !isBraceContinuationIdentifierByte(src[end])
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// isBraceContinuationIdentifierByte reports whether `c` can appear inside an
|
|
253
|
+
// identifier, so a keyword is only accepted as a whole token.
|
|
254
|
+
func isBraceContinuationIdentifierByte(c byte) bool {
|
|
255
|
+
return c == 0x5f || c == 0x24 ||
|
|
256
|
+
(c >= 0x61 && c <= 0x7a) || (c >= 0x41 && c <= 0x5a) || (c >= 0x30 && c <= 0x39)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// isBraceContinuationGapByte reports whether `c` is whitespace that may appear
|
|
260
|
+
// between a clause and the keyword continuing it.
|
|
261
|
+
func isBraceContinuationGapByte(c byte) bool {
|
|
262
|
+
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func init() {
|
|
266
|
+
Register(formatBraceContinuation{})
|
|
267
|
+
}
|
|
@@ -13,16 +13,38 @@ import (
|
|
|
13
13
|
// if (a)
|
|
14
14
|
// b();
|
|
15
15
|
//
|
|
16
|
-
// becomes `if (a) b();` when the joined line fits printWidth. The
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// becomes `if (a) b();` when the joined line fits printWidth. The covered set
|
|
17
|
+
// is Prettier's, not the set whose header happens to end in `)`: the `if`
|
|
18
|
+
// consequent and alternate, the `for`, `for-in`, `for-of`, and `while` bodies,
|
|
19
|
+
// the `do` body, the `with` body, and a labeled statement's body. A braced
|
|
20
|
+
// body, a body that already shares the header line, a multi-line body, or a
|
|
21
|
+
// join that would overflow printWidth is left untouched.
|
|
20
22
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// and the
|
|
23
|
+
// Two clauses join unconditionally because Prettier gives them no group of
|
|
24
|
+
// their own: an `else` whose alternate is another `if` (the `else if` chain),
|
|
25
|
+
// and a labeled statement, which prints `label: statement` on one line whatever
|
|
26
|
+
// the statement is, braces included. Those are the only clauses that can hoist a
|
|
27
|
+
// multi-line body, and their continuation lines move by the same column delta
|
|
28
|
+
// their first line travels, so the join does not settle on output Prettier would
|
|
29
|
+
// still reindent.
|
|
30
|
+
//
|
|
31
|
+
// A continuation line whose newline sits inside a string, a template, or a
|
|
32
|
+
// block comment is skipped rather than shifted: those bytes are content, and
|
|
33
|
+
// moving them would change what the program prints or what a comment says. The
|
|
34
|
+
// join itself still happens, because abandoning it leaves `format/indent` to
|
|
35
|
+
// move the body's interior anyway and settles the file on a hybrid layout
|
|
36
|
+
// Prettier never emits.
|
|
37
|
+
//
|
|
38
|
+
// The rule rewrites the whitespace gap between the clause's own header token
|
|
39
|
+
// and the controlled statement, and, when it hoists a multi-line body, the
|
|
40
|
+
// leading whitespace of that body's continuation lines. That surface overlaps
|
|
41
|
+
// `format/indent`, which owns the same column for a hoisted body that contains
|
|
42
|
+
// a block and cedes it only for a braceless control-flow body. The two agree on
|
|
43
|
+
// the target column, so a collision costs a dropped finding and a re-fire rather
|
|
44
|
+
// than a fight. Hoisting also contends with a nested join over the same bytes,
|
|
45
|
+
// so a staircase settles roughly one level per cascade pass. Idempotent: once
|
|
46
|
+
// joined the gap holds no newline, the shift compares against the text it would
|
|
47
|
+
// write, and the rule abstains.
|
|
26
48
|
type formatClauseJoin struct{ optionsRule }
|
|
27
49
|
|
|
28
50
|
// formatClauseJoinOptions mirrors the printWidth/indent keys the rule
|
|
@@ -46,6 +68,9 @@ func (formatClauseJoin) Visits() []shimast.Kind {
|
|
|
46
68
|
shimast.KindForStatement,
|
|
47
69
|
shimast.KindForInStatement,
|
|
48
70
|
shimast.KindForOfStatement,
|
|
71
|
+
shimast.KindDoStatement,
|
|
72
|
+
shimast.KindWithStatement,
|
|
73
|
+
shimast.KindLabeledStatement,
|
|
49
74
|
}
|
|
50
75
|
}
|
|
51
76
|
|
|
@@ -63,24 +88,67 @@ func (formatClauseJoin) Check(ctx *Context, node *shimast.Node) {
|
|
|
63
88
|
if opts.TabWidth != nil && *opts.TabWidth > 0 {
|
|
64
89
|
tabWidth = *opts.TabWidth
|
|
65
90
|
}
|
|
66
|
-
|
|
91
|
+
src := ctx.File.Text()
|
|
92
|
+
for _, target := range clauseControlledBodies(node) {
|
|
93
|
+
joinClauseBody(ctx, src, node, target, printWidth, tabWidth)
|
|
94
|
+
}
|
|
67
95
|
}
|
|
68
96
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// the
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
97
|
+
// clauseJoinTarget is one joinable clause body plus the token that ends the
|
|
98
|
+
// clause header immediately before it. The anchor is what generalizes the rule
|
|
99
|
+
// past the `)`-headed statements: `else` and `do` end in a keyword, a labeled
|
|
100
|
+
// statement ends in `:`, and requiring the exact token still keeps a comment
|
|
101
|
+
// between header and body from being swallowed.
|
|
102
|
+
type clauseJoinTarget struct {
|
|
103
|
+
body *shimast.Node
|
|
104
|
+
anchor string
|
|
105
|
+
// alwaysJoin marks a clause Prettier keeps on the header line regardless of
|
|
106
|
+
// the body's own line count or the joined width: an `else if` chain and a
|
|
107
|
+
// labeled statement.
|
|
108
|
+
alwaysJoin bool
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// clauseControlledBodies returns every clause body of `node` that Prettier
|
|
112
|
+
// keeps on its header line, paired with the token that header ends in. The set
|
|
113
|
+
// is Prettier's, not the AST's: the `if` consequent and alternate, the loop
|
|
114
|
+
// bodies, the `do` body, the `with` body, and a labeled statement's body.
|
|
115
|
+
func clauseControlledBodies(node *shimast.Node) []clauseJoinTarget {
|
|
75
116
|
switch node.Kind {
|
|
76
117
|
case shimast.KindIfStatement:
|
|
77
|
-
|
|
118
|
+
stmt := node.AsIfStatement()
|
|
119
|
+
if stmt == nil {
|
|
120
|
+
return nil
|
|
121
|
+
}
|
|
122
|
+
targets := []clauseJoinTarget{{body: stmt.ThenStatement, anchor: ")"}}
|
|
123
|
+
if stmt.ElseStatement != nil {
|
|
124
|
+
targets = append(targets, clauseJoinTarget{
|
|
125
|
+
body: stmt.ElseStatement,
|
|
126
|
+
anchor: "else",
|
|
127
|
+
// Prettier prints an `else if` chain flat, so the alternate being an
|
|
128
|
+
// `if` joins even when that nested statement spans lines.
|
|
129
|
+
alwaysJoin: stmt.ElseStatement.Kind == shimast.KindIfStatement,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
return targets
|
|
78
133
|
case shimast.KindWhileStatement:
|
|
79
|
-
return node.AsWhileStatement().Statement
|
|
134
|
+
return []clauseJoinTarget{{body: node.AsWhileStatement().Statement, anchor: ")"}}
|
|
80
135
|
case shimast.KindForStatement:
|
|
81
|
-
return node.AsForStatement().Statement
|
|
136
|
+
return []clauseJoinTarget{{body: node.AsForStatement().Statement, anchor: ")"}}
|
|
82
137
|
case shimast.KindForInStatement, shimast.KindForOfStatement:
|
|
83
|
-
return node.AsForInOrOfStatement().Statement
|
|
138
|
+
return []clauseJoinTarget{{body: node.AsForInOrOfStatement().Statement, anchor: ")"}}
|
|
139
|
+
case shimast.KindDoStatement:
|
|
140
|
+
return []clauseJoinTarget{{body: node.AsDoStatement().Statement, anchor: "do"}}
|
|
141
|
+
case shimast.KindWithStatement:
|
|
142
|
+
return []clauseJoinTarget{{body: node.AsWithStatement().Statement, anchor: ")"}}
|
|
143
|
+
case shimast.KindLabeledStatement:
|
|
144
|
+
// A label carries no group of its own in Prettier: `label: statement` is
|
|
145
|
+
// one line whatever the statement is, so the body may be a block and may
|
|
146
|
+
// span lines.
|
|
147
|
+
return []clauseJoinTarget{{
|
|
148
|
+
body: node.AsLabeledStatement().Statement,
|
|
149
|
+
anchor: ":",
|
|
150
|
+
alwaysJoin: true,
|
|
151
|
+
}}
|
|
84
152
|
}
|
|
85
153
|
return nil
|
|
86
154
|
}
|
|
@@ -89,18 +157,25 @@ func joinClauseBody(
|
|
|
89
157
|
ctx *Context,
|
|
90
158
|
src string,
|
|
91
159
|
node *shimast.Node,
|
|
92
|
-
|
|
160
|
+
target clauseJoinTarget,
|
|
93
161
|
printWidth int,
|
|
94
162
|
tabWidth int,
|
|
95
163
|
) {
|
|
96
|
-
|
|
164
|
+
body := target.body
|
|
165
|
+
if body == nil {
|
|
97
166
|
return
|
|
98
167
|
}
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
168
|
+
// A braced body already owns its own line layout, except behind a label,
|
|
169
|
+
// where Prettier writes `label: {`.
|
|
170
|
+
if body.Kind == shimast.KindBlock && !target.alwaysJoin {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
// An empty-statement body glues directly to the header with NO space:
|
|
174
|
+
// Prettier's adjustClause special-cases EmptyStatement and returns the bare
|
|
175
|
+
// `;` (`while (x);`, `else;`, `do;`, `label:;`), only prepending a space when
|
|
176
|
+
// the empty statement carries a leading comment. This rule joins with a
|
|
177
|
+
// single space and deliberately does not take on the spaceless variant, so it
|
|
178
|
+
// abstains and leaves the source shape rather than emitting `while (x) ;`.
|
|
104
179
|
if body.Kind == shimast.KindEmptyStatement {
|
|
105
180
|
return
|
|
106
181
|
}
|
|
@@ -110,24 +185,29 @@ func joinClauseBody(
|
|
|
110
185
|
return
|
|
111
186
|
}
|
|
112
187
|
// The gap is the whitespace run immediately before the body. Walk back
|
|
113
|
-
// over horizontal whitespace and newlines; the
|
|
114
|
-
//
|
|
188
|
+
// over horizontal whitespace and newlines; the bytes before it must be the
|
|
189
|
+
// clause's own header token so a comment between header and body (which
|
|
115
190
|
// SkipTrivia would have stepped over) can never be swallowed.
|
|
116
191
|
gapStart := bodyStart
|
|
117
192
|
for gapStart > 0 && isClauseGapByte(src[gapStart-1]) {
|
|
118
193
|
gapStart--
|
|
119
194
|
}
|
|
120
|
-
|
|
195
|
+
anchorStart := gapStart - len(target.anchor)
|
|
196
|
+
if anchorStart < 0 || src[anchorStart:gapStart] != target.anchor {
|
|
121
197
|
return
|
|
122
198
|
}
|
|
123
199
|
gap := src[gapStart:bodyStart]
|
|
124
200
|
if !strings.Contains(gap, "\n") {
|
|
125
201
|
return // body already shares the header line
|
|
126
202
|
}
|
|
127
|
-
// The header
|
|
128
|
-
//
|
|
129
|
-
//
|
|
203
|
+
// The header line is measured from the token that opens the clause. For a
|
|
204
|
+
// `)`-headed statement and a label that is the statement's own start; for
|
|
205
|
+
// `else` and `do` it is the keyword, which is where Prettier starts the line
|
|
206
|
+
// it would print.
|
|
130
207
|
headerStart := shimscanner.SkipTrivia(src, node.Pos())
|
|
208
|
+
if isClauseAnchorWord(target.anchor) {
|
|
209
|
+
headerStart = anchorStart
|
|
210
|
+
}
|
|
131
211
|
if headerStart < 0 || headerStart > gapStart {
|
|
132
212
|
return
|
|
133
213
|
}
|
|
@@ -135,28 +215,197 @@ func joinClauseBody(
|
|
|
135
215
|
if strings.ContainsRune(src[headerLineStart:gapStart], '\n') {
|
|
136
216
|
return
|
|
137
217
|
}
|
|
138
|
-
if
|
|
139
|
-
|
|
218
|
+
if !target.alwaysJoin {
|
|
219
|
+
// The body must be single-line; a multi-line body (e.g. a nested clause
|
|
220
|
+
// not yet joined) waits for the cascade to settle its inner join first.
|
|
221
|
+
if strings.ContainsRune(src[bodyStart:bodyEnd], '\n') {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
joined := visualWidth(src[headerLineStart:gapStart], tabWidth) + 1 +
|
|
225
|
+
visualWidth(src[bodyStart:bodyEnd], tabWidth)
|
|
226
|
+
if joined > printWidth {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
140
229
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
230
|
+
edits := []TextEdit{{Pos: gapStart, End: bodyStart, Text: " "}}
|
|
231
|
+
// Hoisting the body's first line moves its base column. Every continuation
|
|
232
|
+
// line has to move with it, or the join settles on output Prettier would
|
|
233
|
+
// still reindent (#1139). Only an alwaysJoin clause can reach a multi-line
|
|
234
|
+
// body; the others already abstained above.
|
|
235
|
+
if strings.ContainsRune(src[bodyStart:bodyEnd], '\n') {
|
|
236
|
+
edits = append(edits, reindentJoinedClauseBody(
|
|
237
|
+
ctx, src, headerLineStart, bodyStart, bodyEnd, loadFormatLayout(ctx),
|
|
238
|
+
)...)
|
|
145
239
|
}
|
|
146
240
|
ctx.ReportRangeFix(
|
|
147
241
|
gapStart,
|
|
148
242
|
bodyStart,
|
|
149
243
|
"Single-statement clause body should join its header line.",
|
|
150
|
-
|
|
244
|
+
edits...,
|
|
151
245
|
)
|
|
152
246
|
}
|
|
153
247
|
|
|
248
|
+
// reindentJoinedClauseBody returns the edits that move a hoisted body's
|
|
249
|
+
// continuation lines by the same column delta its first line travels.
|
|
250
|
+
//
|
|
251
|
+
// The delta is the header line's own indent minus the indent of the line the
|
|
252
|
+
// body currently starts on, because the body's first line lands on the header
|
|
253
|
+
// line and Prettier prints its continuations relative to that. Measuring from
|
|
254
|
+
// the anchor token instead would charge the width of a `} ` prefix and shift
|
|
255
|
+
// every continuation line two columns too far.
|
|
256
|
+
//
|
|
257
|
+
// A line the shift must not touch is skipped, not made to abandon the join.
|
|
258
|
+
// Abandoning is strictly worse: `format/indent` still moves the body's interior
|
|
259
|
+
// to its post-join column, so the file settles on a hybrid layout Prettier never
|
|
260
|
+
// emits, which is the very property this shift exists to remove. Three kinds of
|
|
261
|
+
// line are skipped: one whose newline sits inside a string, template, or block
|
|
262
|
+
// comment, where the bytes are content rather than layout; a blank one, which
|
|
263
|
+
// has no column; and one whose indent is shorter than an outdent needs.
|
|
264
|
+
func reindentJoinedClauseBody(
|
|
265
|
+
ctx *Context,
|
|
266
|
+
src string,
|
|
267
|
+
headerLineStart int,
|
|
268
|
+
bodyStart int,
|
|
269
|
+
bodyEnd int,
|
|
270
|
+
layout formatLayout,
|
|
271
|
+
) []TextEdit {
|
|
272
|
+
protected := collectClauseJoinProtectedRanges(ctx.File, src)
|
|
273
|
+
headerIndentEnd := headerLineStart
|
|
274
|
+
for headerIndentEnd < len(src) &&
|
|
275
|
+
(src[headerIndentEnd] == ' ' || src[headerIndentEnd] == '\t') {
|
|
276
|
+
headerIndentEnd++
|
|
277
|
+
}
|
|
278
|
+
bodyLineStart := lineStartOffset(src, bodyStart)
|
|
279
|
+
delta := visualWidth(src[headerLineStart:headerIndentEnd], layout.tabWidth) -
|
|
280
|
+
visualWidth(src[bodyLineStart:bodyStart], layout.tabWidth)
|
|
281
|
+
if delta == 0 {
|
|
282
|
+
return nil
|
|
283
|
+
}
|
|
284
|
+
var edits []TextEdit
|
|
285
|
+
for offset := bodyStart; offset < bodyEnd; offset++ {
|
|
286
|
+
if src[offset] != '\n' {
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
if inTemplate(protected, offset) {
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
lineStart := offset + 1
|
|
293
|
+
indentEnd := lineStart
|
|
294
|
+
for indentEnd < bodyEnd && (src[indentEnd] == ' ' || src[indentEnd] == '\t') {
|
|
295
|
+
indentEnd++
|
|
296
|
+
}
|
|
297
|
+
if indentEnd >= bodyEnd || src[indentEnd] == '\n' || src[indentEnd] == '\r' {
|
|
298
|
+
continue // a blank line carries no column
|
|
299
|
+
}
|
|
300
|
+
width := visualWidth(src[lineStart:indentEnd], layout.tabWidth) + delta
|
|
301
|
+
if width < 0 {
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
next := clauseJoinIndentOfWidth(layout, width)
|
|
305
|
+
if src[lineStart:indentEnd] == next {
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
edits = append(edits, TextEdit{Pos: lineStart, End: indentEnd, Text: next})
|
|
309
|
+
}
|
|
310
|
+
return edits
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// clauseJoinIndentOfWidth renders a column count in the project's own
|
|
314
|
+
// indentation unit, so a tab-indented file keeps its tabs instead of being
|
|
315
|
+
// silently respaced by the shift.
|
|
316
|
+
func clauseJoinIndentOfWidth(layout formatLayout, width int) string {
|
|
317
|
+
if width <= 0 {
|
|
318
|
+
return ""
|
|
319
|
+
}
|
|
320
|
+
if layout.useTabs && layout.tabWidth > 0 {
|
|
321
|
+
// A width that is not a whole number of tabs keeps the remainder in spaces
|
|
322
|
+
// rather than dropping every tab, which is the respacing this render exists
|
|
323
|
+
// to prevent.
|
|
324
|
+
return layout.indent(width/layout.tabWidth) +
|
|
325
|
+
strings.Repeat(" ", width%layout.tabWidth)
|
|
326
|
+
}
|
|
327
|
+
return strings.Repeat(" ", width)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// collectClauseJoinProtectedRanges returns every byte span whose interior
|
|
331
|
+
// newlines carry content rather than layout: template literals, string literals
|
|
332
|
+
// spanning a line continuation, and block comments. Shifting a line inside one
|
|
333
|
+
// changes what the program prints or what a comment says.
|
|
334
|
+
func collectClauseJoinProtectedRanges(file *shimast.SourceFile, src string) []byteRange {
|
|
335
|
+
ranges := collectTemplateRanges(file, src)
|
|
336
|
+
var walk func(node *shimast.Node)
|
|
337
|
+
walk = func(node *shimast.Node) {
|
|
338
|
+
if node == nil {
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
if node.Kind == shimast.KindStringLiteral {
|
|
342
|
+
pos := shimscanner.SkipTrivia(src, node.Pos())
|
|
343
|
+
end := node.End()
|
|
344
|
+
if pos >= 0 && end <= len(src) && end > pos {
|
|
345
|
+
ranges = append(ranges, byteRange{pos: pos, end: end})
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
349
|
+
walk(child)
|
|
350
|
+
return false
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
if file != nil && file.Statements != nil {
|
|
354
|
+
for _, stmt := range file.Statements.Nodes {
|
|
355
|
+
walk(stmt)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
forEachCommentToken(file, func(kind shimast.Kind, start, end int) {
|
|
359
|
+
if kind != shimast.KindMultiLineCommentTrivia {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
// Only a comment Prettier reprints verbatim is content. An indentable one,
|
|
363
|
+
// every continuation line starting with `*`, is realigned by Prettier to the
|
|
364
|
+
// current indentation, so protecting it would leave a well-formed JSDoc
|
|
365
|
+
// block misaligned behind a hoisted body.
|
|
366
|
+
if isIndentableBlockComment(src, start, end) {
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
ranges = append(ranges, byteRange{pos: start, end: end})
|
|
370
|
+
})
|
|
371
|
+
return ranges
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// isIndentableBlockComment reports whether every continuation line of a block
|
|
375
|
+
// comment begins with `*`, which is the shape Prettier reindents rather than
|
|
376
|
+
// reproducing byte for byte.
|
|
377
|
+
func isIndentableBlockComment(src string, start, end int) bool {
|
|
378
|
+
if start < 0 || end > len(src) || end <= start {
|
|
379
|
+
return false
|
|
380
|
+
}
|
|
381
|
+
body := src[start:end]
|
|
382
|
+
if !strings.Contains(body, "\n") {
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
for _, line := range strings.Split(body, "\n")[1:] {
|
|
386
|
+
trimmed := strings.TrimLeft(line, " \t\r")
|
|
387
|
+
if !strings.HasPrefix(trimmed, "*") {
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return true
|
|
392
|
+
}
|
|
393
|
+
|
|
154
394
|
// isClauseGapByte reports whether `c` is whitespace that may appear in
|
|
155
395
|
// the gap between a clause header and its controlled statement.
|
|
156
396
|
func isClauseGapByte(c byte) bool {
|
|
157
397
|
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
|
|
158
398
|
}
|
|
159
399
|
|
|
400
|
+
// isClauseAnchorWord reports whether an anchor is a keyword rather than
|
|
401
|
+
// punctuation, which decides where the width budget starts measuring. The
|
|
402
|
+
// anchor needs no identifier-boundary check: it is read backward from the AST
|
|
403
|
+
// body's own position, so the token before the gap is the clause's real keyword
|
|
404
|
+
// and an identifier ending in `else` or `do` cannot reach it.
|
|
405
|
+
func isClauseAnchorWord(anchor string) bool {
|
|
406
|
+
return anchor == "else" || anchor == "do"
|
|
407
|
+
}
|
|
408
|
+
|
|
160
409
|
// visualWidth returns the display-column width of `s`: a tab expands to a flat
|
|
161
410
|
// `tabWidth` columns and everything else is charged by displayWidth, which is
|
|
162
411
|
// Prettier's own measurement. The only approximation left is the flat tab
|