@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
|
@@ -0,0 +1,353 @@
|
|
|
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
|
+
// Function-shaped and statement-body node printers: arrow functions,
|
|
11
|
+
// function expressions, parenthesized expressions, block statements,
|
|
12
|
+
// and the two leaf statements a callback body is almost always made of
|
|
13
|
+
// — expression statements and return statements.
|
|
14
|
+
//
|
|
15
|
+
// These printers exist for one job: make callback-bearing code
|
|
16
|
+
// (`new Singleton(() => { … })`, `foo(x, function () { … })`) reflow
|
|
17
|
+
// with *consistent* indentation. The headline bug they fix is the
|
|
18
|
+
// verbatim-column hazard — an un-handled multi-line node keeps the
|
|
19
|
+
// source columns its lines were written at, so when the enclosing call
|
|
20
|
+
// re-indents, the callback header and its body drift apart.
|
|
21
|
+
//
|
|
22
|
+
// Coverage discipline. The signature (parameters, `=>` / `function`
|
|
23
|
+
// keyword, return type) is emitted verbatim because it almost never
|
|
24
|
+
// contains a newline and is not a reflow target. The *body* is where
|
|
25
|
+
// the newlines live, so the body is dispatched through PrintNode and
|
|
26
|
+
// re-indented by the Doc engine. Each printer ANDs the body's `covered`
|
|
27
|
+
// flag into its own result: a body the dispatcher cannot fully control
|
|
28
|
+
// taints the whole subtree and the format/print-width rule abstains.
|
|
29
|
+
//
|
|
30
|
+
// The expression- and return-statement printers carry the same
|
|
31
|
+
// discipline one level deeper: a callback body whose statements are
|
|
32
|
+
// themselves calls (`outer(() => { inner(() => { … }); })`) only
|
|
33
|
+
// reflows when those statements dispatch to a real printer. Without
|
|
34
|
+
// them every multi-line statement would be verbatim and a nested
|
|
35
|
+
// callback would always abstain.
|
|
36
|
+
|
|
37
|
+
// printArrowFunction renders an arrow function. The portion before the
|
|
38
|
+
// body — parameters, optional return type, `=>` token — is emitted
|
|
39
|
+
// verbatim; only the body participates in reflow.
|
|
40
|
+
//
|
|
41
|
+
// verbatim prefix reflowed body
|
|
42
|
+
// ┌──────────────┐ ┌──────────┐
|
|
43
|
+
// (a, b): number => { return a; }
|
|
44
|
+
//
|
|
45
|
+
// A concise (expression) body is dispatched directly. A block body
|
|
46
|
+
// flows through printBlock, which re-indents its statements relative
|
|
47
|
+
// to the printer's current indent.
|
|
48
|
+
//
|
|
49
|
+
// The second return value is the `covered` flag: see PrintNode. The
|
|
50
|
+
// verbatim prefix is single-line in every realistic arrow, but a
|
|
51
|
+
// pathological multi-line parameter list would still taint coverage.
|
|
52
|
+
func printArrowFunction(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
53
|
+
if node == nil {
|
|
54
|
+
return Doc{}, true
|
|
55
|
+
}
|
|
56
|
+
arrow := node.AsArrowFunction()
|
|
57
|
+
if arrow == nil || arrow.Body == nil {
|
|
58
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
59
|
+
}
|
|
60
|
+
return printFunctionLike(ctx, node, arrow.Body)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// printFunctionExpression renders a `function` expression. Like the
|
|
64
|
+
// arrow printer, the signature is verbatim and only the body reflows.
|
|
65
|
+
// Function expressions always carry a block body, so the body always
|
|
66
|
+
// flows through printBlock.
|
|
67
|
+
//
|
|
68
|
+
// The second return value is the `covered` flag: see PrintNode.
|
|
69
|
+
func printFunctionExpression(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
70
|
+
if node == nil {
|
|
71
|
+
return Doc{}, true
|
|
72
|
+
}
|
|
73
|
+
fn := node.AsFunctionExpression()
|
|
74
|
+
if fn == nil || fn.Body == nil {
|
|
75
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
76
|
+
}
|
|
77
|
+
return printFunctionLike(ctx, node, fn.Body)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// printFunctionLike is the shared body of the arrow / function-expression
|
|
81
|
+
// printers. It slices the verbatim signature from the node's first byte
|
|
82
|
+
// up to the body's first byte, dispatches the body, and concatenates
|
|
83
|
+
// the two.
|
|
84
|
+
//
|
|
85
|
+
// The signature slice intentionally includes the whitespace between the
|
|
86
|
+
// signature and the body (`=> ` keeps its trailing space, `) ` before a
|
|
87
|
+
// `function` body keeps its space) so the flat form reads naturally and
|
|
88
|
+
// the body's open brace lands where the user put it.
|
|
89
|
+
func printFunctionLike(ctx *PrintContext, node, body *shimast.Node) (Doc, bool) {
|
|
90
|
+
nodeStart := shimscanner.SkipTrivia(ctx.Source, node.Pos())
|
|
91
|
+
bodyStart := shimscanner.SkipTrivia(ctx.Source, body.Pos())
|
|
92
|
+
if nodeStart < 0 || bodyStart < nodeStart || bodyStart > len(ctx.Source) {
|
|
93
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
94
|
+
}
|
|
95
|
+
prefix := verbatimRange(ctx.Source, nodeStart, bodyStart)
|
|
96
|
+
// A signature that itself spans multiple lines is a verbatim slice
|
|
97
|
+
// with frozen interior columns — taint coverage so the rule abstains.
|
|
98
|
+
prefixCovered := !strings.Contains(ctx.Source[nodeStart:bodyStart], "\n")
|
|
99
|
+
bodyDoc, bodyCovered := PrintNode(ctx, body)
|
|
100
|
+
return Concat(prefix, bodyDoc), prefixCovered && bodyCovered
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// printParenthesizedExpression renders `( expr )`. The parentheses are
|
|
104
|
+
// fixed punctuation; the inner expression is dispatched so a call or
|
|
105
|
+
// object literal wrapped in parens still reflows.
|
|
106
|
+
//
|
|
107
|
+
// The second return value is the `covered` flag: see PrintNode.
|
|
108
|
+
func printParenthesizedExpression(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
109
|
+
if node == nil {
|
|
110
|
+
return Doc{}, true
|
|
111
|
+
}
|
|
112
|
+
paren := node.AsParenthesizedExpression()
|
|
113
|
+
if paren == nil || paren.Expression == nil {
|
|
114
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
115
|
+
}
|
|
116
|
+
inner, covered := PrintNode(ctx, paren.Expression)
|
|
117
|
+
return Concat(Text("("), inner, Text(")")), covered
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// printBlock renders a `{ … }` block statement. A statement-free block
|
|
121
|
+
// collapses to `{}`. A non-empty block always renders multi-line —
|
|
122
|
+
// matching every JavaScript formatter — with each statement on its own
|
|
123
|
+
// line, indented one unit from the block's base indent and the closing
|
|
124
|
+
// brace back at the base indent.
|
|
125
|
+
//
|
|
126
|
+
// {
|
|
127
|
+
// stmt;
|
|
128
|
+
// stmt;
|
|
129
|
+
// }
|
|
130
|
+
//
|
|
131
|
+
// Each statement is dispatched through PrintNode, so a statement that
|
|
132
|
+
// is itself reflowable (a long call) gets reflowed and a plain
|
|
133
|
+
// statement falls back to verbatim. Because the engine re-applies the
|
|
134
|
+
// indent at every Hardline, even a verbatim statement lands at the
|
|
135
|
+
// correct column — the verbatim-column hazard only bites *multi-line*
|
|
136
|
+
// verbatim nodes, and those taint `covered` so the rule abstains.
|
|
137
|
+
//
|
|
138
|
+
// The second return value is the `covered` flag: see PrintNode. A block
|
|
139
|
+
// is uncovered when any statement is uncovered, or when the block
|
|
140
|
+
// carries a comment that lives outside every statement's byte range —
|
|
141
|
+
// the freshly minted Hardline separators have no carrier slot for such
|
|
142
|
+
// trivia, so reflowing would silently drop the comment. The
|
|
143
|
+
// comment check guards the statement-free path too: `{ /* note */ }`
|
|
144
|
+
// has no statements but is *not* an empty block — collapsing it to `{}`
|
|
145
|
+
// would delete the comment, so the printer emits it verbatim instead.
|
|
146
|
+
func printBlock(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
147
|
+
if node == nil {
|
|
148
|
+
return Doc{}, true
|
|
149
|
+
}
|
|
150
|
+
block := node.AsBlock()
|
|
151
|
+
if block == nil || block.Statements == nil {
|
|
152
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
153
|
+
}
|
|
154
|
+
hasComment := blockHasNonStatementComment(ctx, node, block.Statements.Nodes)
|
|
155
|
+
if len(block.Statements.Nodes) == 0 {
|
|
156
|
+
if hasComment {
|
|
157
|
+
// `{ /* … */ }` — not collapsible without dropping the comment.
|
|
158
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
159
|
+
}
|
|
160
|
+
return Text("{}"), true
|
|
161
|
+
}
|
|
162
|
+
stmts := block.Statements.Nodes
|
|
163
|
+
items := make([]Doc, 0, len(stmts))
|
|
164
|
+
covered := !hasComment
|
|
165
|
+
for _, stmt := range stmts {
|
|
166
|
+
if stmt == nil {
|
|
167
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
168
|
+
}
|
|
169
|
+
doc, childCovered := PrintNode(ctx, stmt)
|
|
170
|
+
covered = covered && childCovered
|
|
171
|
+
items = append(items, doc)
|
|
172
|
+
}
|
|
173
|
+
// Join statements with a Hardline, preserving a single user-authored
|
|
174
|
+
// blank line between consecutive statements. The block printer mints
|
|
175
|
+
// fresh separators, so a bare `Join(Hardline, …)` would silently
|
|
176
|
+
// delete every blank line in the body on the first reflow. The blank
|
|
177
|
+
// line is a Literalline — a bare newline with no indent — so the
|
|
178
|
+
// empty line carries no trailing whitespace. Two or more blank lines
|
|
179
|
+
// collapse to one, matching Prettier.
|
|
180
|
+
bodyParts := make([]Doc, 0, len(items)*2)
|
|
181
|
+
for i, item := range items {
|
|
182
|
+
if i > 0 {
|
|
183
|
+
if blankLineBetweenStatements(ctx.Source, stmts[i-1].End(), stmts[i].Pos()) {
|
|
184
|
+
bodyParts = append(bodyParts, Literalline())
|
|
185
|
+
}
|
|
186
|
+
bodyParts = append(bodyParts, Hardline())
|
|
187
|
+
}
|
|
188
|
+
bodyParts = append(bodyParts, item)
|
|
189
|
+
}
|
|
190
|
+
doc := Concat(
|
|
191
|
+
Text("{"),
|
|
192
|
+
Indent(ctx.indentUnit(), Hardline(), Concat(bodyParts...)),
|
|
193
|
+
Hardline(),
|
|
194
|
+
Text("}"),
|
|
195
|
+
)
|
|
196
|
+
return doc, covered
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// blankLineBetweenStatements reports whether the source gap between the
|
|
200
|
+
// end of one block statement and the start of the next contains a blank
|
|
201
|
+
// line — two or more newlines. printBlock uses it to keep a single
|
|
202
|
+
// user-authored blank line between statements. blockHasNonStatementComment
|
|
203
|
+
// has already guaranteed the gap holds no comment when the block is
|
|
204
|
+
// covered, so the gap is pure whitespace and counting newlines suffices.
|
|
205
|
+
func blankLineBetweenStatements(src string, prevEnd, nextPos int) bool {
|
|
206
|
+
nextStart := shimscanner.SkipTrivia(src, nextPos)
|
|
207
|
+
if prevEnd < 0 || nextStart > len(src) || nextStart <= prevEnd {
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
newlines := 0
|
|
211
|
+
for i := prevEnd; i < nextStart; i++ {
|
|
212
|
+
if src[i] == '\n' {
|
|
213
|
+
newlines++
|
|
214
|
+
if newlines >= 2 {
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// blockHasNonStatementComment reports whether the block's byte range
|
|
223
|
+
// holds a `//` or `/*` outside every statement's token range. The block
|
|
224
|
+
// printer joins statements with bare Hardlines that have no slot for
|
|
225
|
+
// inter-statement trivia, so a stray comment would be dropped by a
|
|
226
|
+
// reflow. Detecting it lets printBlock report the block uncovered and
|
|
227
|
+
// the format/print-width rule abstain.
|
|
228
|
+
//
|
|
229
|
+
// The scan mirrors rules_format_print_width.go::hasNonChildComments:
|
|
230
|
+
// comment-shaped bytes inside a complete statement token range (string
|
|
231
|
+
// literals, nested comments) are masked, so only genuine
|
|
232
|
+
// inter-statement comments surface.
|
|
233
|
+
func blockHasNonStatementComment(ctx *PrintContext, node *shimast.Node, stmts []*shimast.Node) bool {
|
|
234
|
+
start := shimscanner.SkipTrivia(ctx.Source, node.Pos())
|
|
235
|
+
end := node.End()
|
|
236
|
+
if start < 0 || end < start || end > len(ctx.Source) {
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
type span struct{ pos, end int }
|
|
240
|
+
ranges := make([]span, 0, len(stmts))
|
|
241
|
+
for _, stmt := range stmts {
|
|
242
|
+
if stmt == nil {
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
ranges = append(ranges, span{shimscanner.SkipTrivia(ctx.Source, stmt.Pos()), stmt.End()})
|
|
246
|
+
}
|
|
247
|
+
inStatement := func(i int) bool {
|
|
248
|
+
for _, r := range ranges {
|
|
249
|
+
if i >= r.pos && i < r.end {
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
src := ctx.Source
|
|
256
|
+
for i := start; i < end-1 && i < len(src)-1; i++ {
|
|
257
|
+
if inStatement(i) {
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
if src[i] == '/' && (src[i+1] == '/' || src[i+1] == '*') {
|
|
261
|
+
return true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// printExpressionStatement renders an `expr;` statement. The expression
|
|
268
|
+
// is dispatched so a callback-body statement that is itself a call or
|
|
269
|
+
// object literal reflows; the trailing `;` is preserved when the source
|
|
270
|
+
// carries one.
|
|
271
|
+
//
|
|
272
|
+
// The dispatcher reaches this printer through the block printer, so
|
|
273
|
+
// nested callbacks (`outer(() => { inner(() => { … }); })`) reflow at
|
|
274
|
+
// every depth instead of stalling on the verbatim fallback.
|
|
275
|
+
//
|
|
276
|
+
// The second return value is the `covered` flag: see PrintNode. The
|
|
277
|
+
// printer falls back to verbatim when the gap between the expression
|
|
278
|
+
// and the statement end holds anything other than the optional `;` and
|
|
279
|
+
// whitespace — a comment in that gap has no carrier slot and would be
|
|
280
|
+
// dropped.
|
|
281
|
+
func printExpressionStatement(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
282
|
+
if node == nil {
|
|
283
|
+
return Doc{}, true
|
|
284
|
+
}
|
|
285
|
+
stmt := node.AsExpressionStatement()
|
|
286
|
+
if stmt == nil || stmt.Expression == nil {
|
|
287
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
288
|
+
}
|
|
289
|
+
if !tailIsCleanTerminator(ctx.Source, stmt.Expression.End(), node.End()) {
|
|
290
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
291
|
+
}
|
|
292
|
+
exprDoc, covered := PrintNode(ctx, stmt.Expression)
|
|
293
|
+
parts := []Doc{exprDoc}
|
|
294
|
+
if sourceHasStatementTerminator(ctx.Source, node.End()) {
|
|
295
|
+
parts = append(parts, Text(";"))
|
|
296
|
+
}
|
|
297
|
+
return Concat(parts...), covered
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// printReturnStatement renders a `return expr;` statement. Like the
|
|
301
|
+
// expression-statement printer, the returned expression is dispatched
|
|
302
|
+
// so a returned callback or object literal reflows.
|
|
303
|
+
//
|
|
304
|
+
// A bare `return;` carries no expression and is emitted verbatim — it
|
|
305
|
+
// is a single token with nothing to reflow.
|
|
306
|
+
//
|
|
307
|
+
// The second return value is the `covered` flag: see PrintNode. The
|
|
308
|
+
// printer falls back to verbatim when the gap between the expression
|
|
309
|
+
// and the statement end holds anything but the optional `;` and
|
|
310
|
+
// whitespace.
|
|
311
|
+
func printReturnStatement(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
312
|
+
if node == nil {
|
|
313
|
+
return Doc{}, true
|
|
314
|
+
}
|
|
315
|
+
stmt := node.AsReturnStatement()
|
|
316
|
+
if stmt == nil || stmt.Expression == nil {
|
|
317
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
318
|
+
}
|
|
319
|
+
if !tailIsCleanTerminator(ctx.Source, stmt.Expression.End(), node.End()) {
|
|
320
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
321
|
+
}
|
|
322
|
+
exprDoc, covered := PrintNode(ctx, stmt.Expression)
|
|
323
|
+
parts := []Doc{Text("return "), exprDoc}
|
|
324
|
+
if sourceHasStatementTerminator(ctx.Source, node.End()) {
|
|
325
|
+
parts = append(parts, Text(";"))
|
|
326
|
+
}
|
|
327
|
+
return Concat(parts...), covered
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// tailIsCleanTerminator reports whether src[exprEnd:stmtEnd] holds only
|
|
331
|
+
// whitespace and at most one `;`. The expression- and return-statement
|
|
332
|
+
// printers consult it before re-minting the trailing `;`: a comment or
|
|
333
|
+
// any other token in that gap would be dropped by the reflow, so the
|
|
334
|
+
// printer must fall back to verbatim instead.
|
|
335
|
+
func tailIsCleanTerminator(src string, exprEnd, stmtEnd int) bool {
|
|
336
|
+
if exprEnd < 0 || stmtEnd < exprEnd || stmtEnd > len(src) {
|
|
337
|
+
return false
|
|
338
|
+
}
|
|
339
|
+
semis := 0
|
|
340
|
+
for i := exprEnd; i < stmtEnd; i++ {
|
|
341
|
+
switch src[i] {
|
|
342
|
+
case ' ', '\t', '\r', '\n':
|
|
343
|
+
case ';':
|
|
344
|
+
semis++
|
|
345
|
+
if semis > 1 {
|
|
346
|
+
return false
|
|
347
|
+
}
|
|
348
|
+
default:
|
|
349
|
+
return false
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
@@ -20,56 +20,70 @@ import (
|
|
|
20
20
|
// mode, matching Prettier's `bracketSpacing: true` default for import
|
|
21
21
|
// declarations (which is hard-coded; Prettier ignores `bracketSpacing`
|
|
22
22
|
// for imports). Empty named imports `{}` collapse cleanly.
|
|
23
|
-
|
|
23
|
+
//
|
|
24
|
+
// The second return value is the `covered` flag: see PrintNode.
|
|
25
|
+
func printNamedImports(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
24
26
|
if node == nil {
|
|
25
|
-
return Doc{}
|
|
27
|
+
return Doc{}, true
|
|
26
28
|
}
|
|
27
29
|
ni := node.AsNamedImports()
|
|
28
30
|
if ni == nil || ni.Elements == nil {
|
|
29
|
-
return verbatim(ctx, node)
|
|
31
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
30
32
|
}
|
|
31
33
|
items := make([]Doc, 0, len(ni.Elements.Nodes))
|
|
34
|
+
covered := true
|
|
32
35
|
for _, spec := range ni.Elements.Nodes {
|
|
33
36
|
if spec == nil {
|
|
34
|
-
return verbatim(ctx, node)
|
|
37
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
35
38
|
}
|
|
36
|
-
doc,
|
|
39
|
+
doc, childCovered := PrintNode(ctx, spec)
|
|
40
|
+
covered = covered && childCovered
|
|
37
41
|
items = append(items, doc)
|
|
38
42
|
}
|
|
43
|
+
// AddComma honors `format.trailingComma`: named imports are an
|
|
44
|
+
// ES5-permitted comma position, so "all" and "es5" both keep the
|
|
45
|
+
// trailing comma; only "none" drops it.
|
|
39
46
|
return printList(ctx, listShape{
|
|
40
47
|
OpenTok: "{",
|
|
41
48
|
CloseTok: "}",
|
|
42
49
|
Items: items,
|
|
43
50
|
Space: true,
|
|
44
|
-
AddComma:
|
|
45
|
-
})
|
|
51
|
+
AddComma: ctx.allowsEs5TrailingComma(),
|
|
52
|
+
}), covered
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
// printNamedExports renders `export { a, b }`. The shape is identical
|
|
49
56
|
// to NamedImports; only the surrounding declaration differs.
|
|
50
|
-
|
|
57
|
+
//
|
|
58
|
+
// The second return value is the `covered` flag: see PrintNode.
|
|
59
|
+
func printNamedExports(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
51
60
|
if node == nil {
|
|
52
|
-
return Doc{}
|
|
61
|
+
return Doc{}, true
|
|
53
62
|
}
|
|
54
63
|
ne := node.AsNamedExports()
|
|
55
64
|
if ne == nil || ne.Elements == nil {
|
|
56
|
-
return verbatim(ctx, node)
|
|
65
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
57
66
|
}
|
|
58
67
|
items := make([]Doc, 0, len(ne.Elements.Nodes))
|
|
68
|
+
covered := true
|
|
59
69
|
for _, spec := range ne.Elements.Nodes {
|
|
60
70
|
if spec == nil {
|
|
61
|
-
return verbatim(ctx, node)
|
|
71
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
62
72
|
}
|
|
63
|
-
doc,
|
|
73
|
+
doc, childCovered := PrintNode(ctx, spec)
|
|
74
|
+
covered = covered && childCovered
|
|
64
75
|
items = append(items, doc)
|
|
65
76
|
}
|
|
77
|
+
// AddComma honors `format.trailingComma`: named exports are an
|
|
78
|
+
// ES5-permitted comma position, so "all" and "es5" both keep the
|
|
79
|
+
// trailing comma; only "none" drops it.
|
|
66
80
|
return printList(ctx, listShape{
|
|
67
81
|
OpenTok: "{",
|
|
68
82
|
CloseTok: "}",
|
|
69
83
|
Items: items,
|
|
70
84
|
Space: true,
|
|
71
|
-
AddComma:
|
|
72
|
-
})
|
|
85
|
+
AddComma: ctx.allowsEs5TrailingComma(),
|
|
86
|
+
}), covered
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
// printImportDeclaration renders the surrounding `import … from "x";`.
|
|
@@ -79,13 +93,15 @@ func printNamedExports(ctx *PrintContext, node *shimast.Node) Doc {
|
|
|
79
93
|
//
|
|
80
94
|
// The dispatcher hands off to the per-clause printers; the top-level
|
|
81
95
|
// frame stitches them together with the keywords and `from` token.
|
|
82
|
-
|
|
96
|
+
//
|
|
97
|
+
// The second return value is the `covered` flag: see PrintNode.
|
|
98
|
+
func printImportDeclaration(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
83
99
|
if node == nil {
|
|
84
|
-
return Doc{}
|
|
100
|
+
return Doc{}, true
|
|
85
101
|
}
|
|
86
102
|
imp := node.AsImportDeclaration()
|
|
87
103
|
if imp == nil {
|
|
88
|
-
return verbatim(ctx, node)
|
|
104
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
89
105
|
}
|
|
90
106
|
// If the declaration uses anything other than a vanilla
|
|
91
107
|
// `import { ... } from "x"` shape (default specifier, namespace
|
|
@@ -93,20 +109,26 @@ func printImportDeclaration(ctx *PrintContext, node *shimast.Node) Doc {
|
|
|
93
109
|
// canonical reflow target is the named-import body.
|
|
94
110
|
clause := imp.ImportClause
|
|
95
111
|
if clause == nil {
|
|
96
|
-
return verbatim(ctx, node)
|
|
112
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
97
113
|
}
|
|
98
114
|
clauseData := clause.AsImportClause()
|
|
99
115
|
if clauseData == nil || clauseData.NamedBindings == nil {
|
|
100
|
-
return verbatim(ctx, node)
|
|
116
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
101
117
|
}
|
|
102
118
|
if clauseData.NamedBindings.Kind != shimast.KindNamedImports {
|
|
103
119
|
// Namespace imports (`import * as ns from "x"`) have no
|
|
104
120
|
// reflow surface; leave them alone.
|
|
105
|
-
return verbatim(ctx, node)
|
|
121
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
106
122
|
}
|
|
107
123
|
if clause.Name() != nil {
|
|
108
124
|
// `import Default, { … } from "x"` — keep verbatim for v1.
|
|
109
|
-
return verbatim(ctx, node)
|
|
125
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
126
|
+
}
|
|
127
|
+
// AttributeClause (`with { ... }` / `assert { ... }`) lives after
|
|
128
|
+
// the module specifier. Fall back to verbatim when present so we
|
|
129
|
+
// don't drop attributes silently.
|
|
130
|
+
if imp.Attributes != nil {
|
|
131
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
// Bracketed clause prefix: `import ` (and optional `type `).
|
|
@@ -114,8 +136,9 @@ func printImportDeclaration(ctx *PrintContext, node *shimast.Node) Doc {
|
|
|
114
136
|
if clause.IsTypeOnly() {
|
|
115
137
|
prefix = "import type "
|
|
116
138
|
}
|
|
117
|
-
named,
|
|
139
|
+
named, covered := PrintNode(ctx, clauseData.NamedBindings)
|
|
118
140
|
moduleSpec := verbatim(ctx, imp.ModuleSpecifier)
|
|
141
|
+
covered = covered && !nodeSpansMultipleLines(ctx, imp.ModuleSpecifier)
|
|
119
142
|
parts := []Doc{Text(prefix), named, Text(" from "), moduleSpec}
|
|
120
143
|
if sourceHasStatementTerminator(ctx.Source, node.End()) {
|
|
121
144
|
// Preserve the user's terminator decision. Emitting `;`
|
|
@@ -124,13 +147,7 @@ func printImportDeclaration(ctx *PrintContext, node *shimast.Node) Doc {
|
|
|
124
147
|
// `;;` — `format/semi` owns terminator placement.
|
|
125
148
|
parts = append(parts, Text(";"))
|
|
126
149
|
}
|
|
127
|
-
|
|
128
|
-
// the module specifier. Fall back to verbatim when present so we
|
|
129
|
-
// don't drop attributes silently.
|
|
130
|
-
if imp.Attributes != nil {
|
|
131
|
-
return verbatim(ctx, node)
|
|
132
|
-
}
|
|
133
|
-
return Concat(parts...)
|
|
150
|
+
return Concat(parts...), covered
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
// sourceHasStatementTerminator reports whether the last non-trivia
|
|
@@ -27,15 +27,53 @@ type listShape struct {
|
|
|
27
27
|
Items []Doc
|
|
28
28
|
Space bool // emit a space after OPEN / before CLOSE in flat mode
|
|
29
29
|
AddComma bool // emit a trailing comma in broken mode
|
|
30
|
+
// HugLast keeps the final item attached to the parens instead of
|
|
31
|
+
// exploding the whole list. It is set by the call/new argument
|
|
32
|
+
// printer when the last argument is a callback or object literal —
|
|
33
|
+
// see printArgList. When true, the last item is emitted directly
|
|
34
|
+
// against OPEN…CLOSE with no leading/trailing soft break, so a
|
|
35
|
+
// multi-line callback body does not force every preceding argument
|
|
36
|
+
// onto its own line.
|
|
37
|
+
HugLast bool
|
|
38
|
+
// ForceBreak commits the list to its broken, one-item-per-line shape
|
|
39
|
+
// even when it would fit flat. The object-literal printer sets it to
|
|
40
|
+
// mirror Prettier's objectWrap:"preserve" — an object the source
|
|
41
|
+
// wrote with a newline after `{` stays expanded.
|
|
42
|
+
ForceBreak bool
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
// printList renders the list shape as a Doc tree. Empty lists collapse
|
|
33
|
-
// to `OPENCLOSE`.
|
|
34
|
-
//
|
|
46
|
+
// to `OPENCLOSE`. A HugLast list becomes a ConditionalGroup of up to
|
|
47
|
+
// three shapes; every other list is the plain fit-or-break Group.
|
|
35
48
|
func printList(ctx *PrintContext, shape listShape) Doc {
|
|
36
49
|
if len(shape.Items) == 0 {
|
|
37
50
|
return Text(shape.OpenTok + shape.CloseTok)
|
|
38
51
|
}
|
|
52
|
+
plain := printListPlain(ctx, shape)
|
|
53
|
+
if !shape.HugLast {
|
|
54
|
+
return plain
|
|
55
|
+
}
|
|
56
|
+
// A HugLast list offers the engine up to three shapes, in preference
|
|
57
|
+
// order:
|
|
58
|
+
// 1. allFlat — every item on one line, chosen when it fits;
|
|
59
|
+
// 2. hugged — leading items inline, the final callback or object
|
|
60
|
+
// committed to its multi-line shape, chosen when its opening
|
|
61
|
+
// line fits but the all-flat form does not;
|
|
62
|
+
// 3. plain — every item exploded onto its own indented line.
|
|
63
|
+
// The all-flat option is dropped when the list cannot render flat
|
|
64
|
+
// (a block-bodied callback argument carries hard line breaks).
|
|
65
|
+
hugged := printListHuggingLast(ctx, shape)
|
|
66
|
+
if allFlat, ok := flatten(plain); ok {
|
|
67
|
+
return ConditionalGroup(allFlat, hugged, plain)
|
|
68
|
+
}
|
|
69
|
+
return ConditionalGroup(hugged, plain)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// printListPlain renders the open-comma-close list as a single
|
|
73
|
+
// fit-or-break Group: flat (`OPEN a, b CLOSE`) when it fits the width
|
|
74
|
+
// budget, one item per indented line — with an optional trailing
|
|
75
|
+
// comma — when it does not.
|
|
76
|
+
func printListPlain(ctx *PrintContext, shape listShape) Doc {
|
|
39
77
|
sep := Concat(Text(","), Line())
|
|
40
78
|
body := Join(sep, shape.Items)
|
|
41
79
|
|
|
@@ -53,12 +91,55 @@ func printList(ctx *PrintContext, shape listShape) Doc {
|
|
|
53
91
|
closeTok := Text(shape.CloseTok)
|
|
54
92
|
|
|
55
93
|
// In flat mode: OPEN [pad] body [pad] CLOSE
|
|
56
|
-
// In broken mode: OPEN \n body, \n CLOSE
|
|
57
|
-
// flat form collapses cleanly.
|
|
94
|
+
// In broken mode: OPEN \n body, \n CLOSE.
|
|
58
95
|
leadingSep := IfBreak(Hardline(), flatPad)
|
|
59
96
|
trailingSep := IfBreak(Hardline(), flatPad)
|
|
60
97
|
|
|
61
98
|
bodyBlock := Indent(ctx.indentUnit(), leadingSep, body, trailing)
|
|
62
99
|
doc := Concat(openTok, bodyBlock, trailingSep, closeTok)
|
|
63
|
-
|
|
100
|
+
group := Group(doc)
|
|
101
|
+
// ForceBreak (object-literal newline preservation) commits the group
|
|
102
|
+
// to its broken shape regardless of fit.
|
|
103
|
+
group.Break = shape.ForceBreak
|
|
104
|
+
return group
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// printListHuggingLast renders the "last-argument hugging" shape that
|
|
108
|
+
// Prettier uses for `foo(a, b, () => { … })`: the leading items flow
|
|
109
|
+
// comma-separated and the final item stays attached to the closing
|
|
110
|
+
// paren instead of being pushed onto its own indented line.
|
|
111
|
+
//
|
|
112
|
+
// hugged: OPEN a, b, OPEN-of-last … CLOSE-of-last CLOSE
|
|
113
|
+
//
|
|
114
|
+
// The result is a plain Concat, not a Group: the hugged last argument
|
|
115
|
+
// is a callback or object literal whose own printer carries the
|
|
116
|
+
// fit-or-break decision for its body, so wrapping here would let a
|
|
117
|
+
// multi-line body force the `a, b,` prefix to break. printList offers
|
|
118
|
+
// this Concat as a ConditionalGroup option; when its opening line would
|
|
119
|
+
// overflow printWidth the engine falls back to the plain exploded list.
|
|
120
|
+
//
|
|
121
|
+
// The hugged final item is forced broken — via forceBreakFirstGroup —
|
|
122
|
+
// so the hugged option is genuinely multi-line and distinct from the
|
|
123
|
+
// all-flat option. A flat hugged object would otherwise be
|
|
124
|
+
// byte-identical to all-flat yet escape its width check. The break
|
|
125
|
+
// reaches the first Group in the item's subtree, so an object or array
|
|
126
|
+
// nested inside an arrow body (`(x) => ({ … })`) breaks too.
|
|
127
|
+
func printListHuggingLast(ctx *PrintContext, shape listShape) Doc {
|
|
128
|
+
last := shape.Items[len(shape.Items)-1]
|
|
129
|
+
lead := shape.Items[:len(shape.Items)-1]
|
|
130
|
+
// Force the hugged item's first Group broken only when the item has
|
|
131
|
+
// no hard line breaks of its own. A block-bodied callback already
|
|
132
|
+
// renders multi-line through its block's hard breaks; descending into
|
|
133
|
+
// it with forceBreakFirstGroup would instead force some unrelated
|
|
134
|
+
// nested Group — a plain statement's call-argument list — broken.
|
|
135
|
+
if _, flat := flatten(last); flat {
|
|
136
|
+
last, _ = forceBreakFirstGroup(last)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
parts := []Doc{Text(shape.OpenTok)}
|
|
140
|
+
for _, item := range lead {
|
|
141
|
+
parts = append(parts, item, Text(", "))
|
|
142
|
+
}
|
|
143
|
+
parts = append(parts, last, Text(shape.CloseTok))
|
|
144
|
+
return Concat(parts...)
|
|
64
145
|
}
|