@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.
- package/lib/defaultFormat.d.ts +10 -11
- package/lib/defaultFormat.js +10 -11
- package/lib/defaultFormat.js.map +1 -1
- package/lib/index.js +10 -4
- 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/ITtscLintPluginMeta.d.ts +9 -1
- package/lib/structures/TtscLintRule.d.ts +10 -1
- package/lib/structures/TtscLintRuleMap.d.ts +2 -2
- package/linthost/ast_helpers.go +17 -0
- package/linthost/compile.go +37 -0
- package/linthost/config.go +184 -35
- package/linthost/config_format.go +257 -244
- package/linthost/directives.go +72 -0
- package/linthost/dispatch.go +33 -33
- package/linthost/engine.go +4 -0
- package/linthost/eslint_runtime.go +31 -0
- package/linthost/fix.go +29 -0
- package/linthost/format.go +23 -0
- package/linthost/host.go +11 -0
- package/linthost/print_dispatch.go +7 -5
- package/linthost/print_doc.go +4 -0
- package/linthost/print_nodes_call.go +9 -0
- 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 +15 -0
- 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 +3 -0
- package/linthost/rules_format_quotes.go +3 -0
- package/linthost/rules_format_sort_imports.go +4 -0
- package/linthost/rules_gap.go +20 -0
- package/linthost/rules_imports.go +5 -7
- package/linthost/rules_logic.go +11 -0
- 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 +24 -1
- package/linthost/rules_promise.go +12 -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 +14 -4
- 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 +14 -1
- 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 +16 -4
- package/src/structures/ITtscLintConfig.ts +2 -2
- package/src/structures/ITtscLintFormatConfig.ts +53 -55
- package/src/structures/ITtscLintPluginMeta.ts +9 -1
- package/src/structures/TtscLintRule.ts +10 -1
- package/src/structures/TtscLintRuleMap.ts +17 -18
package/linthost/directives.go
CHANGED
|
@@ -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/")
|
package/linthost/dispatch.go
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
package linthost
|
|
8
8
|
|
|
9
9
|
import (
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
}
|
package/linthost/engine.go
CHANGED
|
@@ -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
|
package/linthost/format.go
CHANGED
|
@@ -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
|
|
102
|
-
//
|
|
103
|
-
// single AST node (e.g. tokens
|
|
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
|
|
112
|
-
//
|
|
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
|
package/linthost/print_doc.go
CHANGED
|
@@ -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
|