@ttsc/lint 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/lib/defaultFormat.d.ts +10 -11
  2. package/lib/defaultFormat.js +10 -11
  3. package/lib/defaultFormat.js.map +1 -1
  4. package/lib/index.js +233 -135
  5. package/lib/index.js.map +1 -1
  6. package/lib/structures/ITtscLintConfig.d.ts +2 -2
  7. package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
  8. package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
  9. package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
  10. package/lib/structures/TtscLintRule.d.ts +10 -1
  11. package/lib/structures/TtscLintRuleMap.d.ts +2 -2
  12. package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
  13. package/linthost/ast_helpers.go +80 -11
  14. package/linthost/compile.go +116 -112
  15. package/linthost/config.go +647 -687
  16. package/linthost/config_format.go +272 -247
  17. package/linthost/contrib_adapter.go +7 -0
  18. package/linthost/directives.go +116 -0
  19. package/linthost/dispatch.go +33 -33
  20. package/linthost/engine.go +156 -43
  21. package/linthost/fix.go +47 -27
  22. package/linthost/flags_gen.go +31 -0
  23. package/linthost/format.go +119 -3
  24. package/linthost/host.go +115 -5
  25. package/linthost/print_dispatch.go +128 -28
  26. package/linthost/print_doc.go +23 -0
  27. package/linthost/print_engine.go +168 -4
  28. package/linthost/print_nodes_array.go +17 -7
  29. package/linthost/print_nodes_call.go +138 -20
  30. package/linthost/print_nodes_function.go +353 -0
  31. package/linthost/print_nodes_imports.go +46 -29
  32. package/linthost/print_nodes_list.go +86 -5
  33. package/linthost/print_nodes_object.go +56 -11
  34. package/linthost/rules_arrays.go +5 -2
  35. package/linthost/rules_debugger.go +3 -2
  36. package/linthost/rules_dupes.go +7 -4
  37. package/linthost/rules_empty.go +3 -2
  38. package/linthost/rules_escape.go +35 -3
  39. package/linthost/rules_eval.go +3 -0
  40. package/linthost/rules_finally.go +11 -0
  41. package/linthost/rules_format_jsdoc.go +7 -0
  42. package/linthost/rules_format_print_width.go +270 -15
  43. package/linthost/rules_format_quotes.go +3 -0
  44. package/linthost/rules_format_sort_imports.go +4 -0
  45. package/linthost/rules_gap.go +75 -3
  46. package/linthost/rules_imports.go +5 -7
  47. package/linthost/rules_logic.go +75 -5
  48. package/linthost/rules_loops.go +4 -0
  49. package/linthost/rules_misc.go +4 -2
  50. package/linthost/rules_params.go +7 -11
  51. package/linthost/rules_problems.go +89 -22
  52. package/linthost/rules_promise.go +15 -0
  53. package/linthost/rules_protos.go +3 -2
  54. package/linthost/rules_self.go +6 -0
  55. package/linthost/rules_strings.go +10 -0
  56. package/linthost/rules_suggestions.go +174 -9
  57. package/linthost/rules_throw.go +2 -0
  58. package/linthost/rules_ts.go +13 -0
  59. package/linthost/rules_ts_extra.go +25 -8
  60. package/linthost/rules_var.go +15 -2
  61. package/package.json +3 -3
  62. package/plugin/main.go +4 -4
  63. package/rule/astutil/astutil.go +12 -0
  64. package/rule/rule.go +123 -123
  65. package/src/defaultFormat.ts +10 -11
  66. package/src/index.ts +257 -168
  67. package/src/structures/ITtscLintConfig.ts +2 -2
  68. package/src/structures/ITtscLintFormatConfig.ts +53 -55
  69. package/src/structures/ITtscLintPluginConfig.ts +10 -83
  70. package/src/structures/ITtscLintPluginMeta.ts +9 -1
  71. package/src/structures/TtscLintRule.ts +10 -1
  72. package/src/structures/TtscLintRuleMap.ts +17 -18
  73. package/src/structures/TtscLintRuleOptions.ts +15 -0
  74. package/linthost/eslint_runtime.go +0 -320
