@ttsc/lint 0.12.4 → 0.13.1

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 (37) hide show
  1. package/lib/index.js +225 -135
  2. package/lib/index.js.map +1 -1
  3. package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
  4. package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
  5. package/linthost/ast_helpers.go +68 -16
  6. package/linthost/compile.go +117 -119
  7. package/linthost/config.go +518 -707
  8. package/linthost/config_format.go +16 -4
  9. package/linthost/contrib_adapter.go +7 -0
  10. package/linthost/directives.go +44 -0
  11. package/linthost/engine.go +152 -44
  12. package/linthost/fix.go +24 -33
  13. package/linthost/flags_gen.go +33 -0
  14. package/linthost/format.go +96 -3
  15. package/linthost/host.go +144 -8
  16. package/linthost/print_dispatch.go +121 -23
  17. package/linthost/print_doc.go +19 -0
  18. package/linthost/print_engine.go +168 -4
  19. package/linthost/print_nodes_array.go +17 -7
  20. package/linthost/print_nodes_call.go +129 -20
  21. package/linthost/print_nodes_function.go +353 -0
  22. package/linthost/print_nodes_imports.go +46 -29
  23. package/linthost/print_nodes_list.go +86 -5
  24. package/linthost/print_nodes_object.go +56 -11
  25. package/linthost/rules_escape.go +20 -3
  26. package/linthost/rules_format_print_width.go +267 -15
  27. package/linthost/rules_gap.go +55 -3
  28. package/linthost/rules_logic.go +64 -5
  29. package/linthost/rules_problems.go +65 -21
  30. package/linthost/rules_promise.go +3 -0
  31. package/linthost/rules_suggestions.go +160 -5
  32. package/linthost/rules_var.go +7 -1
  33. package/package.json +3 -3
  34. package/src/index.ts +243 -168
  35. package/src/structures/ITtscLintPluginConfig.ts +10 -83
  36. package/src/structures/TtscLintRuleOptions.ts +15 -0
  37. package/linthost/eslint_runtime.go +0 -351
@@ -41,8 +41,12 @@ func runFormat(opts *subcommandOpts) int {
41
41
  fmt.Fprintln(os.Stderr, err)
42
42
  return 2
43
43
  }
44
+ resolver := formatCommandResolver{inner: rules}
45
+ engine := NewEngineWithResolver(resolver)
46
+ engine.SetSerial(opts.singleThreaded)
47
+ needsRuleChecker := engine.NeedsTypeChecker()
44
48
 
45
- prog, code := loadFixProgram(opts)
49
+ prog, code := loadFixProgram(opts, needsRuleChecker)
46
50
  if code != 0 {
47
51
  return code
48
52
  }
