@ttsc/lint 0.12.3 → 0.12.4

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 (62) hide show
  1. package/lib/defaultFormat.d.ts +10 -11
  2. package/lib/defaultFormat.js +10 -11
  3. package/lib/defaultFormat.js.map +1 -1
  4. package/lib/index.js +10 -4
  5. package/lib/index.js.map +1 -1
  6. package/lib/structures/ITtscLintConfig.d.ts +2 -2
  7. package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
  8. package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
  9. package/lib/structures/TtscLintRule.d.ts +10 -1
  10. package/lib/structures/TtscLintRuleMap.d.ts +2 -2
  11. package/linthost/ast_helpers.go +17 -0
  12. package/linthost/compile.go +37 -0
  13. package/linthost/config.go +184 -35
  14. package/linthost/config_format.go +257 -244
  15. package/linthost/directives.go +72 -0
  16. package/linthost/dispatch.go +33 -33
  17. package/linthost/engine.go +4 -0
  18. package/linthost/eslint_runtime.go +31 -0
  19. package/linthost/fix.go +29 -0
  20. package/linthost/format.go +23 -0
  21. package/linthost/host.go +11 -0
  22. package/linthost/print_dispatch.go +7 -5
  23. package/linthost/print_doc.go +4 -0
  24. package/linthost/print_nodes_call.go +9 -0
  25. package/linthost/rules_arrays.go +5 -2
  26. package/linthost/rules_debugger.go +3 -2
  27. package/linthost/rules_dupes.go +7 -4
  28. package/linthost/rules_empty.go +3 -2
  29. package/linthost/rules_escape.go +15 -0
  30. package/linthost/rules_eval.go +3 -0
  31. package/linthost/rules_finally.go +11 -0
  32. package/linthost/rules_format_jsdoc.go +7 -0
  33. package/linthost/rules_format_print_width.go +3 -0
  34. package/linthost/rules_format_quotes.go +3 -0
  35. package/linthost/rules_format_sort_imports.go +4 -0
  36. package/linthost/rules_gap.go +20 -0
  37. package/linthost/rules_imports.go +5 -7
  38. package/linthost/rules_logic.go +11 -0
  39. package/linthost/rules_loops.go +4 -0
  40. package/linthost/rules_misc.go +4 -2
  41. package/linthost/rules_params.go +7 -11
  42. package/linthost/rules_problems.go +24 -1
  43. package/linthost/rules_promise.go +12 -0
  44. package/linthost/rules_protos.go +3 -2
  45. package/linthost/rules_self.go +6 -0
  46. package/linthost/rules_strings.go +10 -0
  47. package/linthost/rules_suggestions.go +14 -4
  48. package/linthost/rules_throw.go +2 -0
  49. package/linthost/rules_ts.go +13 -0
  50. package/linthost/rules_ts_extra.go +25 -8
  51. package/linthost/rules_var.go +14 -1
  52. package/package.json +3 -3
  53. package/plugin/main.go +4 -4
  54. package/rule/astutil/astutil.go +12 -0
  55. package/rule/rule.go +123 -123
  56. package/src/defaultFormat.ts +10 -11
  57. package/src/index.ts +16 -4
  58. package/src/structures/ITtscLintConfig.ts +2 -2
  59. package/src/structures/ITtscLintFormatConfig.ts +53 -55
  60. package/src/structures/ITtscLintPluginMeta.ts +9 -1
  61. package/src/structures/TtscLintRule.ts +10 -1
  62. package/src/structures/TtscLintRuleMap.ts +17 -18
@@ -1,3 +1,10 @@
1
+ // Inline-disable directive parser and filter for the lint engine.
2
+ //
3
+ // Supports both `eslint-disable` and `lint-disable` comment families in
4
+ // four forms: `disable`, `enable`, `disable-line`, and
5
+ // `disable-next-line`. Rule lists are comma- or space-separated; an
6
+ // empty list disables all rules. The `--` description separator (ESLint
7
+ // convention) is also recognized and stripped before parsing rule names.
1
8
  package linthost
2
9
 
