@ttsc/lint 0.10.2 → 0.11.0-dev.20260518

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 (113) hide show
  1. package/README.md +172 -33
  2. package/lib/defineConfig.d.ts +55 -0
  3. package/lib/defineConfig.js +39 -0
  4. package/lib/defineConfig.js.map +1 -0
  5. package/lib/index.d.ts +25 -1
  6. package/lib/index.js +622 -2
  7. package/lib/index.js.map +1 -1
  8. package/lib/structures/ITtscLintPlugin.d.ts +53 -0
  9. package/lib/structures/ITtscLintPlugin.js +3 -0
  10. package/lib/structures/ITtscLintPlugin.js.map +1 -0
  11. package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
  12. package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
  13. package/lib/structures/ITtscLintPluginMeta.js +3 -0
  14. package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
  15. package/lib/structures/PluginRuleNames.d.ts +14 -0
  16. package/lib/structures/PluginRuleNames.js +3 -0
  17. package/lib/structures/PluginRuleNames.js.map +1 -0
  18. package/lib/structures/TtscLintConfig.d.ts +24 -8
  19. package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
  20. package/lib/structures/TtscLintConfigEntry.js +3 -0
  21. package/lib/structures/TtscLintConfigEntry.js.map +1 -0
  22. package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
  23. package/lib/structures/TtscLintFormatConfig.js +3 -0
  24. package/lib/structures/TtscLintFormatConfig.js.map +1 -0
  25. package/lib/structures/TtscLintPlugins.d.ts +9 -0
  26. package/lib/structures/TtscLintPlugins.js +3 -0
  27. package/lib/structures/TtscLintPlugins.js.map +1 -0
  28. package/lib/structures/TtscLintRule.d.ts +1 -1
  29. package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
  30. package/lib/structures/TtscLintRuleEntry.js +3 -0
  31. package/lib/structures/TtscLintRuleEntry.js.map +1 -0
  32. package/lib/structures/TtscLintRuleMap.d.ts +44 -0
  33. package/lib/structures/TtscLintRuleMap.js +3 -0
  34. package/lib/structures/TtscLintRuleMap.js.map +1 -0
  35. package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
  36. package/lib/structures/TtscLintRuleOptions.js +3 -0
  37. package/lib/structures/TtscLintRuleOptions.js.map +1 -0
  38. package/lib/structures/defaultFormat.d.ts +31 -0
  39. package/lib/structures/defaultFormat.js +35 -0
  40. package/lib/structures/defaultFormat.js.map +1 -0
  41. package/lib/structures/index.d.ts +10 -0
  42. package/lib/structures/index.js +10 -0
  43. package/lib/structures/index.js.map +1 -1
  44. package/{plugin → linthost}/ast_helpers.go +92 -1
  45. package/{plugin → linthost}/compile.go +1 -1
  46. package/{plugin → linthost}/config.go +583 -66
  47. package/linthost/config_format.go +360 -0
  48. package/linthost/contrib_adapter.go +134 -0
  49. package/{plugin → linthost}/directives.go +1 -1
  50. package/linthost/dispatch.go +65 -0
  51. package/{plugin → linthost}/engine.go +156 -11
  52. package/{plugin → linthost}/eslint_runtime.go +76 -2
  53. package/linthost/fix.go +249 -0
  54. package/linthost/format.go +107 -0
  55. package/{plugin → linthost}/host.go +1 -1
  56. package/linthost/print_dispatch.go +118 -0
  57. package/linthost/print_doc.go +168 -0
  58. package/linthost/print_engine.go +343 -0
  59. package/linthost/print_nodes_array.go +46 -0
  60. package/linthost/print_nodes_call.go +180 -0
  61. package/linthost/print_nodes_imports.go +171 -0
  62. package/linthost/print_nodes_list.go +64 -0
  63. package/linthost/print_nodes_object.go +51 -0
  64. package/{plugin → linthost}/rules_arrays.go +1 -1
  65. package/{plugin → linthost}/rules_console.go +1 -1
  66. package/{plugin → linthost}/rules_debugger.go +1 -1
  67. package/{plugin → linthost}/rules_dupes.go +1 -1
  68. package/{plugin → linthost}/rules_empty.go +1 -1
  69. package/linthost/rules_escape.go +238 -0
  70. package/{plugin → linthost}/rules_eval.go +1 -1
  71. package/{plugin → linthost}/rules_finally.go +1 -1
  72. package/linthost/rules_format_jsdoc.go +230 -0
  73. package/linthost/rules_format_print_width.go +332 -0
  74. package/linthost/rules_format_quotes.go +219 -0
  75. package/linthost/rules_format_semi.go +190 -0
  76. package/linthost/rules_format_sort_imports.go +462 -0
  77. package/linthost/rules_format_trailing_comma.go +460 -0
  78. package/{plugin → linthost}/rules_gap.go +101 -6
  79. package/linthost/rules_imports.go +101 -0
  80. package/{plugin → linthost}/rules_logic.go +55 -3
  81. package/{plugin → linthost}/rules_loops.go +1 -1
  82. package/{plugin → linthost}/rules_misc.go +1 -1
  83. package/linthost/rules_params.go +64 -0
  84. package/{plugin → linthost}/rules_problems.go +10 -2
  85. package/linthost/rules_promise.go +111 -0
  86. package/{plugin → linthost}/rules_protos.go +1 -1
  87. package/{plugin → linthost}/rules_self.go +1 -1
  88. package/{plugin → linthost}/rules_strings.go +1 -1
  89. package/{plugin → linthost}/rules_suggestions.go +51 -9
  90. package/{plugin → linthost}/rules_throw.go +1 -1
  91. package/{plugin → linthost}/rules_ts.go +12 -2
  92. package/{plugin → linthost}/rules_ts_extra.go +90 -4
  93. package/{plugin → linthost}/rules_var.go +36 -5
  94. package/package.json +8 -6
  95. package/plugin/main.go +14 -27
  96. package/rule/astutil/astutil.go +148 -0
  97. package/rule/rule.go +291 -0
  98. package/src/defineConfig.ts +69 -0
  99. package/src/index.ts +807 -3
  100. package/src/structures/ITtscLintPlugin.ts +56 -0
  101. package/src/structures/ITtscLintPluginConfig.ts +86 -5
  102. package/src/structures/ITtscLintPluginMeta.ts +14 -0
  103. package/src/structures/PluginRuleNames.ts +19 -0
  104. package/src/structures/TtscLintConfig.ts +27 -8
  105. package/src/structures/TtscLintConfigEntry.ts +50 -0
  106. package/src/structures/TtscLintFormatConfig.ts +169 -0
  107. package/src/structures/TtscLintPlugins.ts +10 -0
  108. package/src/structures/TtscLintRule.ts +14 -1
  109. package/src/structures/TtscLintRuleEntry.ts +28 -0
  110. package/src/structures/TtscLintRuleMap.ts +52 -0
  111. package/src/structures/TtscLintRuleOptions.ts +162 -0
  112. package/src/structures/defaultFormat.ts +33 -0
  113. package/src/structures/index.ts +10 -0
