@ttsc/lint 0.19.3 → 0.20.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 (63) hide show
  1. package/README.md +1 -1
  2. package/lib/index.js +26 -4
  3. package/lib/index.js.map +1 -1
  4. package/lib/structures/format/ITtscLintFormat.d.ts +14 -10
  5. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +24 -9
  6. package/lib/structures/rules/ITtscLintSecurityRules.d.ts +18 -0
  7. package/lib/structures/rules/ITtscLintSolidRules.d.ts +22 -2
  8. package/lib/structures/rules/ITtscLintStorybookRules.d.ts +21 -0
  9. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +10 -1
  10. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +13 -2
  11. package/linthost/compile.go +9 -4
  12. package/linthost/config.go +18 -14
  13. package/linthost/contrib_adapter.go +57 -0
  14. package/linthost/dispatch.go +5 -1
  15. package/linthost/display_width.go +179 -41
  16. package/linthost/engine.go +120 -4
  17. package/linthost/flags_gen.go +4 -4
  18. package/linthost/hints.go +207 -0
  19. package/linthost/host.go +68 -0
  20. package/linthost/lsp.go +148 -29
  21. package/linthost/print_dispatch.go +24 -0
  22. package/linthost/print_nodes_array.go +88 -15
  23. package/linthost/print_nodes_call.go +38 -31
  24. package/linthost/print_nodes_control_flow.go +319 -0
  25. package/linthost/print_nodes_function.go +140 -13
  26. package/linthost/print_nodes_list.go +22 -10
  27. package/linthost/project_engine.go +18 -1
  28. package/linthost/project_rules.go +47 -0
  29. package/linthost/rule_docs.go +114 -0
  30. package/linthost/rules_boundaries.go +80 -2
  31. package/linthost/rules_format_bracket_spacing.go +18 -6
  32. package/linthost/rules_format_clause_join.go +11 -8
  33. package/linthost/rules_format_indent.go +155 -4
  34. package/linthost/rules_format_print_width.go +12 -35
  35. package/linthost/rules_format_quote_props.go +88 -27
  36. package/linthost/rules_format_statement_split.go +81 -0
  37. package/linthost/rules_format_trailing_comma.go +62 -94
  38. package/linthost/rules_gap.go +26 -17
  39. package/linthost/rules_jsdoc.go +54 -0
  40. package/linthost/rules_logic.go +30 -15
  41. package/linthost/rules_no_redeclare.go +12 -3
  42. package/linthost/rules_promise.go +37 -15
  43. package/linthost/rules_regexp.go +443 -49
  44. package/linthost/rules_security.go +243 -4
  45. package/linthost/rules_solid.go +430 -10
  46. package/linthost/rules_storybook.go +255 -10
  47. package/linthost/rules_suggestions.go +60 -39
  48. package/linthost/rules_ts.go +7 -0
  49. package/linthost/rules_ts_async.go +80 -2
  50. package/linthost/rules_ts_extra.go +21 -7
  51. package/linthost/serve.go +318 -0
  52. package/linthost/width_tables_gen.go +219 -0
  53. package/package.json +2 -2
  54. package/rule/hint.go +142 -0
  55. package/rule/rule.go +197 -1
  56. package/src/index.ts +35 -4
  57. package/src/structures/format/ITtscLintFormat.ts +14 -10
  58. package/src/structures/rules/ITtscLintRegexpRules.ts +24 -9
  59. package/src/structures/rules/ITtscLintSecurityRules.ts +18 -0
  60. package/src/structures/rules/ITtscLintSolidRules.ts +22 -2
  61. package/src/structures/rules/ITtscLintStorybookRules.ts +21 -0
  62. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +10 -1
  63. package/src/structures/rules/ITtscLintTypeScriptRules.ts +13 -2
@@ -1,7 +1,10 @@
1
1
  package linthost
2
2
 