3
10
  import (
@@ -7,36 +14,61 @@ import (
7
14
  shimscanner "github.com/microsoft/typescript-go/shim/scanner"
8
15
  )
9
16
 
17
+ // lintDirectiveKind classifies the four comment forms the parser recognizes.
10
18
  type lintDirectiveKind int
11
19
 
12
20
  const (
21
+ // lintDirectiveDisable activates suppression from the comment position until
22
+ // a matching `enable` is seen (or end of file).
13
23
  lintDirectiveDisable lintDirectiveKind = iota
24
+ // lintDirectiveEnable cancels a prior `disable` for the named rules (or all
25
+ // rules when no rule list is given).
14
26
  lintDirectiveEnable
27
+ // lintDirectiveDisableLine suppresses findings on the same source line as
28
+ // the directive comment.
15
29
  lintDirectiveDisableLine
30
+ // lintDirectiveDisableNextLine suppresses findings on the line immediately
31
+ // following the directive comment.
16
32
  lintDirectiveDisableNextLine
17
33
  )
18
34
 
35
+ // lintDirective is the parsed representation of one directive comment.
19
36
  type lintDirective struct {
20
37
  kind lintDirectiveKind
21
38
  rules lintDirectiveRules
22
39
  }
23
40
 
41
+ // lintDirectiveRules holds the rule scope of a directive. When `all` is
42
+ // true the directive applies to every rule; otherwise only the named rules
43
+ // in `rules` are affected.
24
44
  type lintDirectiveRules struct {
25
45
  all bool
26
46
  rules map[string]struct{}
27
47
  }
28
48
 
49
+ // lintDirectiveEvent is one enable/disable transition recorded in source
50
+ // order. `pos` is the byte offset of the comment. `on` is true for a
51
+ // disable event and false for an enable event.
29
52
  type lintDirectiveEvent struct {
30
53
  pos int
31
54
  rules lintDirectiveRules
32
55
  on bool
33
56
  }
34
57
 
58
+ // lintInlineDirectives accumulates the per-file directive information
59
+ // extracted by parseLintInlineDirectives. `lines` maps a zero-based line
60
+ // number to any disable-line / disable-next-line directives on that line.
61
+ // `events` is the ordered list of range-style disable/enable transitions.
35
62
  type lintInlineDirectives struct {
36
63
  lines map[int][]lintDirectiveRules
37
64
  events []lintDirectiveEvent
38
65
  }
39
66
 
67
+ // lintDisableState tracks the cumulative suppress/allow state as
68
+ // lintDirectiveEvents are replayed in order up to a finding's position.
69
+ // `all` means every rule is suppressed. `rules` is the set of individually
70
+ // suppressed rules. `enabledInAll` is the set of rules that were
71
+ // re-enabled via `eslint-enable <rule>` while `all` was active.
40
72
  type lintDisableState struct {
41
73
  all bool
42
74
  rules map[string]struct{}
@@ -60,6 +92,9 @@ func filterInlineDisabledFindings(file *shimast.SourceFile, findings []*Finding)
60
92
  return filtered
61
93
  }
62
94
 
95
+ // parseLintInlineDirectives scans all comment tokens in `file` for
96
+ // recognized directive markers and returns a structured summary of the
97
+ // per-line and range-style suppressions found.
63
98
  func parseLintInlineDirectives(file *shimast.SourceFile) *lintInlineDirectives {
64
99
  directives := &lintInlineDirectives{
65
100
  lines: make(map[int][]lintDirectiveRules),
@@ -111,10 +146,16 @@ scan:
111
146
  return directives
112
147
  }
113
148
 
149
+ // empty reports whether no directives were found in the file, allowing the
150
+ // caller to skip the filtering step entirely.
114
151
  func (d *lintInlineDirectives) empty() bool {
115
152
  return d == nil || (len(d.lines) == 0 && len(d.events) == 0)
116
153
  }
117
154
 
155
+ // suppresses reports whether the directive set causes `finding` to be
156
+ // suppressed. It checks the per-line map first (disable-line and
157
+ // disable-next-line directives), then replays the ordered event list to
158
+ // compute the range-style disable/enable state at the finding's position.
118
159
  func (d *lintInlineDirectives) suppresses(file *shimast.SourceFile, finding *Finding) bool {
119
160
  if d == nil || finding == nil || file == nil {
120
161
  return false
@@ -135,6 +176,10 @@ func (d *lintInlineDirectives) suppresses(file *shimast.SourceFile, finding *Fin
135
176
  return state.matches(finding.Rule)
136
177
  }
137
178
 
179
+ // apply updates the disable state by folding in one directive event.
180
+ // Disable events add rules; enable events remove them. When the event
181
+ // targets all rules (`event.rules.all`), the entire state is replaced
182
+ // rather than merged.
138
183
  func (s *lintDisableState) apply(event lintDirectiveEvent) {
139
184
  if event.on {
140
185
  if event.rules.all {
@@ -169,6 +214,9 @@ func (s *lintDisableState) apply(event lintDirectiveEvent) {
169
214
  }
170
215
  }
171
216
 
217
+ // matches reports whether `rule` is currently suppressed given this state.
218
+ // The name is normalized before lookup so that `@typescript-eslint/` prefixes
219
+ // do not prevent a match.
172
220
  func (s lintDisableState) matches(rule string) bool {
173
221
  normalized := normalizeDirectiveRuleName(rule)
174
222
  if _, ok := s.rules[normalized]; ok {
@@ -181,6 +229,9 @@ func (s lintDisableState) matches(rule string) bool {
181
229
  return !enabled
182
230
  }
183
231
 
232
+ // matches reports whether this rule-set covers `rule`. Returns true when the
233
+ // directive targeted all rules or when `rule` (normalized) appears in the
234
+ // named set.
184
235
  func (r lintDirectiveRules) matches(rule string) bool {
185
236
  if r.all {
186
237
  return true
@@ -189,6 +240,9 @@ func (r lintDirectiveRules) matches(rule string) bool {
189
240
  return ok
190
241
  }
191
242
 
243
+ // parseLintDirectiveComment strips comment delimiters from `raw` and
244
+ // delegates to parseLintDirectiveLine. Returns (zero, false) when the
245
+ // comment does not contain a recognized directive marker.
192
246
  func parseLintDirectiveComment(raw string) (lintDirective, bool) {
193
247
  text := stripCommentDelimiters(raw)
194
248
  if directive, ok := parseLintDirectiveLine(text); ok {
@@ -197,6 +251,9 @@ func parseLintDirectiveComment(raw string) (lintDirective, bool) {
197
251
  return lintDirective{}, false
198
252
  }
199
253
 
254
+ // stripCommentDelimiters removes `//` and `/* … */` syntax from `raw` and
255
+ // returns the trimmed inner text. Handles JSDoc-style `* ` prefix on the
256
+ // first content line.
200
257
  func stripCommentDelimiters(raw string) string {
201
258
  switch {
202
259
  case strings.HasPrefix(raw, "//"):
@@ -216,6 +273,9 @@ func stripCommentDelimiters(raw string) string {
216
273
  }
217
274
  }
218
275
 
276
+ // parseLintDirectiveLine matches `text` against all recognized directive
277
+ // markers in declaration order (longest suffix first to avoid prefix
278
+ // ambiguity). Returns the first match found or (zero, false) if none match.
219
279
  func parseLintDirectiveLine(text string) (lintDirective, bool) {
220
280
  for _, prefix := range []string{"eslint", "lint"} {
221
281
  for _, form := range []struct {
@@ -241,6 +301,9 @@ func parseLintDirectiveLine(text string) (lintDirective, bool) {
241
301
  return lintDirective{}, false
242
302
  }
243
303
 
304
+ // directivePayload returns the text after `marker` in `text` when `text`
305
+ // starts with `marker` followed by whitespace or end of string. The
306
+ // returned payload is trimmed of leading/trailing whitespace.
244
307
  func directivePayload(text, marker string) (string, bool) {
245
308
  if !strings.HasPrefix(text, marker) {
246
309
  return "", false
@@ -252,6 +315,9 @@ func directivePayload(text, marker string) (string, bool) {
252
315
  return strings.TrimSpace(rest), true
253
316
  }
254
317
 
318
+ // parseDirectiveRules converts the payload (the text after the directive
319
+ // marker) into a lintDirectiveRules value. The `--` separator strips an
320
+ // optional human-readable description. An empty rule list means "all rules".
255
321
  func parseDirectiveRules(payload string) lintDirectiveRules {
256
322
  payload = stripDirectiveDescription(payload)
257
323
  payload = strings.ReplaceAll(payload, ",", " ")
@@ -272,6 +338,10 @@ func parseDirectiveRules(payload string) lintDirectiveRules {
272
338
  return lintDirectiveRules{rules: rules}
273
339
  }
274
340
 
341
+ // stripDirectiveDescription returns the portion of `payload` before the
342
+ // first ` -- ` token (ESLint's description separator). The separator must
343
+ // be surrounded by whitespace or be at a string boundary to avoid
344
+ // stripping `--` from rule names like `no--foo`.
275
345
  func stripDirectiveDescription(payload string) string {
276
346
  for i := 0; i < len(payload)-1; i++ {
277
347
  if payload[i] != '-' || payload[i+1] != '-' {
@@ -287,6 +357,8 @@ func stripDirectiveDescription(payload string) string {
287
357
  return payload
288
358
  }
289
359
 
360
+ // normalizeDirectiveRuleName strips the common ESLint namespace prefixes so
361
+ // both `@typescript-eslint/no-var` and `no-var` resolve to the same key.
290
362
  func normalizeDirectiveRuleName(name string) string {
291
363
  name = strings.TrimSpace(name)
292
364
  name = strings.TrimPrefix(name, "@typescript-eslint/")
@@ -7,8 +7,8 @@
7
7
  package linthost
8
8
 
9
9
  import (
10
- "fmt"
11
- "os"
10
+ "fmt"
11
+ "os"
12
12
  )
13
13
 
14
14
  // Version is the build banner string the `version` subcommand prints.
@@ -25,41 +25,41 @@ var Version = "dev"
25
25
  // Recognized verbs: `version` / `-v` / `--version`, `check`, `fix`, `format`,
26
26
  // `build`, `transform`. Anything else is a usage error (exit code 2).
27
27
  func Main(args []string) int {
28
- return run(args)
28
+ return run(args)
29
29
  }
30
30
 
31
31
  // run is the package-local dispatcher invoked by Main and by the in-tree
32
32
  // test/command corpus, which exercises end-to-end subcommand routing through
33
33
  // the same entry point the CLI uses.
34
34
  func run(args []string) int {
35
- if len(args) == 0 {
36
- fmt.Fprintln(os.Stderr, "@ttsc/lint: command required (expected check|fix|format|build|transform|version)")
37
- return 2
38
- }
39
- switch args[0] {
40
- case "-v", "--version", "version":
41
- // Don't pay contributor-registration cost for the version banner.
42
- fmt.Fprintf(os.Stdout, "@ttsc/lint %s\n", Version)
43
- return 0
44
- case "check", "fix", "format", "build", "transform":
45
- default:
46
- fmt.Fprintf(os.Stderr, "@ttsc/lint: unknown command %q\n", args[0])
47
- return 2
48
- }
49
- // Wire contributor rules into the engine's dispatch table after every
50
- // package init has settled. See contrib_adapter.go for the rationale.
51
- registerContributors()
52
- switch args[0] {
53
- case "check":
54
- return RunCheck(args[1:])
55
- case "fix":
56
- return RunFix(args[1:])
57
- case "format":
58
- return RunFormat(args[1:])
59
- case "build":
60
- return RunBuild(args[1:])
61
- case "transform":
62
- return RunTransform(args[1:])
63
- }
64
- return 2
35
+ if len(args) == 0 {
36
+ fmt.Fprintln(os.Stderr, "@ttsc/lint: command required (expected check|fix|format|build|transform|version)")
37
+ return 2
38
+ }
39
+ switch args[0] {
40
+ case "-v", "--version", "version":
41
+ // Don't pay contributor-registration cost for the version banner.
42
+ fmt.Fprintf(os.Stdout, "@ttsc/lint %s\n", Version)
43
+ return 0
44
+ case "check", "fix", "format", "build", "transform":
45
+ default:
46
+ fmt.Fprintf(os.Stderr, "@ttsc/lint: unknown command %q\n", args[0])
47
+ return 2
48
+ }
49
+ // Wire contributor rules into the engine's dispatch table after every
50
+ // package init has settled. See contrib_adapter.go for the rationale.
51
+ registerContributors()
52
+ switch args[0] {
53
+ case "check":
54
+ return RunCheck(args[1:])
55
+ case "fix":
56
+ return RunFix(args[1:])
57
+ case "format":
58
+ return RunFormat(args[1:])
59
+ case "build":
60
+ return RunBuild(args[1:])
61
+ case "transform":
62
+ return RunTransform(args[1:])
63
+ }
64
+ return 2
65
65
  }
@@ -184,6 +184,10 @@ func (c *Context) ReportRangeFix(pos, end int, message string, edits ...TextEdit
184
184
  })
185
185
  }
186
186
 
187
+ // cloneTextEdits returns a shallow copy of `edits` so that the caller's
188
+ // variadic slice cannot be mutated through the stored Finding. Returns nil
189
+ // when the input is empty, keeping the Finding.Fix field nil rather than
190
+ // a zero-length slice.
187
191
  func cloneTextEdits(edits []TextEdit) []TextEdit {
188
192
  if len(edits) == 0 {
189
193
  return nil
@@ -13,23 +13,31 @@ import (
13
13
  shimdw "github.com/microsoft/typescript-go/shim/diagnosticwriter"
14
14
  )
15
15
 
16
+ // eslintRuntimeProvider is the optional interface that a RuleResolver
17
+ // implementation may satisfy to enable the external ESLint subprocess path.
18
+ // ConfigStore implements all three methods; other resolvers that do not
19
+ // implement this interface silently bypass the ESLint runtime.
16
20
  type eslintRuntimeProvider interface {
17
21
  ExternalConfigPath() string
18
22
  WantsESLintRuntime() bool
19
23
  RequiresESLintRuntime() bool
20
24
  }
21
25
 
26
+ // eslintRuntimeOutput is the top-level JSON object written to stdout by
27
+ // the embedded externalESLintRunnerScript.
22
28
  type eslintRuntimeOutput struct {
23
29
  Missing bool `json:"missing"`
24
30
  Fixed int `json:"fixed"`
25
31
  Results []eslintRuntimeFile `json:"results"`
26
32
  }
27
33
 
34
+ // eslintRuntimeFile mirrors one entry from ESLint's LintResult array.
28
35
  type eslintRuntimeFile struct {
29
36
  FilePath string `json:"filePath"`
30
37
  Messages []eslintRuntimeMessage `json:"messages"`
31
38
  }
32
39
 
40
+ // eslintRuntimeMessage mirrors one entry from ESLint's LintMessage array.
33
41
  type eslintRuntimeMessage struct {
34
42
  RuleID string `json:"ruleId"`
35
43
  Severity int `json:"severity"`
@@ -40,6 +48,13 @@ type eslintRuntimeMessage struct {
40
48
  EndColumn int `json:"endColumn"`
41
49
  }
42
50
 
51
+ // runExternalESLintDiagnostics delegates to the project's installed ESLint
52
+ // binary (via the embedded JS runner) and converts the JSON output into
53
+ // LintDiagnostic values anchored to their source positions. Returns
54
+ // (nil, false, nil) when the resolver does not want the ESLint runtime,
55
+ // (nil, true, nil) when no source files qualify, and (diags, true, nil)
56
+ // on success. The bool return is "ran ESLint" — callers use it to skip
57
+ // native-rule diagnostics when ESLint was the sole configured source.
43
58
  func runExternalESLintDiagnostics(
44
59
  resolver RuleResolver,
45
60
  cwd string,
@@ -98,14 +113,17 @@ func runExternalESLintDiagnostics(
98
113
  }
99
114
  for _, msg := range result.Messages {
100
115
  if msg.Severity == 0 {
116
+ // ESLint severity 0 means "off" — skip silently.
101
117
  continue
102
118
  }
103
119
  ruleID := strings.TrimSpace(msg.RuleID)
104
120
  if ruleID == "" {
121
+ // ESLint can emit messages without a ruleId for parse errors.
105
122
  ruleID = "eslint"
106
123
  }
107
124
  category := shimdw.LintCategoryWarning
108
125
  if msg.Severity >= 2 {
126
+ // severity 2 = error; values above 2 are treated as error too.
109
127
  category = shimdw.LintCategoryError
110
128
  }
111
129
  pos := positionOfESLintLocation(file.Text(), msg.Line, msg.Column)
@@ -126,10 +144,14 @@ func runExternalESLintDiagnostics(
126
144
  return diagnostics, true, nil
127
145
  }
128
146
 
147
+ // runExternalESLint invokes ESLint in check (read-only) mode.
129
148
  func runExternalESLint(cwd, configPath, fileListJSON string) (*eslintRuntimeOutput, error) {
130
149
  return runExternalESLintWithMode(cwd, configPath, fileListJSON, false)
131
150
  }
132
151
 
152
+ // runExternalESLintFixes invokes ESLint in fix mode and returns the number of
153
+ // files that were actually modified. The fix runner calls ESLint.outputFixes
154
+ // inside the JS subprocess, which writes changes directly to disk.
133
155
  func runExternalESLintFixes(
134
156
  resolver RuleResolver,
135
157
  cwd string,
@@ -180,6 +202,10 @@ func runExternalESLintFixes(
180
202
  return output.Fixed, nil
181
203
  }
182
204
 
205
+ // runExternalESLintWithMode spawns a Node.js subprocess that runs the embedded
206
+ // externalESLintRunnerScript. When fix is true the runner calls
207
+ // ESLint.outputFixes and returns the count of modified files; when false it
208
+ // returns diagnostics only and leaves files untouched.
183
209
  func runExternalESLintWithMode(cwd, configPath, fileListJSON string, fix bool) (*eslintRuntimeOutput, error) {
184
210
  node := os.Getenv("TTSC_NODE_BINARY")
185
211
  if node == "" {
@@ -210,6 +236,11 @@ func runExternalESLintWithMode(cwd, configPath, fileListJSON string, fix bool) (
210
236
  return &output, nil
211
237
  }
212
238
 
239
+ // positionOfESLintLocation converts a 1-based (line, column) position from
240
+ // ESLint's output — where column is a UTF-16 code-unit offset — into a
241
+ // zero-based byte offset into text. This matches how tsgo positions source
242
+ // spans: lines are 1-based, columns are UTF-16 units (so a supplementary
243
+ // codepoint counts as 2). Returns len(text) when the position is past EOF.
213
244
  func positionOfESLintLocation(text string, line, column int) int {
214
245
  if line <= 0 {
215
246
  line = 1
package/linthost/fix.go CHANGED
@@ -1,3 +1,11 @@
1
+ // Autofix orchestration for the `@ttsc/lint fix` subcommand.
2
+ //
3
+ // RunFix drives the fix cascade: it applies ESLint runtime fixes first
4
+ // (one pass, external process), then repeatedly runs the native lint
5
+ // engine and applies any emitted TextEdit suggestions until no more
6
+ // fixable findings remain or maxFixPasses is reached. After the cascade
7
+ // settles, it runs a final diagnostic pass so remaining issues are
8
+ // surfaced in the normal error stream.
1
9
  package linthost
2
10
 
3
11
  import (
@@ -121,6 +129,8 @@ func runFix(opts *subcommandOpts) int {
121
129
  return 0
122
130
  }
123
131
 
132
+ // loadFixProgram loads the TypeScript program for a fix/format pass with
133
+ // NoEmit forced on. Returns (nil, 2) when loading or config parsing fails.
124
134
  func loadFixProgram(opts *subcommandOpts) (*program, int) {
125
135
  prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
126
136
  forceNoEmit: true,
@@ -137,6 +147,9 @@ func loadFixProgram(opts *subcommandOpts) (*program, int) {
137
147
  return prog, 0
138
148
  }
139
149
 
150
+ // reloadFixProgram closes `current` and loads a fresh program from disk.
151
+ // Used between cascade passes so the engine sees edits applied in the
152
+ // previous pass rather than stale in-memory AST nodes.
140
153
  func reloadFixProgram(current *program, opts *subcommandOpts) (*program, int) {
141
154
  if current != nil {
142
155
  current.close()
@@ -144,12 +157,19 @@ func reloadFixProgram(current *program, opts *subcommandOpts) (*program, int) {
144
157
  return loadFixProgram(opts)
145
158
  }
146
159
 
160
+ // fileFixes groups all pending TextEdit suggestions for a single file.
161
+ // `text` is the source content at the time the findings were collected;
162
+ // byte offsets in `edits` are relative to this snapshot.
147
163
  type fileFixes struct {
148
164
  path string
149
165
  text string
150
166
  edits []TextEdit
151
167
  }
152
168
 
169
+ // applyFindingFixes groups all fixable findings by file, resolves each
170
+ // file path to an absolute form, then applies the edit batches in
171
+ // deterministic order (sorted by path). Returns the total number of edits
172
+ // written to disk.
153
173
  func applyFindingFixes(cwd string, findings []*Finding) (int, error) {
154
174
  byFile := map[string]*fileFixes{}
155
175
  for _, finding := range findings {
@@ -191,6 +211,10 @@ func applyFindingFixes(cwd string, findings []*Finding) (int, error) {
191
211
  return total, nil
192
212
  }
193
213
 
214
+ // applyTextEditsToFile selects the non-overlapping edits from `edits`, applies
215
+ // them to `source` in reverse order (right-to-left) to preserve earlier
216
+ // offsets, and writes the result to `path`. Returns the number of edits
217
+ // applied, or 0 when no edits survive selection.
194
218
  func applyTextEditsToFile(path, source string, edits []TextEdit) (int, error) {
195
219
  selected := selectTextEdits(len(source), edits)
196
220
  if len(selected) == 0 {
@@ -210,6 +234,11 @@ func applyTextEditsToFile(path, source string, edits []TextEdit) (int, error) {
210
234
  return len(selected), nil
211
235
  }
212
236
 
237
+ // selectTextEdits filters and sorts `edits` into a non-overlapping
238
+ // application sequence. Out-of-bounds edits and exact duplicates are
239
+ // removed first; the remainder is sorted by start position then end
240
+ // position (left to right). A greedy scan then keeps the earliest-starting
241
+ // edit and drops any that overlap with it, producing a disjoint set.
213
242
  func selectTextEdits(sourceLen int, edits []TextEdit) []TextEdit {
214
243
  if len(edits) == 0 {
215
244
  return nil
@@ -33,6 +33,8 @@ func RunFormat(args []string) int {
33
33
  return runFormat(opts)
34
34
  }
35
35
 
36
+ // runFormat is the internal implementation of RunFormat. It drives the
37
+ // cascade loop and applies format-rule edits until convergence.
36
38
  func runFormat(opts *subcommandOpts) int {
37
39
  rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
38
40
  if err != nil {
@@ -89,10 +91,17 @@ func runFormat(opts *subcommandOpts) int {
89
91
  return 0
90
92
  }
91
93
 
94
+ // formatCommandResolver wraps a RuleResolver and ensures every format-class
95
+ // rule referenced in the loaded plugin options is activated at warn severity,
96
+ // even if the user's config omitted it. This lets `ttsc format` format files
97
+ // without requiring explicit rule declarations in the project config.
92
98
  type formatCommandResolver struct {
93
99
  inner RuleResolver
94
100
  }
95
101
 
102
+ // ResolveRules implements RuleResolver. It delegates to the inner resolver
103
+ // and then upgrades format-rule entries from off to warn so they are applied
104
+ // even when the project config omits them.
96
105
  func (r formatCommandResolver) ResolveRules(fileName string) ResolvedRuleConfig {
97
106
  resolved := r.inner.ResolveRules(fileName)
98
107
  if resolved.Ignored {
@@ -109,6 +118,8 @@ func (r formatCommandResolver) ResolveRules(fileName string) ResolvedRuleConfig
109
118
  return resolved
110
119
  }
111
120
 
121
+ // ActiveRuleNames implements RuleResolver. Returns the union of the inner
122
+ // resolver's active rules and every format-option rule that is registered.
112
123
  func (r formatCommandResolver) ActiveRuleNames() []string {
113
124
  active := map[string]struct{}{}
114
125
  for _, name := range r.inner.ActiveRuleNames() {
@@ -120,6 +131,8 @@ func (r formatCommandResolver) ActiveRuleNames() []string {
120
131
  return sortedKeys(active)
121
132
  }
122
133
 
134
+ // EnabledRuleConfig implements RuleResolver. Merges the inner config with
135
+ // the format-option rules so callers see the full active set.
123
136
  func (r formatCommandResolver) EnabledRuleConfig() RuleConfig {
124
137
  enabled := r.inner.EnabledRuleConfig()
125
138
  if enabled == nil {
@@ -133,10 +146,14 @@ func (r formatCommandResolver) EnabledRuleConfig() RuleConfig {
133
146
  return enabled
134
147
  }
135
148
 
149
+ // RuleOptions implements RuleResolver by delegating directly to the inner resolver.
136
150
  func (r formatCommandResolver) RuleOptions(name string) json.RawMessage {
137
151
  return r.inner.RuleOptions(name)
138
152
  }
139
153
 
154
+ // formatOptionRuleNames returns the sorted list of rule names from the inner
155
+ // resolver's options that are registered as format rules. These are the rules
156
+ // that formatCommandResolver promotes from off to warn.
140
157
  func (r formatCommandResolver) formatOptionRuleNames() []string {
141
158
  options := resolverOptions(r.inner)
142
159
  if len(options) == 0 {
@@ -152,6 +169,9 @@ func (r formatCommandResolver) formatOptionRuleNames() []string {
152
169
  return names
153
170
  }
154
171
 
172
+ // resolverOptions extracts the raw options map from a resolver whose concrete
173
+ // type exposes one. Returns nil for resolver types that don't carry per-rule
174
+ // options (e.g. bare RuleConfig).
155
175
  func resolverOptions(resolver RuleResolver) RuleOptionsMap {
156
176
  switch r := resolver.(type) {
157
177
  case InlineRuleResolver:
@@ -163,11 +183,14 @@ func resolverOptions(resolver RuleResolver) RuleOptionsMap {
163
183
  }
164
184
  }
165
185
 
186
+ // isRegisteredFormatRule reports whether `name` is both registered in the
187
+ // global rule registry and tagged as a format rule via the FormatRule marker.
166
188
  func isRegisteredFormatRule(name string) bool {
167
189
  rule, ok := registered.rules[name]
168
190
  return ok && isFormatRule(rule)
169
191
  }
170
192
 
193
+ // sortedKeys returns the sorted slice of keys from a string-keyed set.
171
194
  func sortedKeys(input map[string]struct{}) []string {
172
195
  names := make([]string, 0, len(input))
173
196
  for name := range input {
package/linthost/host.go CHANGED
@@ -108,6 +108,8 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
108
108
  }, nil, nil
109
109
  }
110
110
 
111
+ // close releases the type checker acquired by loadProgram. Safe to call on
112
+ // a nil receiver and idempotent after the first call.
111
113
  func (p *program) close() {
112
114
  if p == nil {
113
115
  return
@@ -162,6 +164,8 @@ func (p *program) findSourceFile(target string) *shimast.SourceFile {
162
164
  return nil
163
165
  }
164
166
 
167
+ // forceEmit clears the NoEmit and EmitDeclarationOnly flags so the
168
+ // program emits JavaScript even when the tsconfig says otherwise.
165
169
  func forceEmit(parsed *tsoptions.ParsedCommandLine) {
166
170
  if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
167
171
  return
@@ -171,6 +175,9 @@ func forceEmit(parsed *tsoptions.ParsedCommandLine) {
171
175
  options.EmitDeclarationOnly = shimcore.TSFalse
172
176
  }
173
177
 
178
+ // forceNoEmit sets the NoEmit flag regardless of what the tsconfig
179
+ // specifies. Used by fix and check subcommands that must not write output
180
+ // files as a side effect of type-checking.
174
181
  func forceNoEmit(parsed *tsoptions.ParsedCommandLine) {
175
182
  if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
176
183
  return
@@ -178,6 +185,10 @@ func forceNoEmit(parsed *tsoptions.ParsedCommandLine) {
178
185
  parsed.ParsedConfig.CompilerOptions.NoEmit = shimcore.TSTrue
179
186
  }
180
187
 
188
+ // overrideOutDir replaces the parsed config's OutDir with `outDir`.
189
+ // Relative outDir values are resolved against `cwd`; absolute paths are
190
+ // used as-is. Paths are converted to forward slashes for tsgo
191
+ // compatibility.
181
192
  func overrideOutDir(cwd string, parsed *tsoptions.ParsedCommandLine, outDir string) {
182
193
  if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
183
194
  return
@@ -98,9 +98,10 @@ func verbatim(ctx *PrintContext, node *shimast.Node) Doc {
98
98
  return Text(ctx.Source[start:end])
99
99
  }
100
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).
101
+ // verbatimRange returns a Text doc holding src[start:end] verbatim.
102
+ // It is the position-only sibling of verbatim: use it when the sub-range
103
+ // to copy does not correspond to a single AST node (e.g. `<T>` tokens
104
+ // that surround a type-argument NodeList).
104
105
  func verbatimRange(src string, start, end int) Doc {
105
106
  if start < 0 || end < start || end > len(src) {
106
107
  return Doc{}
@@ -108,8 +109,9 @@ func verbatimRange(src string, start, end int) Doc {
108
109
  return Text(src[start:end])
109
110
  }
110
111
 
111
- // indentUnit returns one indentation step's worth of columns. Each
112
- // per-node printer uses this for its own list nesting.
112
+ // indentUnit returns the number of columns in one indentation step,
113
+ // derived from PrintOptions.TabWidth. Falls back to 2 when TabWidth
114
+ // is not set, matching the Prettier default.
113
115
  func (ctx *PrintContext) indentUnit() int {
114
116
  if ctx.Opts.TabWidth > 0 {
115
117
  return ctx.Opts.TabWidth
@@ -56,6 +56,10 @@ package linthost
56
56
  // The doc tree is built by helper constructors (Text, Line, Group, …)
57
57
  // below. Constructors take their children as variadic or slice
58
58
  // arguments so call sites read like a layout DSL.
59
+
60
+ // DocKind is the discriminant tag for a Doc node. Only the variant
61
+ // fields relevant to that kind are populated; all others stay at their
62
+ // zero value.
59
63
  type DocKind uint8
60
64
 
61
65
  const (
@@ -117,6 +117,9 @@ func printArgList(ctx *PrintContext, list *shimast.NodeList) Doc {
117
117
  // Type-argument byte-range helpers. The shim's NodeList.End() points
118
118
  // past the last argument; the surrounding `<` and `>` are not part of
119
119
  // the list's range, so we have to scan around it.
120
+
121
+ // callTypeArgsStart returns the byte offset of the `<` that opens the
122
+ // type-argument list of a CallExpression. Returns -1 when absent.
120
123
  func callTypeArgsStart(ctx *PrintContext, call *shimast.CallExpression) int {
121
124
  if call.TypeArguments == nil || len(call.TypeArguments.Nodes) == 0 {
122
125
  return -1
@@ -136,6 +139,8 @@ func callTypeArgsStart(ctx *PrintContext, call *shimast.CallExpression) int {
136
139
  return -1
137
140
  }
138
141
 
142
+ // callTypeArgsEnd returns the byte offset one past the closing `>` of a
143
+ // CallExpression's type-argument list. Returns -1 when absent.
139
144
  func callTypeArgsEnd(ctx *PrintContext, call *shimast.CallExpression) int {
140
145
  if call.TypeArguments == nil {
141
146
  return -1
@@ -149,6 +154,8 @@ func callTypeArgsEnd(ctx *PrintContext, call *shimast.CallExpression) int {
149
154
  return end
150
155
  }
151
156
 
157
+ // newTypeArgsStart returns the byte offset of the `<` that opens the
158
+ // type-argument list of a NewExpression. Returns -1 when absent.
152
159
  func newTypeArgsStart(ctx *PrintContext, ne *shimast.NewExpression) int {
153
160
  if ne.TypeArguments == nil || len(ne.TypeArguments.Nodes) == 0 {
154
161
  return -1
@@ -166,6 +173,8 @@ func newTypeArgsStart(ctx *PrintContext, ne *shimast.NewExpression) int {
166
173
  return -1
167
174
  }
168
175
 
176
+ // newTypeArgsEnd returns the byte offset one past the closing `>` of a
177
+ // NewExpression's type-argument list. Returns -1 when absent.
169
178
  func newTypeArgsEnd(ctx *PrintContext, ne *shimast.NewExpression) int {
170
179
  if ne.TypeArguments == nil {
171
180
  return -1