@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,8 +1,8 @@
1
- // Bulk implementation of @typescript-eslint rules that work off the
2
- // AST alone (no checker, no scope analysis). The set is curated to
3
- // match the rules `eslint-plugin-typescript`'s recommended preset relies
4
- // on most heavily the rest of that plugin's catalog is type-aware and
5
- // out of scope for v0.
1
+ // Extended @typescript-eslint rules that work off the AST alone (no
2
+ // checker, no scope analysis). Rules here complement the core set in
3
+ // rules_ts.go; the split is by recommendation tier — this file covers
4
+ // rules that appear in typescript-eslint strict, stylistic, or as
5
+ // commonly-requested extras. Each rule is registered in init() below.
6
6
  package linthost
7
7
 
8
8
  import (
@@ -187,6 +187,10 @@ func (noNonNullAssertedOptionalChain) Check(ctx *Context, node *shimast.Node) {
187
187
  }
188
188
  }
189
189
 
190
+ // containsOptionalChain reports whether node or any of its left-hand
191
+ // sub-expressions uses the optional-chaining operator (?.). Only descends
192
+ // into PropertyAccessExpression, ElementAccessExpression, and
193
+ // CallExpression chains — stops at any other node kind.
190
194
  func containsOptionalChain(node *shimast.Node) bool {
191
195
  if node == nil {
192
196
  return false
@@ -333,6 +337,8 @@ func (preferForOf) Check(ctx *Context, node *shimast.Node) {
333
337
  ctx.Report(node, "Prefer a 'for-of' loop instead of a 'for' loop with this simple iteration.")
334
338
  }
335
339
 
340
+ // isCounterIncrement reports whether node is a prefix or postfix `++`
341
+ // applied to the identifier named counter. Used by prefer-for-of.
336
342
  func isCounterIncrement(node *shimast.Node, counter string) bool {
337
343
  switch node.Kind {
338
344
  case shimast.KindPostfixUnaryExpression:
@@ -501,6 +507,11 @@ func (consistentTypeImports) Check(ctx *Context, node *shimast.Node) {
501
507
  ctx.Report(node, "All imports in the declaration are only used as types. Use `import type`.")
502
508
  }
503
509
 
510
+ // allUsesAreTypeOnly reports whether every reference to any of the given
511
+ // names in the subtree rooted at root occurs inside a type-only position
512
+ // (TypeReferenceNode, TypeAliasDeclaration, InterfaceDeclaration, etc.).
513
+ // A reference inside another ImportDeclaration is skipped entirely.
514
+ // Returns false as soon as a value-position reference is found.
504
515
  func allUsesAreTypeOnly(root *shimast.Node, names []string) bool {
505
516
  want := map[string]bool{}
506
517
  for _, n := range names {
@@ -675,6 +686,9 @@ func (adjacentOverloadSignatures) Check(ctx *Context, node *shimast.Node) {
675
686
  }
676
687
  }
677
688
 
689
+ // containerMembers returns the direct child member/statement list of a
690
+ // container node (interface, type literal, class, module block, or source
691
+ // file). Returns nil for node kinds that don't have member lists.
678
692
  func containerMembers(node *shimast.Node) []*shimast.Node {
679
693
  switch node.Kind {
680
694
  case shimast.KindInterfaceDeclaration:
@@ -711,6 +725,12 @@ func containerMembers(node *shimast.Node) []*shimast.Node {
711
725
  return nil
712
726
  }
713
727
 
728
+ // overloadName extracts the canonical name and kind of an overloadable
729
+ // member node. Returns (name, kind, true) for method signatures, method
730
+ // declarations, function declarations, call signatures, and construct
731
+ // signatures; otherwise returns ("", 0, false). Call and construct
732
+ // signatures use a synthesized name that includes the kind string so
733
+ // they compare equal only to other signatures of the same shape.
714
734
  func overloadName(m *shimast.Node) (string, shimast.Kind, bool) {
715
735
  if m == nil {
716
736
  return "", 0, false
@@ -737,9 +757,6 @@ func overloadName(m *shimast.Node) (string, shimast.Kind, bool) {
737
757
  return "", 0, false
738
758
  }
739
759
 
740
- // no-this-alias-helper: shared helpers for ts rules above.
741
- var _ = struct{}{}
742
-
743
760
  func init() {
744
761
  Register(noConfusingNonNullAssertion{})
745
762
  Register(noDuplicateEnumValues{})
@@ -31,7 +31,8 @@ func (noVar) Check(ctx *Context, node *shimast.Node) {
31
31
  // prefer-const: flag `let` declarations whose binding is never reassigned.
32
32
  // This follows ESLint's core rule for the common AST-local cases. It is
33
33
  // intentionally conservative: destructuring and declaration-only `let`
34
- // variables are skipped until the lint host grows full scope/data-flow state.
34
+ // variables (those without an initializer and not in a for-of/for-in
35
+ // header) are skipped until the lint host grows full scope/data-flow state.
35
36
  // ESLint canonical: https://eslint.org/docs/latest/rules/prefer-const
36
37
  type preferConst struct{}
37
38
 
@@ -115,6 +116,10 @@ func (preferConst) Check(ctx *Context, node *shimast.Node) {
115
116
  }
116
117
  }
117
118
 
119
+ // isSingleDeclarationList reports whether the VariableDeclarationList node
120
+ // declares exactly one binding, which is required before the `let` keyword
121
+ // can safely be rewritten to `const` (a multi-binding list shares a single
122
+ // keyword, so replacing just one binding's keyword is not valid).
118
123
  func isSingleDeclarationList(node *shimast.Node) bool {
119
124
  if node == nil {
120
125
  return false
@@ -123,6 +128,14 @@ func isSingleDeclarationList(node *shimast.Node) bool {
123
128
  return list != nil && list.Declarations != nil && len(list.Declarations.Nodes) == 1
124
129
  }
125
130
 
131
+ // isConstEligibleLetDeclaration reports whether a `let` VariableDeclaration
132
+ // node is eligible for prefer-const analysis. A declaration is eligible when:
133
+ // - it has an initializer (the value is set immediately), or
134
+ // - it is the loop variable of a for-in or for-of statement (e.g. `for (let k of m)`).
135
+ //
136
+ // For-statement initializers (`for (let i = 0; …)`) are eligible only when
137
+ // the declaration list is a single binding; the loop index is a well-known
138
+ // reassignment target so multi-binding for-statement lists are excluded.
126
139
  func isConstEligibleLetDeclaration(node *shimast.Node, decl *shimast.VariableDeclaration) bool {
127
140
  if decl.Initializer != nil {
128
141
  if node.Parent != nil && node.Parent.Parent != nil && node.Parent.Parent.Kind == shimast.KindForStatement {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/lint",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
4
4
  "description": "Reference ttsc plugin: ESLint-style lint rules hosted in the same Program/Checker as the type-check pass.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -33,10 +33,10 @@
33
33
  "src"
34
34
  ],
35
35
  "devDependencies": {
36
- "@typescript/native-preview": "7.0.0-dev.20260518.1",
36
+ "@typescript/native-preview": "7.0.0-dev.20260519.1",
37
37
  "@types/node": "^25.3.0",
38
38
  "rimraf": "^6.1.2",
39
- "ttsc": "0.12.3"
39
+ "ttsc": "0.12.4"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",
package/plugin/main.go CHANGED
@@ -1,6 +1,6 @@
1
1
  // Command @ttsc/lint is the native backend for the `@ttsc/lint` plugin.
2
2
  //
3
- // The plugin host (ttsc / ttsx) spawns this binary with one of five
3
+ // The plugin host (ttsc / ttsx) spawns this binary with one of six
4
4
  // subcommands:
5
5
  //
6
6
  // - `version` / `-v` / `--version` — print the binary banner.
@@ -25,11 +25,11 @@
25
25
  package main
26
26
 
27
27
  import (
28
- "os"
28
+ "os"
29
29
 
30
- "github.com/samchon/ttsc/packages/lint/linthost"
30
+ "github.com/samchon/ttsc/packages/lint/linthost"
31
31
  )
32
32
 
33
33
  func main() {
34
- os.Exit(linthost.Main(os.Args[1:]))
34
+ os.Exit(linthost.Main(os.Args[1:]))
35
35
  }
@@ -68,6 +68,9 @@ func KeywordStart(file *shimast.SourceFile, node *shimast.Node, keyword string)
68
68
  if limit > len(src) {
69
69
  limit = len(src)
70
70
  }
71
+ // Scan at most 32 bytes past the trivia-adjusted start. Declaration keywords
72
+ // ("var", "let", "const", etc.) always appear within the first few bytes of
73
+ // a node's token range; the cap avoids runaway scanning on malformed nodes.
71
74
  for i := pos; i+len(keyword) <= limit && i < pos+32; i++ {
72
75
  end = i + len(keyword)
73
76
  if strings.HasPrefix(src[i:], keyword) &&
@@ -139,6 +142,15 @@ func TokenRange(file *shimast.SourceFile, node *shimast.Node) (int, int) {
139
142
  return pos, end
140
143
  }
141
144
 
145
+ // isIdentifierPart reports whether the ASCII byte ch can appear inside a
146
+ // JavaScript/TypeScript identifier (letters, digits, underscore, dollar sign).
147
+ // Used to detect word boundaries when searching for keyword tokens so that
148
+ // a search for "let" does not match "letters".
149
+ // Note: this is a byte-level check; non-ASCII identifier characters (e.g.
150
+ // Unicode letters) are treated as non-identifier bytes, which is conservative
151
+ // — the keyword boundary check may produce a false positive only for source
152
+ // files that use non-ASCII characters immediately adjacent to a keyword,
153
+ // an extremely rare pattern in practice.
142
154
  func isIdentifierPart(ch byte) bool {
143
155
  return (ch >= 'a' && ch <= 'z') ||
144
156
  (ch >= 'A' && ch <= 'Z') ||
package/rule/rule.go CHANGED
@@ -21,30 +21,30 @@
21
21
  //
22
22
  // Example contributor:
23
23
  //
24
- // package myrules
24
+ // package myrules
25
25
  //
26
- // import (
27
- // shimast "github.com/microsoft/typescript-go/shim/ast"
28
- // "github.com/samchon/ttsc/packages/lint/rule"
29
- // )
26
+ // import (
27
+ // shimast "github.com/microsoft/typescript-go/shim/ast"
28
+ // "github.com/samchon/ttsc/packages/lint/rule"
29
+ // )
30
30
  //
31
- // func init() { rule.Register(noTodoComment{}) }
31
+ // func init() { rule.Register(noTodoComment{}) }
32
32
  //
33
- // type noTodoComment struct{}
33
+ // type noTodoComment struct{}
34
34
  //
35
- // func (noTodoComment) Name() string { return "demo/no-todo-comment" }
36
- // func (noTodoComment) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
37
- // func (noTodoComment) Check(ctx *rule.Context, node *shimast.Node) {
38
- // // ctx.File, ctx.Checker, ctx.Severity available; ctx.Report(node, msg)
39
- // // or ctx.ReportRange(pos, end, msg) push a finding through the engine.
40
- // }
35
+ // func (noTodoComment) Name() string { return "demo/no-todo-comment" }
36
+ // func (noTodoComment) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
37
+ // func (noTodoComment) Check(ctx *rule.Context, node *shimast.Node) {
38
+ // // ctx.File, ctx.Checker, ctx.Severity available; ctx.Report(node, msg)
39
+ // // or ctx.ReportRange(pos, end, msg) push a finding through the engine.
40
+ // }
41
41
  package rule
42
42
 
43
43
  import (
44
- "encoding/json"
44
+ "encoding/json"
45
45
 
46
- shimast "github.com/microsoft/typescript-go/shim/ast"
47
- shimchecker "github.com/microsoft/typescript-go/shim/checker"
46
+ shimast "github.com/microsoft/typescript-go/shim/ast"
47
+ shimchecker "github.com/microsoft/typescript-go/shim/checker"
48
48
  )
49
49
 
50
50
  // Severity mirrors the engine's three-level severity ladder. The
@@ -53,31 +53,31 @@ import (
53
53
  type Severity int
54
54
 
55
55
  const (
56
- // SeverityOff means the rule is disabled. Engine skips dispatch.
57
- SeverityOff Severity = iota
58
- // SeverityWarn produces a warning diagnostic (does not change exit
59
- // code).
60
- SeverityWarn
61
- // SeverityError produces an error diagnostic and fails the command.
62
- SeverityError
56
+ // SeverityOff means the rule is disabled. Engine skips dispatch.
57
+ SeverityOff Severity = iota
58
+ // SeverityWarn produces a warning diagnostic (does not change exit
59
+ // code).
60
+ SeverityWarn
61
+ // SeverityError produces an error diagnostic and fails the command.
62
+ SeverityError
63
63
  )
64
64
 
65
65
  // Rule is the contract every contributor rule satisfies. Mirrors the
66
66
  // internal host interface so the host can dispatch via a thin adapter
67
67
  // without re-implementing the engine.
68
68
  type Rule interface {
69
- // Name is the identifier users put in their `rules` map.
70
- // Conventionally namespaced as "<plugin-namespace>/<rule-name>" to
71
- // avoid colliding with built-in rule names.
72
- Name() string
69
+ // Name is the identifier users put in their `rules` map.
70
+ // Conventionally namespaced as "<plugin-namespace>/<rule-name>" to
71
+ // avoid colliding with built-in rule names.
72
+ Name() string
73
73
 
74
- // Visits returns the AST kinds the rule cares about. The engine only
75
- // dispatches to rules that registered for the visited node's kind.
76
- Visits() []shimast.Kind
74
+ // Visits returns the AST kinds the rule cares about. The engine only
75
+ // dispatches to rules that registered for the visited node's kind.
76
+ Visits() []shimast.Kind
77
77
 
78
- // Check is invoked once per relevant node. Use `ctx.Report` /
79
- // `ctx.ReportRange` to emit findings.
80
- Check(ctx *Context, node *shimast.Node)
78
+ // Check is invoked once per relevant node. Use `ctx.Report` /
79
+ // `ctx.ReportRange` to emit findings.
80
+ Check(ctx *Context, node *shimast.Node)
81
81
  }
82
82
 
83
83
  // FormatRule is an optional marker contributors implement when a rule
@@ -93,20 +93,20 @@ type Rule interface {
93
93
  // returning `false` is equivalent to not implementing the interface at
94
94
  // all, and the host treats either form the same way.
95
95
  type FormatRule interface {
96
- Rule
97
- IsFormat() bool
96
+ Rule
97
+ IsFormat() bool
98
98
  }
99
99
 
100
100
  // Reporter is the engine-supplied callback that records a finding. The
101
101
  // host implements this and passes it to `NewContext` when invoking a
102
102
  // contributor rule.
103
103
  type Reporter interface {
104
- // Report records a finding at the given node's source range.
105
- Report(node *shimast.Node, message string)
106
- // ReportRange records a finding at an explicit byte range inside the
107
- // current file. Use this when the rule wants to highlight a
108
- // sub-token.
109
- ReportRange(pos, end int, message string)
104
+ // Report records a finding at the given node's source range.
105
+ Report(node *shimast.Node, message string)
106
+ // ReportRange records a finding at an explicit byte range inside the
107
+ // current file. Use this when the rule wants to highlight a
108
+ // sub-token.
109
+ ReportRange(pos, end int, message string)
110
110
  }
111
111
 
112
112
  // FixReporter is the optional extension a host implements to receive
@@ -122,8 +122,8 @@ type Reporter interface {
122
122
  // a fake that wants the fix path must implement BOTH `ReportFix` and
123
123
  // `ReportRangeFix`.
124
124
  type FixReporter interface {
125
- ReportFix(node *shimast.Node, message string, edits ...TextEdit)
126
- ReportRangeFix(pos, end int, message string, edits ...TextEdit)
125
+ ReportFix(node *shimast.Node, message string, edits ...TextEdit)
126
+ ReportRangeFix(pos, end int, message string, edits ...TextEdit)
127
127
  }
128
128
 
129
129
  // TextEdit is one byte-range replacement offered by an autofixable finding.
@@ -143,9 +143,9 @@ type FixReporter interface {
143
143
  // falls inside a deletion range. Design fixes so each finding emits one
144
144
  // contiguous TextEdit covering the entire replacement region.
145
145
  type TextEdit struct {
146
- Pos int
147
- End int
148
- Text string
146
+ Pos int
147
+ End int
148
+ Text string
149
149
  }
150
150
 
151
151
  // Context is the per-(file, rule) handle the engine passes to `Check`.
@@ -153,70 +153,70 @@ type TextEdit struct {
153
153
  // contributors call `ctx.Report` / `ctx.ReportRange` directly through
154
154
  // this Context rather than touching the reporter.
155
155
  type Context struct {
156
- // File is the source file currently being walked. Always non-nil
157
- // when `Check` is invoked.
158
- File *shimast.SourceFile
156
+ // File is the source file currently being walked. Always non-nil
157
+ // when `Check` is invoked.
158
+ File *shimast.SourceFile
159
159
 
160
- // Checker is the host's tsgo type checker. Available for type-aware
161
- // rules; nil-safe enough that AST-only rules can ignore it.
162
- Checker *shimchecker.Checker
160
+ // Checker is the host's tsgo type checker. Available for type-aware
161
+ // rules; nil-safe enough that AST-only rules can ignore it.
162
+ Checker *shimchecker.Checker
163
163
 
164
- // Severity is the rule's resolved severity for this file. Already
165
- // filtered by the engine — rules do not need to check for
166
- // SeverityOff.
167
- Severity Severity
164
+ // Severity is the rule's resolved severity for this file. Already
165
+ // filtered by the engine — rules do not need to check for
166
+ // SeverityOff.
167
+ Severity Severity
168
168
 
169
- // Options is the raw JSON blob the user wrote in the second slot of
170
- // their `[severity, options]` rule configuration tuple. Nil when the
171
- // rule was configured with a bare severity literal. Contributors that
172
- // accept options decode the blob into their own struct via
173
- // `(*Context).DecodeOptions`.
174
- Options json.RawMessage
169
+ // Options is the raw JSON blob the user wrote in the second slot of
170
+ // their `[severity, options]` rule configuration tuple. Nil when the
171
+ // rule was configured with a bare severity literal. Contributors that
172
+ // accept options decode the blob into their own struct via
173
+ // `(*Context).DecodeOptions`.
174
+ Options json.RawMessage
175
175
 
176
- reporter Reporter
176
+ reporter Reporter
177
177
  }
178
178
 
179
179
  // NewContext constructs a Context for the engine to pass into a
180
180
  // contributor rule's `Check`. Reserved for host code; contributors
181
181
  // should not need to call this.
182
182
  func NewContext(
183
- file *shimast.SourceFile,
184
- checker *shimchecker.Checker,
185
- severity Severity,
186
- options json.RawMessage,
187
- reporter Reporter,
183
+ file *shimast.SourceFile,
184
+ checker *shimchecker.Checker,
185
+ severity Severity,
186
+ options json.RawMessage,
187
+ reporter Reporter,
188
188
  ) *Context {
189
- return &Context{
190
- File: file,
191
- Checker: checker,
192
- Severity: severity,
193
- Options: options,
194
- reporter: reporter,
195
- }
189
+ return &Context{
190
+ File: file,
191
+ Checker: checker,
192
+ Severity: severity,
193
+ Options: options,
194
+ reporter: reporter,
195
+ }
196
196
  }
197
197
 
198
198
  // DecodeOptions unmarshals the rule's options blob into `out`. Returns
199
199
  // nil with no side effect when the rule was configured with severity
200
200
  // alone, so contributors can write:
201
201
  //
202
- // var opts myRuleOptions
203
- // _ = ctx.DecodeOptions(&opts)
204
- // // opts now holds either the user's settings or the zero value.
202
+ // var opts myRuleOptions
203
+ // _ = ctx.DecodeOptions(&opts)
204
+ // // opts now holds either the user's settings or the zero value.
205
205
  func (c *Context) DecodeOptions(out interface{}) error {
206
- if c == nil || len(c.Options) == 0 {
207
- return nil
208
- }
209
- return json.Unmarshal(c.Options, out)
206
+ if c == nil || len(c.Options) == 0 {
207
+ return nil
208
+ }
209
+ return json.Unmarshal(c.Options, out)
210
210
  }
211
211
 
212
212
  // Report records a finding at the given node's source range. Silently
213
213
  // ignored when severity is `off` (defensive — the engine already filters
214
214
  // by severity before invoking Check) or when no reporter is attached.
215
215
  func (c *Context) Report(node *shimast.Node, message string) {
216
- if c == nil || c.reporter == nil || c.Severity == SeverityOff || node == nil {
217
- return
218
- }
219
- c.reporter.Report(node, message)
216
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff || node == nil {
217
+ return
218
+ }
219
+ c.reporter.Report(node, message)
220
220
  }
221
221
 
222
222
  // ReportFix records a finding at the given node's source range with optional
@@ -224,28 +224,28 @@ func (c *Context) Report(node *shimast.Node, message string) {
224
224
  // diagnostic without edits.
225
225
  // Treat edits as best-effort: design the rule so the diagnostic alone is useful.
226
226
  func (c *Context) ReportFix(node *shimast.Node, message string, edits ...TextEdit) {
227
- if c == nil || c.reporter == nil || c.Severity == SeverityOff || node == nil {
228
- return
229
- }
230
- if len(edits) == 0 {
231
- c.reporter.Report(node, message)
232
- return
233
- }
234
- fixer, ok := c.reporter.(FixReporter)
235
- if !ok {
236
- c.reporter.Report(node, message)
237
- return
238
- }
239
- fixer.ReportFix(node, message, edits...)
227
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff || node == nil {
228
+ return
229
+ }
230
+ if len(edits) == 0 {
231
+ c.reporter.Report(node, message)
232
+ return
233
+ }
234
+ fixer, ok := c.reporter.(FixReporter)
235
+ if !ok {
236
+ c.reporter.Report(node, message)
237
+ return
238
+ }
239
+ fixer.ReportFix(node, message, edits...)
240
240
  }
241
241
 
242
242
  // ReportRange records a finding at an explicit byte range inside the
243
243
  // current file.
244
244
  func (c *Context) ReportRange(pos, end int, message string) {
245
- if c == nil || c.reporter == nil || c.Severity == SeverityOff {
246
- return
247
- }
248
- c.reporter.ReportRange(pos, end, message)
245
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff {
246
+ return
247
+ }
248
+ c.reporter.ReportRange(pos, end, message)
249
249
  }
250
250
 
251
251
  // ReportRangeFix records a finding at an explicit byte range with optional
@@ -253,19 +253,19 @@ func (c *Context) ReportRange(pos, end int, message string) {
253
253
  // diagnostic without edits.
254
254
  // Treat edits as best-effort: design the rule so the diagnostic alone is useful.
255
255
  func (c *Context) ReportRangeFix(pos, end int, message string, edits ...TextEdit) {
256
- if c == nil || c.reporter == nil || c.Severity == SeverityOff {
257
- return
258
- }
259
- if len(edits) == 0 {
260
- c.reporter.ReportRange(pos, end, message)
261
- return
262
- }
263
- fixer, ok := c.reporter.(FixReporter)
264
- if !ok {
265
- c.reporter.ReportRange(pos, end, message)
266
- return
267
- }
268
- fixer.ReportRangeFix(pos, end, message, edits...)
256
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff {
257
+ return
258
+ }
259
+ if len(edits) == 0 {
260
+ c.reporter.ReportRange(pos, end, message)
261
+ return
262
+ }
263
+ fixer, ok := c.reporter.(FixReporter)
264
+ if !ok {
265
+ c.reporter.ReportRange(pos, end, message)
266
+ return
267
+ }
268
+ fixer.ReportRangeFix(pos, end, message, edits...)
269
269
  }
270
270
 
271
271
  var registry []Rule
@@ -275,17 +275,17 @@ var registry []Rule
275
275
  // — the host's adapter layer surfaces collisions with a clearer error
276
276
  // than a raw panic.
277
277
  func Register(r Rule) {
278
- if r == nil {
279
- panic("rule: Register called with nil rule")
280
- }
281
- registry = append(registry, r)
278
+ if r == nil {
279
+ panic("rule: Register called with nil rule")
280
+ }
281
+ registry = append(registry, r)
282
282
  }
283
283
 
284
284
  // Registered returns every contributor rule registered via `Register`.
285
285
  // Called once by the host during engine bootstrap. The returned slice is
286
286
  // a defensive copy so the host cannot mutate the registry.
287
287
  func Registered() []Rule {
288
- out := make([]Rule, len(registry))
289
- copy(out, registry)
290
- return out
288
+ out := make([]Rule, len(registry))
289
+ copy(out, registry)
290
+ return out
291
291
  }
@@ -1,25 +1,24 @@
1
1
  import type { ITtscLintFormatConfig } from "./structures/ITtscLintFormatConfig";
2
2
 
3
3
  /**
4
- * Documented defaults for the `format` block's *always-on* rules
4
+ * Documented defaults for the `format` block's _always-on_ rules
5
5
  * (`format/semi`, `format/quotes`, `format/trailing-comma`,
6
6
  * `format/print-width`).
7
7
  *
8
8
  * Exported so users can spread defaults next to overrides:
9
9
  *
10
- * import { defaultFormat, type ITtscLintConfig } from "@ttsc/lint";
10
+ * Import { defaultFormat, type ITtscLintConfig } from "@ttsc/lint";
11
11
  *
12
- * export default {
13
- * format: { ...defaultFormat, printWidth: 100 },
14
- * } satisfies ITtscLintConfig;
12
+ * Export default { format: { ...defaultFormat, printWidth: 100 }, } satisfies
13
+ * ITtscLintConfig;
15
14
  *
16
- * The values mirror Prettier 1:1 except for the documented
17
- * `endOfLine` narrowing (no `"cr"` / `"auto"`).
15
+ * The values mirror Prettier 1:1 except for the documented `endOfLine`
16
+ * narrowing (no `"cr"` / `"auto"`).
18
17
  *
19
- * Notably absent: `importOrder` and `jsdoc`. `format/sort-imports`
20
- * and `format/jsdoc` are opt-in by setting their corresponding
21
- * fields; the defaults const only seeds the rules that turn on
22
- * unconditionally with a non-empty `format` block.
18
+ * Notably absent: `importOrder` and `jsdoc`. `format/sort-imports` and
19
+ * `format/jsdoc` are opt-in by setting their corresponding fields; the defaults
20
+ * const only seeds the rules that turn on unconditionally with a non-empty
21
+ * `format` block.
23
22
  */
24
23
  export const defaultFormat = Object.freeze({
25
24
  severity: "off",
package/src/index.ts CHANGED
@@ -9,11 +9,13 @@ import type { ITtscLintPlugin, ITtscLintPluginConfig } from "./structures";
9
9
  export * from "./defaultFormat";
10
10
  export * from "./structures/index";
11
11
 
12
+ /** A resolved contributor: Go sub-package name + absolute source directory. */
12
13
  type TtscPluginContributor = {
13
14
  name: string;
14
15
  source: string;
15
16
  };
16
17
 
18
+ /** Descriptor shape returned to ttsc's plugin builder by the factory. */
17
19
  type TtscPluginDescriptor = {
18
20
  name: string;
19
21
  source: string;
@@ -21,6 +23,10 @@ type TtscPluginDescriptor = {
21
23
  contributors?: TtscPluginContributor[];
22
24
  };
23
25
 
26
+ /**
27
+ * Context object injected by ttsc into every plugin factory call. The generic
28
+ * `TConfig` is the tsconfig plugin entry shape.
29
+ */
24
30
  type TtscPluginFactoryContext<TConfig> = {
25
31
  binary: string;
26
32
  cwd: string;
@@ -79,10 +85,10 @@ const LINT_CONFIG_FILENAMES = [
79
85
  * 1. The tsconfig plugin entry's `plugins` map — namespace → npm specifier. Inline
80
86
  * for projects that prefer to keep everything in `tsconfig.json`.
81
87
  * 2. The companion `lint.config.{ts,cts,mts,js,cjs,mjs,json}` (or
82
- * `eslint.config.*`) file — an object with an in-memory `plugins: {
83
- * ns: pluginObject }` map. The factory evaluates the config (via ttsx for TS
84
- * / ESM sources, `require` for CommonJS, `JSON.parse` for JSON) and reads
85
- * the `plugins` field.
88
+ * `eslint.config.*`) file — an object with an in-memory `plugins: { ns:
89
+ * pluginObject }` map. The factory evaluates the config (via ttsx for TS /
90
+ * ESM sources, `require` for CommonJS, `JSON.parse` for JSON) and reads the
91
+ * `plugins` field.
86
92
  *
87
93
  * Contributions from both sources are merged with the tsconfig entry winning on
88
94
  * namespace collisions, so a project can opt into a hand-curated subset of an
@@ -448,6 +454,12 @@ function readCjsConfigPlugins(configPath: string): ConfigPluginEntry[] {
448
454
  );
449
455
  }
450
456
 
457
+ // TypeScript source written to a temp file and executed via ttsx. The
458
+ // %CONFIG_IMPORT% placeholder is replaced with a JSON-quoted relative path
459
+ // before the file hits disk. The script walks the exported config object,
460
+ // collects every `plugins` map, and serialises each plugin's `source` field
461
+ // as a JSON array for the parent process to parse — avoiding the need to
462
+ // serialise arbitrary in-memory plugin objects across the process boundary.
451
463
  const TTSX_EXTRACTOR_SCRIPT = `import * as importedConfig from %CONFIG_IMPORT%;
452
464
 
453
465
  declare const process: {
@@ -5,8 +5,8 @@ import type { TtscLintRuleMap } from "./TtscLintRuleMap";
5
5
  /**
6
6
  * Top-level object accepted by `@ttsc/lint` config files.
7
7
  *
8
- * Keep the file shape plain: users export an object and use
9
- * `satisfies ITtscLintConfig` when they want type checking.
8
+ * Keep the file shape plain: users export an object and use `satisfies
9
+ * ITtscLintConfig` when they want type checking.
10
10
  */
11
11
  export interface ITtscLintConfig {
12
12
  /** Globs that select the files this entry applies to. */