@ttsc/lint 0.10.2 → 0.11.0-dev.20260517

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 +62 -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,107 @@
1
+ package linthost
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ )
7
+
8
+ // maxFormatPasses bounds the format cascade for the same reason
9
+ // `maxFixPasses` does in fix.go: a rule that re-reports its own edit
10
+ // could otherwise loop forever. Format rules touch surface details
11
+ // (quotes, semicolons, trailing commas, import order) so a real-world
12
+ // cascade settles in a handful of passes; the cap is the safety net,
13
+ // not the expected steady state.
14
+ const maxFormatPasses = 10
15
+
16
+ // RunFormat implements `@ttsc/lint format` — apply format-rule edits
17
+ // only. Write-only by contract: no diagnostic output, no typecheck
18
+ // recheck. Mirrors RunFix in flag handling so the host launcher can
19
+ // forward the same option shape.
20
+ func RunFormat(args []string) int {
21
+ opts, err := parseSubcommandFlags("format", args)
22
+ if err != nil {
23
+ fmt.Fprintln(os.Stderr, err)
24
+ return 2
25
+ }
26
+ if opts.emit {
27
+ fmt.Fprintln(os.Stderr, "@ttsc/lint format: --emit is not supported")
28
+ return 2
29
+ }
30
+ opts.noEmit = true
31
+ return runFormat(opts)
32
+ }
33
+
34
+ func runFormat(opts *subcommandOpts) int {
35
+ rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
36
+ if err != nil {
37
+ fmt.Fprintln(os.Stderr, err)
38
+ return 2
39
+ }
40
+
41
+ prog, code := loadFixProgram(opts)
42
+ if code != 0 {
43
+ return code
44
+ }
45
+ defer func() {
46
+ if prog != nil {
47
+ prog.close()
48
+ }
49
+ }()
50
+
51
+ totalFixes := 0
52
+ cascadeConverged := false
53
+ for pass := 0; pass < maxFormatPasses; pass++ {
54
+ engine := NewEngineWithResolver(rules)
55
+ findings := engine.Run(prog.userSourceFiles(), prog.checker)
56
+ fixed, err := applyFindingFixes(opts.cwd, filterFormatFindings(findings))
57
+ if err != nil {
58
+ fmt.Fprintln(os.Stderr, err)
59
+ return 3
60
+ }
61
+ if fixed == 0 {
62
+ cascadeConverged = true
63
+ break
64
+ }
65
+ totalFixes += fixed
66
+ prog, code = reloadFixProgram(prog, opts)
67
+ if code != 0 {
68
+ return code
69
+ }
70
+ }
71
+ if !cascadeConverged {
72
+ // Format runs are write-only by contract, so a non-converged exit
73
+ // leaves the user's files in a partially-formatted state with no
74
+ // diagnostic surface to expose the cause. Emit an explicit signal
75
+ // and a non-zero exit code so a CI gate like
76
+ // `ttsc format && echo done` does not silently accept the
77
+ // non-idempotent state.
78
+ fmt.Fprintf(os.Stderr,
79
+ "@ttsc/lint: format cascade did not converge after %d passes; rerun or check for a non-idempotent format rule\n",
80
+ maxFormatPasses)
81
+ return 2
82
+ }
83
+
84
+ if opts.verbose && totalFixes > 0 {
85
+ fmt.Fprintf(os.Stdout, "@ttsc/lint: formatted=%d edits\n", totalFixes)
86
+ }
87
+ return 0
88
+ }
89
+
90
+ // filterFormatFindings keeps only findings produced by FormatRule
91
+ // implementations that also carry at least one autofix edit.
92
+ // `RunFormat` calls this so the format-only subcommand never applies
93
+ // lint-class edits, and so a contributor format rule that reports a
94
+ // fixable diagnostic via bare `ctx.Report` (no edits attached) does
95
+ // not silently disappear — format mode is write-only, so a no-edit
96
+ // finding has nothing to do here. `RunFix`, by contrast, applies
97
+ // every finding regardless of category — fix is the run-everything
98
+ // entry point.
99
+ func filterFormatFindings(findings []*Finding) []*Finding {
100
+ out := make([]*Finding, 0, len(findings))
101
+ for _, finding := range findings {
102
+ if finding != nil && finding.IsFormat && len(finding.Fix) > 0 {
103
+ out = append(out, finding)
104
+ }
105
+ }
106
+ return out
107
+ }
@@ -7,7 +7,7 @@
7
7
  // ttsc's runtime-generated go.work overlay. Instead, this file inlines a
