@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
@@ -20,17 +20,23 @@ import (
20
20
  // Type arguments (`foo<A, B>(x)`) are preserved verbatim. Trailing
21
21
  // commas on type arguments are intentionally avoided — Prettier omits
22
22
  // them too (see prettier#10353).
23
- func printCallExpression(ctx *PrintContext, node *shimast.Node) Doc {
23
+ //
24
+ // The second return value is the `covered` flag: see PrintNode. The
25
+ // callee, optional `?.` token and type arguments are verbatim, so a
26
+ // multi-line callee taints coverage just as a multi-line argument does.
27
+ func printCallExpression(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
24
28
  if node == nil {
25
- return Doc{}
29
+ return Doc{}, true
26
30
  }
27
31
  call := node.AsCallExpression()
28
32
  if call == nil {
29
- return verbatim(ctx, node)
33
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
30
34
  }
31
35
  parts := []Doc{}
36
+ covered := true
32
37
  if call.Expression != nil {
33
38
  parts = append(parts, verbatim(ctx, call.Expression))
39
+ covered = covered && !nodeSpansMultipleLines(ctx, call.Expression)
34
40
  }
35
41
  // Question-dot for optional call: `foo?.(x)`. The token byte range
36
42
  // lives between Expression.End() and the open paren; copy
@@ -43,38 +49,45 @@ func printCallExpression(ctx *PrintContext, node *shimast.Node) Doc {
43
49
  parts = append(parts, verbatimRange(ctx.Source, callTypeArgsStart(ctx, call), callTypeArgsEnd(ctx, call)))
44
50
  }
45
51
  if hasNilEntry(call.Arguments) {
46
- return verbatim(ctx, node)
52
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
47
53
  }
48
- parts = append(parts, printArgList(ctx, call.Arguments))
49
- return Concat(parts...)
54
+ argDoc, argCovered := printArgList(ctx, call.Arguments)
55
+ parts = append(parts, argDoc)
56
+ return Concat(parts...), covered && argCovered
50
57
  }
51
58
 
52
59
  // printNewExpression renders a NewExpression. It mirrors the call
53
60
  // expression printer; the only difference is the leading `new ` keyword
54
61
  // and the optional argument list (NewExpression may omit args entirely,
55
62
  // e.g. `new Foo`).
56
- func printNewExpression(ctx *PrintContext, node *shimast.Node) Doc {
63
+ //
64
+ // The second return value is the `covered` flag: see PrintNode.
65
+ func printNewExpression(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
57
66
  if node == nil {
58
- return Doc{}
67
+ return Doc{}, true
59
68
  }
60
69
  ne := node.AsNewExpression()
61
70
  if ne == nil {
62
- return verbatim(ctx, node)
71
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
63
72
  }
64
73
  parts := []Doc{Text("new ")}
74
+ covered := true
65
75
  if ne.Expression != nil {
66
76
  parts = append(parts, verbatim(ctx, ne.Expression))
77
+ covered = covered && !nodeSpansMultipleLines(ctx, ne.Expression)
67
78
  }
68
79
  if ne.TypeArguments != nil {
69
80
  parts = append(parts, verbatimRange(ctx.Source, newTypeArgsStart(ctx, ne), newTypeArgsEnd(ctx, ne)))
70
81
  }
71
82
  if ne.Arguments != nil {
72
83
  if hasNilEntry(ne.Arguments) {
73
- return verbatim(ctx, node)
84
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
74
85
  }
75
- parts = append(parts, printArgList(ctx, ne.Arguments))
86
+ argDoc, argCovered := printArgList(ctx, ne.Arguments)
87
+ parts = append(parts, argDoc)
88
+ covered = covered && argCovered
76
89
  }
77
- return Concat(parts...)
90
+ return Concat(parts...), covered
78
91
  }
79
92
 
80
93
  // hasNilEntry reports whether any entry of `list` is a nil pointer.
@@ -94,29 +107,128 @@ func hasNilEntry(list *shimast.NodeList) bool {
94
107
  }
95
108
 
96
109
  // printArgList renders an argument node list. The shared printList
97
- // handles the open-comma-close shape; this helper just gathers the
98
- // per-argument docs.
99
- func printArgList(ctx *PrintContext, list *shimast.NodeList) Doc {
110
+ // handles the open-comma-close shape; this helper gathers the
111
+ // per-argument docs and threads each argument's `covered` flag up.
112
+ //
113
+ // When the final argument is a block-bodied callback or object literal,
114
+ // the list renders in the "last-argument hugging" shape (see
115
+ // printListHuggingLast): the callback's own body carries the multi-line
116
+ // layout, so the parens stay attached and the preceding arguments are
117
+ // not exploded onto their own lines. This is the Prettier behavior for
118
+ // `foo(x, () => { … })`.
119
+ func printArgList(ctx *PrintContext, list *shimast.NodeList) (Doc, bool) {
100
120
  if list == nil {
101
- return Text("()")
121
+ return Text("()"), true
102
122
  }
103
123
  items := make([]Doc, 0, len(list.Nodes))
124
+ covered := true
104
125
  for _, arg := range list.Nodes {
105
- doc, _ := PrintNode(ctx, arg)
126
+ doc, childCovered := PrintNode(ctx, arg)
127
+ covered = covered && childCovered
106
128
  items = append(items, doc)
107
129
  }
108
- return printList(ctx, listShape{
130
+ // AddComma honors `format.trailingComma`: call / new argument lists
131
+ // accepted trailing commas only in ES2017+, so Prettier's "es5" and
132
+ // "none" modes skip them here. Hardcoding `true` would oscillate
133
+ // against Prettier on every cascade pass on any project configured
134
+ // with "es5" (rxjs hit this on ajax.ts and several operators / testing
135
+ // helpers — the rule said "no comma needed" while the printer added
136
+ // one back on its own reflow).
137
+ shape := listShape{
109
138
  OpenTok: "(",
110
139
  CloseTok: ")",
111
140
  Items: items,
112
141
  Space: false,
113
- AddComma: true,
114
- })
142
+ AddComma: ctx.allowsCallArgumentTrailingComma(),
143
+ HugLast: shouldHugLastArgument(list.Nodes),
144
+ }
145
+ return printList(ctx, shape), covered
146
+ }
147
+
148
+ // shouldHugLastArgument reports whether the final entry of `args` is a
149
+ // shape Prettier keeps hugging the closing paren: an object or array
150
+ // literal, a function expression, or an arrow function whose body is a
151
+ // block, an object literal, or an array literal. Hugging only applies
152
+ // when that argument is genuinely the last one; a callback in the
153
+ // middle of the list does not trigger the shape.
154
+ //
155
+ // An arrow with any other expression body (`(x) => x.id`) is
156
+ // deliberately excluded. Such a body carries no internal break point,
157
+ // so the hugging shape — a flat `Concat` with no Group — would pin the
158
+ // whole call to one line even when that line overflows printWidth.
159
+ // Routing it through the normal list shape instead lets the argument
160
+ // list explode onto its own line when the call does not fit, which is
161
+ // what Prettier does.
162
+ func shouldHugLastArgument(args []*shimast.Node) bool {
163
+ if len(args) == 0 {
164
+ return false
165
+ }
166
+ last := args[len(args)-1]
167
+ if last == nil {
168
+ return false
169
+ }
170
+ switch last.Kind {
171
+ case shimast.KindFunctionExpression,
172
+ shimast.KindObjectLiteralExpression,
173
+ shimast.KindArrayLiteralExpression:
174
+ return true
175
+ case shimast.KindArrowFunction:
176
+ arrow := last.AsArrowFunction()
177
+ if arrow == nil || arrow.Body == nil {
178
+ return false
179
+ }
180
+ body := arrow.Body
181
+ // `(x) => ({ … })` parenthesizes its object body; hug on the inner
182
+ // expression, mirroring Prettier's couldExpandArg.
183
+ if body.Kind == shimast.KindParenthesizedExpression {
184
+ if p := body.AsParenthesizedExpression(); p != nil && p.Expression != nil {
185
+ body = p.Expression
186
+ }
187
+ }
188
+ switch body.Kind {
189
+ case shimast.KindBlock,
190
+ shimast.KindObjectLiteralExpression,
191
+ shimast.KindArrayLiteralExpression:
192
+ return true
193
+ }
194
+ }
195
+ return false
196
+ }
197
+
198
+ // forceBreakFirstGroup returns `doc` with the first Group found in a
199
+ // left-to-right walk of its subtree forced broken, and reports whether
200
+ // one was found. printListHuggingLast uses it to commit a hugged
201
+ // argument — an object or array literal, possibly nested inside an
202
+ // arrow body (`(x) => ({ … })`) — to its multi-line shape. The caller
203
+ // guards the walk with flatten: it is only run on an item that has no
204
+ // hard line breaks of its own, so the first Group reached is the
205
+ // hugged literal itself, never an unrelated Group inside a block body.
206
+ func forceBreakFirstGroup(doc Doc) (Doc, bool) {
207
+ switch doc.Kind {
208
+ case docGroup:
209
+ doc.Break = true
210
+ return doc, true
211
+ case docConcat, docIndent, docAlign:
212
+ children := make([]Doc, len(doc.Children))
213
+ copy(children, doc.Children)
214
+ for i, child := range children {
215
+ broken, done := forceBreakFirstGroup(child)
216
+ if done {
217
+ children[i] = broken
218
+ doc.Children = children
219
+ return doc, true
220
+ }
221
+ }
222
+ }
223
+ return doc, false
115
224
  }
116
225
 
117
226
  // Type-argument byte-range helpers. The shim's NodeList.End() points
118
227
  // past the last argument; the surrounding `<` and `>` are not part of
119
228
  // the list's range, so we have to scan around it.
229
+
230
+ // callTypeArgsStart returns the byte offset of the `<` that opens the
231
+ // type-argument list of a CallExpression. Returns -1 when absent.
120
232
  func callTypeArgsStart(ctx *PrintContext, call *shimast.CallExpression) int {
121
233
  if call.TypeArguments == nil || len(call.TypeArguments.Nodes) == 0 {
122
234
  return -1
@@ -136,6 +248,8 @@ func callTypeArgsStart(ctx *PrintContext, call *shimast.CallExpression) int {
136
248
  return -1
137
249
  }
138
250
 
251
+ // callTypeArgsEnd returns the byte offset one past the closing `>` of a
252
+ // CallExpression's type-argument list. Returns -1 when absent.
139
253
  func callTypeArgsEnd(ctx *PrintContext, call *shimast.CallExpression) int {
140
254
  if call.TypeArguments == nil {
141
255
  return -1
@@ -149,6 +263,8 @@ func callTypeArgsEnd(ctx *PrintContext, call *shimast.CallExpression) int {
149
263
  return end
150
264
  }
151
265
 
266
+ // newTypeArgsStart returns the byte offset of the `<` that opens the
267
+ // type-argument list of a NewExpression. Returns -1 when absent.
152
268
  func newTypeArgsStart(ctx *PrintContext, ne *shimast.NewExpression) int {
153
269
  if ne.TypeArguments == nil || len(ne.TypeArguments.Nodes) == 0 {
154
270
  return -1
@@ -166,6 +282,8 @@ func newTypeArgsStart(ctx *PrintContext, ne *shimast.NewExpression) int {
166
282
  return -1
167
283
  }
168
284
 
285
+ // newTypeArgsEnd returns the byte offset one past the closing `>` of a
286
+ // NewExpression's type-argument list. Returns -1 when absent.
169
287
  func newTypeArgsEnd(ctx *PrintContext, ne *shimast.NewExpression) int {
170
288
  if ne.TypeArguments == nil {
171
289
  return -1
@@ -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
+ }