@ttsc/lint 0.19.2 → 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
@@ -158,18 +158,21 @@ func isClauseGapByte(c byte) bool {
158
158
  }
159
159
 
160
160
  // visualWidth returns the display-column width of `s`: a tab expands to a flat
161
- // `tabWidth` columns and every other rune is charged its display width via
162
- // runeWidth (combining marks 0, wide East-Asian/emoji 2), matching displayWidth.
163
- // The only approximation left is the flat tab expansion (no tab-stop rounding),
164
- // which never changes a real clause-join decision.
161
+ // `tabWidth` columns and everything else is charged by displayWidth, which is
162
+ // Prettier's own measurement. The only approximation left is the flat tab
163
+ // expansion (no tab-stop rounding), which never changes a real clause-join
164
+ // decision.
165
+ //
166
+ // Split on tabs rather than walked per rune, because displayWidth is not a sum
167
+ // over runes: an emoji sequence is measured whole, and splitting it would
168
+ // charge its parts.
165
169
  func visualWidth(s string, tabWidth int) int {
166
170
  width := 0
167
- for _, r := range s {
168
- if r == '\t' {
171
+ for i, segment := range strings.Split(s, "\t") {
172
+ if i > 0 {
169
173
  width += tabWidth
170
- continue
171
174
  }
172
- width += runeWidth(r)
175
+ width += displayWidth(segment)
173
176
  }
174
177
  return width
175
178
  }
@@ -150,13 +150,69 @@ func (formatIndent) Check(ctx *Context, node *shimast.Node) {
150
150
  return
151
151
  }
152
152
  lineStart := lineStartOffset(src, closeBrace)
153
- // The `}` must be the first non-whitespace byte on its line; a brace
154
- // sharing a line with content (`} else {`, `{ x }`) is not this
155
- // rule's to move.
153
+ // A `}` sharing its line with content splits into two cases, and
154
+ // abstaining on both is what let the cascade converge on a malformed
155
+ // block. When the block opened on this same line it is a one-line block
156
+ // the cascade left whole (`{ x }`), and the brace is not this rule's to
157
+ // move. When the block opened earlier, the statement split already broke
158
+ // the body onto its own lines and stranded the brace at the end of the
159
+ // last statement — a hybrid Prettier never produces, and a fixed point,
160
+ // because nothing else claimed that brace. This rule takes it, so the two
161
+ // stay edit-disjoint within a pass instead of both abstaining.
162
+ stranded := false
156
163
  for i := lineStart; i < closeBrace; i++ {
157
164
  if src[i] != ' ' && src[i] != '\t' {
165
+ stranded = true
166
+ break
167
+ }
168
+ }
169
+ if stranded {
170
+ // An empty body is `{}` in Prettier however its header wrapped, so a
171
+ // `}` whose only preceding content on the line is its own `{` is never
172
+ // this rule's to move. Checked first, and independently of the frame
173
+ // span below: `format/declaration-header` deliberately GLUES `{}` to
174
+ // the last line of a broken class or interface header, and that head
175
+ // starts lines above the brace, so the span test alone would unglue on
176
+ // the next pass exactly what that rule just gathered.
177
+ if bodyIsEmptyAtBrace(src, lineStart, closeBrace) {
178
+ return
179
+ }
180
+ // A frame that begins on the brace's own line is still whole
181
+ // (`{ x }`, `class C { m() {} }` before anything has split): the
182
+ // cascade has not broken the body out yet, so there is nothing for the
183
+ // brace to be consistent with. Once another pass moves the body onto
184
+ // its own lines the frame spans lines, this test stops holding, and
185
+ // the brace is claimed then.
186
+ //
187
+ // Measured on the frame node, which for a Block, ModuleBlock,
188
+ // CaseBlock, or type literal IS its `{`, and for a class or interface
189
+ // is the first byte of the declaration. The declaration start is never
190
+ // below its own `{`, so a body that has been broken out still reads as
191
+ // spanning; the one shape where the two differ — a wrapped header over
192
+ // a one-line body — is a shape Prettier expands as well.
193
+ if start := shimscanner.SkipTrivia(src, block.Pos()); start < 0 ||
194
+ start > len(src) || lineStartOffset(src, start) == lineStart {
195
+ return
196
+ }
197
+ if indentCededToReflow(block) || cededUnderBracelessBody(block) ||
198
+ typeLiteralIndentCeded(src, block, ownerDepth, layout) ||
199
+ cededUnderWrappedFunctionExpression(src, block) ||
200
+ cededByChainedArrowAncestor(block) {
158
201
  return
159
202
  }
203
+ // Replace the run of spaces and tabs before the brace rather than
204
+ // inserting beside it, so a re-run finds the brace already alone on its
205
+ // line and this branch never fires twice.
206
+ gap := closeBrace
207
+ for gap > lineStart && (src[gap-1] == ' ' || src[gap-1] == '\t') {
208
+ gap--
209
+ }
210
+ edits = append(edits, TextEdit{
211
+ Pos: gap,
212
+ End: closeBrace,
213
+ Text: layout.eol + layout.indent(ownerDepth),
214
+ })
215
+ return
160
216
  }
161
217
  // indentCededToReflow walks block.Parent upward, the same ancestor
162
218
  // chain a body statement would, so a callback / expression-nested
@@ -186,11 +242,21 @@ func (formatIndent) Check(ctx *Context, node *shimast.Node) {
186
242
  return
187
243
  }
188
244
  want := layout.indent(depth)
245
+ // A member or clause label sharing a line with the `{` that opened its
246
+ // body, or with the member before it, is the other half of the stranded
247
+ // brace: nothing else claims it, so a flat `class C { m() { … } }` or
248
+ // `switch (n) { case 1: … }` had no first edit and the whole cascade
249
+ // stalled on it. Breaking it out is what lets the statement split and the
250
+ // brace pass see the multi-line frame they act on.
251
+ pos := shimscanner.SkipTrivia(src, header.Pos())
252
+ if breakOntoOwnLine(src, header, pos, depth, want, layout, &edits) {
253
+ return
254
+ }
189
255
  decorators := header.Decorators()
190
256
  if len(decorators) == 0 {
191
257
  // Undecorated member: header.Pos() is the declaration's first byte,
192
258
  // so its line is the only one to align.
193
- reindentHeaderLine(src, shimscanner.SkipTrivia(src, header.Pos()), want, &edits)
259
+ reindentHeaderLine(src, pos, want, &edits)
194
260
  return
195
261
  }
196
262
  // A decorated member spans multiple physical lines when its decorators
@@ -563,6 +629,91 @@ func reindentHeaderLine(src string, pos int, want string, edits *[]TextEdit) {
563
629
  *edits = append(*edits, TextEdit{Pos: lineStart, End: pos, Text: want})
564
630
  }
565
631
 
632
+ // bodyIsEmptyAtBrace reports whether the only thing before `closeBrace` on its
633
+ // line, past horizontal whitespace, is the `{` that opened the same body.
634
+ func bodyIsEmptyAtBrace(src string, lineStart int, closeBrace int) bool {
635
+ probe := closeBrace
636
+ for probe > lineStart && (src[probe-1] == ' ' || src[probe-1] == '\t') {
637
+ probe--
638
+ }
639
+ return probe > 0 && src[probe-1] == '{'
640
+ }
641
+
642
+ // frameLineIsWhereDepthSays reports whether the line a frame opens on is
643
+ // indented to exactly the depth the block model computes for it. It is
644
+ // typeLiteralIndentCeded's test, generalized: when it does not hold, some other
645
+ // owner (a printer reflow, a wrapped initializer, a chain continuation) placed
646
+ // the frame, and a member indented to `layout.indent(memberDepth)` would land
647
+ // at a column unrelated to its own `{`.
648
+ func frameLineIsWhereDepthSays(
649
+ src string,
650
+ frame *shimast.Node,
651
+ frameDepth int,
652
+ layout formatLayout,
653
+ ) bool {
654
+ if frame == nil {
655
+ return false
656
+ }
657
+ open := shimscanner.SkipTrivia(src, frame.Pos())
658
+ if open < 0 || open > len(src) {
659
+ return false
660
+ }
661
+ lineStart := lineStartOffset(src, open)
662
+ i := lineStart
663
+ for i < len(src) && (src[i] == ' ' || src[i] == '\t') {
664
+ i++
665
+ }
666
+ return src[lineStart:i] == layout.indent(frameDepth)
667
+ }
668
+
669
+ // breakOntoOwnLine moves a member or clause header that shares its line with
670
+ // earlier content down to a line of its own at `want`, and reports whether it
671
+ // emitted that edit. It replaces the run of spaces and tabs before the header
672
+ // rather than inserting beside it, so a re-run finds the header already alone
673
+ // on its line and this never fires twice.
674
+ //
675
+ // A type-literal member is exempt. Prettier expands a class or interface body
676
+ // unconditionally, but an object TYPE keeps the author's shape when the source
677
+ // wrote no line break after `{` — `type T = { a: number }` stays one line — so
678
+ // breaking its members out would reformat conforming source. The type
679
+ // literal's own `}` is not exempt: a member already on its own line above a
680
+ // stranded `}` is a shape Prettier does not produce either way.
681
+ //
682
+ // A frame whose own line is not at the column the depth model computes is
683
+ // exempt too. `want` is derived from block nesting alone, so under a wrapped
684
+ // initializer (`const C: Ctor =\n class { m() {} };`) it names a column
685
+ // shallower than the `{` the member belongs to, and breaking there would place
686
+ // the member left of its own frame.
687
+ func breakOntoOwnLine(
688
+ src string,
689
+ header *shimast.Node,
690
+ pos int,
691
+ depth int,
692
+ want string,
693
+ layout formatLayout,
694
+ edits *[]TextEdit,
695
+ ) bool {
696
+ if pos < 0 || pos > len(src) {
697
+ return false
698
+ }
699
+ if header.Parent != nil && header.Parent.Kind == shimast.KindTypeLiteral {
700
+ return false
701
+ }
702
+ if !frameLineIsWhereDepthSays(src, header.Parent, depth-1, layout) {
703
+ return false
704
+ }
705
+ lineStart := lineStartOffset(src, pos)
706
+ gap := pos
707
+ for gap > lineStart && (src[gap-1] == ' ' || src[gap-1] == '\t') {
708
+ gap--
709
+ }
710
+ if gap == lineStart {
711
+ return false
712
+ }
713
+ *edits = append(*edits, TextEdit{Pos: gap, End: pos, Text: layout.eol + want})
714
+ return true
715
+ }
716
+
566
717
  // memberDeclarationStart returns the byte offset where a decorated member's
567
718
  // actual declaration begins (just past its last leading decorator), or -1
568
719
  // when the member has no decorators. The declaration start is the first
@@ -157,15 +157,13 @@ func (formatPrintWidth) Check(ctx *Context, node *shimast.Node) {
157
157
  // the per-byte comment scan in hasNonChildComments. Charging that
158
158
  // cost only on nodes that actually overflow keeps the hot path on
159
159
  // well-formatted code allocation- and scan-free.
160
- // A call/new with two or more callback arguments explodes regardless of width
161
- // (Prettier's multiple-callback rule), and an array of same-kind multi-child
162
- // arrays/objects explodes under Prettier's shouldBreak heuristic even when
163
- // such a node is nested inside an otherwise-fitting call/new/array. So a flat
164
- // one-line node containing either shape still needs a reflow. fastPathForcesBreak
165
- // walks the reflow subtree; skip the fast path for it so the printer's
166
- // ForceBreak produces the exploded shape. Everything else that fits flat is
167
- // byte-identical after reflow, so the fast path stands.
168
- if !fastPathForcesBreak(node) &&
160
+ // Some layouts break regardless of width: function composition, same-kind
161
+ // nested arrays/objects, and any non-empty expression-position block. Such a
162
+ // shape can sit below a fitting call, array, conditional, object member, or
163
+ // parenthesized expression, so fastPathForcesBreak walks the whole subtree the
164
+ // structured printer can reach. Skip the fast path when it finds one; every
165
+ // other fitting flat node is byte-identical after reflow.
166
+ if !fastPathForcesBreak(node, src) &&
169
167
  !sliceContainsNewline(src, start, end) &&
170
168
  printOpts.StartingColumn+displayWidth(src[start:end])+trailingWidth <= printOpts.PrintWidth {
171
169
  return
@@ -315,15 +313,7 @@ func trailingLineWidth(src string, end int, tabWidth int) int {
315
313
  tabWidth = 2
316
314
  }
317
315
  lineEnd := trailingSuffixEnd(src, end)
318
- col := 0
319
- for _, r := range src[end:lineEnd] {
320
- if r == '\t' {
321
- col += tabWidth - (col % tabWidth)
322
- } else {
323
- col += runeWidth(r)
324
- }
325
- }
326
- return col
316
+ return displayWidthFromColumn(src[end:lineEnd], tabWidth, 0)
327
317
  }
328
318
 
329
319
  // trailingSuffixEnd returns the byte offset where the node's un-movable
@@ -376,14 +366,9 @@ func maxLineWidth(text string, startingColumn, trailingWidth, tabWidth int) int
376
366
  lines := strings.Split(text, "\n")
377
367
  widest := 0
378
368
  for i, line := range lines {
379
- width := 0
380
- for _, r := range line {
381
- if r == '\t' {
382
- width += tabWidth - (width % tabWidth)
383
- } else if r != '\r' {
384
- width += runeWidth(r)
385
- }
386
- }
369
+ // A `\r` left by a CRLF split charges nothing: displayWidth reads it as the
370
+ // control character it is, exactly as Prettier's own loop does.
371
+ width := displayWidthFromColumn(line, tabWidth, 0)
387
372
  if i == 0 {
388
373
  width += startingColumn
389
374
  }
@@ -409,15 +394,7 @@ func leadingColumn(src string, pos int, tabWidth int) int {
409
394
  tabWidth = 2
410
395
  }
411
396
  lineStart := lineStartOffset(src, pos)
412
- col := 0
413
- for _, r := range src[lineStart:pos] {
414
- if r == '\t' {
415
- col += tabWidth - (col % tabWidth)
416
- } else {
417
- col += runeWidth(r)
418
- }
419
- }
420
- return col
397
+ return displayWidthFromColumn(src[lineStart:pos], tabWidth, 0)
421
398
  }
422
399
 
423
400
  // lineLeadingIndent returns the visual column of the first non-blank
@@ -1,25 +1,29 @@
1
1
  package linthost
2
2
 
3
3
  import (
4
+ "strconv"
5
+
4
6
  shimast "github.com/microsoft/typescript-go/shim/ast"
5
7
  shimscanner "github.com/microsoft/typescript-go/shim/scanner"
6
8
  )
7
9
 
8
- // formatQuoteProps normalizes quoting of object-literal property keys,
9
- // mirroring Prettier's `quoteProps`:
10
+ // formatQuoteProps normalizes quoting of object keys and method/type-member
11
+ // names, mirroring Prettier's `quoteProps` except for the documented semantic
12
+ // preservation of `__proto__` and non-ASCII identifier keys:
10
13
  //
11
14
  // - "as-needed" (default): drop the quotes from a string key that is a
12
15
  // valid identifier and not a numeric-looking key. `{ "foo": 1 }` becomes
13
16
  // `{ foo: 1 }`; `{ "bar-baz": 1 }` and `{ "123": 1 }` keep their quotes.
14
- // - "consistent": if ANY key in the object needs quotes, leave the object
15
- // alone; otherwise unquote every removable key.
17
+ // - "consistent": if ANY object key needs quotes, quote its removable
18
+ // identifier siblings; otherwise unquote every removable key.
16
19
  // - "preserve": never change quoting.
17
20
  //
18
21
  // Only quoted keys the rule can SAFELY unquote are ever touched: a string
19
22
  // whose content is a plain ASCII identifier (letters, `_`, `$`, digits after
20
23
  // the first) with no escapes. A key with escapes, unicode, a leading digit
21
24
  // (numeric-looking), or any non-identifier byte is left quoted in every
22
- // mode, so the rule can never produce an invalid key. Idempotent.
25
+ // mode, so the rule can never produce an invalid key. Class fields remain
26
+ // outside the surface because Prettier preserves their quoted spelling.
23
27
  type formatQuoteProps struct{ optionsRule }
24
28
 
25
29
  type formatQuotePropsOptions struct {
@@ -30,15 +34,21 @@ func (formatQuoteProps) Name() string { return "format/quote-props" }
30
34
  func (formatQuoteProps) IsFormat() bool { return true }
31
35
 
32
36
  func (formatQuoteProps) Visits() []shimast.Kind {
33
- return []shimast.Kind{shimast.KindObjectLiteralExpression}
37
+ return []shimast.Kind{
38
+ shimast.KindObjectLiteralExpression,
39
+ shimast.KindClassDeclaration,
40
+ shimast.KindClassExpression,
41
+ shimast.KindInterfaceDeclaration,
42
+ shimast.KindTypeLiteral,
43
+ }
34
44
  }
35
45
 
36
46
  func (formatQuoteProps) Check(ctx *Context, node *shimast.Node) {
37
47
  if ctx == nil || ctx.File == nil || node == nil {
38
48
  return
39
49
  }
40
- obj := node.AsObjectLiteralExpression()
41
- if obj == nil || obj.Properties == nil {
50
+ members, isObject := quotePropsMembers(node)
51
+ if len(members) == 0 {
42
52
  return
43
53
  }
44
54
  var opts formatQuotePropsOptions
@@ -56,27 +66,39 @@ func (formatQuoteProps) Check(ctx *Context, node *shimast.Node) {
56
66
  src := ctx.File.Text()
57
67
 
58
68
  // removable collects each quoted key that is safely unquotable, paired
59
- // with its bare identifier. anyMustStayQuoted records whether some quoted
60
- // key cannot be unquoted (drives "consistent").
69
+ // with its bare identifier. bare collects identifier keys that need quotes
70
+ // when a mixed object is normalized under `consistent`.
61
71
  type removableKey struct {
62
72
  start, end int
63
73
  ident string
64
74
  }
75
+ type bareKey struct {
76
+ start, end int
77
+ text string
78
+ }
65
79
  var removable []removableKey
80
+ var bare []bareKey
66
81
  anyMustStayQuoted := false
67
82
 
68
- for _, prop := range obj.Properties.Nodes {
83
+ for _, prop := range members {
69
84
  if prop == nil {
70
85
  continue
71
86
  }
72
87
  name := propertyKeyName(prop)
73
- if name == nil || name.Kind != shimast.KindStringLiteral {
88
+ if name == nil {
74
89
  continue
75
90
  }
76
91
  ks := shimscanner.SkipTrivia(src, name.Pos())
77
92
  ke := name.End()
78
93
  if ks < 0 || ke <= ks || ke > len(src) {
79
- return // give up on the whole object rather than risk a partial edit
94
+ return // give up on the whole holder rather than risk a partial edit
95
+ }
96
+ if name.Kind == shimast.KindIdentifier {
97
+ bare = append(bare, bareKey{start: ks, end: ke, text: src[ks:ke]})
98
+ continue
99
+ }
100
+ if name.Kind != shimast.KindStringLiteral {
101
+ continue
80
102
  }
81
103
  ident := unquotableIdentifier(src[ks:ke])
82
104
  if ident == "" {
@@ -86,19 +108,22 @@ func (formatQuoteProps) Check(ctx *Context, node *shimast.Node) {
86
108
  removable = append(removable, removableKey{start: ks, end: ke, ident: ident})
87
109
  }
88
110
 
89
- if len(removable) == 0 {
90
- return
111
+ var edits []TextEdit
112
+ // Prettier's consistency rule applies to object literals only. Class and
113
+ // type members still follow the as-needed direction because they do not
114
+ // form an object-key group in Prettier's printer.
115
+ if mode == "consistent" && isObject && anyMustStayQuoted {
116
+ for _, key := range bare {
117
+ edits = append(edits, TextEdit{Pos: key.start, End: key.end, Text: strconv.Quote(key.text)})
118
+ }
119
+ } else {
120
+ for _, key := range removable {
121
+ edits = append(edits, TextEdit{Pos: key.start, End: key.end, Text: key.ident})
122
+ }
91
123
  }
92
- // In "consistent" mode, if any key must stay quoted, the whole object is
93
- // kept quoted, so unquote nothing.
94
- if mode == "consistent" && anyMustStayQuoted {
124
+ if len(edits) == 0 {
95
125
  return
96
126
  }
97
-
98
- var edits []TextEdit
99
- for _, k := range removable {
100
- edits = append(edits, TextEdit{Pos: k.start, End: k.end, Text: k.ident})
101
- }
102
127
  ctx.ReportRangeFix(
103
128
  edits[0].Pos,
104
129
  edits[0].End,
@@ -107,8 +132,41 @@ func (formatQuoteProps) Check(ctx *Context, node *shimast.Node) {
107
132
  )
108
133
  }
109
134
 
110
- // propertyKeyName returns the static name node of an object-literal property
111
- // (assignment, method, or accessor), or nil when it has no static name.
135
+ // quotePropsMembers returns the members whose static names are governed by
136
+ // Prettier's quoteProps option. Class fields are intentionally excluded:
137
+ // Prettier preserves their quoted spelling, while class methods, interface
138
+ // members, and type-literal members follow the normal as-needed rule.
139
+ func quotePropsMembers(node *shimast.Node) ([]*shimast.Node, bool) {
140
+ if node == nil {
141
+ return nil, false
142
+ }
143
+ switch node.Kind {
144
+ case shimast.KindObjectLiteralExpression:
145
+ if obj := node.AsObjectLiteralExpression(); obj != nil && obj.Properties != nil {
146
+ return obj.Properties.Nodes, true
147
+ }
148
+ case shimast.KindClassDeclaration:
149
+ if decl := node.AsClassDeclaration(); decl != nil && decl.Members != nil {
150
+ return decl.Members.Nodes, false
151
+ }
152
+ case shimast.KindClassExpression:
153
+ if expr := node.AsClassExpression(); expr != nil && expr.Members != nil {
154
+ return expr.Members.Nodes, false
155
+ }
156
+ case shimast.KindInterfaceDeclaration:
157
+ if decl := node.AsInterfaceDeclaration(); decl != nil && decl.Members != nil {
158
+ return decl.Members.Nodes, false
159
+ }
160
+ case shimast.KindTypeLiteral:
161
+ if literal := node.AsTypeLiteralNode(); literal != nil && literal.Members != nil {
162
+ return literal.Members.Nodes, false
163
+ }
164
+ }
165
+ return nil, false
166
+ }
167
+
168
+ // propertyKeyName returns the static name node of a member governed by
169
+ // quoteProps, or nil when the member has no static name or is a class field.
112
170
  func propertyKeyName(prop *shimast.Node) *shimast.Node {
113
171
  switch prop.Kind {
114
172
  case shimast.KindPropertyAssignment:
@@ -117,7 +175,9 @@ func propertyKeyName(prop *shimast.Node) *shimast.Node {
117
175
  }
118
176
  case shimast.KindMethodDeclaration,
119
177
  shimast.KindGetAccessor,
120
- shimast.KindSetAccessor:
178
+ shimast.KindSetAccessor,
179
+ shimast.KindMethodSignature,
180
+ shimast.KindPropertySignature:
121
181
  return prop.Name()
122
182
  }
123
183
  return nil
@@ -146,7 +206,8 @@ func unquotableIdentifier(raw string) string {
146
206
  // A bare `__proto__:` key in an object literal is the spec-special
147
207
  // prototype setter (sets [[Prototype]]), whereas a quoted `"__proto__"`
148
208
  // key is an ordinary own data property. Unquoting would change runtime
149
- // semantics, so keep it quoted (Prettier does the same).
209
+ // semantics, so ttsc deliberately keeps it quoted even though Prettier
210
+ // does not.
150
211
  return ""
151
212
  }
152
213
  for i := 0; i < len(inner); i++ {
@@ -61,12 +61,30 @@ func (formatStatementSplit) Check(ctx *Context, node *shimast.Node) {
61
61
  layout := loadFormatLayout(ctx)
62
62
  src := ctx.File.Text()
63
63
  var edits []TextEdit
64
+ scannedCommentLists := map[*shimast.Node]bool{}
65
+ commentLists := map[*shimast.Node]bool{}
64
66
  forEachStatementInList(ctx.File, func(stmt *shimast.Node, depth int) {
65
67
  // Empty statements (`;`) carry no content. Splitting each one onto
66
68
  // its own line only multiplies blank-ish noise, so abstain.
67
69
  if stmt.Kind == shimast.KindEmptyStatement {
68
70
  return
69
71
  }
72
+ // Preflight the whole immediate statement list before splitting any one
73
+ // entry. Otherwise an early statement can move before a later boundary
74
+ // comment makes that later statement abstain, leaving a half-formatted
75
+ // list (`case 1:\n f(); /* keep */ break;`). The structured printer also
76
+ // abstains on this boundary, so statement-split must preserve the same
77
+ // all-or-nothing safety floor.
78
+ if indentCededToReflow(stmt) {
79
+ parent := stmt.Parent
80
+ if parent != nil && !scannedCommentLists[parent] {
81
+ scannedCommentLists[parent] = true
82
+ commentLists[parent] = statementListHasInterStatementComment(src, parent)
83
+ }
84
+ if parent != nil && commentLists[parent] {
85
+ return
86
+ }
87
+ }
70
88
  start := shimscanner.SkipTrivia(src, stmt.Pos())
71
89
  if start <= 0 || start > len(src) {
72
90
  return
@@ -104,6 +122,17 @@ func (formatStatementSplit) Check(ctx *Context, node *shimast.Node) {
104
122
  if stmt.Kind == shimast.KindBlock && firstStatementAfterCaseLabel(stmt) {
105
123
  return
106
124
  }
125
+ // A statement sharing its line with the `{` that opened its own block,
126
+ // where that block sits in expression position, has no brace owner:
127
+ // format/indent cedes such a block's `}` to format/print-width, so
128
+ // breaking the body out here would strand the brace on the last statement
129
+ // and make that hybrid the cascade's fixed point — the one shape neither
130
+ // rule can undo. Leave the block whole and let the printer decide whether
131
+ // it reflows. The two rules read the same predicate, so they cede the same
132
+ // blocks.
133
+ if indentCededToReflow(stmt) && sharesLineWithBlockOpenBrace(src, stmt, start) {
134
+ return
135
+ }
107
136
  // The gap between the previous statement and this one must be pure
108
137
  // whitespace. A `//` or `/*` anywhere from the previous statement's
109
138
  // end to this one would be eaten by the replacement, so abstain. The
@@ -130,6 +159,58 @@ func (formatStatementSplit) Check(ctx *Context, node *shimast.Node) {
130
159
  )
131
160
  }
132
161
 
162
+ // statementListHasInterStatementComment reports whether a block, module,
163
+ // source file, or switch clause has a comment between two consecutive
164
+ // statements. Prefix comments remain owned by the existing per-statement gap
165
+ // guard; suffix comments do not affect a split. Scanning every interior gap up
166
+ // front prevents an edit before one boundary from partially formatting a list
167
+ // whose later boundary is intentionally immutable.
168
+ func statementListHasInterStatementComment(src string, parent *shimast.Node) bool {
169
+ if parent == nil {
170
+ return false
171
+ }
172
+ var statements []*shimast.Node
173
+ switch parent.Kind {
174
+ case shimast.KindSourceFile, shimast.KindBlock, shimast.KindModuleBlock:
175
+ statements = parent.Statements()
176
+ case shimast.KindCaseClause, shimast.KindDefaultClause:
177
+ clause := parent.AsCaseOrDefaultClause()
178
+ if clause != nil && clause.Statements != nil {
179
+ statements = clause.Statements.Nodes
180
+ }
181
+ default:
182
+ return false
183
+ }
184
+ for i := 1; i < len(statements); i++ {
185
+ previous, next := statements[i-1], statements[i]
186
+ if previous == nil || next == nil {
187
+ continue
188
+ }
189
+ nextStart := shimscanner.SkipTrivia(src, next.Pos())
190
+ if gapHasComment(src, previous.End(), nextStart) {
191
+ return true
192
+ }
193
+ }
194
+ return false
195
+ }
196
+
197
+ // sharesLineWithBlockOpenBrace reports whether `start`, the first byte of a
198
+ // statement, sits on the same physical line as the `{` of the Block that
199
+ // directly contains it. It is the test for "this block is still written on one
200
+ // line", which is the state in which splitting the body out would strand the
201
+ // closing brace.
202
+ func sharesLineWithBlockOpenBrace(src string, stmt *shimast.Node, start int) bool {
203
+ block := stmt.Parent
204
+ if block == nil || block.Kind != shimast.KindBlock {
205
+ return false
206
+ }
207
+ open := shimscanner.SkipTrivia(src, block.Pos())
208
+ if open < 0 || open >= len(src) || src[open] != '{' {
209
+ return false
210
+ }
211
+ return lineStartOffset(src, open) == lineStartOffset(src, start)
212
+ }
213
+
133
214
  // gapHasComment reports whether the byte range [start, end) contains the
134
215
  // opening bytes of a `//` or `/*` comment. The split rule abstains when
135
216
  // the inter-statement gap carries a comment so its line-break insertion