@ttsc/lint 0.12.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/lib/index.js +227 -135
  2. package/lib/index.js.map +1 -1
  3. package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
  4. package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
  5. package/linthost/ast_helpers.go +68 -16
  6. package/linthost/compile.go +91 -124
  7. package/linthost/config.go +518 -707
  8. package/linthost/config_format.go +16 -4
  9. package/linthost/contrib_adapter.go +7 -0
  10. package/linthost/directives.go +44 -0
  11. package/linthost/engine.go +152 -43
  12. package/linthost/fix.go +24 -33
  13. package/linthost/flags_gen.go +31 -0
  14. package/linthost/format.go +96 -3
  15. package/linthost/host.go +104 -5
  16. package/linthost/print_dispatch.go +121 -23
  17. package/linthost/print_doc.go +19 -0
  18. package/linthost/print_engine.go +168 -4
  19. package/linthost/print_nodes_array.go +17 -7
  20. package/linthost/print_nodes_call.go +129 -20
  21. package/linthost/print_nodes_function.go +353 -0
  22. package/linthost/print_nodes_imports.go +46 -29
  23. package/linthost/print_nodes_list.go +86 -5
  24. package/linthost/print_nodes_object.go +56 -11
  25. package/linthost/rules_escape.go +20 -3
  26. package/linthost/rules_format_print_width.go +267 -15
  27. package/linthost/rules_gap.go +55 -3
  28. package/linthost/rules_logic.go +64 -5
  29. package/linthost/rules_problems.go +65 -21
  30. package/linthost/rules_promise.go +3 -0
  31. package/linthost/rules_suggestions.go +160 -5
  32. package/linthost/rules_var.go +1 -1
  33. package/package.json +3 -3
  34. package/src/index.ts +245 -168
  35. package/src/structures/ITtscLintPluginConfig.ts +10 -83
  36. package/src/structures/TtscLintRuleOptions.ts +15 -0
  37. 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
- func printNamedImports(ctx *PrintContext, node *shimast.Node) Doc {
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, _ := PrintNode(ctx, spec)
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: true,
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
- func printNamedExports(ctx *PrintContext, node *shimast.Node) Doc {
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, _ := PrintNode(ctx, spec)
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: true,
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
- func printImportDeclaration(ctx *PrintContext, node *shimast.Node) Doc {
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, _ := PrintNode(ctx, clauseData.NamedBindings)
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
- // 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)
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`. Single-element lists still wrap so trailing-comma
34
- // logic stays uniform.
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 — using Softline so the
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
- return Group(doc)
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
  }