3
3
  import (
4
+ "strings"
5
+
4
6
  shimast "github.com/microsoft/typescript-go/shim/ast"
7
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
5
8
  )
6
9
 
7
10
  // printArrayLiteral renders an ArrayLiteralExpression with width-aware
@@ -127,19 +130,18 @@ func arrayForcesBreak(node *shimast.Node) bool {
127
130
  return arrayShouldForceBreak(arr.Elements.Nodes)
128
131
  }
129
132
 
130
- // fastPathForcesBreak reports whether `node`, OR a force-breaking node nested
131
- // within the subtree a reflow of `node` would print (its call/new arguments and
132
- // array elements, recursively), must explode even though it fits flat. Prettier
133
- // breaks such a descendant an array `shouldBreak` or a function-composition
134
- // call/new when the enclosing node reflows; ttsc's print-width fast path
135
- // returns first while the descendant abstains via hasReflowAncestor, leaving
136
- // both flat (`new Map([["a", 1], ["b", 2]])`, `foo([[1, 2], [3, 4]])`). So the
137
- // fast path must consult this, not only the visited node itself.
138
- func fastPathForcesBreak(node *shimast.Node) bool {
133
+ // fastPathForcesBreak reports whether `node`, or a force-breaking node nested
134
+ // within the subtree its structured printer reaches, must explode even though
135
+ // the outer source fits flat. It follows call/new arguments, array elements,
136
+ // function bodies, conditional parts, parenthesized expressions, and printable
137
+ // object-member bodies. The forced descendant can be a same-kind composite
138
+ // array, a function-composition call, or a non-empty block. Without the walk,
139
+ // the outer node's fast path would return before the descendant could deny it.
140
+ func fastPathForcesBreak(node *shimast.Node, src string) bool {
139
141
  if node == nil {
140
142
  return false
141
143
  }
142
- if callForcesFunctionBreak(node) || arrayForcesBreak(node) {
144
+ if callForcesFunctionBreak(node) || arrayForcesBreak(node) || blockForcesBreak(node, src) {
143
145
  return true
144
146
  }
145
147
  var children []*shimast.Node
@@ -156,6 +158,16 @@ func fastPathForcesBreak(node *shimast.Node) bool {
156
158
  if a := node.AsArrayLiteralExpression(); a != nil && a.Elements != nil {
157
159
  children = a.Elements.Nodes
158
160
  }
161
+ case shimast.KindArrowFunction, shimast.KindFunctionExpression:
162
+ children = append(children, functionLikeBody(node))
163
+ case shimast.KindConditionalExpression:
164
+ if conditional := node.AsConditionalExpression(); conditional != nil {
165
+ children = append(children, conditional.Condition, conditional.WhenTrue, conditional.WhenFalse)
166
+ }
167
+ case shimast.KindParenthesizedExpression:
168
+ if parenthesized := node.AsParenthesizedExpression(); parenthesized != nil {
169
+ children = append(children, parenthesized.Expression)
170
+ }
159
171
  case shimast.KindObjectLiteralExpression:
160
172
  // A force-breaking array/object nested in an object PROPERTY value
161
173
  // (`{ m: [[1, 2], [3, 4]] }`) must also deny the fast path: the value
@@ -164,22 +176,83 @@ func fastPathForcesBreak(node *shimast.Node) bool {
164
176
  // would stay flat where Prettier breaks them.
165
177
  if o := node.AsObjectLiteralExpression(); o != nil && o.Properties != nil {
166
178
  for _, p := range o.Properties.Nodes {
167
- if p != nil && p.Kind == shimast.KindPropertyAssignment {
168
- if pa := p.AsPropertyAssignment(); pa != nil && pa.Initializer != nil {
169
- children = append(children, pa.Initializer)
170
- }
179
+ if p == nil {
180
+ continue
181
+ }
182
+ switch p.Kind {
183
+ case shimast.KindPropertyAssignment,
184
+ shimast.KindMethodDeclaration,
185
+ shimast.KindGetAccessor,
186
+ shimast.KindSetAccessor:
187
+ children = append(children, functionLikeBody(p))
171
188
  }
172
189
  }
173
190
  }
174
191
  }
175
192
  for _, ch := range children {
176
- if fastPathForcesBreak(ch) {
193
+ if fastPathForcesBreak(ch, src) {
177
194
  return true
178
195
  }
179
196
  }
180
197
  return false
181
198
  }
182
199
 
200
+ // blockForcesBreak reports whether `node` is a block Prettier expands even
201
+ // when it fits: one carrying a statement or a comment. Whitespace-only `{}` is
202
+ // the empty negative twin and remains flat.
203
+ func blockForcesBreak(node *shimast.Node, src string) bool {
204
+ if node == nil || node.Kind != shimast.KindBlock {
205
+ return false
206
+ }
207
+ block := node.AsBlock()
208
+ if block == nil || block.Statements == nil {
209
+ return false
210
+ }
211
+ if len(block.Statements.Nodes) > 0 {
212
+ return true
213
+ }
214
+ start := shimscanner.SkipTrivia(src, node.Pos())
215
+ end := node.End()
216
+ return start >= 0 && end > start+1 && end <= len(src) &&
217
+ src[start] == '{' && src[end-1] == '}' &&
218
+ strings.TrimSpace(src[start+1:end-1]) != ""
219
+ }
220
+
221
+ // functionLikeBody returns the part of an arrow, function expression, or
222
+ // object member that an enclosing reflow prints structurally.
223
+ func functionLikeBody(node *shimast.Node) *shimast.Node {
224
+ if node == nil {
225
+ return nil
226
+ }
227
+ switch node.Kind {
228
+ case shimast.KindArrowFunction:
229
+ if arrow := node.AsArrowFunction(); arrow != nil {
230
+ return arrow.Body
231
+ }
232
+ case shimast.KindFunctionExpression:
233
+ if function := node.AsFunctionExpression(); function != nil {
234
+ return function.Body
235
+ }
236
+ case shimast.KindPropertyAssignment:
237
+ if property := node.AsPropertyAssignment(); property != nil {
238
+ return property.Initializer
239
+ }
240
+ case shimast.KindMethodDeclaration:
241
+ if method := node.AsMethodDeclaration(); method != nil {
242
+ return method.Body
243
+ }
244
+ case shimast.KindGetAccessor:
245
+ if accessor := node.AsGetAccessorDeclaration(); accessor != nil {
246
+ return accessor.Body
247
+ }
248
+ case shimast.KindSetAccessor:
249
+ if accessor := node.AsSetAccessorDeclaration(); accessor != nil {
250
+ return accessor.Body
251
+ }
252
+ }
253
+ return nil
254
+ }
255
+
183
256
  // isConciselyPrintedArray reports whether an array should use Prettier's
184
257
  // concise "fill" layout: at least one element and every element a numeric
185
258
  // literal (optionally a `+`/`-` signed numeric). Prettier packs such arrays
@@ -206,15 +206,17 @@ func printArgList(ctx *PrintContext, list *shimast.NodeList, addComma bool, deco
206
206
  forceFnBreak = false
207
207
  }
208
208
  hugFirst := !hugLast && !forceFnBreak && shouldHugFirstArgument(ctx, list.Nodes)
209
+ hugLastBreakClose := hugLast && lastArgumentBreaksAfterArrow(list.Nodes)
209
210
  shape := listShape{
210
- OpenTok: "(",
211
- CloseTok: ")",
212
- Items: items,
213
- Space: false,
214
- AddComma: addComma,
215
- HugLast: hugLast,
216
- HugLastForce: forceHugLast,
217
- HugFirst: hugFirst,
211
+ OpenTok: "(",
212
+ CloseTok: ")",
213
+ Items: items,
214
+ Space: false,
215
+ AddComma: addComma,
216
+ HugLast: hugLast,
217
+ HugLastBreakClose: hugLastBreakClose,
218
+ HugLastForce: forceHugLast,
219
+ HugFirst: hugFirst,
218
220
  // A non-empty array second argument reaches HugFirst only via the
219
221
  // React-hook deps branch of shouldHugFirstArgument; let that deps array
220
222
  // break one-per-line instead of being pinned flat on the close line.
@@ -228,6 +230,18 @@ func printArgList(ctx *PrintContext, list *shimast.NodeList, addComma bool, deco
228
230
  return printList(ctx, shape), covered
229
231
  }
230
232
 
233
+ // lastArgumentBreaksAfterArrow reports the trailing arrow shape whose hugged
234
+ // layout keeps the opening line through `=>` but leaves the call's closing
235
+ // parenthesis on its own line after the expression body.
236
+ func lastArgumentBreaksAfterArrow(args []*shimast.Node) bool {
237
+ if len(args) == 0 || args[len(args)-1] == nil ||
238
+ args[len(args)-1].Kind != shimast.KindArrowFunction {
239
+ return false
240
+ }
241
+ arrow := args[len(args)-1].AsArrowFunction()
242
+ return arrow != nil && arrowBodyBreaksAfterArrow(arrow.Body)
243
+ }
244
+
231
245
  // testCalleePatterns is Prettier's exact testCallCalleePatterns set (the dotted
232
246
  // callee chains its isTestCallCallee matches). Matching the precise set, rather
233
247
  // than "any base with a {only,skip,…} tail", avoids both over-matching
@@ -500,20 +514,17 @@ func anyLeadingItemBreaks(items []Doc) bool {
500
514
  return false
501
515
  }
502
516
 
503
- // shouldHugLastArgument reports whether the final entry of `args` is a
504
- // shape Prettier keeps hugging the closing paren: an object or array
505
- // literal, a function expression, or an arrow function whose body is a
506
- // block, an object literal, or an array literal. Hugging only applies
507
- // when that argument is genuinely the last one; a callback in the
508
- // middle of the list does not trigger the shape.
517
+ // shouldHugLastArgument reports whether the final entry of `args` is a shape
518
+ // Prettier keeps attached to the call's opening line: an object or array
519
+ // literal, a function expression, or an arrow function whose body has an
520
+ // expandable layout. A block/object/array body also hugs the closing paren; a
521
+ // call/conditional/JSX body breaks after `=>` and closes the outer list on its
522
+ // own line. Hugging only applies when the argument is genuinely last.
509
523
  //
510
- // An arrow with any other expression body (`(x) => x.id`) is
511
- // deliberately excluded. Such a body carries no internal break point,
512
- // so the hugging shape a flat `Concat` with no Group would pin the
513
- // whole call to one line even when that line overflows printWidth.
514
- // Routing it through the normal list shape instead lets the argument
515
- // list explode onto its own line when the call does not fit, which is
516
- // what Prettier does.
524
+ // An arrow with any other expression body (`(x) => x.id`) is deliberately
525
+ // excluded. It carries no internal break point, so the hugging shape would pin
526
+ // the whole call to one line even when it overflows printWidth. The normal list
527
+ // shape can instead explode that argument onto its own line.
517
528
  func shouldHugLastArgument(args []*shimast.Node) bool {
518
529
  if len(args) == 0 {
519
530
  return false
@@ -544,11 +555,11 @@ func shouldHugLastArgument(args []*shimast.Node) bool {
544
555
  return true
545
556
  }
546
557
 
547
- // lastArgHuggableShape reports whether `node` is the shape Prettier keeps
548
- // hugging the closing paren: an object/array literal, a function expression,
549
- // or an arrow whose body is a block, object, or array. An expression-bodied
550
- // arrow (`(x) => x.id`) is excluded: it has no internal break point, so the
551
- // flat hugging Concat would pin an overflowing call to one line.
558
+ // lastArgHuggableShape reports whether `node` has a Prettier-style expandable
559
+ // last-argument layout. Objects, arrays, and functions expand internally; an
560
+ // arrow with a call/conditional/JSX body can break immediately after `=>`.
561
+ // Other concise arrows (`(x) => x.id`) have no internal break point and are
562
+ // excluded so an overflowing call can use the ordinary exploded list.
552
563
  func lastArgHuggableShape(last *shimast.Node) bool {
553
564
  switch last.Kind {
554
565
  case shimast.KindFunctionExpression:
@@ -595,14 +606,10 @@ func lastArgHuggableShape(last *shimast.Node) bool {
595
606
  // explodes the whole list. A block body hugs regardless (handled above).
596
607
  return arrow.Type == nil || arrow.Type.Kind != shimast.KindTypeReference
597
608
  }
609
+ return arrowBodyBreaksAfterArrow(body)
598
610
  }
599
611
  // DEFERRED (architectural, not a one-line predicate fix; do not re-add to the
600
612
  // switch above without the supporting layout work):
601
- // - A call/conditional/JSX-expression arrow body. Prettier's couldExpandArg
602
- // hugs it, but ttsc emits the arrow signature (`=> `) verbatim with no
603
- // break point after `=>`, so hugging here would force the FIRST group
604
- // inside the call instead of breaking after `=>` — a different shape, not
605
- // parity. Needs a break-after-`=>` arrow-body layout variant first.
606
613
  // - A trailing `as`/`satisfies` cast wrapping a huggable last argument.
607
614
  // Prettier's couldExpandArg recurses into the cast, but ttsc does not
608
615
  // dispatch KindAsExpression (it prints verbatim), so forceBreakFirstGroup
@@ -0,0 +1,319 @@
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
+ // printControlFlowStatement renders a loop, `if`, `try`, or `switch` by
11
+ // preserving its headers and clause keywords exactly as written while laying
12
+ // out the blocks and statements they control.
13
+ //
14
+ // The preserved gaps keep `format/clause-join` as the owner of header spacing
15
+ // and retain comments without minting new `else`, `catch`, or `finally`
16
+ // tokens. A multi-line preserved gap taints coverage because its original
17
+ // interior columns cannot be re-indented safely.
18
+ //
19
+ // A BRACELESS body is left alone entirely. Prettier indents it one level past
20
+ // the header, which is layout the block-depth model has no frame for, and
21
+ // `format/indent` cedes it for exactly that reason (`cededUnderBracelessBody`).
22
+ //
23
+ // The second return value is the `covered` flag: see PrintNode.
24
+ func printControlFlowStatement(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
25
+ if node == nil {
26
+ return Doc{}, true
27
+ }
28
+ switch node.Kind {
29
+ case shimast.KindForStatement,
30
+ shimast.KindForOfStatement,
31
+ shimast.KindForInStatement,
32
+ shimast.KindWhileStatement:
33
+ body := singleControlFlowBody(node)
34
+ if body == nil || body.Kind != shimast.KindBlock {
35
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
36
+ }
37
+ return printNodeAroundChildren(ctx, node, []*shimast.Node{body})
38
+ case shimast.KindIfStatement:
39
+ children, ok := printableIfChildren(node)
40
+ if !ok {
41
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
42
+ }
43
+ return printNodeAroundChildren(ctx, node, children)
44
+ case shimast.KindTryStatement:
45
+ children, ok := printableTryChildren(node)
46
+ if !ok {
47
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
48
+ }
49
+ return printNodeAroundChildren(ctx, node, children)
50
+ case shimast.KindSwitchStatement:
51
+ stmt := node.AsSwitchStatement()
52
+ if stmt == nil || stmt.CaseBlock == nil {
53
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
54
+ }
55
+ return printNodeAroundChildren(ctx, node, []*shimast.Node{stmt.CaseBlock})
56
+ }
57
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
58
+ }
59
+
60
+ // singleControlFlowBody returns the one statement a loop controls.
61
+ func singleControlFlowBody(node *shimast.Node) *shimast.Node {
62
+ switch node.Kind {
63
+ case shimast.KindForStatement:
64
+ if stmt := node.AsForStatement(); stmt != nil {
65
+ return stmt.Statement
66
+ }
67
+ case shimast.KindForOfStatement, shimast.KindForInStatement:
68
+ if stmt := node.AsForInOrOfStatement(); stmt != nil {
69
+ return stmt.Statement
70
+ }
71
+ case shimast.KindWhileStatement:
72
+ if stmt := node.AsWhileStatement(); stmt != nil {
73
+ return stmt.Statement
74
+ }
75
+ }
76
+ return nil
77
+ }
78
+
79
+ // printableIfChildren returns the branches an if-printer may lay out. Every
80
+ // concrete branch must be a block; an `else if` is accepted recursively because
81
+ // its own printer applies the same invariant. A braceless branch makes the
82
+ // whole statement fall back to verbatim rather than formatting only one half.
83
+ func printableIfChildren(node *shimast.Node) ([]*shimast.Node, bool) {
84
+ if node == nil {
85
+ return nil, false
86
+ }
87
+ stmt := node.AsIfStatement()
88
+ if stmt == nil || stmt.ThenStatement == nil || stmt.ThenStatement.Kind != shimast.KindBlock {
89
+ return nil, false
90
+ }
91
+ children := []*shimast.Node{stmt.ThenStatement}
92
+ if stmt.ElseStatement == nil {
93
+ return children, true
94
+ }
95
+ if stmt.ElseStatement.Kind == shimast.KindBlock {
96
+ return append(children, stmt.ElseStatement), true
97
+ }
98
+ if stmt.ElseStatement.Kind == shimast.KindIfStatement {
99
+ if _, ok := printableIfChildren(stmt.ElseStatement); ok {
100
+ return append(children, stmt.ElseStatement), true
101
+ }
102
+ }
103
+ return nil, false
104
+ }
105
+
106
+ // printableTryChildren returns the try, catch, and finally blocks in source
107
+ // order. The catch declaration stays in the preserved gap before its block.
108
+ func printableTryChildren(node *shimast.Node) ([]*shimast.Node, bool) {
109
+ stmt := node.AsTryStatement()
110
+ if stmt == nil || stmt.TryBlock == nil {
111
+ return nil, false
112
+ }
113
+ children := []*shimast.Node{stmt.TryBlock}
114
+ if stmt.CatchClause != nil {
115
+ clause := stmt.CatchClause.AsCatchClause()
116
+ if clause == nil || clause.Block == nil {
117
+ return nil, false
118
+ }
119
+ children = append(children, clause.Block)
120
+ }
121
+ if stmt.FinallyBlock != nil {
122
+ children = append(children, stmt.FinallyBlock)
123
+ }
124
+ return children, true
125
+ }
126
+
127
+ // printNodeAroundChildren preserves the source gaps around selected children
128
+ // and dispatches those children through the structured printer. The selected
129
+ // ranges must be non-overlapping and in source order.
130
+ func printNodeAroundChildren(ctx *PrintContext, node *shimast.Node, children []*shimast.Node) (Doc, bool) {
131
+ start := shimscanner.SkipTrivia(ctx.Source, node.Pos())
132
+ end := node.End()
133
+ if start < 0 || end < start || end > len(ctx.Source) {
134
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
135
+ }
136
+ cursor := start
137
+ covered := true
138
+ parts := make([]Doc, 0, len(children)*2+1)
139
+ for _, child := range children {
140
+ if child == nil {
141
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
142
+ }
143
+ childStart := shimscanner.SkipTrivia(ctx.Source, child.Pos())
144
+ childEnd := child.End()
145
+ if childStart < cursor || childEnd < childStart || childEnd > end {
146
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
147
+ }
148
+ gap := ctx.Source[cursor:childStart]
149
+ covered = covered && !strings.Contains(gap, "\n")
150
+ parts = append(parts, Text(gap))
151
+ childDoc, childCovered := PrintNode(ctx, child)
152
+ covered = covered && childCovered
153
+ parts = append(parts, childDoc)
154
+ cursor = childEnd
155
+ }
156
+ suffix := ctx.Source[cursor:end]
157
+ covered = covered && !strings.Contains(suffix, "\n")
158
+ parts = append(parts, Text(suffix))
159
+ return Concat(parts...), covered
160
+ }
161
+
162
+ // printSwitchCaseBlock renders the braces and clauses of a switch. Clauses are
163
+ // indented once beneath the switch; their statements are indented once more.
164
+ func printSwitchCaseBlock(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
165
+ if node == nil {
166
+ return Doc{}, true
167
+ }
168
+ block := node.AsCaseBlock()
169
+ start := shimscanner.SkipTrivia(ctx.Source, node.Pos())
170
+ end := node.End()
171
+ if block == nil || block.Clauses == nil || start < 0 || end <= start || end > len(ctx.Source) ||
172
+ ctx.Source[start] != '{' || ctx.Source[end-1] != '}' {
173
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
174
+ }
175
+ if nodeHasNonItemComment(ctx, node, block.Clauses.Nodes) {
176
+ return verbatim(ctx, node), false
177
+ }
178
+ clauses := block.Clauses.Nodes
179
+ if len(clauses) == 0 {
180
+ return Text("{}"), true
181
+ }
182
+ docs := make([]Doc, 0, len(clauses))
183
+ covered := true
184
+ for _, clause := range clauses {
185
+ if clause == nil {
186
+ return verbatim(ctx, node), false
187
+ }
188
+ doc, childCovered := printSwitchClause(ctx, clause)
189
+ covered = covered && childCovered
190
+ docs = append(docs, doc)
191
+ }
192
+ body := joinPrintedNodes(ctx, clauses, docs)
193
+ return Concat(
194
+ Text("{"),
195
+ Indent(ctx.indentUnit(), Hardline(), body),
196
+ Hardline(),
197
+ Text("}"),
198
+ ), covered
199
+ }
200
+
201
+ // printSwitchClause renders `case` or `default` plus its statement list. A
202
+ // block written on the clause header line stays there, matching Prettier's
203
+ // `case X: {` layout; ordinary statements begin on the next indented line.
204
+ func printSwitchClause(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
205
+ if node == nil {
206
+ return Doc{}, true
207
+ }
208
+ clause := node.AsCaseOrDefaultClause()
209
+ start := shimscanner.SkipTrivia(ctx.Source, node.Pos())
210
+ end := node.End()
211
+ if clause == nil || clause.Statements == nil || start < 0 || end < start || end > len(ctx.Source) {
212
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
213
+ }
214
+ if nodeHasNonItemComment(ctx, node, clause.Statements.Nodes) {
215
+ return verbatim(ctx, node), false
216
+ }
217
+ statements := clause.Statements.Nodes
218
+ if len(statements) == 0 {
219
+ source := strings.TrimRight(ctx.Source[start:end], " \t\r\n")
220
+ return Text(source), !strings.Contains(source, "\n")
221
+ }
222
+ if statements[0] == nil {
223
+ return verbatim(ctx, node), false
224
+ }
225
+ firstStart := shimscanner.SkipTrivia(ctx.Source, statements[0].Pos())
226
+ if firstStart < start || firstStart > end {
227
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
228
+ }
229
+ header := strings.TrimRight(ctx.Source[start:firstStart], " \t\r\n")
230
+ covered := !strings.Contains(header, "\n")
231
+ docs := make([]Doc, 0, len(statements))
232
+ for _, statement := range statements {
233
+ if statement == nil {
234
+ return verbatim(ctx, node), false
235
+ }
236
+ doc, childCovered := PrintNode(ctx, statement)
237
+ covered = covered && childCovered
238
+ docs = append(docs, doc)
239
+ }
240
+ if statements[0] != nil && statements[0].Kind == shimast.KindBlock &&
241
+ lineStartOffset(ctx.Source, start) == lineStartOffset(ctx.Source, firstStart) {
242
+ parts := []Doc{Text(header), Text(" "), docs[0]}
243
+ if len(statements) > 1 {
244
+ rest := joinPrintedNodes(ctx, statements[1:], docs[1:])
245
+ parts = append(parts, Indent(ctx.indentUnit(), Hardline(), rest))
246
+ }
247
+ return Concat(parts...), covered
248
+ }
249
+ return Concat(
250
+ Text(header),
251
+ Indent(ctx.indentUnit(), Hardline(), joinPrintedNodes(ctx, statements, docs)),
252
+ ), covered
253
+ }
254
+
255
+ // printVariableStatement dispatches every initializer while preserving names,
256
+ // type annotations, commas, modifiers, and the terminator verbatim.
257
+ func printVariableStatement(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
258
+ if node == nil {
259
+ return Doc{}, true
260
+ }
261
+ stmt := node.AsVariableStatement()
262
+ if stmt == nil || stmt.DeclarationList == nil {
263
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
264
+ }
265
+ list := stmt.DeclarationList.AsVariableDeclarationList()
266
+ if list == nil || list.Declarations == nil {
267
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
268
+ }
269
+ children := make([]*shimast.Node, 0, len(list.Declarations.Nodes))
270
+ for _, declaration := range list.Declarations.Nodes {
271
+ if declaration == nil {
272
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
273
+ }
274
+ value := declaration.AsVariableDeclaration()
275
+ if value != nil && value.Initializer != nil {
276
+ children = append(children, value.Initializer)
277
+ }
278
+ }
279
+ if len(children) == 0 {
280
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
281
+ }
282
+ return printNodeAroundChildren(ctx, node, children)
283
+ }
284
+
285
+ // printThrowStatement preserves `throw` and its terminator while dispatching
286
+ // the thrown expression so a nested callback or literal can reflow.
287
+ func printThrowStatement(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
288
+ if node == nil {
289
+ return Doc{}, true
290
+ }
291
+ stmt := node.AsThrowStatement()
292
+ if stmt == nil || stmt.Expression == nil {
293
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
294
+ }
295
+ return printNodeAroundChildren(ctx, node, []*shimast.Node{stmt.Expression})
296
+ }
297
+
298
+ // joinPrintedNodes joins source-ordered nodes with hard lines while retaining
299
+ // one author-written blank line. An empty-statement ASI guard remains glued to
300
+ // the statement it protects, mirroring printBlock's established behavior.
301
+ func joinPrintedNodes(ctx *PrintContext, nodes []*shimast.Node, docs []Doc) Doc {
302
+ parts := make([]Doc, 0, len(docs)*2)
303
+ for i, doc := range docs {
304
+ if i > 0 {
305
+ if nodes[i-1] != nil && nodes[i] != nil &&
306
+ nodes[i-1].Kind == shimast.KindEmptyStatement &&
307
+ !rangeHasNewline(ctx.Source, nodes[i-1].End(), shimscanner.SkipTrivia(ctx.Source, nodes[i].Pos())) {
308
+ parts = append(parts, doc)
309
+ continue
310
+ }
311
+ if nodes[i-1] != nil && nodes[i] != nil && blankLineBetweenStatements(ctx.Source, nodes[i-1].End(), nodes[i].Pos()) {
312
+ parts = append(parts, Literalline())
313
+ }
314
+ parts = append(parts, Hardline())
315
+ }
316
+ parts = append(parts, doc)
317
+ }
318
+ return Concat(parts...)
319
+ }