@ttsc/lint 0.10.2 → 0.11.0-dev.20260518
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/README.md +172 -33
- package/lib/defineConfig.d.ts +55 -0
- package/lib/defineConfig.js +39 -0
- package/lib/defineConfig.js.map +1 -0
- package/lib/index.d.ts +25 -1
- package/lib/index.js +622 -2
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPlugin.d.ts +53 -0
- package/lib/structures/ITtscLintPlugin.js +3 -0
- package/lib/structures/ITtscLintPlugin.js.map +1 -0
- package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
- package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
- package/lib/structures/ITtscLintPluginMeta.js +3 -0
- package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
- package/lib/structures/PluginRuleNames.d.ts +14 -0
- package/lib/structures/PluginRuleNames.js +3 -0
- package/lib/structures/PluginRuleNames.js.map +1 -0
- package/lib/structures/TtscLintConfig.d.ts +24 -8
- package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
- package/lib/structures/TtscLintConfigEntry.js +3 -0
- package/lib/structures/TtscLintConfigEntry.js.map +1 -0
- package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
- package/lib/structures/TtscLintFormatConfig.js +3 -0
- package/lib/structures/TtscLintFormatConfig.js.map +1 -0
- package/lib/structures/TtscLintPlugins.d.ts +9 -0
- package/lib/structures/TtscLintPlugins.js +3 -0
- package/lib/structures/TtscLintPlugins.js.map +1 -0
- package/lib/structures/TtscLintRule.d.ts +1 -1
- package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
- package/lib/structures/TtscLintRuleEntry.js +3 -0
- package/lib/structures/TtscLintRuleEntry.js.map +1 -0
- package/lib/structures/TtscLintRuleMap.d.ts +44 -0
- package/lib/structures/TtscLintRuleMap.js +3 -0
- package/lib/structures/TtscLintRuleMap.js.map +1 -0
- package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
- package/lib/structures/TtscLintRuleOptions.js +3 -0
- package/lib/structures/TtscLintRuleOptions.js.map +1 -0
- package/lib/structures/defaultFormat.d.ts +31 -0
- package/lib/structures/defaultFormat.js +35 -0
- package/lib/structures/defaultFormat.js.map +1 -0
- package/lib/structures/index.d.ts +10 -0
- package/lib/structures/index.js +10 -0
- package/lib/structures/index.js.map +1 -1
- package/{plugin → linthost}/ast_helpers.go +92 -1
- package/{plugin → linthost}/compile.go +1 -1
- package/{plugin → linthost}/config.go +583 -66
- package/linthost/config_format.go +360 -0
- package/linthost/contrib_adapter.go +134 -0
- package/{plugin → linthost}/directives.go +1 -1
- package/linthost/dispatch.go +65 -0
- package/{plugin → linthost}/engine.go +156 -11
- package/{plugin → linthost}/eslint_runtime.go +76 -2
- package/linthost/fix.go +249 -0
- package/linthost/format.go +107 -0
- package/{plugin → linthost}/host.go +1 -1
- package/linthost/print_dispatch.go +118 -0
- package/linthost/print_doc.go +168 -0
- package/linthost/print_engine.go +343 -0
- package/linthost/print_nodes_array.go +46 -0
- package/linthost/print_nodes_call.go +180 -0
- package/linthost/print_nodes_imports.go +171 -0
- package/linthost/print_nodes_list.go +64 -0
- package/linthost/print_nodes_object.go +51 -0
- package/{plugin → linthost}/rules_arrays.go +1 -1
- package/{plugin → linthost}/rules_console.go +1 -1
- package/{plugin → linthost}/rules_debugger.go +1 -1
- package/{plugin → linthost}/rules_dupes.go +1 -1
- package/{plugin → linthost}/rules_empty.go +1 -1
- package/linthost/rules_escape.go +238 -0
- package/{plugin → linthost}/rules_eval.go +1 -1
- package/{plugin → linthost}/rules_finally.go +1 -1
- package/linthost/rules_format_jsdoc.go +230 -0
- package/linthost/rules_format_print_width.go +332 -0
- package/linthost/rules_format_quotes.go +219 -0
- package/linthost/rules_format_semi.go +190 -0
- package/linthost/rules_format_sort_imports.go +462 -0
- package/linthost/rules_format_trailing_comma.go +460 -0
- package/{plugin → linthost}/rules_gap.go +101 -6
- package/linthost/rules_imports.go +101 -0
- package/{plugin → linthost}/rules_logic.go +55 -3
- package/{plugin → linthost}/rules_loops.go +1 -1
- package/{plugin → linthost}/rules_misc.go +1 -1
- package/linthost/rules_params.go +64 -0
- package/{plugin → linthost}/rules_problems.go +10 -2
- package/linthost/rules_promise.go +111 -0
- package/{plugin → linthost}/rules_protos.go +1 -1
- package/{plugin → linthost}/rules_self.go +1 -1
- package/{plugin → linthost}/rules_strings.go +1 -1
- package/{plugin → linthost}/rules_suggestions.go +51 -9
- package/{plugin → linthost}/rules_throw.go +1 -1
- package/{plugin → linthost}/rules_ts.go +12 -2
- package/{plugin → linthost}/rules_ts_extra.go +90 -4
- package/{plugin → linthost}/rules_var.go +36 -5
- package/package.json +8 -6
- package/plugin/main.go +14 -27
- package/rule/astutil/astutil.go +148 -0
- package/rule/rule.go +291 -0
- package/src/defineConfig.ts +69 -0
- package/src/index.ts +807 -3
- package/src/structures/ITtscLintPlugin.ts +56 -0
- package/src/structures/ITtscLintPluginConfig.ts +86 -5
- package/src/structures/ITtscLintPluginMeta.ts +14 -0
- package/src/structures/PluginRuleNames.ts +19 -0
- package/src/structures/TtscLintConfig.ts +27 -8
- package/src/structures/TtscLintConfigEntry.ts +50 -0
- package/src/structures/TtscLintFormatConfig.ts +169 -0
- package/src/structures/TtscLintPlugins.ts +10 -0
- package/src/structures/TtscLintRule.ts +14 -1
- package/src/structures/TtscLintRuleEntry.ts +28 -0
- package/src/structures/TtscLintRuleMap.ts +52 -0
- package/src/structures/TtscLintRuleOptions.ts +162 -0
- package/src/structures/defaultFormat.ts +33 -0
- package/src/structures/index.ts +10 -0
package/plugin/main.go
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
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
|
|
3
|
+
// The plugin host (ttsc / ttsx) spawns this binary with one of five
|
|
4
4
|
// subcommands:
|
|
5
5
|
//
|
|
6
6
|
// - `version` / `-v` / `--version` — print the binary banner.
|
|
7
7
|
// - `check` — typecheck + lint without emit. Failure exit code if any
|
|
8
8
|
// error-severity diagnostic fires.
|
|
9
|
+
// - `fix` — apply lint-rule autofixes, then typecheck + lint without
|
|
10
|
+
// emit.
|
|
11
|
+
// - `format` — apply format-rule edits only. Write-only: no diagnostic
|
|
12
|
+
// output, no typecheck recheck.
|
|
9
13
|
// - `build` — typecheck + lint, then run the standard tsgo emit pipeline
|
|
10
14
|
// so JS files land on disk.
|
|
11
15
|
// - `transform --file=PATH` — single-file emit with the same lint pass.
|
|
@@ -13,36 +17,19 @@
|
|
|
13
17
|
// Diagnostics share the renderer with tsgo's own output, so warnings come
|
|
14
18
|
// out yellow, errors come out red, and the trailing `Found N errors`
|
|
15
19
|
// summary is consistent with `tsc --noEmit`.
|
|
20
|
+
//
|
|
21
|
+
// Behavior lives in the sibling `linthost` library package. This binary is a
|
|
22
|
+
// thin wrapper so out-of-process consumers (the native CLI here) and
|
|
23
|
+
// in-process consumers (the ttsc.dev playground wasm) share the same
|
|
24
|
+
// dispatch surface.
|
|
16
25
|
package main
|
|
17
26
|
|
|
18
27
|
import (
|
|
19
|
-
|
|
20
|
-
"os"
|
|
21
|
-
)
|
|
28
|
+
"os"
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
"github.com/samchon/ttsc/packages/lint/linthost"
|
|
31
|
+
)
|
|
24
32
|
|
|
25
33
|
func main() {
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
func run(args []string) int {
|
|
30
|
-
if len(args) == 0 {
|
|
31
|
-
fmt.Fprintln(os.Stderr, "@ttsc/lint: command required (expected check|build|transform|version)")
|
|
32
|
-
return 2
|
|
33
|
-
}
|
|
34
|
-
switch args[0] {
|
|
35
|
-
case "-v", "--version", "version":
|
|
36
|
-
fmt.Fprintf(os.Stdout, "@ttsc/lint %s\n", version)
|
|
37
|
-
return 0
|
|
38
|
-
case "check":
|
|
39
|
-
return RunCheck(args[1:])
|
|
40
|
-
case "build":
|
|
41
|
-
return RunBuild(args[1:])
|
|
42
|
-
case "transform":
|
|
43
|
-
return RunTransform(args[1:])
|
|
44
|
-
default:
|
|
45
|
-
fmt.Fprintf(os.Stderr, "@ttsc/lint: unknown command %q\n", args[0])
|
|
46
|
-
return 2
|
|
47
|
-
}
|
|
34
|
+
os.Exit(linthost.Main(os.Args[1:]))
|
|
48
35
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Package astutil exposes the AST/text helpers `@ttsc/lint` built-in rules
|
|
2
|
+
// already rely on, so third-party contributor rules can build autofixes
|
|
3
|
+
// without re-implementing trivia, keyword location, or token-range math.
|
|
4
|
+
//
|
|
5
|
+
// These helpers are deliberately byte-oriented to match the
|
|
6
|
+
// `rule.TextEdit` contract: positions returned from this package can be
|
|
7
|
+
// fed directly into a `TextEdit{Pos, End, Text}` literal.
|
|
8
|
+
//
|
|
9
|
+
// All functions are pure (no shared state) and safe to call from any
|
|
10
|
+
// goroutine — `*shimast.SourceFile` and `*shimast.Node` are read-only
|
|
11
|
+
// from the rule's perspective.
|
|
12
|
+
package astutil
|
|
13
|
+
|
|
14
|
+
import (
|
|
15
|
+
"strings"
|
|
16
|
+
|
|
17
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
18
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// NodeText returns the source text under `node` with leading trivia
|
|
22
|
+
// (whitespace + comments) stripped. Mirrors `nodeText` in the built-in
|
|
23
|
+
// engine — useful for rules that compare textual identity (the
|
|
24
|
+
// `no-self-assign` / `no-self-compare` shape) or that splice a sub-node's
|
|
25
|
+
// text into a fix string.
|
|
26
|
+
//
|
|
27
|
+
// Returns "" when `file` or `node` is nil, or when the computed range
|
|
28
|
+
// falls outside the file (defensive — shouldn't happen for engine-supplied
|
|
29
|
+
// nodes).
|
|
30
|
+
func NodeText(file *shimast.SourceFile, node *shimast.Node) string {
|
|
31
|
+
if file == nil || node == nil {
|
|
32
|
+
return ""
|
|
33
|
+
}
|
|
34
|
+
src := file.Text()
|
|
35
|
+
end := node.End()
|
|
36
|
+
pos := shimscanner.SkipTrivia(src, node.Pos())
|
|
37
|
+
if pos < 0 || end > len(src) || pos >= end {
|
|
38
|
+
return ""
|
|
39
|
+
}
|
|
40
|
+
return strings.TrimRight(src[pos:end], " \t\r\n")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// KeywordStart returns the source offset of a declaration keyword such as
|
|
44
|
+
// `var`, `let`, `const`, `module`, `namespace`, or `function` that lives
|
|
45
|
+
// at the start of `node` (after leading trivia). Returns -1 if not found.
|
|
46
|
+
//
|
|
47
|
+
// Use this to anchor TextEdits that swap a leading keyword:
|
|
48
|
+
//
|
|
49
|
+
// start := astutil.KeywordStart(file, node, "let")
|
|
50
|
+
// if start >= 0 {
|
|
51
|
+
// ctx.ReportFix(node, "use const",
|
|
52
|
+
// rule.TextEdit{Pos: start, End: start + len("let"), Text: "const"})
|
|
53
|
+
// }
|
|
54
|
+
func KeywordStart(file *shimast.SourceFile, node *shimast.Node, keyword string) int {
|
|
55
|
+
if file == nil || node == nil || keyword == "" {
|
|
56
|
+
return -1
|
|
57
|
+
}
|
|
58
|
+
src := file.Text()
|
|
59
|
+
pos := shimscanner.SkipTrivia(src, node.Pos())
|
|
60
|
+
end := pos + len(keyword)
|
|
61
|
+
if pos < 0 || end > len(src) {
|
|
62
|
+
return -1
|
|
63
|
+
}
|
|
64
|
+
if strings.HasPrefix(src[pos:], keyword) && (end == len(src) || !isIdentifierPart(src[end])) {
|
|
65
|
+
return pos
|
|
66
|
+
}
|
|
67
|
+
limit := node.End()
|
|
68
|
+
if limit > len(src) {
|
|
69
|
+
limit = len(src)
|
|
70
|
+
}
|
|
71
|
+
for i := pos; i+len(keyword) <= limit && i < pos+32; i++ {
|
|
72
|
+
end = i + len(keyword)
|
|
73
|
+
if strings.HasPrefix(src[i:], keyword) &&
|
|
74
|
+
(i == 0 || !isIdentifierPart(src[i-1])) &&
|
|
75
|
+
(end == len(src) || !isIdentifierPart(src[end])) {
|
|
76
|
+
return i
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return -1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// FindKeyword scans `[pos, end)` for a keyword token whose lexeme is
|
|
83
|
+
// `keyword` and returns its first-byte offset, or -1 if not found.
|
|
84
|
+
// Differs from KeywordStart in that it works on an arbitrary byte range
|
|
85
|
+
// instead of a node's leading-trivia-adjusted start — use this for fixes
|
|
86
|
+
// that need to splice text after `import` or before `from`.
|
|
87
|
+
//
|
|
88
|
+
// The match is identifier-aware: a hit must be flanked by non-identifier
|
|
89
|
+
// bytes (or file edges) so that searching for `import` does not match
|
|
90
|
+
// the `import` prefix of `importMap`.
|
|
91
|
+
func FindKeyword(file *shimast.SourceFile, pos, end int, keyword string) int {
|
|
92
|
+
if file == nil || keyword == "" {
|
|
93
|
+
return -1
|
|
94
|
+
}
|
|
95
|
+
src := file.Text()
|
|
96
|
+
if pos < 0 {
|
|
97
|
+
pos = 0
|
|
98
|
+
}
|
|
99
|
+
if end > len(src) {
|
|
100
|
+
end = len(src)
|
|
101
|
+
}
|
|
102
|
+
limit := end - len(keyword)
|
|
103
|
+
for i := pos; i <= limit; i++ {
|
|
104
|
+
if src[i] != keyword[0] {
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
tail := i + len(keyword)
|
|
108
|
+
if src[i:tail] != keyword {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
if i > 0 && isIdentifierPart(src[i-1]) {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
if tail < len(src) && isIdentifierPart(src[tail]) {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
return i
|
|
118
|
+
}
|
|
119
|
+
return -1
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// TokenRange returns the `[pos, end)` range of a node's primary token
|
|
123
|
+
// with leading trivia stripped from the start. Useful for rules that
|
|
124
|
+
// want their diagnostic and fix range aligned to the token rather than
|
|
125
|
+
// the surrounding whitespace.
|
|
126
|
+
//
|
|
127
|
+
// Returns `(-1, -1)` when `file` or `node` is nil or when the computed
|
|
128
|
+
// range is malformed.
|
|
129
|
+
func TokenRange(file *shimast.SourceFile, node *shimast.Node) (int, int) {
|
|
130
|
+
if file == nil || node == nil {
|
|
131
|
+
return -1, -1
|
|
132
|
+
}
|
|
133
|
+
src := file.Text()
|
|
134
|
+
pos := shimscanner.SkipTrivia(src, node.Pos())
|
|
135
|
+
end := node.End()
|
|
136
|
+
if pos < 0 || pos > len(src) || end < pos || end > len(src) {
|
|
137
|
+
return -1, -1
|
|
138
|
+
}
|
|
139
|
+
return pos, end
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func isIdentifierPart(ch byte) bool {
|
|
143
|
+
return (ch >= 'a' && ch <= 'z') ||
|
|
144
|
+
(ch >= 'A' && ch <= 'Z') ||
|
|
145
|
+
(ch >= '0' && ch <= '9') ||
|
|
146
|
+
ch == '_' ||
|
|
147
|
+
ch == '$'
|
|
148
|
+
}
|
package/rule/rule.go
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Package rule is the public API for `@ttsc/lint` rule contributors.
|
|
2
|
+
//
|
|
3
|
+
// Third-party lint rule packages ("contributors") import this package and
|
|
4
|
+
// register their rules in an `init()`. At build time, ttsc copies a
|
|
5
|
+
// contributor's Go source into a sub-package of the `@ttsc/lint` Go module
|
|
6
|
+
// and synthesizes a blank-import in the host binary, which triggers the
|
|
7
|
+
// contributor's `init()` and populates the registry below.
|
|
8
|
+
//
|
|
9
|
+
// The host (`@ttsc/lint`) walks this registry during engine bootstrap and
|
|
10
|
+
// adapts each contributor rule onto the same dispatch table that drives
|
|
11
|
+
// the built-in rules.
|
|
12
|
+
//
|
|
13
|
+
// Contributors operate on the same shim AST the host's first-party
|
|
14
|
+
// plugins use (`github.com/microsoft/typescript-go/shim/ast` and friends)
|
|
15
|
+
// — there is no facade layer in between. The shim packages are the
|
|
16
|
+
// publicly maintained boundary ttsc already exposes; adding another
|
|
17
|
+
// wrapper here would duplicate that maintenance burden without earning
|
|
18
|
+
// any extra stability. Contributors get the full AST surface the host
|
|
19
|
+
// has, so authoring a contributor rule and authoring a built-in rule are
|
|
20
|
+
// the same exercise.
|
|
21
|
+
//
|
|
22
|
+
// Example contributor:
|
|
23
|
+
//
|
|
24
|
+
// package myrules
|
|
25
|
+
//
|
|
26
|
+
// import (
|
|
27
|
+
// shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
28
|
+
// "github.com/samchon/ttsc/packages/lint/rule"
|
|
29
|
+
// )
|
|
30
|
+
//
|
|
31
|
+
// func init() { rule.Register(noTodoComment{}) }
|
|
32
|
+
//
|
|
33
|
+
// type noTodoComment struct{}
|
|
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
|
+
// }
|
|
41
|
+
package rule
|
|
42
|
+
|
|
43
|
+
import (
|
|
44
|
+
"encoding/json"
|
|
45
|
+
|
|
46
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
47
|
+
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// Severity mirrors the engine's three-level severity ladder. The
|
|
51
|
+
// constants are kept value-compatible with the engine's internal
|
|
52
|
+
// `Severity` type so the adapter layer can cast safely.
|
|
53
|
+
type Severity int
|
|
54
|
+
|
|
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
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// Rule is the contract every contributor rule satisfies. Mirrors the
|
|
66
|
+
// internal host interface so the host can dispatch via a thin adapter
|
|
67
|
+
// without re-implementing the engine.
|
|
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
|
|
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
|
|
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)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// FormatRule is an optional marker contributors implement when a rule
|
|
84
|
+
// belongs to the "format" category instead of the default "lint"
|
|
85
|
+
// category. `ttsc fix` is the run-everything entry point and applies
|
|
86
|
+
// edits from BOTH lint-class and format-class rules. `ttsc format` is
|
|
87
|
+
// the format-only convenience: it filters to FormatRule findings so
|
|
88
|
+
// lint-class rewrites are skipped. Lint rules (rules that do not
|
|
89
|
+
// implement FormatRule) participate only in `ttsc fix` (and in
|
|
90
|
+
// diagnostics during `ttsc check`).
|
|
91
|
+
//
|
|
92
|
+
// `IsFormat` exists as a structural marker, not a runtime toggle:
|
|
93
|
+
// returning `false` is equivalent to not implementing the interface at
|
|
94
|
+
// all, and the host treats either form the same way.
|
|
95
|
+
type FormatRule interface {
|
|
96
|
+
Rule
|
|
97
|
+
IsFormat() bool
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Reporter is the engine-supplied callback that records a finding. The
|
|
101
|
+
// host implements this and passes it to `NewContext` when invoking a
|
|
102
|
+
// contributor rule.
|
|
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)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// FixReporter is the optional extension a host implements to receive
|
|
113
|
+
// autofix edits alongside a finding. The public `rule.Context` type-asserts
|
|
114
|
+
// against this shape so any host whose reporter exposes both methods opts
|
|
115
|
+
// into fix support without depending on a private interface name.
|
|
116
|
+
//
|
|
117
|
+
// Contributor rules do NOT implement this interface — it is the host-side
|
|
118
|
+
// counterpart to `Context.ReportFix` / `Context.ReportRangeFix`. A
|
|
119
|
+
// contributor authoring a fake reporter for unit tests can declare
|
|
120
|
+
// `var _ rule.FixReporter = &myReporter{}` to compile-check that the fake
|
|
121
|
+
// satisfies the fix surface. Go interface satisfaction is all-or-nothing:
|
|
122
|
+
// a fake that wants the fix path must implement BOTH `ReportFix` and
|
|
123
|
+
// `ReportRangeFix`.
|
|
124
|
+
type FixReporter interface {
|
|
125
|
+
ReportFix(node *shimast.Node, message string, edits ...TextEdit)
|
|
126
|
+
ReportRangeFix(pos, end int, message string, edits ...TextEdit)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// TextEdit is one byte-range replacement offered by an autofixable finding.
|
|
130
|
+
// Positions use the same byte offsets as shim AST nodes and must point inside
|
|
131
|
+
// the current source file. An empty `Text` deletes the range; positions are
|
|
132
|
+
// in lexer byte order, not visual order, so a UTF-8 multi-byte sequence must
|
|
133
|
+
// be replaced as a whole.
|
|
134
|
+
//
|
|
135
|
+
// Application policy: a rule may emit several `TextEdit`s in one
|
|
136
|
+
// `ReportFix` / `ReportRangeFix` call, in any order. The host treats the
|
|
137
|
+
// per-pass edit set as a candidate list. Within a single fix pass, edits
|
|
138
|
+
// must not overlap each other; when two edits cover overlapping ranges
|
|
139
|
+
// (either from one rule emitting multiple edits in one call, or from two
|
|
140
|
+
// different rules in the same pass), the host applies the earliest-starting
|
|
141
|
+
// / shortest edit and silently drops the rest. There is no diagnostic for
|
|
142
|
+
// dropped edits, and the host does not currently report when a comment
|
|
143
|
+
// falls inside a deletion range. Design fixes so each finding emits one
|
|
144
|
+
// contiguous TextEdit covering the entire replacement region.
|
|
145
|
+
type TextEdit struct {
|
|
146
|
+
Pos int
|
|
147
|
+
End int
|
|
148
|
+
Text string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Context is the per-(file, rule) handle the engine passes to `Check`.
|
|
152
|
+
// The `Reporter` is supplied by the host when constructing the context;
|
|
153
|
+
// contributors call `ctx.Report` / `ctx.ReportRange` directly through
|
|
154
|
+
// this Context rather than touching the reporter.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
175
|
+
|
|
176
|
+
reporter Reporter
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// NewContext constructs a Context for the engine to pass into a
|
|
180
|
+
// contributor rule's `Check`. Reserved for host code; contributors
|
|
181
|
+
// should not need to call this.
|
|
182
|
+
func NewContext(
|
|
183
|
+
file *shimast.SourceFile,
|
|
184
|
+
checker *shimchecker.Checker,
|
|
185
|
+
severity Severity,
|
|
186
|
+
options json.RawMessage,
|
|
187
|
+
reporter Reporter,
|
|
188
|
+
) *Context {
|
|
189
|
+
return &Context{
|
|
190
|
+
File: file,
|
|
191
|
+
Checker: checker,
|
|
192
|
+
Severity: severity,
|
|
193
|
+
Options: options,
|
|
194
|
+
reporter: reporter,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// DecodeOptions unmarshals the rule's options blob into `out`. Returns
|
|
199
|
+
// nil with no side effect when the rule was configured with severity
|
|
200
|
+
// alone, so contributors can write:
|
|
201
|
+
//
|
|
202
|
+
// var opts myRuleOptions
|
|
203
|
+
// _ = ctx.DecodeOptions(&opts)
|
|
204
|
+
// // opts now holds either the user's settings or the zero value.
|
|
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)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Report records a finding at the given node's source range. Silently
|
|
213
|
+
// ignored when severity is `off` (defensive — the engine already filters
|
|
214
|
+
// by severity before invoking Check) or when no reporter is attached.
|
|
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)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ReportFix records a finding at the given node's source range with optional
|
|
223
|
+
// autofix edits. Older hosts that do not implement fix reporting receive the
|
|
224
|
+
// diagnostic without edits.
|
|
225
|
+
// Treat edits as best-effort: design the rule so the diagnostic alone is useful.
|
|
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...)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ReportRange records a finding at an explicit byte range inside the
|
|
243
|
+
// current file.
|
|
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)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ReportRangeFix records a finding at an explicit byte range with optional
|
|
252
|
+
// autofix edits. Older hosts that do not implement fix reporting receive the
|
|
253
|
+
// diagnostic without edits.
|
|
254
|
+
// Treat edits as best-effort: design the rule so the diagnostic alone is useful.
|
|
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...)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var registry []Rule
|
|
272
|
+
|
|
273
|
+
// Register adds a contributor rule to the global registry. Called from a
|
|
274
|
+
// contributor package's `init()`. Duplicate names are NOT checked here
|
|
275
|
+
// — the host's adapter layer surfaces collisions with a clearer error
|
|
276
|
+
// than a raw panic.
|
|
277
|
+
func Register(r Rule) {
|
|
278
|
+
if r == nil {
|
|
279
|
+
panic("rule: Register called with nil rule")
|
|
280
|
+
}
|
|
281
|
+
registry = append(registry, r)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Registered returns every contributor rule registered via `Register`.
|
|
285
|
+
// Called once by the host during engine bootstrap. The returned slice is
|
|
286
|
+
// a defensive copy so the host cannot mutate the registry.
|
|
287
|
+
func Registered() []Rule {
|
|
288
|
+
out := make([]Rule, len(registry))
|
|
289
|
+
copy(out, registry)
|
|
290
|
+
return out
|
|
291
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { TtscLintConfig } from "./structures/TtscLintConfig";
|
|
2
|
+
import type { TtscLintPlugins } from "./structures/TtscLintPlugins";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authoring helper that preserves the literal type of a lint config.
|
|
6
|
+
*
|
|
7
|
+
* ESLint's flat-config experience relies on `defineConfig` to capture the
|
|
8
|
+
* const-narrowed type of the configuration array, which is what makes plugin
|
|
9
|
+
* rule names autocomplete across entries. `@ttsc/lint` follows the same
|
|
10
|
+
* pattern: pass your config through this helper and the contributor plugin
|
|
11
|
+
* objects in `plugins` propagate their `rules` tuples into the rule-name
|
|
12
|
+
* union.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { defineConfig } from "@ttsc/lint";
|
|
16
|
+
* import importPlugin from "@ttsc/lint-plugin-import";
|
|
17
|
+
*
|
|
18
|
+
* export default defineConfig([
|
|
19
|
+
* {
|
|
20
|
+
* plugins: { import: importPlugin },
|
|
21
|
+
* rules: {
|
|
22
|
+
* "no-var": "error",
|
|
23
|
+
* "import/no-cycle": ["error", { maxDepth: 1 }],
|
|
24
|
+
* },
|
|
25
|
+
* },
|
|
26
|
+
* ]);
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* The function is a pure pass-through at runtime. The generic gymnastics below
|
|
30
|
+
* gather every `plugins` map across the array of config entries into one
|
|
31
|
+
* intersected `TtscLintPlugins` shape that gets threaded back into the
|
|
32
|
+
* `TtscLintConfig<P>` constraint. Without this, `TtscLintConfig`'s default `P =
|
|
33
|
+
* Record<string, never>` rules out every namespaced rule name and `{
|
|
34
|
+
* "import/no-cycle": "error" }` would be flagged as a typo.
|
|
35
|
+
*/
|
|
36
|
+
export function defineConfig<const T extends TtscLintConfig<GatherPlugins<T>>>(
|
|
37
|
+
config: T,
|
|
38
|
+
): T {
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Walks the input type to collect every plugin map declared across entries.
|
|
44
|
+
* Single entries yield the entry's `plugins`; arrays intersect every entry's
|
|
45
|
+
* `plugins` so each entry's rule-name union remains valid for the whole array.
|
|
46
|
+
* The intersection (vs. union) is load-bearing — TypeScript's `keyof (A | B)`
|
|
47
|
+
* collapses to `never`, which would reject every namespaced rule name.
|
|
48
|
+
*
|
|
49
|
+
* Plugins declared exclusively inside an `extends` chain are NOT collected:
|
|
50
|
+
* recursing into `T extends { extends?: infer E } ? GatherPlugins<E> : {}`
|
|
51
|
+
* blows TypeScript's instantiation budget (`extends` is itself a
|
|
52
|
+
* `TtscLintConfigEntry` tree). Authors should declare each plugin's `plugins`
|
|
53
|
+
* map on the outermost entry where its rules are configured. The runtime
|
|
54
|
+
* resolver still picks up plugins from `extends` entries; only the autocomplete
|
|
55
|
+
* domain is bounded.
|
|
56
|
+
*/
|
|
57
|
+
type GatherPlugins<T> = T extends { plugins?: infer P }
|
|
58
|
+
? P extends TtscLintPlugins
|
|
59
|
+
? P
|
|
60
|
+
: {}
|
|
61
|
+
: T extends readonly (infer Item)[]
|
|
62
|
+
? UnionToIntersection<Item extends unknown ? GatherPlugins<Item> : never>
|
|
63
|
+
: {};
|
|
64
|
+
|
|
65
|
+
type UnionToIntersection<U> = (
|
|
66
|
+
U extends unknown ? (x: U) => void : never
|
|
67
|
+
) extends (x: infer I) => void
|
|
68
|
+
? I
|
|
69
|
+
: never;
|