@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.
- package/README.md +172 -33
- package/lib/defineConfig.d.ts +55 -0
- package/lib/defineConfig.js +39 -0
- package/lib/defineConfig.js.map +1 -0
- package/lib/index.d.ts +25 -1
- package/lib/index.js +622 -2
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPlugin.d.ts +53 -0
- package/lib/structures/ITtscLintPlugin.js +3 -0
- package/lib/structures/ITtscLintPlugin.js.map +1 -0
- package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
- package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
- package/lib/structures/ITtscLintPluginMeta.js +3 -0
- package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
- package/lib/structures/PluginRuleNames.d.ts +14 -0
- package/lib/structures/PluginRuleNames.js +3 -0
- package/lib/structures/PluginRuleNames.js.map +1 -0
- package/lib/structures/TtscLintConfig.d.ts +24 -8
- package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
- package/lib/structures/TtscLintConfigEntry.js +3 -0
- package/lib/structures/TtscLintConfigEntry.js.map +1 -0
- package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
- package/lib/structures/TtscLintFormatConfig.js +3 -0
- package/lib/structures/TtscLintFormatConfig.js.map +1 -0
- package/lib/structures/TtscLintPlugins.d.ts +9 -0
- package/lib/structures/TtscLintPlugins.js +3 -0
- package/lib/structures/TtscLintPlugins.js.map +1 -0
- package/lib/structures/TtscLintRule.d.ts +1 -1
- package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
- package/lib/structures/TtscLintRuleEntry.js +3 -0
- package/lib/structures/TtscLintRuleEntry.js.map +1 -0
- package/lib/structures/TtscLintRuleMap.d.ts +44 -0
- package/lib/structures/TtscLintRuleMap.js +3 -0
- package/lib/structures/TtscLintRuleMap.js.map +1 -0
- package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
- package/lib/structures/TtscLintRuleOptions.js +3 -0
- package/lib/structures/TtscLintRuleOptions.js.map +1 -0
- package/lib/structures/defaultFormat.d.ts +31 -0
- package/lib/structures/defaultFormat.js +35 -0
- package/lib/structures/defaultFormat.js.map +1 -0
- package/lib/structures/index.d.ts +10 -0
- package/lib/structures/index.js +10 -0
- package/lib/structures/index.js.map +1 -1
- package/{plugin → linthost}/ast_helpers.go +92 -1
- package/{plugin → linthost}/compile.go +1 -1
- package/{plugin → linthost}/config.go +583 -66
- package/linthost/config_format.go +360 -0
- package/linthost/contrib_adapter.go +134 -0
- package/{plugin → linthost}/directives.go +1 -1
- package/linthost/dispatch.go +62 -0
- package/{plugin → linthost}/engine.go +156 -11
- package/{plugin → linthost}/eslint_runtime.go +76 -2
- package/linthost/fix.go +249 -0
- package/linthost/format.go +107 -0
- package/{plugin → linthost}/host.go +1 -1
- package/linthost/print_dispatch.go +118 -0
- package/linthost/print_doc.go +168 -0
- package/linthost/print_engine.go +343 -0
- package/linthost/print_nodes_array.go +46 -0
- package/linthost/print_nodes_call.go +180 -0
- package/linthost/print_nodes_imports.go +171 -0
- package/linthost/print_nodes_list.go +64 -0
- package/linthost/print_nodes_object.go +51 -0
- package/{plugin → linthost}/rules_arrays.go +1 -1
- package/{plugin → linthost}/rules_console.go +1 -1
- package/{plugin → linthost}/rules_debugger.go +1 -1
- package/{plugin → linthost}/rules_dupes.go +1 -1
- package/{plugin → linthost}/rules_empty.go +1 -1
- package/linthost/rules_escape.go +238 -0
- package/{plugin → linthost}/rules_eval.go +1 -1
- package/{plugin → linthost}/rules_finally.go +1 -1
- package/linthost/rules_format_jsdoc.go +230 -0
- package/linthost/rules_format_print_width.go +332 -0
- package/linthost/rules_format_quotes.go +219 -0
- package/linthost/rules_format_semi.go +190 -0
- package/linthost/rules_format_sort_imports.go +462 -0
- package/linthost/rules_format_trailing_comma.go +460 -0
- package/{plugin → linthost}/rules_gap.go +101 -6
- package/linthost/rules_imports.go +101 -0
- package/{plugin → linthost}/rules_logic.go +55 -3
- package/{plugin → linthost}/rules_loops.go +1 -1
- package/{plugin → linthost}/rules_misc.go +1 -1
- package/linthost/rules_params.go +64 -0
- package/{plugin → linthost}/rules_problems.go +10 -2
- package/linthost/rules_promise.go +111 -0
- package/{plugin → linthost}/rules_protos.go +1 -1
- package/{plugin → linthost}/rules_self.go +1 -1
- package/{plugin → linthost}/rules_strings.go +1 -1
- package/{plugin → linthost}/rules_suggestions.go +51 -9
- package/{plugin → linthost}/rules_throw.go +1 -1
- package/{plugin → linthost}/rules_ts.go +12 -2
- package/{plugin → linthost}/rules_ts_extra.go +90 -4
- package/{plugin → linthost}/rules_var.go +36 -5
- package/package.json +8 -6
- package/plugin/main.go +14 -27
- package/rule/astutil/astutil.go +148 -0
- package/rule/rule.go +291 -0
- package/src/defineConfig.ts +69 -0
- package/src/index.ts +807 -3
- package/src/structures/ITtscLintPlugin.ts +56 -0
- package/src/structures/ITtscLintPluginConfig.ts +86 -5
- package/src/structures/ITtscLintPluginMeta.ts +14 -0
- package/src/structures/PluginRuleNames.ts +19 -0
- package/src/structures/TtscLintConfig.ts +27 -8
- package/src/structures/TtscLintConfigEntry.ts +50 -0
- package/src/structures/TtscLintFormatConfig.ts +169 -0
- package/src/structures/TtscLintPlugins.ts +10 -0
- package/src/structures/TtscLintRule.ts +14 -1
- package/src/structures/TtscLintRuleEntry.ts +28 -0
- package/src/structures/TtscLintRuleMap.ts +52 -0
- package/src/structures/TtscLintRuleOptions.ts +162 -0
- package/src/structures/defaultFormat.ts +33 -0
- 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
|
|
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 }
|