@@ -9,10 +9,10 @@ import (
9
9
  //
10
10
  // The dispatcher is the bridge between the TypeScript-Go AST and the
11
11
  // printer engine. It walks one node at a time and emits a Doc tree
12
- // shaped to that node's grammar. Coverage is intentionally partial in
13
- // v1; the verbatim fallback below guarantees that an un-handled node
14
- // kind contributes its original source bytes verbatim, so the printer
15
- // can be wired up to a rule without breaking files that happen to use
12
+ // shaped to that node's grammar. Coverage is intentionally partial;
13
+ // the verbatim fallback below guarantees that an un-handled node kind
14
+ // contributes its original source bytes verbatim, so the printer can
15
+ // be wired up to a rule without breaking files that happen to use
16
16
  // shapes the per-node printers don't yet understand.
17
17
  //
18
18
  // The price of verbatim fallback is that reflow stops at the boundary
@@ -20,6 +20,19 @@ import (
20
20
  // dispatcher doesn't recognize stays long. That trade-off is preferable
21
21
  // to corrupting unfamiliar shapes — extension over time turns each
22
22
  // verbatim hop into a real reflow.
23
+ //
24
+ // Coverage signal. A verbatim slice keeps its *original* source column.
25
+ // When an un-handled node spans multiple lines, its interior lines are
26
+ // frozen at the columns the user wrote while the enclosing reflow
27
+ // re-indents everything around it — the result is inconsistently
28
+ // indented, corrupt output. To prevent that, every printer reports a
29
+ // `covered` boolean alongside its Doc: `true` means the whole printed
30
+ // subtree is reflow-safe (no multi-line verbatim node), `false` means a
31
+ // multi-line verbatim node is buried inside. The format/print-width
32
+ // rule abstains entirely when `covered` is false, so `ttsc format`
33
+ // either reflows correctly or leaves the bytes untouched — it never
34
+ // emits the half-reflowed shape. Single-line verbatim is always safe:
35
+ // a node confined to one source line has no interior column to freeze.
23
36
 
24
37
  // PrintContext bundles the per-file inputs every per-node printer
25
38
  // needs. The dispatcher constructs one per top-level reflow and threads
@@ -42,46 +55,96 @@ func NewPrintContext(file *shimast.SourceFile, opts PrintOptions) *PrintContext
42
55
 
43
56
  // PrintNode is the dispatcher entry. It picks a per-node printer based
44
57
  // on `node.Kind` and falls back to the verbatim source slice when no
45
- // printer is registered. Returns the printed Doc and a boolean
46
- // indicating whether the dispatch actually reformatted the node. The
47
- // boolean is currently only used by the format/print-width rule, which
48
- // skips edit emission when every printable child fell back to
49
- // verbatim (no behavior change, no diagnostic).
58
+ // printer is registered. Returns the printed Doc and a `covered`
59
+ // boolean: `true` when the whole printed subtree is reflow-safe,
60
+ // `false` when a multi-line verbatim node is buried inside it.
61
+ //
62
+ // The format/print-width rule consults `covered` to decide whether to
63
+ // emit an edit at all — see the coverage-signal note at the top of this
64
+ // file. A `false` reading is a hard abstain, not a soft hint.
50
65
  func PrintNode(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
51
66
  if node == nil {
52
- return Doc{}, false
67
+ return Doc{}, true
53
68
  }
54
- if doc, reformatted, ok := dispatchNode(ctx, node); ok {
55
- return doc, reformatted
69
+ if doc, covered, ok := dispatchNode(ctx, node); ok {
70
+ return doc, covered
56
71
  }
57
- return verbatim(ctx, node), false
72
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
58
73
  }
59
74
 
60
75
  // dispatchNode is the per-kind switch. Each branch returns
61
- // (doc, reformatted, true) when it produces a structured Doc, or
76
+ // (doc, covered, true) when it produces a structured Doc, or
62
77
  // (zero, false, false) to let the caller fall back to verbatim. The
63
- // reformatted flag is true when the per-node printer asserts the
64
- // result may differ from the original bytes.
78
+ // `covered` flag is `true` only when the per-node printer guarantees
79
+ // the whole subtree it produced is free of multi-line verbatim slices.
65
80
  func dispatchNode(ctx *PrintContext, node *shimast.Node) (Doc, bool, bool) {
66
81
  switch node.Kind {
67
82
  case shimast.KindObjectLiteralExpression:
68
- return printObjectLiteral(ctx, node), true, true
83
+ doc, covered := printObjectLiteral(ctx, node)
84
+ return doc, covered, true
69
85
  case shimast.KindArrayLiteralExpression:
70
- return printArrayLiteral(ctx, node), true, true
86
+ doc, covered := printArrayLiteral(ctx, node)
87
+ return doc, covered, true
71
88
  case shimast.KindCallExpression:
72
- return printCallExpression(ctx, node), true, true
89
+ doc, covered := printCallExpression(ctx, node)
90
+ return doc, covered, true
73
91
  case shimast.KindNewExpression:
74
- return printNewExpression(ctx, node), true, true
92
+ doc, covered := printNewExpression(ctx, node)
93
+ return doc, covered, true
75
94
  case shimast.KindNamedImports:
76
- return printNamedImports(ctx, node), true, true
95
+ doc, covered := printNamedImports(ctx, node)
96
+ return doc, covered, true
77
97
  case shimast.KindNamedExports:
78
- return printNamedExports(ctx, node), true, true
98
+ doc, covered := printNamedExports(ctx, node)
99
+ return doc, covered, true
79
100
  case shimast.KindImportDeclaration:
80
- return printImportDeclaration(ctx, node), true, true
101
+ doc, covered := printImportDeclaration(ctx, node)
102
+ return doc, covered, true
103
+ case shimast.KindArrowFunction:
104
+ doc, covered := printArrowFunction(ctx, node)
105
+ return doc, covered, true
106
+ case shimast.KindFunctionExpression:
107
+ doc, covered := printFunctionExpression(ctx, node)
108
+ return doc, covered, true
109
+ case shimast.KindParenthesizedExpression:
110
+ doc, covered := printParenthesizedExpression(ctx, node)
111
+ return doc, covered, true
112
+ case shimast.KindBlock:
113
+ doc, covered := printBlock(ctx, node)
114
+ return doc, covered, true
115
+ case shimast.KindExpressionStatement:
116
+ doc, covered := printExpressionStatement(ctx, node)
117
+ return doc, covered, true
118
+ case shimast.KindReturnStatement:
119
+ doc, covered := printReturnStatement(ctx, node)
120
+ return doc, covered, true
81
121
  }
82
122
  return Doc{}, false, false
83
123
  }
84
124
 
125
+ // nodeSpansMultipleLines reports whether `node`'s trivia-trimmed source
126
+ // range crosses a newline. A verbatim slice that stays on one line is
127
+ // always reflow-safe — there is no interior column for the enclosing
128
+ // re-indent to leave stranded — so the dispatcher treats single-line
129
+ // verbatim as `covered`. A multi-line verbatim node freezes its
130
+ // interior columns and is reported uncovered.
131
+ func nodeSpansMultipleLines(ctx *PrintContext, node *shimast.Node) bool {
132
+ if node == nil {
133
+ return false
134
+ }
135
+ start := shimscanner.SkipTrivia(ctx.Source, node.Pos())
136
+ end := node.End()
137
+ if start < 0 || end < start || end > len(ctx.Source) {
138
+ return false
139
+ }
140
+ for i := start; i < end; i++ {
141
+ if ctx.Source[i] == '\n' {
142
+ return true
143
+ }
144
+ }
145
+ return false
146
+ }
147
+
85
148
  // verbatim returns the original source bytes for `node`, leading trivia
86
149
  // trimmed. Use this whenever a printer cannot fully cover a node — the
87
150
  // surrounding doc tree still flows, but the verbatim slice carries
@@ -98,9 +161,10 @@ func verbatim(ctx *PrintContext, node *shimast.Node) Doc {
98
161
  return Text(ctx.Source[start:end])
99
162
  }
100
163
 
101
- // verbatimRange is the position-only sibling of verbatim — useful when
102
- // the printer needs to copy a sub-range that does not correspond to a
103
- // single AST node (e.g. tokens between two siblings).
164
+ // verbatimRange returns a Text doc holding src[start:end] verbatim.
165
+ // It is the position-only sibling of verbatim: use it when the sub-range
166
+ // to copy does not correspond to a single AST node (e.g. `<T>` tokens
167
+ // that surround a type-argument NodeList).
104
168
  func verbatimRange(src string, start, end int) Doc {
105
169
  if start < 0 || end < start || end > len(src) {
106
170
  return Doc{}
@@ -108,11 +172,47 @@ func verbatimRange(src string, start, end int) Doc {
108
172
  return Text(src[start:end])
109
173
  }
110
174
 
111
- // indentUnit returns one indentation step's worth of columns. Each
112
- // per-node printer uses this for its own list nesting.
175
+ // indentUnit returns the number of columns in one indentation step,
176
+ // derived from PrintOptions.TabWidth. Falls back to 2 when TabWidth
177
+ // is not set, matching the Prettier default.
113
178
  func (ctx *PrintContext) indentUnit() int {
114
179
  if ctx.Opts.TabWidth > 0 {
115
180
  return ctx.Opts.TabWidth
116
181
  }
117
182
  return 2
118
183
  }
184
+
185
+ // trailingCommaMode normalizes ctx.Opts.TrailingComma to one of "all",
186
+ // "es5", or "none". An empty value — the zero value for the field —
187
+ // reads as "all" to keep pre-existing callers and tests that built a
188
+ // PrintOptions{} without setting the field on Prettier's default
189
+ // behavior. Any other value also reads as "all"; the config layer
190
+ // rejects unknown strings before they reach the printer (see
191
+ // `expandFormatBlock` in config_format.go), so a stray value here is a
192
+ // programmer error and the safest fallback is the most-commas mode.
193
+ func (ctx *PrintContext) trailingCommaMode() string {
194
+ switch ctx.Opts.TrailingComma {
195
+ case "es5", "none":
196
+ return ctx.Opts.TrailingComma
197
+ }
198
+ return "all"
199
+ }
200
+
201
+ // allowsCallArgumentTrailingComma reports whether a multi-line call /
202
+ // new argument list should emit a trailing comma under the current
203
+ // trailingComma setting. Trailing commas in call arguments arrived in
204
+ // ES2017, so Prettier's "es5" mode excludes them just like "none" does;
205
+ // only "all" keeps them. Parameter lists are not printed by the
206
+ // dispatcher today, but the same rule applies to them when they are.
207
+ func (ctx *PrintContext) allowsCallArgumentTrailingComma() bool {
208
+ return ctx.trailingCommaMode() == "all"
209
+ }
210
+
211
+ // allowsEs5TrailingComma reports whether a multi-line ES5-permitted
212
+ // list — arrays, objects, named imports / exports — should emit a
213
+ // trailing comma. Only "none" suppresses the comma here; "es5" and
214
+ // "all" both keep it because ES5 has accepted trailing commas in these
215
+ // positions since the language's inception.
216
+ func (ctx *PrintContext) allowsEs5TrailingComma() bool {
217
+ return ctx.trailingCommaMode() != "none"
218
+ }
@@ -52,10 +52,17 @@ package linthost
52
52
  // - DocLineSuffix queues output until the next hardline/softline that
53
53
  // actually breaks; used for trailing line comments that must stick
54
54
  // to their source line.
55
+ // - DocConditionalGroup offers an ordered list of layout options; the
56
+ // engine renders the first whose first line fits the width budget
57
+ // and uses the last option as the unconditional fallback.
55
58
  //
56
59
  // The doc tree is built by helper constructors (Text, Line, Group, …)
57
60
  // below. Constructors take their children as variadic or slice
58
61
  // arguments so call sites read like a layout DSL.
62
+
63
+ // DocKind is the discriminant tag for a Doc node. Only the variant
64
+ // fields relevant to that kind are populated; all others stay at their
65
+ // zero value.
59
66
  type DocKind uint8
60
67
 
61
68
  const (
@@ -71,6 +78,7 @@ const (
71
78
  docIfBreak
72
79
  docConcat
73
80
  docLineSuffix
81
+ docConditionalGroup
74
82
  )
75
83
 
76
84
  // Doc is one node in the layout tree. Only the fields relevant to the
@@ -89,6 +97,11 @@ type Doc struct {
89
97
  Text string
90
98
  Children []Doc
91
99
  Width int
100
+ // Break, meaningful only on a docGroup, forces the group to render
101
+ // broken regardless of whether its flat form would fit. A
102
+ // ConditionalGroup option uses it to commit its last argument — a
103
+ // hugged object literal — to the multi-line shape.
104
+ Break bool
92
105
  // IfBreak pairs: BreakChild stored in Children[0], FlatChild in Children[1].
93
106
  }
94
107
 
@@ -112,6 +125,16 @@ func Literalline() Doc { return Doc{Kind: docLiteralline} }
112
125
  // concatenated.
113
126
  func Group(parts ...Doc) Doc { return Doc{Kind: docGroup, Children: parts} }
114
127
 
128
+ // ConditionalGroup picks the first option whose first line fits the
129
+ // remaining width budget, falling back to the last option when none
130
+ // fit. Where Group makes a single flat-or-break decision, a
131
+ // ConditionalGroup lets a printer offer several distinct shapes — a
132
+ // call's hugged vs. exploded argument list — and have the engine choose
133
+ // between them. The last option must always be a safe fallback.
134
+ func ConditionalGroup(options ...Doc) Doc {
135
+ return Doc{Kind: docConditionalGroup, Children: options}
136
+ }
137
+
115
138
  // Indent adds `width` columns of indentation to every newline emitted by
116
139
  // the child doc. Nesting composes.
117
140
  func Indent(width int, parts ...Doc) Doc {
@@ -63,6 +63,21 @@ import "strings"
63
63
  // makes the close brace of a reflowed list land at `BaseIndent`
64
64
  // while its children sit at `BaseIndent + indentUnit`.
65
65
  //
66
+ // TrailingComma mirrors Prettier's `trailingComma` setting and controls
67
+ // which broken-list shapes the printer emits a trailing comma on:
68
+ //
69
+ // - "all" (default) every multi-line list gets one.
70
+ // - "es5" arrays, objects, named imports / exports get one;
71
+ // call arguments, parameter lists, and type-level
72
+ // lists do not — those positions accepted trailing
73
+ // commas only in ES2017+, so es5 mode skips them
74
+ // to match Prettier and avoid oscillating against
75
+ // the formatter on every cascade pass.
76
+ // - "none" no list gets one.
77
+ //
78
+ // An empty string is treated as "all", which keeps `DefaultPrintOptions()`
79
+ // callers and tests that pre-date this field on their original behavior.
80
+ //
66
81
  // Defaults of 0 keep the engine usable for top-of-file reflow without
67
82
  // a wrapper.
68
83
  type PrintOptions struct {
@@ -70,14 +85,17 @@ type PrintOptions struct {
70
85
  TabWidth int
71
86
  UseTabs bool
72
87
  EndOfLine string
88
+ TrailingComma string
73
89
  StartingColumn int
74
90
  BaseIndent int
75
91
  }
76
92
 
77
93
  // DefaultPrintOptions returns the Prettier defaults: 80-column lines,
78
- // 2-space indentation, LF line terminators.
94
+ // 2-space indentation, LF line terminators, trailing commas on every
95
+ // multi-line list (the `trailingComma: "all"` default Prettier adopted
96
+ // in v2).
79
97
  func DefaultPrintOptions() PrintOptions {
80
- return PrintOptions{PrintWidth: 80, TabWidth: 2, UseTabs: false, EndOfLine: "lf"}
98
+ return PrintOptions{PrintWidth: 80, TabWidth: 2, UseTabs: false, EndOfLine: "lf", TrailingComma: "all"}
81
99
  }
82
100
 
83
101
  // printMode is the per-group choice made by the fit measurement.
@@ -191,13 +209,30 @@ func Print(doc Doc, opts PrintOptions) string {
191
209
  child := Concat(top.doc.Children...)
192
210
  stack = append(stack, printFrame{indent: col, mode: top.mode, doc: child})
193
211
  case docGroup:
194
- // Try flat unless the group contains a hardline.
212
+ // Try flat unless the group is forced broken or its flat form
213
+ // would overflow the remaining width.
195
214
  child := Concat(top.doc.Children...)
196
- if fits(child, opts.PrintWidth-col, top.indent) {
215
+ if !top.doc.Break && fits(child, opts.PrintWidth-col, top.indent) {
197
216
  stack = append(stack, printFrame{indent: top.indent, mode: modeFlat, doc: child})
198
217
  } else {
199
218
  stack = append(stack, printFrame{indent: top.indent, mode: modeBreak, doc: child})
200
219
  }
220
+ case docConditionalGroup:
221
+ // Render the first option whose first line fits the remaining
222
+ // width; fall back to the last option when none do. fitsFirstLine
223
+ // measures only up to an option's first break, so an option whose
224
+ // later lines wrap — a hugged callback body — is still eligible.
225
+ options := top.doc.Children
226
+ if len(options) > 0 {
227
+ chosen := options[len(options)-1]
228
+ for i := 0; i < len(options)-1; i++ {
229
+ if fitsFirstLine(options[i], opts.PrintWidth-col) {
230
+ chosen = options[i]
231
+ break
232
+ }
233
+ }
234
+ stack = append(stack, printFrame{indent: top.indent, mode: top.mode, doc: chosen})
235
+ }
201
236
  case docIfBreak:
202
237
  pick := top.doc.Children[1] // flat
203
238
  if top.mode == modeBreak {
@@ -306,6 +341,11 @@ func fits(doc Doc, remaining int, indent int) bool {
306
341
  stack = append(stack, frame{mode: top.mode, doc: top.doc.Children[i]})
307
342
  }
308
343
  case docGroup:
344
+ // A forced-broken group cannot contribute a flat layout; treat it
345
+ // like a hard line break for the enclosing measurement.
346
+ if top.doc.Break {
347
+ return false
348
+ }
309
349
  // Measure nested groups in flat mode too — that is the
310
350
  // standard Wadler choice: the outer group's "does my
311
351
  // flat form fit" question is answered by treating every
@@ -313,6 +353,11 @@ func fits(doc Doc, remaining int, indent int) bool {
313
353
  for i := len(top.doc.Children) - 1; i >= 0; i-- {
314
354
  stack = append(stack, frame{mode: modeFlat, doc: top.doc.Children[i]})
315
355
  }
356
+ case docConditionalGroup:
357
+ // A conditional group's flat form is its first (flattest) option.
358
+ if len(top.doc.Children) > 0 {
359
+ stack = append(stack, frame{mode: top.mode, doc: top.doc.Children[0]})
360
+ }
316
361
  case docIfBreak:
317
362
  pick := top.doc.Children[1]
318
363
  if top.mode == modeBreak {
@@ -341,3 +386,122 @@ func fits(doc Doc, remaining int, indent int) bool {
341
386
  }
342
387
  return true
343
388
  }
389
+
390
+ // fitsFirstLine reports whether the first line of `doc` — every column
391
+ // up to its first line break — renders within `remaining` columns. It
392
+ // drives ConditionalGroup option selection: an option is eligible when
393
+ // its opening line fits, even if its later lines wrap.
394
+ //
395
+ // The walk treats every top-level break point as broken — an IfBreak
396
+ // takes its break branch, and a Line / Softline / Hardline ends the
397
+ // measurement — so it counts exactly the columns the option would place
398
+ // on the line the group starts on. A nested ConditionalGroup
399
+ // contributes its own first option.
400
+ //
401
+ // A nested Group is the exception: a Group with no hard break renders
402
+ // flat when it fits, so its Line separators collapse to spaces and stay
403
+ // on the first line. The walk measures such a Group's flattened width
404
+ // rather than stopping at its first Line; only a Group that carries a
405
+ // Hardline (flatten reports it cannot render flat) ends the first line
406
+ // at its break.
407
+ func fitsFirstLine(doc Doc, remaining int) bool {
408
+ if remaining < 0 {
409
+ return false
410
+ }
411
+ stack := []Doc{doc}
412
+ for len(stack) > 0 {
413
+ top := stack[len(stack)-1]
414
+ stack = stack[:len(stack)-1]
415
+ switch top.Kind {
416
+ case docText:
417
+ if idx := strings.IndexByte(top.Text, '\n'); idx >= 0 {
418
+ // A multi-line Text ends the first line at its first newline.
419
+ return remaining-idx >= 0
420
+ }
421
+ remaining -= len(top.Text)
422
+ if remaining < 0 {
423
+ return false
424
+ }
425
+ case docGroup:
426
+ // A Group that can render flat keeps its Lines on the first line
427
+ // as spaces — measure the flattened form. One that cannot (a
428
+ // Hardline or forced break inside) breaks, so descend and let the
429
+ // Line/Hardline case end the first line at that break.
430
+ if flat, ok := flatten(top); ok {
431
+ stack = append(stack, flat)
432
+ } else {
433
+ for i := len(top.Children) - 1; i >= 0; i-- {
434
+ stack = append(stack, top.Children[i])
435
+ }
436
+ }
437
+ case docConcat, docIndent, docAlign:
438
+ for i := len(top.Children) - 1; i >= 0; i-- {
439
+ stack = append(stack, top.Children[i])
440
+ }
441
+ case docConditionalGroup:
442
+ if len(top.Children) > 0 {
443
+ stack = append(stack, top.Children[0])
444
+ }
445
+ case docIfBreak:
446
+ // Measuring in break mode: take the broken branch.
447
+ stack = append(stack, top.Children[0])
448
+ case docLine, docSoftline, docHardline, docLiteralline:
449
+ // The first break ends the first line; what fit so far fits.
450
+ return true
451
+ case docNil, docLineSuffix:
452
+ // No first-line width contribution.
453
+ }
454
+ }
455
+ return true
456
+ }
457
+
458
+ // flatten returns the all-flat rendering of `doc`: every Group rendered
459
+ // flat, every IfBreak resolved to its flat branch, every Line collapsed
460
+ // to a single space and every Softline to nothing. The second result is
461
+ // false when the doc cannot render flat at all — it carries a Hardline,
462
+ // a Literalline, a forced-broken Group, a multi-line Text or a queued
463
+ // LineSuffix — and the caller must then drop the flat layout option.
464
+ func flatten(doc Doc) (Doc, bool) {
465
+ switch doc.Kind {
466
+ case docText:
467
+ if strings.Contains(doc.Text, "\n") {
468
+ return Doc{}, false
469
+ }
470
+ return doc, true
471
+ case docLine:
472
+ return Text(" "), true
473
+ case docNil, docSoftline:
474
+ return Doc{Kind: docNil}, true
475
+ case docHardline, docLiteralline, docLineSuffix:
476
+ return Doc{}, false
477
+ case docIfBreak:
478
+ return flatten(doc.Children[1])
479
+ case docConditionalGroup:
480
+ if len(doc.Children) == 0 {
481
+ return Doc{Kind: docNil}, true
482
+ }
483
+ return flatten(doc.Children[0])
484
+ case docGroup:
485
+ if doc.Break {
486
+ return Doc{}, false
487
+ }
488
+ return flattenChildren(doc.Children)
489
+ case docConcat, docIndent, docAlign:
490
+ return flattenChildren(doc.Children)
491
+ }
492
+ return doc, true
493
+ }
494
+
495
+ // flattenChildren flattens each child and concatenates the results,
496
+ // short-circuiting to (zero, false) when any child cannot render flat.
497
+ func flattenChildren(children []Doc) (Doc, bool) {
498
+ out := make([]Doc, 0, len(children))
499
+ for _, c := range children {
500
+ fc, ok := flatten(c)
501
+ if !ok {
502
+ return Doc{}, false
503
+ }
504
+ out = append(out, fc)
505
+ }
506
+ return Concat(out...), true
507
+ }
@@ -20,27 +20,37 @@ import (
20
20
  // Array literals do NOT carry a leading/trailing space inside the
21
21
  // brackets in flat mode (`[a, b]`, not `[ a, b ]`), matching every
22
22
  // JavaScript formatter. Empty arrays collapse to `[]`.
23
- func printArrayLiteral(ctx *PrintContext, node *shimast.Node) Doc {
23
+ //
24
+ // The second return value is the `covered` flag: see PrintNode. It is
25
+ // the AND of every element's coverage.
26
+ func printArrayLiteral(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
24
27
  if node == nil {
25
- return Doc{}
28
+ return Doc{}, true
26
29
  }
27
30
  arr := node.AsArrayLiteralExpression()
28
31
  if arr == nil || arr.Elements == nil {
29
- return verbatim(ctx, node)
32
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
30
33
  }
31
34
  items := make([]Doc, 0, len(arr.Elements.Nodes))
35
+ covered := true
32
36
  for _, elem := range arr.Elements.Nodes {
33
37
  if elem == nil {
34
- return verbatim(ctx, node)
38
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
35
39
  }
36
- doc, _ := PrintNode(ctx, elem)
40
+ doc, childCovered := PrintNode(ctx, elem)
41
+ covered = covered && childCovered
37
42
  items = append(items, doc)
38
43
  }
44
+ // AddComma honors `format.trailingComma`: arrays accept trailing
45
+ // commas in ES5 so both "all" and "es5" keep them; only "none"
46
+ // suppresses. Hardcoding `true` would oscillate against Prettier on
47
+ // every `none` project (the trailing-comma rule wouldn't insert one
48
+ // and the printer would put one back).
39
49
  return printList(ctx, listShape{
40
50
  OpenTok: "[",
41
51
  CloseTok: "]",
42
52
  Items: items,
43
53
  Space: false,
44
- AddComma: true,
45
- })
54
+ AddComma: ctx.allowsEs5TrailingComma(),
55
+ }), covered
46
56
  }