@@ -0,0 +1,343 @@
1
+ package linthost
2
+
3
+ import "strings"
4
+
5
+ // Printer engine: lay out a Doc tree under a column budget.
6
+ //
7
+ // The algorithm is the same fit-or-break decision Prettier and dprint
8
+ // use, originally from Wadler's "A Prettier Printer" with Lindig's
9
+ // strictness fix:
10
+ //
11
+ // 1. Walk a stack of (indent, mode, doc) frames.
12
+ // 2. When a Group is popped in unknown mode, peek ahead to see whether
13
+ // its flat layout fits within the remaining columns on the current
14
+ // line. The peek uses a separate stack so the main walk is not
15
+ // disturbed.
16
+ // 3. If it fits, the group renders flat: Lines collapse to single
17
+ // spaces, Softlines to nothing, IfBreak picks the flat side.
18
+ // 4. If it does not fit, the group renders broken: Lines and Softlines
19
+ // emit newline+indent, IfBreak picks the break side, every nested
20
+ // Group is measured again under its own remaining-column budget
21
+ // (this is the strictness fix; a naive "broken propagates to all
22
+ // descendants" rule produces uglier output for nested structures).
23
+ // 5. Hardline always emits a newline and forces its enclosing Group
24
+ // into broken mode irrespective of width.
25
+ // 6. LineSuffix output is queued in a local buffer and flushed on the
26
+ // next newline emission, so trailing comments stick to their
27
+ // originating source line.
28
+ //
29
+ // The engine does not modify the input Doc tree, so the same tree can
30
+ // be printed multiple times (e.g. for testing under several widths).
31
+
32
+ // PrintOptions configures the layout pass.
33
+ //
34
+ // PrintWidth is the column budget per line (Prettier's `printWidth`).
35
+ // TabWidth is the indentation increment in columns (Prettier's
36
+ // `tabWidth`). UseTabs swaps space indentation for tab characters; when
37
+ // it is true the printer emits one tab per `TabWidth` columns and falls
38
+ // back to spaces for the remainder, matching dprint's "indent with
39
+ // tabs, align with spaces" convention.
40
+ //
41
+ // EndOfLine is the line terminator: "lf" (default) or "crlf".
42
+ //
43
+ // StartingColumn is the column the first character of the printed
44
+ // output will land in. When the printer is invoked to reflow an
45
+ // interior node (e.g. an object literal that begins partway through
46
+ // its enclosing source line), fit measurement must charge that
47
+ // column against the remaining budget.
48
+ //
49
+ // BaseIndent is the column continuation lines indent to. It is
50
+ // usually the indent of the line containing the printed node — not
51
+ // the column of the node itself. The two differ for "right-hand-side"
52
+ // reflows such as `const x = { … }`: the `{` may live at column 10
53
+ // but its broken-form children should indent to column 2
54
+ // (BaseIndent=0 + indentUnit=2), matching every JS formatter's
55
+ // convention of indenting relative to the surrounding line's left
56
+ // edge.
57
+ //
58
+ // Asymmetry on the first line: the engine never emits an indent for
59
+ // the first character of the printed output. The first line uses
60
+ // `StartingColumn` only — fit measurement charges it, but
61
+ // `writeIndent` is not called. Every newline after that emits
62
+ // `BaseIndent` plus any nested `Indent` contributions, which is what
63
+ // makes the close brace of a reflowed list land at `BaseIndent`
64
+ // while its children sit at `BaseIndent + indentUnit`.
65
+ //
66
+ // Defaults of 0 keep the engine usable for top-of-file reflow without
67
+ // a wrapper.
68
+ type PrintOptions struct {
69
+ PrintWidth int
70
+ TabWidth int
71
+ UseTabs bool
72
+ EndOfLine string
73
+ StartingColumn int
74
+ BaseIndent int
75
+ }
76
+
77
+ // DefaultPrintOptions returns the Prettier defaults: 80-column lines,
78
+ // 2-space indentation, LF line terminators.
79
+ func DefaultPrintOptions() PrintOptions {
80
+ return PrintOptions{PrintWidth: 80, TabWidth: 2, UseTabs: false, EndOfLine: "lf"}
81
+ }
82
+
83
+ // printMode is the per-group choice made by the fit measurement.
84
+ type printMode uint8
85
+
86
+ const (
87
+ modeFlat printMode = iota // group fits flat
88
+ modeBreak // group renders broken
89
+ )
90
+
91
+ // printFrame is one entry on the layout stack.
92
+ type printFrame struct {
93
+ indent int
94
+ mode printMode
95
+ doc Doc
96
+ }
97
+
98
+ // Print renders the doc tree under the supplied options and returns the
99
+ // resulting string. The output never contains a trailing line break the
100
+ // doc tree did not request; trailing whitespace inside text fragments
101
+ // is preserved verbatim.
102
+ func Print(doc Doc, opts PrintOptions) string {
103
+ if opts.PrintWidth <= 0 {
104
+ opts.PrintWidth = 80
105
+ }
106
+ if opts.TabWidth <= 0 {
107
+ opts.TabWidth = 2
108
+ }
109
+ if opts.EndOfLine == "" {
110
+ opts.EndOfLine = "lf"
111
+ }
112
+ newline := "\n"
113
+ if opts.EndOfLine == "crlf" {
114
+ newline = "\r\n"
115
+ }
116
+
117
+ var out strings.Builder
118
+ col := opts.StartingColumn
119
+ var lineSuffix []Doc
120
+ stack := []printFrame{{indent: opts.BaseIndent, mode: modeBreak, doc: doc}}
121
+
122
+ flushLineSuffix := func() {
123
+ if len(lineSuffix) == 0 {
124
+ return
125
+ }
126
+ // LineSuffix children must land *before* the newline that
127
+ // triggered the flush. Render them synchronously into the
128
+ // output buffer. LineSuffix payloads are leaf-ish by contract
129
+ // (the canonical use is a trailing `// comment` Text), so a
130
+ // recursive Print is bounded and avoids the lifecycle hazard
131
+ // of pushing onto the main stack — which would queue the
132
+ // docs after the caller has already written the newline,
133
+ // inverting the source order.
134
+ pending := lineSuffix
135
+ lineSuffix = nil
136
+ for _, d := range pending {
137
+ s := Print(d, opts)
138
+ out.WriteString(s)
139
+ if idx := strings.LastIndex(s, "\n"); idx >= 0 {
140
+ col = len(s) - idx - 1
141
+ } else {
142
+ col += len(s)
143
+ }
144
+ }
145
+ }
146
+
147
+ writeIndent := func(indent int) int {
148
+ if opts.UseTabs {
149
+ tabs := indent / opts.TabWidth
150
+ spaces := indent - tabs*opts.TabWidth
151
+ for i := 0; i < tabs; i++ {
152
+ out.WriteByte('\t')
153
+ }
154
+ for i := 0; i < spaces; i++ {
155
+ out.WriteByte(' ')
156
+ }
157
+ return tabs*opts.TabWidth + spaces
158
+ }
159
+ for i := 0; i < indent; i++ {
160
+ out.WriteByte(' ')
161
+ }
162
+ return indent
163
+ }
164
+
165
+ for len(stack) > 0 {
166
+ top := stack[len(stack)-1]
167
+ stack = stack[:len(stack)-1]
168
+
169
+ switch top.doc.Kind {
170
+ case docNil:
171
+ // no-op
172
+ case docText:
173
+ out.WriteString(top.doc.Text)
174
+ // Update column. A text fragment may contain embedded
175
+ // newlines (verbatim slices). The column tracker counts
176
+ // from the last newline.
177
+ if idx := strings.LastIndex(top.doc.Text, "\n"); idx >= 0 {
178
+ col = len(top.doc.Text) - idx - 1
179
+ } else {
180
+ col += len(top.doc.Text)
181
+ }
182
+ case docConcat:
183
+ // Push children in reverse so they pop in source order.
184
+ for i := len(top.doc.Children) - 1; i >= 0; i-- {
185
+ stack = append(stack, printFrame{indent: top.indent, mode: top.mode, doc: top.doc.Children[i]})
186
+ }
187
+ case docIndent:
188
+ child := Concat(top.doc.Children...)
189
+ stack = append(stack, printFrame{indent: top.indent + top.doc.Width, mode: top.mode, doc: child})
190
+ case docAlign:
191
+ child := Concat(top.doc.Children...)
192
+ stack = append(stack, printFrame{indent: col, mode: top.mode, doc: child})
193
+ case docGroup:
194
+ // Try flat unless the group contains a hardline.
195
+ child := Concat(top.doc.Children...)
196
+ if fits(child, opts.PrintWidth-col, top.indent) {
197
+ stack = append(stack, printFrame{indent: top.indent, mode: modeFlat, doc: child})
198
+ } else {
199
+ stack = append(stack, printFrame{indent: top.indent, mode: modeBreak, doc: child})
200
+ }
201
+ case docIfBreak:
202
+ pick := top.doc.Children[1] // flat
203
+ if top.mode == modeBreak {
204
+ pick = top.doc.Children[0]
205
+ }
206
+ stack = append(stack, printFrame{indent: top.indent, mode: top.mode, doc: pick})
207
+ case docLineSuffix:
208
+ lineSuffix = append(lineSuffix, top.doc.Children...)
209
+ case docLine:
210
+ if top.mode == modeFlat {
211
+ out.WriteByte(' ')
212
+ col++
213
+ } else {
214
+ flushLineSuffix()
215
+ out.WriteString(newline)
216
+ col = writeIndent(top.indent)
217
+ }
218
+ case docSoftline:
219
+ if top.mode == modeFlat {
220
+ // renders to nothing
221
+ } else {
222
+ flushLineSuffix()
223
+ out.WriteString(newline)
224
+ col = writeIndent(top.indent)
225
+ }
226
+ case docHardline:
227
+ flushLineSuffix()
228
+ out.WriteString(newline)
229
+ col = writeIndent(top.indent)
230
+ case docLiteralline:
231
+ flushLineSuffix()
232
+ out.WriteString(newline)
233
+ col = 0
234
+ }
235
+ }
236
+
237
+ // Drain any line suffix that never met a break. They flow inline.
238
+ // Keep `col` in sync with what we emit so any future post-drain
239
+ // emission can rely on the invariant; today the drain is the last
240
+ // statement, but the bookkeeping discipline shouldn't depend on
241
+ // that.
242
+ for _, d := range lineSuffix {
243
+ s := Print(d, opts)
244
+ out.WriteString(s)
245
+ if idx := strings.LastIndex(s, "\n"); idx >= 0 {
246
+ col = len(s) - idx - 1
247
+ } else {
248
+ col += len(s)
249
+ }
250
+ }
251
+ _ = col
252
+
253
+ return out.String()
254
+ }
255
+
256
+ // fits reports whether `doc` renders flat within `remaining` columns,
257
+ // where the current indent is `indent`. The peek is iterative and
258
+ // stops at the first break-forcing event:
259
+ //
260
+ // - a Hardline / Literalline: the doc cannot render flat.
261
+ // - a Text that overflows: the doc does not fit.
262
+ // - a Line / Softline encountered in *flat* mode: counts as space
263
+ // (Line) or empty (Softline) and the walk continues.
264
+ // - a Line / Softline encountered while the engine has already
265
+ // committed the enclosing group to break mode: the peek returns
266
+ // true immediately because the upstream caller will not place this
267
+ // doc on the current line at all (the newline absorbs the
268
+ // remaining budget).
269
+ //
270
+ // The implementation is a small interpreter that mirrors the main loop,
271
+ // but it only tracks remaining columns and only handles the subset of
272
+ // kinds that matter for measurement.
273
+ func fits(doc Doc, remaining int, indent int) bool {
274
+ if remaining < 0 {
275
+ return false
276
+ }
277
+ type frame struct {
278
+ mode printMode
279
+ doc Doc
280
+ }
281
+ stack := []frame{{mode: modeFlat, doc: doc}}
282
+ for len(stack) > 0 {
283
+ top := stack[len(stack)-1]
284
+ stack = stack[:len(stack)-1]
285
+ switch top.doc.Kind {
286
+ case docNil:
287
+ case docText:
288
+ remaining -= len(top.doc.Text)
289
+ if remaining < 0 {
290
+ return false
291
+ }
292
+ // A Text whose payload spans multiple lines cannot render
293
+ // flat — by definition its flat form is multi-line. Returning
294
+ // true here would let the surrounding group commit to a flat
295
+ // layout that already contains a newline, defeating the entire
296
+ // fit-or-break decision. Force the caller to broken mode.
297
+ if strings.Contains(top.doc.Text, "\n") {
298
+ return false
299
+ }
300
+ case docConcat:
301
+ for i := len(top.doc.Children) - 1; i >= 0; i-- {
302
+ stack = append(stack, frame{mode: top.mode, doc: top.doc.Children[i]})
303
+ }
304
+ case docIndent, docAlign:
305
+ for i := len(top.doc.Children) - 1; i >= 0; i-- {
306
+ stack = append(stack, frame{mode: top.mode, doc: top.doc.Children[i]})
307
+ }
308
+ case docGroup:
309
+ // Measure nested groups in flat mode too — that is the
310
+ // standard Wadler choice: the outer group's "does my
311
+ // flat form fit" question is answered by treating every
312
+ // inner group as flat.
313
+ for i := len(top.doc.Children) - 1; i >= 0; i-- {
314
+ stack = append(stack, frame{mode: modeFlat, doc: top.doc.Children[i]})
315
+ }
316
+ case docIfBreak:
317
+ pick := top.doc.Children[1]
318
+ if top.mode == modeBreak {
319
+ pick = top.doc.Children[0]
320
+ }
321
+ stack = append(stack, frame{mode: top.mode, doc: pick})
322
+ case docLineSuffix:
323
+ // Line suffix never contributes to flat width — it queues
324
+ // for a future break. Skip.
325
+ case docLine:
326
+ if top.mode == modeFlat {
327
+ remaining--
328
+ if remaining < 0 {
329
+ return false
330
+ }
331
+ } else {
332
+ return true
333
+ }
334
+ case docSoftline:
335
+ if top.mode == modeBreak {
336
+ return true
337
+ }
338
+ case docHardline, docLiteralline:
339
+ return false
340
+ }
341
+ }
342
+ return true
343
+ }
@@ -0,0 +1,46 @@
1
+ package linthost
2
+
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ )
6
+
7
+ // printArrayLiteral renders an ArrayLiteralExpression with width-aware
8
+ // fit-or-break. Like the object printer, elements are emitted verbatim
9
+ // — only the outer brackets and the inter-element commas participate
10
+ // in reflow.
11
+ //
12
+ // Flat: `[1, 2, 3]`
13
+ // Broken: `[
14
+ //
15
+ // 1,
16
+ // 2,
17
+ // 3,
18
+ // ]`
19
+ //
20
+ // Array literals do NOT carry a leading/trailing space inside the
21
+ // brackets in flat mode (`[a, b]`, not `[ a, b ]`), matching every
22
+ // JavaScript formatter. Empty arrays collapse to `[]`.
23
+ func printArrayLiteral(ctx *PrintContext, node *shimast.Node) Doc {
24
+ if node == nil {
25
+ return Doc{}
26
+ }
27
+ arr := node.AsArrayLiteralExpression()
28
+ if arr == nil || arr.Elements == nil {
29
+ return verbatim(ctx, node)
30
+ }
31
+ items := make([]Doc, 0, len(arr.Elements.Nodes))
32
+ for _, elem := range arr.Elements.Nodes {
33
+ if elem == nil {
34
+ return verbatim(ctx, node)
35
+ }
36
+ doc, _ := PrintNode(ctx, elem)
37
+ items = append(items, doc)
38
+ }
39
+ return printList(ctx, listShape{
40
+ OpenTok: "[",
41
+ CloseTok: "]",
42
+ Items: items,
43
+ Space: false,
44
+ AddComma: true,
45
+ })
46
+ }
@@ -0,0 +1,180 @@
1
+ package linthost
2
+
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ )
6
+
7
+ // printCallExpression renders a CallExpression with width-aware
8
+ // argument reflow. The callee, optional question token (`?.`) and
9
+ // optional type-argument list are emitted verbatim — only the
10
+ // argument list participates in reflow.
11
+ //
12
+ // Flat: `foo(a, b, c)`
13
+ // Broken: `foo(
14
+ //
15
+ // a,
16
+ // b,
17
+ // c,
18
+ // )`
19
+ //
20
+ // Type arguments (`foo<A, B>(x)`) are preserved verbatim. Trailing
21
+ // commas on type arguments are intentionally avoided — Prettier omits
22
+ // them too (see prettier#10353).
23
+ func printCallExpression(ctx *PrintContext, node *shimast.Node) Doc {
24
+ if node == nil {
25
+ return Doc{}
26
+ }
27
+ call := node.AsCallExpression()
28
+ if call == nil {
29
+ return verbatim(ctx, node)
30
+ }
31
+ parts := []Doc{}
32
+ if call.Expression != nil {
33
+ parts = append(parts, verbatim(ctx, call.Expression))
34
+ }
35
+ // Question-dot for optional call: `foo?.(x)`. The token byte range
36
+ // lives between Expression.End() and the open paren; copy
37
+ // verbatim if present.
38
+ if call.QuestionDotToken != nil {
39
+ parts = append(parts, verbatim(ctx, call.QuestionDotToken))
40
+ }
41
+ if call.TypeArguments != nil {
42
+ // Verbatim range covering `<A, B>` punctuation and members.
43
+ parts = append(parts, verbatimRange(ctx.Source, callTypeArgsStart(ctx, call), callTypeArgsEnd(ctx, call)))
44
+ }
45
+ if hasNilEntry(call.Arguments) {
46
+ return verbatim(ctx, node)
47
+ }
48
+ parts = append(parts, printArgList(ctx, call.Arguments))
49
+ return Concat(parts...)
50
+ }
51
+
52
+ // printNewExpression renders a NewExpression. It mirrors the call
53
+ // expression printer; the only difference is the leading `new ` keyword
54
+ // and the optional argument list (NewExpression may omit args entirely,
55
+ // e.g. `new Foo`).
56
+ func printNewExpression(ctx *PrintContext, node *shimast.Node) Doc {
57
+ if node == nil {
58
+ return Doc{}
59
+ }
60
+ ne := node.AsNewExpression()
61
+ if ne == nil {
62
+ return verbatim(ctx, node)
63
+ }
64
+ parts := []Doc{Text("new ")}
65
+ if ne.Expression != nil {
66
+ parts = append(parts, verbatim(ctx, ne.Expression))
67
+ }
68
+ if ne.TypeArguments != nil {
69
+ parts = append(parts, verbatimRange(ctx.Source, newTypeArgsStart(ctx, ne), newTypeArgsEnd(ctx, ne)))
70
+ }
71
+ if ne.Arguments != nil {
72
+ if hasNilEntry(ne.Arguments) {
73
+ return verbatim(ctx, node)
74
+ }
75
+ parts = append(parts, printArgList(ctx, ne.Arguments))
76
+ }
77
+ return Concat(parts...)
78
+ }
79
+
80
+ // hasNilEntry reports whether any entry of `list` is a nil pointer.
81
+ // Per-node printers consult this before delegating to printArgList /
82
+ // printList — a nil child would render as an empty Doc and produce
83
+ // `(a, , b)` in the output. Bailing to verbatim is byte-safe.
84
+ func hasNilEntry(list *shimast.NodeList) bool {
85
+ if list == nil {
86
+ return false
87
+ }
88
+ for _, n := range list.Nodes {
89
+ if n == nil {
90
+ return true
91
+ }
92
+ }
93
+ return false
94
+ }
95
+
96
+ // 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 {
100
+ if list == nil {
101
+ return Text("()")
102
+ }
103
+ items := make([]Doc, 0, len(list.Nodes))
104
+ for _, arg := range list.Nodes {
105
+ doc, _ := PrintNode(ctx, arg)
106
+ items = append(items, doc)
107
+ }
108
+ return printList(ctx, listShape{
109
+ OpenTok: "(",
110
+ CloseTok: ")",
111
+ Items: items,
112
+ Space: false,
113
+ AddComma: true,
114
+ })
115
+ }
116
+
117
+ // Type-argument byte-range helpers. The shim's NodeList.End() points
118
+ // past the last argument; the surrounding `<` and `>` are not part of
119
+ // the list's range, so we have to scan around it.
120
+ func callTypeArgsStart(ctx *PrintContext, call *shimast.CallExpression) int {
121
+ if call.TypeArguments == nil || len(call.TypeArguments.Nodes) == 0 {
122
+ return -1
123
+ }
124
+ first := call.TypeArguments.Nodes[0]
125
+ if first == nil {
126
+ return -1
127
+ }
128
+ // `<` is the byte immediately before the first type argument
129
+ // (modulo whitespace).
130
+ pos := first.Pos()
131
+ for i := pos - 1; i >= 0; i-- {
132
+ if ctx.Source[i] == '<' {
133
+ return i
134
+ }
135
+ }
136
+ return -1
137
+ }
138
+
139
+ func callTypeArgsEnd(ctx *PrintContext, call *shimast.CallExpression) int {
140
+ if call.TypeArguments == nil {
141
+ return -1
142
+ }
143
+ end := call.TypeArguments.End()
144
+ for i := end; i < len(ctx.Source); i++ {
145
+ if ctx.Source[i] == '>' {
146
+ return i + 1
147
+ }
148
+ }
149
+ return end
150
+ }
151
+
152
+ func newTypeArgsStart(ctx *PrintContext, ne *shimast.NewExpression) int {
153
+ if ne.TypeArguments == nil || len(ne.TypeArguments.Nodes) == 0 {
154
+ return -1
155
+ }
156
+ first := ne.TypeArguments.Nodes[0]
157
+ if first == nil {
158
+ return -1
159
+ }
160
+ pos := first.Pos()
161
+ for i := pos - 1; i >= 0; i-- {
162
+ if ctx.Source[i] == '<' {
163
+ return i
164
+ }
165
+ }
166
+ return -1
167
+ }
168
+
169
+ func newTypeArgsEnd(ctx *PrintContext, ne *shimast.NewExpression) int {
170
+ if ne.TypeArguments == nil {
171
+ return -1
172
+ }
173
+ end := ne.TypeArguments.End()
174
+ for i := end; i < len(ctx.Source); i++ {
175
+ if ctx.Source[i] == '>' {
176
+ return i + 1
177
+ }
178
+ }
179
+ return end
180
+ }