@ttsc/lint 0.12.3 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/defaultFormat.d.ts +10 -11
- package/lib/defaultFormat.js +10 -11
- package/lib/defaultFormat.js.map +1 -1
- package/lib/index.js +233 -135
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +2 -2
- package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
- package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
- package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
- package/lib/structures/TtscLintRule.d.ts +10 -1
- package/lib/structures/TtscLintRuleMap.d.ts +2 -2
- package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
- package/linthost/ast_helpers.go +80 -11
- package/linthost/compile.go +116 -112
- package/linthost/config.go +647 -687
- package/linthost/config_format.go +272 -247
- package/linthost/contrib_adapter.go +7 -0
- package/linthost/directives.go +116 -0
- package/linthost/dispatch.go +33 -33
- package/linthost/engine.go +156 -43
- package/linthost/fix.go +47 -27
- package/linthost/flags_gen.go +31 -0
- package/linthost/format.go +119 -3
- package/linthost/host.go +115 -5
- package/linthost/print_dispatch.go +128 -28
- package/linthost/print_doc.go +23 -0
- package/linthost/print_engine.go +168 -4
- package/linthost/print_nodes_array.go +17 -7
- package/linthost/print_nodes_call.go +138 -20
- package/linthost/print_nodes_function.go +353 -0
- package/linthost/print_nodes_imports.go +46 -29
- package/linthost/print_nodes_list.go +86 -5
- package/linthost/print_nodes_object.go +56 -11
- package/linthost/rules_arrays.go +5 -2
- package/linthost/rules_debugger.go +3 -2
- package/linthost/rules_dupes.go +7 -4
- package/linthost/rules_empty.go +3 -2
- package/linthost/rules_escape.go +35 -3
- package/linthost/rules_eval.go +3 -0
- package/linthost/rules_finally.go +11 -0
- package/linthost/rules_format_jsdoc.go +7 -0
- package/linthost/rules_format_print_width.go +270 -15
- package/linthost/rules_format_quotes.go +3 -0
- package/linthost/rules_format_sort_imports.go +4 -0
- package/linthost/rules_gap.go +75 -3
- package/linthost/rules_imports.go +5 -7
- package/linthost/rules_logic.go +75 -5
- package/linthost/rules_loops.go +4 -0
- package/linthost/rules_misc.go +4 -2
- package/linthost/rules_params.go +7 -11
- package/linthost/rules_problems.go +89 -22
- package/linthost/rules_promise.go +15 -0
- package/linthost/rules_protos.go +3 -2
- package/linthost/rules_self.go +6 -0
- package/linthost/rules_strings.go +10 -0
- package/linthost/rules_suggestions.go +174 -9
- package/linthost/rules_throw.go +2 -0
- package/linthost/rules_ts.go +13 -0
- package/linthost/rules_ts_extra.go +25 -8
- package/linthost/rules_var.go +15 -2
- package/package.json +3 -3
- package/plugin/main.go +4 -4
- package/rule/astutil/astutil.go +12 -0
- package/rule/rule.go +123 -123
- package/src/defaultFormat.ts +10 -11
- package/src/index.ts +257 -168
- package/src/structures/ITtscLintConfig.ts +2 -2
- package/src/structures/ITtscLintFormatConfig.ts +53 -55
- package/src/structures/ITtscLintPluginConfig.ts +10 -83
- package/src/structures/ITtscLintPluginMeta.ts +9 -1
- package/src/structures/TtscLintRule.ts +10 -1
- package/src/structures/TtscLintRuleMap.ts +17 -18
- package/src/structures/TtscLintRuleOptions.ts +15 -0
- package/linthost/eslint_runtime.go +0 -320
package/linthost/fix.go
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Autofix orchestration for the `@ttsc/lint fix` subcommand.
|
|
2
|
+
//
|
|
3
|
+
// RunFix drives the fix cascade: it repeatedly runs the native lint engine
|
|
4
|
+
// and applies any emitted TextEdit suggestions until no more fixable
|
|
5
|
+
// findings remain or maxFixPasses is reached. After the cascade settles, it
|
|
6
|
+
// runs a final diagnostic pass so remaining issues are surfaced in the
|
|
7
|
+
// normal error stream.
|
|
1
8
|
package linthost
|
|
2
9
|
|
|
3
10
|
import (
|
|
@@ -9,10 +16,9 @@ import (
|
|
|
9
16
|
shimdw "github.com/microsoft/typescript-go/shim/diagnosticwriter"
|
|
10
17
|
)
|
|
11
18
|
|
|
12
|
-
// maxFixPasses bounds the native cascade
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// edit cannot loop forever.
|
|
19
|
+
// maxFixPasses bounds the native fix cascade. Real-world cascades (no-var →
|
|
20
|
+
// prefer-const → eqeqeq …) settle in a handful of passes; the cap exists so
|
|
21
|
+
// a buggy rule that re-reports its own edit cannot loop forever.
|
|
16
22
|
const maxFixPasses = 10
|
|
17
23
|
|
|
18
24
|
// RunFix implements `@ttsc/lint fix` — apply autofixes, then report any
|
|
@@ -37,8 +43,11 @@ func runFix(opts *subcommandOpts) int {
|
|
|
37
43
|
fmt.Fprintln(os.Stderr, err)
|
|
38
44
|
return 2
|
|
39
45
|
}
|
|
46
|
+
engine := NewEngineWithResolver(rules)
|
|
47
|
+
engine.SetSerial(opts.singleThreaded)
|
|
48
|
+
needsRuleChecker := engine.NeedsTypeChecker()
|
|
40
49
|
|
|
41
|
-
prog, code := loadFixProgram(opts)
|
|
50
|
+
prog, code := loadFixProgram(opts, needsRuleChecker)
|
|
42
51
|
if code != 0 {
|
|
43
52
|
return code
|
|
44
53
|
}
|
|
@@ -49,16 +58,6 @@ func runFix(opts *subcommandOpts) int {
|
|
|
49
58
|
}()
|
|
50
59
|
|
|
51
60
|
totalFixes := 0
|
|
52
|
-
if fixed, err := runExternalESLintFixes(rules, opts.cwd, prog.userSourceFiles()); err != nil {
|
|
53
|
-
fmt.Fprintln(os.Stderr, err)
|
|
54
|
-
return 2
|
|
55
|
-
} else if fixed > 0 {
|
|
56
|
-
totalFixes += fixed
|
|
57
|
-
prog, code = reloadFixProgram(prog, opts)
|
|
58
|
-
if code != 0 {
|
|
59
|
-
return code
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
61
|
|
|
63
62
|
// `ttsc fix` applies edits from BOTH lint-class rules and
|
|
64
63
|
// format-class rules. The dual `ttsc format` subcommand exists for
|
|
@@ -67,7 +66,6 @@ func runFix(opts *subcommandOpts) int {
|
|
|
67
66
|
// kinds of findings in one pass — no filtering needed here.
|
|
68
67
|
cascadeConverged := false
|
|
69
68
|
for pass := 0; pass < maxFixPasses; pass++ {
|
|
70
|
-
engine := NewEngineWithResolver(rules)
|
|
71
69
|
findings := engine.Run(prog.userSourceFiles(), prog.checker)
|
|
72
70
|
fixed, err := applyFindingFixes(opts.cwd, findings)
|
|
73
71
|
if err != nil {
|
|
@@ -79,7 +77,7 @@ func runFix(opts *subcommandOpts) int {
|
|
|
79
77
|
break
|
|
80
78
|
}
|
|
81
79
|
totalFixes += fixed
|
|
82
|
-
prog, code = reloadFixProgram(prog, opts)
|
|
80
|
+
prog, code = reloadFixProgram(prog, opts, needsRuleChecker)
|
|
83
81
|
if code != 0 {
|
|
84
82
|
return code
|
|
85
83
|
}
|
|
@@ -96,15 +94,12 @@ func runFix(opts *subcommandOpts) int {
|
|
|
96
94
|
maxFixPasses)
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
astDiags, lintDiags, externalRan, err := collectDiagnostics(prog, engine)
|
|
97
|
+
astDiags, lintDiags, err := collectDiagnostics(prog, engine)
|
|
101
98
|
if err != nil {
|
|
102
99
|
fmt.Fprintln(os.Stderr, err)
|
|
103
100
|
return 2
|
|
104
101
|
}
|
|
105
|
-
|
|
106
|
-
warnUnknownRules(os.Stderr, engine.UnknownRules())
|
|
107
|
-
}
|
|
102
|
+
warnUnknownRules(os.Stderr, engine.UnknownRules())
|
|
108
103
|
errCount := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, opts.cwd)
|
|
109
104
|
if errCount > 0 {
|
|
110
105
|
return 2
|
|
@@ -121,10 +116,16 @@ func runFix(opts *subcommandOpts) int {
|
|
|
121
116
|
return 0
|
|
122
117
|
}
|
|
123
118
|
|
|
124
|
-
|
|
119
|
+
// loadFixProgram loads the TypeScript program for a fix/format pass with
|
|
120
|
+
// NoEmit forced on. Returns (nil, 2) when loading or config parsing fails.
|
|
121
|
+
func loadFixProgram(opts *subcommandOpts, needsRuleChecker bool) (*program, int) {
|
|
125
122
|
prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
|
|
126
|
-
forceNoEmit:
|
|
127
|
-
outDir:
|
|
123
|
+
forceNoEmit: true,
|
|
124
|
+
outDir: opts.outDir,
|
|
125
|
+
needsRuleChecker: needsRuleChecker,
|
|
126
|
+
singleThreaded: opts.singleThreaded,
|
|
127
|
+
checkers: opts.checkers,
|
|
128
|
+
tsgoArgs: opts.tsgoArgs,
|
|
128
129
|
})
|
|
129
130
|
if err != nil {
|
|
130
131
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
|
|
@@ -137,19 +138,29 @@ func loadFixProgram(opts *subcommandOpts) (*program, int) {
|
|
|
137
138
|
return prog, 0
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
// reloadFixProgram closes `current` and loads a fresh program from disk.
|
|
142
|
+
// Used between cascade passes so the engine sees edits applied in the
|
|
143
|
+
// previous pass rather than stale in-memory AST nodes.
|
|
144
|
+
func reloadFixProgram(current *program, opts *subcommandOpts, needsRuleChecker bool) (*program, int) {
|
|
141
145
|
if current != nil {
|
|
142
146
|
current.close()
|
|
143
147
|
}
|
|
144
|
-
return loadFixProgram(opts)
|
|
148
|
+
return loadFixProgram(opts, needsRuleChecker)
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
// fileFixes groups all pending TextEdit suggestions for a single file.
|
|
152
|
+
// `text` is the source content at the time the findings were collected;
|
|
153
|
+
// byte offsets in `edits` are relative to this snapshot.
|
|
147
154
|
type fileFixes struct {
|
|
148
155
|
path string
|
|
149
156
|
text string
|
|
150
157
|
edits []TextEdit
|
|
151
158
|
}
|
|
152
159
|
|
|
160
|
+
// applyFindingFixes groups all fixable findings by file, resolves each
|
|
161
|
+
// file path to an absolute form, then applies the edit batches in
|
|
162
|
+
// deterministic order (sorted by path). Returns the total number of edits
|
|
163
|
+
// written to disk.
|
|
153
164
|
func applyFindingFixes(cwd string, findings []*Finding) (int, error) {
|
|
154
165
|
byFile := map[string]*fileFixes{}
|
|
155
166
|
for _, finding := range findings {
|
|
@@ -191,6 +202,10 @@ func applyFindingFixes(cwd string, findings []*Finding) (int, error) {
|
|
|
191
202
|
return total, nil
|
|
192
203
|
}
|
|
193
204
|
|
|
205
|
+
// applyTextEditsToFile selects the non-overlapping edits from `edits`, applies
|
|
206
|
+
// them to `source` in reverse order (right-to-left) to preserve earlier
|
|
207
|
+
// offsets, and writes the result to `path`. Returns the number of edits
|
|
208
|
+
// applied, or 0 when no edits survive selection.
|
|
194
209
|
func applyTextEditsToFile(path, source string, edits []TextEdit) (int, error) {
|
|
195
210
|
selected := selectTextEdits(len(source), edits)
|
|
196
211
|
if len(selected) == 0 {
|
|
@@ -210,6 +225,11 @@ func applyTextEditsToFile(path, source string, edits []TextEdit) (int, error) {
|
|
|
210
225
|
return len(selected), nil
|
|
211
226
|
}
|
|
212
227
|
|
|
228
|
+
// selectTextEdits filters and sorts `edits` into a non-overlapping
|
|
229
|
+
// application sequence. Out-of-bounds edits and exact duplicates are
|
|
230
|
+
// removed first; the remainder is sorted by start position then end
|
|
231
|
+
// position (left to right). A greedy scan then keeps the earliest-starting
|
|
232
|
+
// edit and drops any that overlap with it, producing a disjoint set.
|
|
213
233
|
func selectTextEdits(sourceLen int, edits []TextEdit) []TextEdit {
|
|
214
234
|
if len(edits) == 0 {
|
|
215
235
|
return nil
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Code generated by packages/ttsc/scripts/gen-flags.mts. DO NOT EDIT.
|
|
2
|
+
//
|
|
3
|
+
// Source of truth: packages/ttsc/src/flags/schema.ts.
|
|
4
|
+
// Regenerate with: pnpm format
|
|
5
|
+
// Verify in CI with: node packages/ttsc/scripts/check-flags.cjs
|
|
6
|
+
|
|
7
|
+
package linthost
|
|
8
|
+
|
|
9
|
+
// LintFlagAllowList is the allow-list of CLI flags this Go layer accepts. The map's
|
|
10
|
+
// value is true when the flag carries a separate value token (--flag VALUE
|
|
11
|
+
// or --flag=VALUE) and false when the flag is boolean (--flag).
|
|
12
|
+
//
|
|
13
|
+
// Generated from packages/ttsc/src/flags/schema.ts. Edit the schema, not
|
|
14
|
+
// this file.
|
|
15
|
+
var LintFlagAllowList = map[string]bool{
|
|
16
|
+
"checkers": true,
|
|
17
|
+
"cwd": true,
|
|
18
|
+
"emit": false,
|
|
19
|
+
"file": true,
|
|
20
|
+
"noEmit": false,
|
|
21
|
+
"out": true,
|
|
22
|
+
"outDir": true,
|
|
23
|
+
"p": true,
|
|
24
|
+
"plugins-json": true,
|
|
25
|
+
"project": true,
|
|
26
|
+
"quiet": false,
|
|
27
|
+
"singleThreaded": false,
|
|
28
|
+
"tsconfig": true,
|
|
29
|
+
"tsgo-args": true,
|
|
30
|
+
"verbose": false,
|
|
31
|
+
}
|
package/linthost/format.go
CHANGED
|
@@ -33,14 +33,20 @@ 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 {
|
|
39
41
|
fmt.Fprintln(os.Stderr, err)
|
|
40
42
|
return 2
|
|
41
43
|
}
|
|
44
|
+
resolver := formatCommandResolver{inner: rules}
|
|
45
|
+
engine := NewEngineWithResolver(resolver)
|
|
46
|
+
engine.SetSerial(opts.singleThreaded)
|
|
47
|
+
needsRuleChecker := engine.NeedsTypeChecker()
|
|
42
48
|
|
|
43
|
-
prog, code := loadFixProgram(opts)
|
|
49
|
+
prog, code := loadFixProgram(opts, needsRuleChecker)
|
|
44
50
|
if code != 0 {
|
|
45
51
|
return code
|
|
46
52
|
}
|
|
@@ -53,7 +59,6 @@ func runFormat(opts *subcommandOpts) int {
|
|
|
53
59
|
totalFixes := 0
|
|
54
60
|
cascadeConverged := false
|
|
55
61
|
for pass := 0; pass < maxFormatPasses; pass++ {
|
|
56
|
-
engine := NewEngineWithResolver(formatCommandResolver{inner: rules})
|
|
57
62
|
findings := engine.Run(prog.userSourceFiles(), prog.checker)
|
|
58
63
|
fixed, err := applyFindingFixes(opts.cwd, filterFormatFindings(findings))
|
|
59
64
|
if err != nil {
|
|
@@ -65,7 +70,7 @@ func runFormat(opts *subcommandOpts) int {
|
|
|
65
70
|
break
|
|
66
71
|
}
|
|
67
72
|
totalFixes += fixed
|
|
68
|
-
prog, code = reloadFixProgram(prog, opts)
|
|
73
|
+
prog, code = reloadFixProgram(prog, opts, needsRuleChecker)
|
|
69
74
|
if code != 0 {
|
|
70
75
|
return code
|
|
71
76
|
}
|
|
@@ -89,15 +94,45 @@ func runFormat(opts *subcommandOpts) int {
|
|
|
89
94
|
return 0
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
// formatCommandResolver wraps a RuleResolver and ensures every format-class
|
|
98
|
+
// rule referenced in the loaded plugin options is activated at warn severity,
|
|
99
|
+
// even if the user's config omitted it. This lets `ttsc format` format files
|
|
100
|
+
// without requiring explicit rule declarations in the project config.
|
|
92
101
|
type formatCommandResolver struct {
|
|
93
102
|
inner RuleResolver
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
// ResolveRules implements RuleResolver. It delegates to the inner resolver
|
|
106
|
+
// and then upgrades format-rule entries from off to warn so they are applied
|
|
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`.
|
|
96
125
|
func (r formatCommandResolver) ResolveRules(fileName string) ResolvedRuleConfig {
|
|
97
126
|
resolved := r.inner.ResolveRules(fileName)
|
|
98
127
|
if resolved.Ignored {
|
|
99
128
|
return resolved
|
|
100
129
|
}
|
|
130
|
+
if r.fileIsIgnoredByEntry(fileName) {
|
|
131
|
+
return resolved
|
|
132
|
+
}
|
|
133
|
+
if !r.fileMatchesAnyEntry(fileName) {
|
|
134
|
+
return resolved
|
|
135
|
+
}
|
|
101
136
|
if resolved.Rules == nil {
|
|
102
137
|
resolved.Rules = RuleConfig{}
|
|
103
138
|
}
|
|
@@ -109,6 +144,75 @@ func (r formatCommandResolver) ResolveRules(fileName string) ResolvedRuleConfig
|
|
|
109
144
|
return resolved
|
|
110
145
|
}
|
|
111
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
|
+
|
|
214
|
+
// ActiveRuleNames implements RuleResolver. Returns the union of the inner
|
|
215
|
+
// resolver's active rules and every format-option rule that is registered.
|
|
112
216
|
func (r formatCommandResolver) ActiveRuleNames() []string {
|
|
113
217
|
active := map[string]struct{}{}
|
|
114
218
|
for _, name := range r.inner.ActiveRuleNames() {
|
|
@@ -120,6 +224,8 @@ func (r formatCommandResolver) ActiveRuleNames() []string {
|
|
|
120
224
|
return sortedKeys(active)
|
|
121
225
|
}
|
|
122
226
|
|
|
227
|
+
// EnabledRuleConfig implements RuleResolver. Merges the inner config with
|
|
228
|
+
// the format-option rules so callers see the full active set.
|
|
123
229
|
func (r formatCommandResolver) EnabledRuleConfig() RuleConfig {
|
|
124
230
|
enabled := r.inner.EnabledRuleConfig()
|
|
125
231
|
if enabled == nil {
|
|
@@ -133,10 +239,14 @@ func (r formatCommandResolver) EnabledRuleConfig() RuleConfig {
|
|
|
133
239
|
return enabled
|
|
134
240
|
}
|
|
135
241
|
|
|
242
|
+
// RuleOptions implements RuleResolver by delegating directly to the inner resolver.
|
|
136
243
|
func (r formatCommandResolver) RuleOptions(name string) json.RawMessage {
|
|
137
244
|
return r.inner.RuleOptions(name)
|
|
138
245
|
}
|
|
139
246
|
|
|
247
|
+
// formatOptionRuleNames returns the sorted list of rule names from the inner
|
|
248
|
+
// resolver's options that are registered as format rules. These are the rules
|
|
249
|
+
// that formatCommandResolver promotes from off to warn.
|
|
140
250
|
func (r formatCommandResolver) formatOptionRuleNames() []string {
|
|
141
251
|
options := resolverOptions(r.inner)
|
|
142
252
|
if len(options) == 0 {
|
|
@@ -152,6 +262,9 @@ func (r formatCommandResolver) formatOptionRuleNames() []string {
|
|
|
152
262
|
return names
|
|
153
263
|
}
|
|
154
264
|
|
|
265
|
+
// resolverOptions extracts the raw options map from a resolver whose concrete
|
|
266
|
+
// type exposes one. Returns nil for resolver types that don't carry per-rule
|
|
267
|
+
// options (e.g. bare RuleConfig).
|
|
155
268
|
func resolverOptions(resolver RuleResolver) RuleOptionsMap {
|
|
156
269
|
switch r := resolver.(type) {
|
|
157
270
|
case InlineRuleResolver:
|
|
@@ -163,11 +276,14 @@ func resolverOptions(resolver RuleResolver) RuleOptionsMap {
|
|
|
163
276
|
}
|
|
164
277
|
}
|
|
165
278
|
|
|
279
|
+
// isRegisteredFormatRule reports whether `name` is both registered in the
|
|
280
|
+
// global rule registry and tagged as a format rule via the FormatRule marker.
|
|
166
281
|
func isRegisteredFormatRule(name string) bool {
|
|
167
282
|
rule, ok := registered.rules[name]
|
|
168
283
|
return ok && isFormatRule(rule)
|
|
169
284
|
}
|
|
170
285
|
|
|
286
|
+
// sortedKeys returns the sorted slice of keys from a string-keyed set.
|
|
171
287
|
func sortedKeys(input map[string]struct{}) []string {
|
|
172
288
|
names := make([]string, 0, len(input))
|
|
173
289
|
for name := range input {
|
package/linthost/host.go
CHANGED
|
@@ -39,10 +39,25 @@ type loadProgramOptions struct {
|
|
|
39
39
|
forceEmit bool
|
|
40
40
|
forceNoEmit bool
|
|
41
41
|
outDir string
|
|
42
|
+
// needsRuleChecker asks loadProgram to pin the checker pool and acquire the
|
|
43
|
+
// checker that type-aware lint rules receive through Context.Checker.
|
|
44
|
+
needsRuleChecker bool
|
|
45
|
+
// singleThreaded mirrors `tsgo --singleThreaded`: one checker, serial
|
|
46
|
+
// parse/check/emit.
|
|
47
|
+
singleThreaded bool
|
|
48
|
+
// checkers mirrors `tsgo --checkers`: type-checker pool size. Zero leaves
|
|
49
|
+
// TypeScript-Go's default; ignored when singleThreaded is set.
|
|
50
|
+
checkers int
|
|
51
|
+
// tsgoArgs carries tsgo CLI flags the `ttsc` launcher forwarded (`--strict`,
|
|
52
|
+
// `--target es2020`, …). They are parsed through TypeScript-Go's own
|
|
53
|
+
// command-line parser into a CompilerOptions overlay that wins over the
|
|
54
|
+
// tsconfig, exactly as tsgo's CLI merges them.
|
|
55
|
+
tsgoArgs []string
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
// loadProgram parses the given tsconfig
|
|
45
|
-
//
|
|
58
|
+
// loadProgram parses the given tsconfig and builds a Program. When
|
|
59
|
+
// needsRuleChecker is set, it also acquires a type checker for lint rules.
|
|
60
|
+
// Mirrors the canonical bootstrap pattern from
|
|
46
61
|
// `03-tsgo.md` — the only ttsc-specific bit is that `forceEmit`/
|
|
47
62
|
// `forceNoEmit`/`outDir` overrides are merged into the parsed config
|
|
48
63
|
// before the program is created so `--noEmit` and friends behave like
|
|
@@ -63,9 +78,14 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
|
|
|
63
78
|
fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
|
|
64
79
|
host := shimcompiler.NewCompilerHost(cwd, fs, bundled.LibPath(), nil, nil)
|
|
65
80
|
|
|
81
|
+
cliOptions, cliDiags := parseTsgoArgs(options.tsgoArgs, host)
|
|
82
|
+
if len(cliDiags) > 0 {
|
|
83
|
+
return nil, cliDiags, nil
|
|
84
|
+
}
|
|
85
|
+
|
|
66
86
|
parsed, parseDiags := tsoptions.GetParsedCommandLineOfConfigFile(
|
|
67
87
|
resolved,
|
|
68
|
-
|
|
88
|
+
cliOptions,
|
|
69
89
|
nil,
|
|
70
90
|
host,
|
|
71
91
|
nil,
|
|
@@ -88,17 +108,34 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
|
|
|
88
108
|
if options.outDir != "" {
|
|
89
109
|
overrideOutDir(cwd, parsed, options.outDir)
|
|
90
110
|
}
|
|
111
|
+
applyThreading(parsed, options.singleThreaded, options.checkers)
|
|
112
|
+
if options.needsRuleChecker {
|
|
113
|
+
forceSingleChecker(parsed)
|
|
114
|
+
}
|
|
91
115
|
|
|
116
|
+
// SingleThreaded is left unset so the program keeps TypeScript-Go's parallel
|
|
117
|
+
// source parsing and parallel emit. For type-aware lint rules, the checker
|
|
118
|
+
// pool is pinned to a single checker (see forceSingleChecker): the lint
|
|
119
|
+
// engine walks files serially against the one checker GetTypeChecker hands
|
|
120
|
+
// back, and rules ask that checker to resolve types in nodes drawn from every
|
|
121
|
+
// source file. TypeScript-Go's multi-checker pool affinitizes each file to a
|
|
122
|
+
// different checker and forbids mixing types across them, so a type whose
|
|
123
|
+
// declarations span files on different checkers (e.g. a circular
|
|
124
|
+
// indexed-access alias) resolves to `any` on the borrowed checker. AST-only
|
|
125
|
+
// lint rules do not receive a checker, so they keep the user's checker pool.
|
|
92
126
|
tsProgram := shimcompiler.NewProgram(shimcompiler.ProgramOptions{
|
|
93
127
|
Config: parsed,
|
|
94
|
-
SingleThreaded: shimcore.TSTrue,
|
|
95
128
|
Host: host,
|
|
96
129
|
UseSourceOfProjectReference: true,
|
|
97
130
|
})
|
|
98
131
|
if tsProgram == nil {
|
|
99
132
|
return nil, nil, errors.New("compiler.NewProgram returned nil")
|
|
100
133
|
}
|
|
101
|
-
checker
|
|
134
|
+
var checker *shimchecker.Checker
|
|
135
|
+
var release func()
|
|
136
|
+
if options.needsRuleChecker {
|
|
137
|
+
checker, release = tsProgram.GetTypeChecker(context.Background())
|
|
138
|
+
}
|
|
102
139
|
return &program{
|
|
103
140
|
cwd: cwd,
|
|
104
141
|
tsProgram: tsProgram,
|
|
@@ -108,6 +145,8 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
|
|
|
108
145
|
}, nil, nil
|
|
109
146
|
}
|
|
110
147
|
|
|
148
|
+
// close releases the type checker acquired by loadProgram. Safe to call on
|
|
149
|
+
// a nil receiver and idempotent after the first call.
|
|
111
150
|
func (p *program) close() {
|
|
112
151
|
if p == nil {
|
|
113
152
|
return
|
|
@@ -162,6 +201,8 @@ func (p *program) findSourceFile(target string) *shimast.SourceFile {
|
|
|
162
201
|
return nil
|
|
163
202
|
}
|
|
164
203
|
|
|
204
|
+
// forceEmit clears the NoEmit and EmitDeclarationOnly flags so the
|
|
205
|
+
// program emits JavaScript even when the tsconfig says otherwise.
|
|
165
206
|
func forceEmit(parsed *tsoptions.ParsedCommandLine) {
|
|
166
207
|
if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
|
|
167
208
|
return
|
|
@@ -171,6 +212,9 @@ func forceEmit(parsed *tsoptions.ParsedCommandLine) {
|
|
|
171
212
|
options.EmitDeclarationOnly = shimcore.TSFalse
|
|
172
213
|
}
|
|
173
214
|
|
|
215
|
+
// forceNoEmit sets the NoEmit flag regardless of what the tsconfig
|
|
216
|
+
// specifies. Used by fix and check subcommands that must not write output
|
|
217
|
+
// files as a side effect of type-checking.
|
|
174
218
|
func forceNoEmit(parsed *tsoptions.ParsedCommandLine) {
|
|
175
219
|
if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
|
|
176
220
|
return
|
|
@@ -178,6 +222,72 @@ func forceNoEmit(parsed *tsoptions.ParsedCommandLine) {
|
|
|
178
222
|
parsed.ParsedConfig.CompilerOptions.NoEmit = shimcore.TSTrue
|
|
179
223
|
}
|
|
180
224
|
|
|
225
|
+
// parseTsgoArgs runs forwarded tsgo CLI flags through TypeScript-Go's own
|
|
226
|
+
// command-line parser, yielding a CompilerOptions overlay loadProgram merges
|
|
227
|
+
// over the tsconfig — so a flag like `ttsc --strict` reaches the in-process
|
|
228
|
+
// lint program even though @ttsc/lint never shells out to `tsgo`. Returns an
|
|
229
|
+
// empty (non-nil) options value when there are no forwarded flags.
|
|
230
|
+
func parseTsgoArgs(args []string, host shimcompiler.CompilerHost) (*shimcore.CompilerOptions, []*shimast.Diagnostic) {
|
|
231
|
+
if len(args) == 0 {
|
|
232
|
+
return &shimcore.CompilerOptions{}, nil
|
|
233
|
+
}
|
|
234
|
+
cli := tsoptions.ParseCommandLine(args, host)
|
|
235
|
+
if cli == nil {
|
|
236
|
+
return &shimcore.CompilerOptions{}, nil
|
|
237
|
+
}
|
|
238
|
+
if len(cli.Errors) > 0 {
|
|
239
|
+
return nil, cli.Errors
|
|
240
|
+
}
|
|
241
|
+
return cli.CompilerOptions(), nil
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// applyThreading forwards the --singleThreaded / --checkers knobs onto the
|
|
245
|
+
// parsed compiler options. ttsc mirrors tsgo here: the values land in
|
|
246
|
+
// CompilerOptions, and both Program.SingleThreaded() and the checker pool read
|
|
247
|
+
// them from there. SingleThreaded wins over Checkers, matching the pool.
|
|
248
|
+
//
|
|
249
|
+
// When a type-aware lint rule is active, loadProgram calls forceSingleChecker
|
|
250
|
+
// afterwards, so a `--checkers N` greater than 1 is recorded here and then
|
|
251
|
+
// clamped back to a single checker. AST-only lint runs keep the recorded
|
|
252
|
+
// checker count. `--singleThreaded` still takes full effect.
|
|
253
|
+
func applyThreading(parsed *tsoptions.ParsedCommandLine, singleThreaded bool, checkers int) {
|
|
254
|
+
if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
options := parsed.ParsedConfig.CompilerOptions
|
|
258
|
+
if singleThreaded {
|
|
259
|
+
options.SingleThreaded = shimcore.TSTrue
|
|
260
|
+
}
|
|
261
|
+
if checkers > 0 {
|
|
262
|
+
n := checkers
|
|
263
|
+
options.Checkers = &n
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// forceSingleChecker pins the TypeScript-Go checker pool to a single checker.
|
|
268
|
+
//
|
|
269
|
+
// The lint engine walks the program serially and obtains types through the
|
|
270
|
+
// single checker GetTypeChecker hands back. Rules query types on nodes from
|
|
271
|
+
// arbitrary source files, so the checker must be the same one that checked
|
|
272
|
+
// every file. A pool of size > 1 affinitizes files to distinct checkers;
|
|
273
|
+
// resolving a type whose declarations cross that boundary yields `any`.
|
|
274
|
+
// Parallel parsing and emit are unaffected — they do not consult the count.
|
|
275
|
+
func forceSingleChecker(parsed *tsoptions.ParsedCommandLine) {
|
|
276
|
+
if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
options := parsed.ParsedConfig.CompilerOptions
|
|
280
|
+
if options.SingleThreaded == shimcore.TSTrue {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
one := 1
|
|
284
|
+
options.Checkers = &one
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// overrideOutDir replaces the parsed config's OutDir with `outDir`.
|
|
288
|
+
// Relative outDir values are resolved against `cwd`; absolute paths are
|
|
289
|
+
// used as-is. Paths are converted to forward slashes for tsgo
|
|
290
|
+
// compatibility.
|
|
181
291
|
func overrideOutDir(cwd string, parsed *tsoptions.ParsedCommandLine, outDir string) {
|
|
182
292
|
if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
|
|
183
293
|
return
|