8
8
  // minimal Program/Checker bootstrap (the same pattern documented in
9
9
  // 03-tsgo.md and used by every other source-plugin reference fixture).
10
- package main
10
+ package linthost
11
11
 
12
12
  import (
13
13
  "context"
@@ -0,0 +1,118 @@
1
+ package linthost
2
+
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
6
+ )
7
+
8
+ // AST → Doc dispatcher.
9
+ //
10
+ // The dispatcher is the bridge between the TypeScript-Go AST and the
11
+ // printer engine. It walks one node at a time and emits a Doc tree
12
+ // shaped to that node's grammar. Coverage is intentionally partial in
13
+ // v1; the verbatim fallback below guarantees that an un-handled node
14
+ // kind contributes its original source bytes verbatim, so the printer
15
+ // can be wired up to a rule without breaking files that happen to use
16
+ // shapes the per-node printers don't yet understand.
17
+ //
18
+ // The price of verbatim fallback is that reflow stops at the boundary
19
+ // of an un-handled node. A long line buried inside an expression the
20
+ // dispatcher doesn't recognize stays long. That trade-off is preferable
21
+ // to corrupting unfamiliar shapes — extension over time turns each
22
+ // verbatim hop into a real reflow.
23
+
24
+ // PrintContext bundles the per-file inputs every per-node printer
25
+ // needs. The dispatcher constructs one per top-level reflow and threads
26
+ // it into every recursive call.
27
+ type PrintContext struct {
28
+ File *shimast.SourceFile
29
+ Source string
30
+ Opts PrintOptions
31
+ }
32
+
33
+ // NewPrintContext returns a context wired to `file` and `opts`. The
34
+ // helper exists so call sites do not have to remember to read
35
+ // `file.Text()` and Opts defaults at every level.
36
+ func NewPrintContext(file *shimast.SourceFile, opts PrintOptions) *PrintContext {
37
+ if opts.PrintWidth == 0 {
38
+ opts = DefaultPrintOptions()
39
+ }
40
+ return &PrintContext{File: file, Source: file.Text(), Opts: opts}
41
+ }
42
+
43
+ // PrintNode is the dispatcher entry. It picks a per-node printer based
44
+ // on `node.Kind` and falls back to the verbatim source slice when no
45
+ // printer is registered. Returns the printed Doc and a boolean
46
+ // indicating whether the dispatch actually reformatted the node. The
47
+ // boolean is currently only used by the format/print-width rule, which
48
+ // skips edit emission when every printable child fell back to
49
+ // verbatim (no behavior change, no diagnostic).
50
+ func PrintNode(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
51
+ if node == nil {
52
+ return Doc{}, false
53
+ }
54
+ if doc, reformatted, ok := dispatchNode(ctx, node); ok {
55
+ return doc, reformatted
56
+ }
57
+ return verbatim(ctx, node), false
58
+ }
59
+
60
+ // dispatchNode is the per-kind switch. Each branch returns
61
+ // (doc, reformatted, true) when it produces a structured Doc, or
62
+ // (zero, false, false) to let the caller fall back to verbatim. The
63
+ // reformatted flag is true when the per-node printer asserts the
64
+ // result may differ from the original bytes.
65
+ func dispatchNode(ctx *PrintContext, node *shimast.Node) (Doc, bool, bool) {
66
+ switch node.Kind {
67
+ case shimast.KindObjectLiteralExpression:
68
+ return printObjectLiteral(ctx, node), true, true
69
+ case shimast.KindArrayLiteralExpression:
70
+ return printArrayLiteral(ctx, node), true, true
71
+ case shimast.KindCallExpression:
72
+ return printCallExpression(ctx, node), true, true
73
+ case shimast.KindNewExpression:
74
+ return printNewExpression(ctx, node), true, true
75
+ case shimast.KindNamedImports:
76
+ return printNamedImports(ctx, node), true, true
77
+ case shimast.KindNamedExports:
78
+ return printNamedExports(ctx, node), true, true
79
+ case shimast.KindImportDeclaration:
80
+ return printImportDeclaration(ctx, node), true, true
81
+ }
82
+ return Doc{}, false, false
83
+ }
84
+
85
+ // verbatim returns the original source bytes for `node`, leading trivia
86
+ // trimmed. Use this whenever a printer cannot fully cover a node — the
87
+ // surrounding doc tree still flows, but the verbatim slice carries
88
+ // whatever the user wrote, including comments and embedded line breaks.
89
+ func verbatim(ctx *PrintContext, node *shimast.Node) Doc {
90
+ if node == nil {
91
+ return Doc{}
92
+ }
93
+ start := shimscanner.SkipTrivia(ctx.Source, node.Pos())
94
+ end := node.End()
95
+ if start < 0 || end < start || end > len(ctx.Source) {
96
+ return Doc{}
97
+ }
98
+ return Text(ctx.Source[start:end])
99
+ }
100
+
101
+ // verbatimRange is the position-only sibling of verbatim — useful when
102
+ // the printer needs to copy a sub-range that does not correspond to a
103
+ // single AST node (e.g. tokens between two siblings).
104
+ func verbatimRange(src string, start, end int) Doc {
105
+ if start < 0 || end < start || end > len(src) {
106
+ return Doc{}
107
+ }
108
+ return Text(src[start:end])
109
+ }
110
+
111
+ // indentUnit returns one indentation step's worth of columns. Each
112
+ // per-node printer uses this for its own list nesting.
113
+ func (ctx *PrintContext) indentUnit() int {
114
+ if ctx.Opts.TabWidth > 0 {
115
+ return ctx.Opts.TabWidth
116
+ }
117
+ return 2
118
+ }
@@ -0,0 +1,168 @@
1
+ package linthost
2
+
3
+ // Pretty-printer doc IR.
4
+ //
5
+ // This file defines the abstract layout language consumed by the printer
6
+ // engine in print_engine.go. The design is a Go port of the Wadler /
7
+ // Lindig algebra of pretty-printers that Prettier and dprint implement:
8
+ // a node-level printer translates an AST node into a tree of Doc
9
+ // values, and the engine lays the tree out under a width budget,
10
+ // breaking groups whose flat form would overflow.
11
+ //
12
+ // Doc is intentionally a sum type encoded as a tagged struct rather than
13
+ // an interface. The hot path of the engine is a tight loop over a slice
14
+ // of (indent, mode, doc) frames; an interface dispatch per node would
15
+ // dominate the cost. The tag is a small enum (DocKind) and each field is
16
+ // only read by the case that owns it.
17
+ //
18
+ // Layout contract:
19
+ //
20
+ // - DocText is verbatim output. The printer never reflows or wraps it.
21
+ // The caller is responsible for keeping its width meaningful — a
22
+ // text fragment longer than printWidth still flows verbatim, but it
23
+ // will force surrounding groups to break.
24
+ // - DocLine renders as either a single space or a newline + indent,
25
+ // depending on the surrounding group's chosen mode. Use this for
26
+ // soft separators (e.g. between call arguments).
27
+ // - DocSoftline is the empty-or-newline variant: flat mode emits
28
+ // nothing, break mode emits a newline + indent.
29
+ // - DocHardline always emits a newline and propagates "break" upward to
30
+ // every enclosing group. Use it for declarations that must stand on
31
+ // their own line regardless of width.
32
+ // - DocLiteralline is a hardline that does NOT emit indentation after
33
+ // the newline. Used for template-literal interior lines where the
34
+ // original spacing must be preserved.
35
+ // - DocGroup is the fit-or-break primitive: the engine measures the
36
+ // group's flat width; if it fits in the remaining column budget the
37
+ // group renders flat (Lines collapse to spaces, Softlines to
38
+ // nothing), otherwise it breaks (Lines and Softlines emit
39
+ // newline+indent).
40
+ // - DocIndent adds N columns of indentation to every newline emitted
41
+ // by its child doc. Nesting composes: an Indent inside another
42
+ // Indent adds the two amounts.
43
+ // - DocAlign is like Indent but the increment is the current output
44
+ // column rather than a fixed offset. Used to align continuation
45
+ // lines under an opening token (e.g. inside a call expression's
46
+ // arguments).
47
+ // - DocIfBreak renders one doc when the surrounding group breaks and
48
+ // another when it stays flat. The canonical use is a trailing comma
49
+ // that should appear only in multi-line lists.
50
+ // - DocConcat is a sequence of child docs. The printer flattens nested
51
+ // concats inline.
52
+ // - DocLineSuffix queues output until the next hardline/softline that
53
+ // actually breaks; used for trailing line comments that must stick
54
+ // to their source line.
55
+ //
56
+ // The doc tree is built by helper constructors (Text, Line, Group, …)
57
+ // below. Constructors take their children as variadic or slice
58
+ // arguments so call sites read like a layout DSL.
59
+ type DocKind uint8
60
+
61
+ const (
62
+ docNil DocKind = iota
63
+ docText
64
+ docLine
65
+ docSoftline
66
+ docHardline
67
+ docLiteralline
68
+ docGroup
69
+ docIndent
70
+ docAlign
71
+ docIfBreak
72
+ docConcat
73
+ docLineSuffix
74
+ )
75
+
76
+ // Doc is one node in the layout tree. Only the fields relevant to the
77
+ // kind are populated; the rest stay at their zero value.
78
+ //
79
+ // Doc is a value type. Helper constructors return a fresh Doc, so the
80
+ // caller never needs to copy. The engine reads Doc trees but never
81
+ // mutates them, so concurrent prints over a shared tree are safe.
82
+ //
83
+ // The `Width` field carries the column increment for `docIndent`
84
+ // nodes. It is named `Width` rather than `Indent` to avoid a
85
+ // collision with the `Indent()` constructor, which would otherwise
86
+ // shadow the field name at every constructor body.
87
+ type Doc struct {
88
+ Kind DocKind
89
+ Text string
90
+ Children []Doc
91
+ Width int
92
+ // IfBreak pairs: BreakChild stored in Children[0], FlatChild in Children[1].
93
+ }
94
+
95
+ // Text constructs a verbatim text doc.
96
+ func Text(s string) Doc { return Doc{Kind: docText, Text: s} }
97
+
98
+ // Line is the soft separator: space when flat, newline+indent when broken.
99
+ func Line() Doc { return Doc{Kind: docLine} }
100
+
101
+ // Softline is the empty-or-newline separator.
102
+ func Softline() Doc { return Doc{Kind: docSoftline} }
103
+
104
+ // Hardline forces a newline and propagates break upward.
105
+ func Hardline() Doc { return Doc{Kind: docHardline} }
106
+
107
+ // Literalline is a hardline that emits the newline without applying
108
+ // indentation. The next characters appear in column 0 of the new line.
109
+ func Literalline() Doc { return Doc{Kind: docLiteralline} }
110
+
111
+ // Group wraps a child doc in a fit-or-break decision. Variadic args are
112
+ // concatenated.
113
+ func Group(parts ...Doc) Doc { return Doc{Kind: docGroup, Children: parts} }
114
+
115
+ // Indent adds `width` columns of indentation to every newline emitted by
116
+ // the child doc. Nesting composes.
117
+ func Indent(width int, parts ...Doc) Doc {
118
+ return Doc{Kind: docIndent, Width: width, Children: parts}
119
+ }
120
+
121
+ // Align makes every newline emitted by the child doc align to the
122
+ // current output column rather than to a fixed indent.
123
+ func Align(parts ...Doc) Doc { return Doc{Kind: docAlign, Children: parts} }
124
+
125
+ // IfBreak emits `whenBroken` when the surrounding group breaks and
126
+ // `whenFlat` when it stays flat. The two arguments are stored as
127
+ // Children[0] and Children[1] respectively.
128
+ func IfBreak(whenBroken, whenFlat Doc) Doc {
129
+ return Doc{Kind: docIfBreak, Children: []Doc{whenBroken, whenFlat}}
130
+ }
131
+
132
+ // Concat sequences child docs. Empty Concat is the layout no-op.
133
+ func Concat(parts ...Doc) Doc {
134
+ if len(parts) == 1 {
135
+ return parts[0]
136
+ }
137
+ return Doc{Kind: docConcat, Children: parts}
138
+ }
139
+
140
+ // LineSuffix queues output until the next line break. Used for trailing
141
+ // line comments that must appear after the current source line ends.
142
+ func LineSuffix(parts ...Doc) Doc {
143
+ return Doc{Kind: docLineSuffix, Children: parts}
144
+ }
145
+
146
+ // Join interleaves `sep` between the entries of `parts` and returns the
147
+ // flattened concat. Empty input returns a no-op doc. Single-entry input
148
+ // returns the entry verbatim.
149
+ func Join(sep Doc, parts []Doc) Doc {
150
+ switch len(parts) {
151
+ case 0:
152
+ return Doc{Kind: docNil}
153
+ case 1:
154
+ return parts[0]
155
+ }
156
+ out := make([]Doc, 0, len(parts)*2-1)
157
+ for i, p := range parts {
158
+ if i > 0 {
159
+ out = append(out, sep)
160
+ }
161
+ out = append(out, p)
162
+ }
163
+ return Concat(out...)
164
+ }
165
+
166
+ // IsNil reports whether the doc is the zero-value no-op. Helpful when a
167
+ // helper returns "nothing to print" — the engine ignores nil docs.
168
+ func (d Doc) IsNil() bool { return d.Kind == docNil }