@@ -55,7 +59,6 @@ func runFormat(opts *subcommandOpts) int {
55
59
  totalFixes := 0
56
60
  cascadeConverged := false
57
61
  for pass := 0; pass < maxFormatPasses; pass++ {
58
- engine := NewEngineWithResolver(formatCommandResolver{inner: rules})
59
62
  findings := engine.Run(prog.userSourceFiles(), prog.checker)
60
63
  fixed, err := applyFindingFixes(opts.cwd, filterFormatFindings(findings))
61
64
  if err != nil {
@@ -67,7 +70,7 @@ func runFormat(opts *subcommandOpts) int {
67
70
  break
68
71
  }
69
72
  totalFixes += fixed
70
- prog, code = reloadFixProgram(prog, opts)
73
+ prog, code = reloadFixProgram(prog, opts, needsRuleChecker)
71
74
  if code != 0 {
72
75
  return code
73
76
  }
@@ -102,11 +105,34 @@ type formatCommandResolver struct {
102
105
  // ResolveRules implements RuleResolver. It delegates to the inner resolver
103
106
  // and then upgrades format-rule entries from off to warn so they are applied
104
107
  // even when the project config omits them.
108
+ //
109
+ // A user-authored entry whose `ignores` list matches `fileName` is honored
110
+ // here even when the entry also carries a `rules` block: the engine already
111
+ // skips that entry's rule contributions via `ConfigEntry.matchesFile`, so the
112
+ // format command must skip its blanket format-rule upgrade as well. Without
113
+ // this guard, `ttsc format` would rewrite files that the user explicitly
114
+ // asked the lint config to ignore — the engine's lint walk would skip the
115
+ // file but the formatter would still touch it.
116
+ //
117
+ // The symmetric guard applies to `files`: if every non-IgnoreOnly entry
118
+ // carries a `files` filter and `fileName` matches none of them, the engine
119
+ // would not run any rules on the file (`ConfigEntry.matchesFile` returns
120
+ // false for every entry). The format command must skip its blanket
121
+ // format-rule upgrade for the same reason — otherwise `ttsc format` would
122
+ // rewrite files that fall outside every entry's scope, e.g. a `.json`
123
+ // resolved into the program via `resolveJsonModule` when the only entry
124
+ // targets `src/**/*.ts`.
105
125
  func (r formatCommandResolver) ResolveRules(fileName string) ResolvedRuleConfig {
106
126
  resolved := r.inner.ResolveRules(fileName)
107
127
  if resolved.Ignored {
108
128
  return resolved
109
129
  }
130
+ if r.fileIsIgnoredByEntry(fileName) {
131
+ return resolved
132
+ }
133
+ if !r.fileMatchesAnyEntry(fileName) {
134
+ return resolved
135
+ }
110
136
  if resolved.Rules == nil {
111
137
  resolved.Rules = RuleConfig{}
112
138
  }
@@ -118,6 +144,73 @@ func (r formatCommandResolver) ResolveRules(fileName string) ResolvedRuleConfig
118
144
  return resolved
119
145
  }
120
146
 
147
+ // fileIsIgnoredByEntry reports whether any non-IgnoreOnly entry in the inner
148
+ // ConfigStore has an `ignores` glob that matches `fileName`. IgnoreOnly
149
+ // entries are already handled by ResolvedRuleConfig.Ignored — they are
150
+ // checked first by ConfigStore.ResolveRules and short-circuit the walk. This
151
+ // helper covers the complementary case: an entry that carries both `rules`
152
+ // and `ignores`, where the engine skips the entry's rule contributions via
153
+ // `ConfigEntry.matchesFile` but the format command must learn the same fact
154
+ // independently because its job is to upgrade format rules to `warn`, not to
155
+ // read the engine's resolved severity map.
156
+ func (r formatCommandResolver) fileIsIgnoredByEntry(fileName string) bool {
157
+ store, ok := r.inner.(*ConfigStore)
158
+ if !ok || store == nil {
159
+ return false
160
+ }
161
+ for _, entry := range store.entries {
162
+ if entry.IgnoreOnly {
163
+ continue
164
+ }
165
+ if entry.matchesIgnores(fileName) {
166
+ return true
167
+ }
168
+ }
169
+ return false
170
+ }
171
+
172
+ // fileMatchesAnyEntry reports whether `fileName` falls inside the `files`
173
+ // scope of at least one non-IgnoreOnly entry. An entry without an explicit
174
+ // `files` list matches every file by definition (eslint flat-config
175
+ // semantics), so a config with any unrestricted non-IgnoreOnly entry returns
176
+ // true for every file. The format command treats a `false` result the same
177
+ // way the engine does: no entry contributes rules, so the blanket
178
+ // format-rule upgrade must be skipped.
179
+ //
180
+ // Base cases:
181
+ //
182
+ // - Empty entries slice: returns `true`. A store with no entries cannot
183
+ // skip a file by scope (the engine has nothing to walk), so format
184
+ // mode must be allowed to apply its default upgrade. Matching engine
185
+ // behavior at `ConfigStore.ResolveRules` for the same input
186
+ // (`Ignored = false`, empty rule map).
187
+ // - All entries are IgnoreOnly: returns `false`. No non-IgnoreOnly
188
+ // entry contributes a `files` scope, so the file is out-of-scope by
189
+ // construction.
190
+ // - Inner resolver is not a *ConfigStore: returns `true` (conservative
191
+ // default). The `files` concept is store-specific; an in-process
192
+ // custom resolver that does not surface a scope cannot have its
193
+ // rule-eligibility reasoned about from the outside, so the format
194
+ // upgrade applies the same as it does for an entry without `files`.
195
+ func (r formatCommandResolver) fileMatchesAnyEntry(fileName string) bool {
196
+ store, ok := r.inner.(*ConfigStore)
197
+ if !ok || store == nil {
198
+ return true
199
+ }
200
+ if len(store.entries) == 0 {
201
+ return true
202
+ }
203
+ for _, entry := range store.entries {
204
+ if entry.IgnoreOnly {
205
+ continue
206
+ }
207
+ if entry.matchesFile(fileName) {
208
+ return true
209
+ }
210
+ }
211
+ return false
212
+ }
213
+
121
214
  // ActiveRuleNames implements RuleResolver. Returns the union of the inner
122
215
  // resolver's active rules and every format-option rule that is registered.
123
216
  func (r formatCommandResolver) ActiveRuleNames() []string {
package/linthost/host.go CHANGED
@@ -14,6 +14,7 @@ import (
14
14
  "errors"
15
15
  "fmt"
16
16
  "path/filepath"
17
+ "strings"
17
18
 
18
19
  shimast "github.com/microsoft/typescript-go/shim/ast"
19
20
  "github.com/microsoft/typescript-go/shim/bundled"
@@ -39,10 +40,25 @@ type loadProgramOptions struct {
39
40
  forceEmit bool
40
41
  forceNoEmit bool
41
42
  outDir string
43
+ // needsRuleChecker asks loadProgram to pin the checker pool and acquire the
44
+ // checker that type-aware lint rules receive through Context.Checker.
45
+ needsRuleChecker bool
46
+ // singleThreaded mirrors `tsgo --singleThreaded`: one checker, serial
47
+ // parse/check/emit.
48
+ singleThreaded bool
49
+ // checkers mirrors `tsgo --checkers`: type-checker pool size. Zero leaves
50
+ // TypeScript-Go's default; ignored when singleThreaded is set.
51
+ checkers int
52
+ // tsgoArgs carries tsgo CLI flags the `ttsc` launcher forwarded (`--strict`,
53
+ // `--target es2020`, …). They are parsed through TypeScript-Go's own
54
+ // command-line parser into a CompilerOptions overlay that wins over the
55
+ // tsconfig, exactly as tsgo's CLI merges them.
56
+ tsgoArgs []string
42
57
  }
43
58
 
44
- // loadProgram parses the given tsconfig, builds a Program, and acquires a
45
- // type checker. Mirrors the canonical bootstrap pattern from
59
+ // loadProgram parses the given tsconfig and builds a Program. When
60
+ // needsRuleChecker is set, it also acquires a type checker for lint rules.
61
+ // Mirrors the canonical bootstrap pattern from
46
62
  // `03-tsgo.md` — the only ttsc-specific bit is that `forceEmit`/
47
63
  // `forceNoEmit`/`outDir` overrides are merged into the parsed config
48
64
  // before the program is created so `--noEmit` and friends behave like
@@ -63,9 +79,14 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
63
79
  fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
64
80
  host := shimcompiler.NewCompilerHost(cwd, fs, bundled.LibPath(), nil, nil)
65
81
 
82
+ cliOptions, cliDiags := parseTsgoArgs(options.tsgoArgs, host)
83
+ if len(cliDiags) > 0 {
84
+ return nil, cliDiags, nil
85
+ }
86
+
66
87
  parsed, parseDiags := tsoptions.GetParsedCommandLineOfConfigFile(
67
88
  resolved,
68
- &shimcore.CompilerOptions{},
89
+ cliOptions,
69
90
  nil,
70
91
  host,
71
92
  nil,
@@ -88,17 +109,34 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
88
109
  if options.outDir != "" {
89
110
  overrideOutDir(cwd, parsed, options.outDir)
90
111
  }
112
+ applyThreading(parsed, options.singleThreaded, options.checkers)
113
+ if options.needsRuleChecker {
114
+ forceSingleChecker(parsed)
115
+ }
91
116
 
117
+ // SingleThreaded is left unset so the program keeps TypeScript-Go's parallel
118
+ // source parsing and parallel emit. For type-aware lint rules, the checker
119
+ // pool is pinned to a single checker (see forceSingleChecker): the lint
120
+ // engine walks files serially against the one checker GetTypeChecker hands
121
+ // back, and rules ask that checker to resolve types in nodes drawn from every
122
+ // source file. TypeScript-Go's multi-checker pool affinitizes each file to a
123
+ // different checker and forbids mixing types across them, so a type whose
124
+ // declarations span files on different checkers (e.g. a circular
125
+ // indexed-access alias) resolves to `any` on the borrowed checker. AST-only
126
+ // lint rules do not receive a checker, so they keep the user's checker pool.
92
127
  tsProgram := shimcompiler.NewProgram(shimcompiler.ProgramOptions{
93
128
  Config: parsed,
94
- SingleThreaded: shimcore.TSTrue,
95
129
  Host: host,
96
130
  UseSourceOfProjectReference: true,
97
131
  })
98
132
  if tsProgram == nil {
99
133
  return nil, nil, errors.New("compiler.NewProgram returned nil")
100
134
  }
101
- checker, release := tsProgram.GetTypeChecker(context.Background())
135
+ var checker *shimchecker.Checker
136
+ var release func()
137
+ if options.needsRuleChecker {
138
+ checker, release = tsProgram.GetTypeChecker(context.Background())
139
+ }
102
140
  return &program{
103
141
  cwd: cwd,
104
142
  tsProgram: tsProgram,
@@ -120,12 +158,19 @@ func (p *program) close() {
120
158
  }
121
159
  }
122
160
 
123
- // userSourceFiles returns the program's user-authored source files
124
- // (declaration files filtered out those belong to library typings).
161
+ // userSourceFiles returns the tsconfig-selected source files the lint engine
162
+ // owns. The tsconfig file list is the boundary: imported libraries, generated
163
+ // output, and JSON modules may still appear in Program.SourceFiles(), but lint
164
+ // and format should not walk them unless the project selected them as TS/JS
165
+ // source roots.
125
166
  func (p *program) userSourceFiles() []*shimast.SourceFile {
167
+ roots := p.userSourceFileNames()
126
168
  out := make([]*shimast.SourceFile, 0)
127
169
  for _, f := range p.tsProgram.SourceFiles() {
128
- if f == nil || f.IsDeclarationFile {
170
+ if f == nil {
171
+ continue
172
+ }
173
+ if _, ok := roots[canonicalProjectPath(p.cwd, f.FileName())]; !ok {
129
174
  continue
130
175
  }
131
176
  out = append(out, f)
@@ -133,6 +178,35 @@ func (p *program) userSourceFiles() []*shimast.SourceFile {
133
178
  return out
134
179
  }
135
180
 
181
+ func (p *program) userSourceFileNames() map[string]struct{} {
182
+ out := make(map[string]struct{})
183
+ if p == nil || p.parsed == nil || p.parsed.ParsedConfig == nil {
184
+ return out
185
+ }
186
+ for _, fileName := range p.parsed.ParsedConfig.FileNames {
187
+ if isLintSourceFileName(fileName) {
188
+ out[canonicalProjectPath(p.cwd, fileName)] = struct{}{}
189
+ }
190
+ }
191
+ return out
192
+ }
193
+
194
+ func canonicalProjectPath(cwd, fileName string) string {
195
+ if !filepath.IsAbs(fileName) {
196
+ fileName = filepath.Join(cwd, fileName)
197
+ }
198
+ return filepath.ToSlash(filepath.Clean(fileName))
199
+ }
200
+
201
+ func isLintSourceFileName(fileName string) bool {
202
+ switch strings.ToLower(filepath.Ext(fileName)) {
203
+ case ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs":
204
+ return true
205
+ default:
206
+ return false
207
+ }
208
+ }
209
+
136
210
  // programDiagnostics returns the bind + semantic diagnostics for the
137
211
  // loaded program. Same surface tsgo's CLI prints when you run a regular
138
212
  // `tsgo --noEmit`.
@@ -185,6 +259,68 @@ func forceNoEmit(parsed *tsoptions.ParsedCommandLine) {
185
259
  parsed.ParsedConfig.CompilerOptions.NoEmit = shimcore.TSTrue
186
260
  }
187
261
 
262
+ // parseTsgoArgs runs forwarded tsgo CLI flags through TypeScript-Go's own
263
+ // command-line parser, yielding a CompilerOptions overlay loadProgram merges
264
+ // over the tsconfig — so a flag like `ttsc --strict` reaches the in-process
265
+ // lint program even though @ttsc/lint never shells out to `tsgo`. Returns an
266
+ // empty (non-nil) options value when there are no forwarded flags.
267
+ func parseTsgoArgs(args []string, host shimcompiler.CompilerHost) (*shimcore.CompilerOptions, []*shimast.Diagnostic) {
268
+ if len(args) == 0 {
269
+ return &shimcore.CompilerOptions{}, nil
270
+ }
271
+ cli := tsoptions.ParseCommandLine(args, host)
272
+ if cli == nil {
273
+ return &shimcore.CompilerOptions{}, nil
274
+ }
275
+ if len(cli.Errors) > 0 {
276
+ return nil, cli.Errors
277
+ }
278
+ return cli.CompilerOptions(), nil
279
+ }
280
+
281
+ // applyThreading forwards the --singleThreaded / --checkers knobs onto the
282
+ // parsed compiler options. ttsc mirrors tsgo here: the values land in
283
+ // CompilerOptions, and both Program.SingleThreaded() and the checker pool read
284
+ // them from there. SingleThreaded wins over Checkers, matching the pool.
285
+ //
286
+ // When a type-aware lint rule is active, loadProgram calls forceSingleChecker
287
+ // afterwards, so a `--checkers N` greater than 1 is recorded here and then
288
+ // clamped back to a single checker. AST-only lint runs keep the recorded
289
+ // checker count. `--singleThreaded` still takes full effect.
290
+ func applyThreading(parsed *tsoptions.ParsedCommandLine, singleThreaded bool, checkers int) {
291
+ if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
292
+ return
293
+ }
294
+ options := parsed.ParsedConfig.CompilerOptions
295
+ if singleThreaded {
296
+ options.SingleThreaded = shimcore.TSTrue
297
+ }
298
+ if checkers > 0 {
299
+ n := checkers
300
+ options.Checkers = &n
301
+ }
302
+ }
303
+
304
+ // forceSingleChecker pins the TypeScript-Go checker pool to a single checker.
305
+ //
306
+ // The lint engine walks the program serially and obtains types through the
307
+ // single checker GetTypeChecker hands back. Rules query types on nodes from
308
+ // arbitrary source files, so the checker must be the same one that checked
309
+ // every file. A pool of size > 1 affinitizes files to distinct checkers;
310
+ // resolving a type whose declarations cross that boundary yields `any`.
311
+ // Parallel parsing and emit are unaffected — they do not consult the count.
312
+ func forceSingleChecker(parsed *tsoptions.ParsedCommandLine) {
313
+ if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
314
+ return
315
+ }
316
+ options := parsed.ParsedConfig.CompilerOptions
317
+ if options.SingleThreaded == shimcore.TSTrue {
318
+ return
319
+ }
320
+ one := 1
321
+ options.Checkers = &one
322
+ }
323
+
188
324
  // overrideOutDir replaces the parsed config's OutDir with `outDir`.
189
325
  // Relative outDir values are resolved against `cwd`; absolute paths are
190
326
  // used as-is. Paths are converted to forward slashes for tsgo
@@ -9,10 +9,10 @@ import (
9
9
  //
10
10
  // The dispatcher is the bridge between the TypeScript-Go AST and the
11
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
12
+ // shaped to that node's grammar. Coverage is intentionally partial;
13
+ // the verbatim fallback below guarantees that an un-handled node kind
14
+ // contributes its original source bytes verbatim, so the printer can
15
+ // be wired up to a rule without breaking files that happen to use
16
16
  // shapes the per-node printers don't yet understand.
17
17
  //
18
18
  // The price of verbatim fallback is that reflow stops at the boundary
@@ -20,6 +20,19 @@ import (
20
20
  // dispatcher doesn't recognize stays long. That trade-off is preferable
21
21
  // to corrupting unfamiliar shapes — extension over time turns each
22
22
  // verbatim hop into a real reflow.
23
+ //
24
+ // Coverage signal. A verbatim slice keeps its *original* source column.
25
+ // When an un-handled node spans multiple lines, its interior lines are
26
+ // frozen at the columns the user wrote while the enclosing reflow
27
+ // re-indents everything around it — the result is inconsistently
28
+ // indented, corrupt output. To prevent that, every printer reports a
29
+ // `covered` boolean alongside its Doc: `true` means the whole printed
30
+ // subtree is reflow-safe (no multi-line verbatim node), `false` means a
31
+ // multi-line verbatim node is buried inside. The format/print-width
32
+ // rule abstains entirely when `covered` is false, so `ttsc format`
33
+ // either reflows correctly or leaves the bytes untouched — it never
34
+ // emits the half-reflowed shape. Single-line verbatim is always safe:
35
+ // a node confined to one source line has no interior column to freeze.
23
36
 
24
37
  // PrintContext bundles the per-file inputs every per-node printer
25
38
  // needs. The dispatcher constructs one per top-level reflow and threads
@@ -42,46 +55,96 @@ func NewPrintContext(file *shimast.SourceFile, opts PrintOptions) *PrintContext
42
55
 
43
56
  // PrintNode is the dispatcher entry. It picks a per-node printer based
44
57
  // 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).
58
+ // printer is registered. Returns the printed Doc and a `covered`
59
+ // boolean: `true` when the whole printed subtree is reflow-safe,
60
+ // `false` when a multi-line verbatim node is buried inside it.
61
+ //
62
+ // The format/print-width rule consults `covered` to decide whether to
63
+ // emit an edit at all — see the coverage-signal note at the top of this
64
+ // file. A `false` reading is a hard abstain, not a soft hint.
50
65
  func PrintNode(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
51
66
  if node == nil {
52
- return Doc{}, false
67
+ return Doc{}, true
53
68
  }
54
- if doc, reformatted, ok := dispatchNode(ctx, node); ok {
55
- return doc, reformatted
69
+ if doc, covered, ok := dispatchNode(ctx, node); ok {
70
+ return doc, covered
56
71
  }
57
- return verbatim(ctx, node), false
72
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
58
73
  }
59
74
 
60
75
  // dispatchNode is the per-kind switch. Each branch returns
61
- // (doc, reformatted, true) when it produces a structured Doc, or
76
+ // (doc, covered, true) when it produces a structured Doc, or
62
77
  // (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.
78
+ // `covered` flag is `true` only when the per-node printer guarantees
79
+ // the whole subtree it produced is free of multi-line verbatim slices.
65
80
  func dispatchNode(ctx *PrintContext, node *shimast.Node) (Doc, bool, bool) {
66
81
  switch node.Kind {
67
82
  case shimast.KindObjectLiteralExpression:
68
- return printObjectLiteral(ctx, node), true, true
83
+ doc, covered := printObjectLiteral(ctx, node)
84
+ return doc, covered, true
69
85
  case shimast.KindArrayLiteralExpression:
70
- return printArrayLiteral(ctx, node), true, true
86
+ doc, covered := printArrayLiteral(ctx, node)
87
+ return doc, covered, true
71
88
  case shimast.KindCallExpression:
72
- return printCallExpression(ctx, node), true, true
89
+ doc, covered := printCallExpression(ctx, node)
90
+ return doc, covered, true
73
91
  case shimast.KindNewExpression:
74
- return printNewExpression(ctx, node), true, true
92
+ doc, covered := printNewExpression(ctx, node)
93
+ return doc, covered, true
75
94
  case shimast.KindNamedImports:
76
- return printNamedImports(ctx, node), true, true
95
+ doc, covered := printNamedImports(ctx, node)
96
+ return doc, covered, true
77
97
  case shimast.KindNamedExports:
78
- return printNamedExports(ctx, node), true, true
98
+ doc, covered := printNamedExports(ctx, node)
99
+ return doc, covered, true
79
100
  case shimast.KindImportDeclaration:
80
- return printImportDeclaration(ctx, node), true, true
101
+ doc, covered := printImportDeclaration(ctx, node)
102
+ return doc, covered, true
103
+ case shimast.KindArrowFunction:
104
+ doc, covered := printArrowFunction(ctx, node)
105
+ return doc, covered, true
106
+ case shimast.KindFunctionExpression:
107
+ doc, covered := printFunctionExpression(ctx, node)
108
+ return doc, covered, true
109
+ case shimast.KindParenthesizedExpression:
110
+ doc, covered := printParenthesizedExpression(ctx, node)
111
+ return doc, covered, true
112
+ case shimast.KindBlock:
113
+ doc, covered := printBlock(ctx, node)
114
+ return doc, covered, true
115
+ case shimast.KindExpressionStatement:
116
+ doc, covered := printExpressionStatement(ctx, node)
117
+ return doc, covered, true
118
+ case shimast.KindReturnStatement:
119
+ doc, covered := printReturnStatement(ctx, node)
120
+ return doc, covered, true
81
121
  }
82
122
  return Doc{}, false, false
83
123
  }
84
124
 
125
+ // nodeSpansMultipleLines reports whether `node`'s trivia-trimmed source
126
+ // range crosses a newline. A verbatim slice that stays on one line is
127
+ // always reflow-safe — there is no interior column for the enclosing
128
+ // re-indent to leave stranded — so the dispatcher treats single-line
129
+ // verbatim as `covered`. A multi-line verbatim node freezes its
130
+ // interior columns and is reported uncovered.
131
+ func nodeSpansMultipleLines(ctx *PrintContext, node *shimast.Node) bool {
132
+ if node == nil {
133
+ return false
134
+ }
135
+ start := shimscanner.SkipTrivia(ctx.Source, node.Pos())
136
+ end := node.End()
137
+ if start < 0 || end < start || end > len(ctx.Source) {
138
+ return false
139
+ }
140
+ for i := start; i < end; i++ {
141
+ if ctx.Source[i] == '\n' {
142
+ return true
143
+ }
144
+ }
145
+ return false
146
+ }
147
+
85
148
  // verbatim returns the original source bytes for `node`, leading trivia
86
149
  // trimmed. Use this whenever a printer cannot fully cover a node — the
87
150
  // surrounding doc tree still flows, but the verbatim slice carries
@@ -118,3 +181,38 @@ func (ctx *PrintContext) indentUnit() int {
118
181
  }
119
182
  return 2
120
183
  }
184
+
185
+ // trailingCommaMode normalizes ctx.Opts.TrailingComma to one of "all",
186
+ // "es5", or "none". An empty value — the zero value for the field —
187
+ // reads as "all" to keep pre-existing callers and tests that built a
188
+ // PrintOptions{} without setting the field on Prettier's default
189
+ // behavior. Any other value also reads as "all"; the config layer
190
+ // rejects unknown strings before they reach the printer (see
191
+ // `expandFormatBlock` in config_format.go), so a stray value here is a
192
+ // programmer error and the safest fallback is the most-commas mode.
193
+ func (ctx *PrintContext) trailingCommaMode() string {
194
+ switch ctx.Opts.TrailingComma {
195
+ case "es5", "none":
196
+ return ctx.Opts.TrailingComma
197
+ }
198
+ return "all"
199
+ }
200
+
201
+ // allowsCallArgumentTrailingComma reports whether a multi-line call /
202
+ // new argument list should emit a trailing comma under the current
203
+ // trailingComma setting. Trailing commas in call arguments arrived in
204
+ // ES2017, so Prettier's "es5" mode excludes them just like "none" does;
205
+ // only "all" keeps them. Parameter lists are not printed by the
206
+ // dispatcher today, but the same rule applies to them when they are.
207
+ func (ctx *PrintContext) allowsCallArgumentTrailingComma() bool {
208
+ return ctx.trailingCommaMode() == "all"
209
+ }
210
+
211
+ // allowsEs5TrailingComma reports whether a multi-line ES5-permitted
212
+ // list — arrays, objects, named imports / exports — should emit a
213
+ // trailing comma. Only "none" suppresses the comma here; "es5" and
214
+ // "all" both keep it because ES5 has accepted trailing commas in these
215
+ // positions since the language's inception.
216
+ func (ctx *PrintContext) allowsEs5TrailingComma() bool {
217
+ return ctx.trailingCommaMode() != "none"
218
+ }
@@ -52,6 +52,9 @@ package linthost
52
52
  // - DocLineSuffix queues output until the next hardline/softline that
53
53
  // actually breaks; used for trailing line comments that must stick
54
54
  // to their source line.
55
+ // - DocConditionalGroup offers an ordered list of layout options; the
56
+ // engine renders the first whose first line fits the width budget
57
+ // and uses the last option as the unconditional fallback.
55
58
  //
56
59
  // The doc tree is built by helper constructors (Text, Line, Group, …)
57
60
  // below. Constructors take their children as variadic or slice
@@ -75,6 +78,7 @@ const (
75
78
  docIfBreak
76
79
  docConcat
77
80
  docLineSuffix
81
+ docConditionalGroup
78
82
  )
79
83
 
80
84
  // Doc is one node in the layout tree. Only the fields relevant to the
@@ -93,6 +97,11 @@ type Doc struct {
93
97
  Text string
94
98
  Children []Doc
95
99
  Width int
100
+ // Break, meaningful only on a docGroup, forces the group to render
101
+ // broken regardless of whether its flat form would fit. A
102
+ // ConditionalGroup option uses it to commit its last argument — a
103
+ // hugged object literal — to the multi-line shape.
104
+ Break bool
96
105
  // IfBreak pairs: BreakChild stored in Children[0], FlatChild in Children[1].
97
106
  }
98
107
 
@@ -116,6 +125,16 @@ func Literalline() Doc { return Doc{Kind: docLiteralline} }
116
125
  // concatenated.
117
126
  func Group(parts ...Doc) Doc { return Doc{Kind: docGroup, Children: parts} }
118
127
 
128
+ // ConditionalGroup picks the first option whose first line fits the
129
+ // remaining width budget, falling back to the last option when none
130
+ // fit. Where Group makes a single flat-or-break decision, a
131
+ // ConditionalGroup lets a printer offer several distinct shapes — a
132
+ // call's hugged vs. exploded argument list — and have the engine choose
133
+ // between them. The last option must always be a safe fallback.
134
+ func ConditionalGroup(options ...Doc) Doc {
135
+ return Doc{Kind: docConditionalGroup, Children: options}
136
+ }
137
+
119
138
  // Indent adds `width` columns of indentation to every newline emitted by
120
139
  // the child doc. Nesting composes.
121
140
  func Indent(width int, parts ...Doc) Doc {