@ttsc/lint 0.5.0-dev.20260429
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/LICENSE +21 -0
- package/README.md +204 -0
- package/go-plugin/go.mod +38 -0
- package/go-plugin/lint/ast_helpers.go +170 -0
- package/go-plugin/lint/compile.go +393 -0
- package/go-plugin/lint/config.go +134 -0
- package/go-plugin/lint/engine.go +259 -0
- package/go-plugin/lint/host.go +190 -0
- package/go-plugin/lint/rules_arrays.go +78 -0
- package/go-plugin/lint/rules_console.go +37 -0
- package/go-plugin/lint/rules_debugger.go +29 -0
- package/go-plugin/lint/rules_dupes.go +179 -0
- package/go-plugin/lint/rules_empty.go +110 -0
- package/go-plugin/lint/rules_eval.go +57 -0
- package/go-plugin/lint/rules_finally.go +123 -0
- package/go-plugin/lint/rules_logic.go +368 -0
- package/go-plugin/lint/rules_loops.go +107 -0
- package/go-plugin/lint/rules_misc.go +69 -0
- package/go-plugin/lint/rules_problems.go +571 -0
- package/go-plugin/lint/rules_protos.go +42 -0
- package/go-plugin/lint/rules_self.go +89 -0
- package/go-plugin/lint/rules_strings.go +123 -0
- package/go-plugin/lint/rules_suggestions.go +1236 -0
- package/go-plugin/lint/rules_throw.go +31 -0
- package/go-plugin/lint/rules_ts.go +250 -0
- package/go-plugin/lint/rules_ts_extra.go +654 -0
- package/go-plugin/lint/rules_var.go +40 -0
- package/go-plugin/main.go +50 -0
- package/index.cjs +28 -0
- package/package.json +41 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Package lint hosts the rule registry, the AST-walking engine, and the
|
|
2
|
+
// orchestration glue that the `@ttsc/lint` native plugin uses to run rules
|
|
3
|
+
// against a tsgo Program.
|
|
4
|
+
//
|
|
5
|
+
// Layering:
|
|
6
|
+
//
|
|
7
|
+
// - `Rule` is the interface every rule implements. Rules are
|
|
8
|
+
// registered at package init time and never mutated.
|
|
9
|
+
// - `Engine` walks every user source file once, dispatching each visited
|
|
10
|
+
// node to the rules that opted in via `Visits()`.
|
|
11
|
+
// - `Context` is what a rule receives when it fires; it owns the
|
|
12
|
+
// report channel back to the engine.
|
|
13
|
+
//
|
|
14
|
+
// Rules are stateless across files: each invocation gets a fresh `Context`
|
|
15
|
+
// and may not retain references to the previous file. This keeps the
|
|
16
|
+
// engine concurrent-friendly even though the v0 implementation runs
|
|
17
|
+
// serially.
|
|
18
|
+
package lint
|
|
19
|
+
|
|
20
|
+
import (
|
|
21
|
+
"sort"
|
|
22
|
+
|
|
23
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
24
|
+
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
25
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// Rule is the contract every lint rule satisfies.
|
|
29
|
+
type Rule interface {
|
|
30
|
+
// Name is the identifier that users put in their `rules` map. Use
|
|
31
|
+
// the same names as `eslint` / `@typescript-eslint` where possible —
|
|
32
|
+
// this plugin is a host, not a renaming exercise.
|
|
33
|
+
Name() string
|
|
34
|
+
|
|
35
|
+
// Visits returns the AST kinds the rule cares about. The engine only
|
|
36
|
+
// dispatches to rules that registered for the visited node's kind,
|
|
37
|
+
// which keeps the per-node hot path linear in active rules rather
|
|
38
|
+
// than total rules.
|
|
39
|
+
Visits() []shimast.Kind
|
|
40
|
+
|
|
41
|
+
// Check is invoked once per relevant node. Use `ctx.Report` to emit
|
|
42
|
+
// findings.
|
|
43
|
+
Check(ctx *Context, node *shimast.Node)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Context is the per-(file, rule) handle the engine passes to `Check`.
|
|
47
|
+
type Context struct {
|
|
48
|
+
File *shimast.SourceFile
|
|
49
|
+
Checker *shimchecker.Checker
|
|
50
|
+
Severity Severity
|
|
51
|
+
|
|
52
|
+
rule Rule
|
|
53
|
+
collect func(*Finding)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Finding is one rule-emitted diagnostic before it gets converted into a
|
|
57
|
+
// driver Diagnostic.
|
|
58
|
+
type Finding struct {
|
|
59
|
+
Rule string
|
|
60
|
+
Severity Severity
|
|
61
|
+
File *shimast.SourceFile
|
|
62
|
+
Pos int
|
|
63
|
+
End int
|
|
64
|
+
Message string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Report records a finding at the given node's source range. The pos is
|
|
68
|
+
// trimmed past leading trivia (whitespace + comments) so the renderer's
|
|
69
|
+
// `path:line:col` banner points at the offending token, not the start of
|
|
70
|
+
// the surrounding indentation. A finding is silently dropped if the
|
|
71
|
+
// configured severity is `off` (defensive — the engine already filters
|
|
72
|
+
// by severity before calling Check, but a rule might lazy-evaluate in
|
|
73
|
+
// the future).
|
|
74
|
+
func (c *Context) Report(node *shimast.Node, message string) {
|
|
75
|
+
if c.Severity == SeverityOff || node == nil {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
pos := node.Pos()
|
|
79
|
+
if c.File != nil {
|
|
80
|
+
pos = shimscanner.SkipTrivia(c.File.Text(), pos)
|
|
81
|
+
}
|
|
82
|
+
c.collect(&Finding{
|
|
83
|
+
Rule: c.rule.Name(),
|
|
84
|
+
Severity: c.Severity,
|
|
85
|
+
File: c.File,
|
|
86
|
+
Pos: pos,
|
|
87
|
+
End: node.End(),
|
|
88
|
+
Message: message,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ReportRange records a finding at an explicit byte range inside the
|
|
93
|
+
// current file. Use this when the rule wants to highlight a sub-token of
|
|
94
|
+
// a node (e.g. an operator inside a BinaryExpression).
|
|
95
|
+
func (c *Context) ReportRange(pos, end int, message string) {
|
|
96
|
+
if c.Severity == SeverityOff || c.File == nil {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
if end <= pos {
|
|
100
|
+
end = pos + 1
|
|
101
|
+
}
|
|
102
|
+
c.collect(&Finding{
|
|
103
|
+
Rule: c.rule.Name(),
|
|
104
|
+
Severity: c.Severity,
|
|
105
|
+
File: c.File,
|
|
106
|
+
Pos: pos,
|
|
107
|
+
End: end,
|
|
108
|
+
Message: message,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// registry stores the package-global rule list keyed by name. Tests can
|
|
113
|
+
// also reach into it via `LookupRule`.
|
|
114
|
+
type registry struct {
|
|
115
|
+
rules map[string]Rule
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var registered = ®istry{rules: map[string]Rule{}}
|
|
119
|
+
|
|
120
|
+
// Register adds a rule to the global registry. Called from each rule's
|
|
121
|
+
// `init()`. Duplicate names are a programmer error and panic.
|
|
122
|
+
func Register(rule Rule) {
|
|
123
|
+
if rule == nil {
|
|
124
|
+
panic("@ttsc/lint: Register called with nil rule")
|
|
125
|
+
}
|
|
126
|
+
if _, exists := registered.rules[rule.Name()]; exists {
|
|
127
|
+
panic("@ttsc/lint: rule " + rule.Name() + " registered twice")
|
|
128
|
+
}
|
|
129
|
+
registered.rules[rule.Name()] = rule
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// LookupRule returns the registered rule by name, or nil if missing.
|
|
133
|
+
func LookupRule(name string) Rule { return registered.rules[name] }
|
|
134
|
+
|
|
135
|
+
// AllRuleNames returns the registry sorted alphabetically. Useful for
|
|
136
|
+
// `--list-rules` style introspection and stable test snapshots.
|
|
137
|
+
func AllRuleNames() []string {
|
|
138
|
+
names := make([]string, 0, len(registered.rules))
|
|
139
|
+
for n := range registered.rules {
|
|
140
|
+
names = append(names, n)
|
|
141
|
+
}
|
|
142
|
+
sort.Strings(names)
|
|
143
|
+
return names
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Engine binds a rule configuration to a Program and walks the AST once
|
|
147
|
+
// per source file, dispatching each visited node to its interested rules.
|
|
148
|
+
type Engine struct {
|
|
149
|
+
config RuleConfig
|
|
150
|
+
rules map[shimast.Kind][]Rule
|
|
151
|
+
enabled map[string]Severity
|
|
152
|
+
unknown []string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// NewEngine returns an engine configured for `config`. Rules whose
|
|
156
|
+
// severity is `off` are skipped entirely. Configuration entries that name
|
|
157
|
+
// an unknown rule are recorded so the caller can surface them as a
|
|
158
|
+
// configuration warning rather than a silent typo.
|
|
159
|
+
func NewEngine(config RuleConfig) *Engine {
|
|
160
|
+
eng := &Engine{
|
|
161
|
+
config: config,
|
|
162
|
+
rules: make(map[shimast.Kind][]Rule),
|
|
163
|
+
enabled: make(map[string]Severity),
|
|
164
|
+
}
|
|
165
|
+
for name, sev := range config {
|
|
166
|
+
rule, ok := registered.rules[name]
|
|
167
|
+
if !ok {
|
|
168
|
+
eng.unknown = append(eng.unknown, name)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if sev == SeverityOff {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
eng.enabled[name] = sev
|
|
175
|
+
for _, kind := range rule.Visits() {
|
|
176
|
+
eng.rules[kind] = append(eng.rules[kind], rule)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
sort.Strings(eng.unknown)
|
|
180
|
+
return eng
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// UnknownRules returns the names of rules that appeared in the config but
|
|
184
|
+
// have no registered implementation.
|
|
185
|
+
func (e *Engine) UnknownRules() []string { return e.unknown }
|
|
186
|
+
|
|
187
|
+
// EnabledRules returns the active rule set keyed by name. Mostly for
|
|
188
|
+
// tests + introspection.
|
|
189
|
+
func (e *Engine) EnabledRules() map[string]Severity { return e.enabled }
|
|
190
|
+
|
|
191
|
+
// Run walks every non-declaration source file in the program and
|
|
192
|
+
// returns the collected findings.
|
|
193
|
+
func (e *Engine) Run(files []*shimast.SourceFile, checker *shimchecker.Checker) []*Finding {
|
|
194
|
+
var findings []*Finding
|
|
195
|
+
for _, file := range files {
|
|
196
|
+
if file == nil || file.IsDeclarationFile {
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
findings = append(findings, e.runFile(file, checker)...)
|
|
200
|
+
}
|
|
201
|
+
return findings
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// runFile is the per-file driver. The visitor is allocated once per file
|
|
205
|
+
// to keep the per-node hot path branch-free; it visits children
|
|
206
|
+
// post-order so parents see their already-checked subtrees.
|
|
207
|
+
func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker) []*Finding {
|
|
208
|
+
var collected []*Finding
|
|
209
|
+
collect := func(f *Finding) { collected = append(collected, f) }
|
|
210
|
+
|
|
211
|
+
var walk func(node *shimast.Node)
|
|
212
|
+
walk = func(node *shimast.Node) {
|
|
213
|
+
if node == nil {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if rules, ok := e.rules[node.Kind]; ok {
|
|
217
|
+
for _, rule := range rules {
|
|
218
|
+
ctx := &Context{
|
|
219
|
+
File: file,
|
|
220
|
+
Checker: checker,
|
|
221
|
+
Severity: e.enabled[rule.Name()],
|
|
222
|
+
rule: rule,
|
|
223
|
+
collect: collect,
|
|
224
|
+
}
|
|
225
|
+
rule.Check(ctx, node)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
229
|
+
walk(child)
|
|
230
|
+
return false // visit every child
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// SourceFile dispatches into its statement list directly; we walk
|
|
235
|
+
// statements explicitly so the file node itself can be inspected by
|
|
236
|
+
// rules (e.g., `ban-ts-comment` reads CommentDirectives off the
|
|
237
|
+
// SourceFile).
|
|
238
|
+
if rules, ok := e.rules[shimast.KindSourceFile]; ok {
|
|
239
|
+
for _, rule := range rules {
|
|
240
|
+
ctx := &Context{
|
|
241
|
+
File: file,
|
|
242
|
+
Checker: checker,
|
|
243
|
+
Severity: e.enabled[rule.Name()],
|
|
244
|
+
rule: rule,
|
|
245
|
+
collect: collect,
|
|
246
|
+
}
|
|
247
|
+
rule.Check(ctx, file.AsNode())
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
statements := file.Statements
|
|
252
|
+
if statements == nil {
|
|
253
|
+
return collected
|
|
254
|
+
}
|
|
255
|
+
for _, stmt := range statements.Nodes {
|
|
256
|
+
walk(stmt)
|
|
257
|
+
}
|
|
258
|
+
return collected
|
|
259
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Bootstrap glue for the @ttsc/lint native binary.
|
|
2
|
+
//
|
|
3
|
+
// We don't import `github.com/samchon/ttsc/packages/ttsc/driver` from a
|
|
4
|
+
// source plugin because that would force every consumer of @ttsc/lint to
|
|
5
|
+
// have the in-tree samchon/ttsc/packages/ttsc module on their go.work — a
|
|
6
|
+
// dependency the public proxy cannot satisfy and that conflicts with
|
|
7
|
+
// ttsc's runtime-generated go.work overlay. Instead, this file inlines a
|
|
8
|
+
// minimal Program/Checker bootstrap (the same pattern documented in
|
|
9
|
+
// 03-tsgo.md and used by every other source-plugin reference fixture).
|
|
10
|
+
package lint
|
|
11
|
+
|
|
12
|
+
import (
|
|
13
|
+
"context"
|
|
14
|
+
"errors"
|
|
15
|
+
"fmt"
|
|
16
|
+
"path/filepath"
|
|
17
|
+
|
|
18
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
19
|
+
"github.com/microsoft/typescript-go/shim/bundled"
|
|
20
|
+
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
21
|
+
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
22
|
+
shimcore "github.com/microsoft/typescript-go/shim/core"
|
|
23
|
+
"github.com/microsoft/typescript-go/shim/tsoptions"
|
|
24
|
+
"github.com/microsoft/typescript-go/shim/vfs/cachedvfs"
|
|
25
|
+
"github.com/microsoft/typescript-go/shim/vfs/osvfs"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// program bundles the tsgo Program with the parsed config and a checker
|
|
29
|
+
// release callback so the orchestration code can clean up after itself.
|
|
30
|
+
type program struct {
|
|
31
|
+
cwd string
|
|
32
|
+
tsProgram *shimcompiler.Program
|
|
33
|
+
parsed *tsoptions.ParsedCommandLine
|
|
34
|
+
checker *shimchecker.Checker
|
|
35
|
+
releaseChecker func()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type loadProgramOptions struct {
|
|
39
|
+
forceEmit bool
|
|
40
|
+
forceNoEmit bool
|
|
41
|
+
outDir string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// loadProgram parses the given tsconfig, builds a Program, and acquires a
|
|
45
|
+
// type checker. Mirrors the canonical bootstrap pattern from
|
|
46
|
+
// `03-tsgo.md` — the only ttsc-specific bit is that `forceEmit`/
|
|
47
|
+
// `forceNoEmit`/`outDir` overrides are merged into the parsed config
|
|
48
|
+
// before the program is created so `--noEmit` and friends behave like
|
|
49
|
+
// they do in `ttsc check`.
|
|
50
|
+
func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program, []*shimast.Diagnostic, error) {
|
|
51
|
+
if !filepath.IsAbs(cwd) {
|
|
52
|
+
abs, err := filepath.Abs(cwd)
|
|
53
|
+
if err != nil {
|
|
54
|
+
return nil, nil, fmt.Errorf("loadProgram: cwd: %w", err)
|
|
55
|
+
}
|
|
56
|
+
cwd = abs
|
|
57
|
+
}
|
|
58
|
+
resolved := tsconfigPath
|
|
59
|
+
if !filepath.IsAbs(resolved) {
|
|
60
|
+
resolved = filepath.Join(cwd, resolved)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
|
|
64
|
+
host := shimcompiler.NewCompilerHost(cwd, fs, bundled.LibPath(), nil, nil)
|
|
65
|
+
|
|
66
|
+
parsed, parseDiags := tsoptions.GetParsedCommandLineOfConfigFile(
|
|
67
|
+
resolved,
|
|
68
|
+
&shimcore.CompilerOptions{},
|
|
69
|
+
nil,
|
|
70
|
+
host,
|
|
71
|
+
nil,
|
|
72
|
+
)
|
|
73
|
+
if parsed == nil {
|
|
74
|
+
return nil, nil, fmt.Errorf("tsoptions: parsed command line was nil for %s", resolved)
|
|
75
|
+
}
|
|
76
|
+
if len(parseDiags) > 0 {
|
|
77
|
+
return nil, parseDiags, nil
|
|
78
|
+
}
|
|
79
|
+
if len(parsed.Errors) > 0 {
|
|
80
|
+
return nil, parsed.Errors, nil
|
|
81
|
+
}
|
|
82
|
+
if options.forceNoEmit {
|
|
83
|
+
forceNoEmit(parsed)
|
|
84
|
+
}
|
|
85
|
+
if options.forceEmit {
|
|
86
|
+
forceEmit(parsed)
|
|
87
|
+
}
|
|
88
|
+
if options.outDir != "" {
|
|
89
|
+
overrideOutDir(cwd, parsed, options.outDir)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
tsProgram := shimcompiler.NewProgram(shimcompiler.ProgramOptions{
|
|
93
|
+
Config: parsed,
|
|
94
|
+
SingleThreaded: shimcore.TSTrue,
|
|
95
|
+
Host: host,
|
|
96
|
+
UseSourceOfProjectReference: true,
|
|
97
|
+
})
|
|
98
|
+
if tsProgram == nil {
|
|
99
|
+
return nil, nil, errors.New("compiler.NewProgram returned nil")
|
|
100
|
+
}
|
|
101
|
+
checker, release := tsProgram.GetTypeChecker(context.Background())
|
|
102
|
+
return &program{
|
|
103
|
+
cwd: cwd,
|
|
104
|
+
tsProgram: tsProgram,
|
|
105
|
+
parsed: parsed,
|
|
106
|
+
checker: checker,
|
|
107
|
+
releaseChecker: release,
|
|
108
|
+
}, nil, nil
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func (p *program) close() {
|
|
112
|
+
if p == nil {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
if p.releaseChecker != nil {
|
|
116
|
+
p.releaseChecker()
|
|
117
|
+
p.releaseChecker = nil
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// userSourceFiles returns the program's user-authored source files
|
|
122
|
+
// (declaration files filtered out — those belong to library typings).
|
|
123
|
+
func (p *program) userSourceFiles() []*shimast.SourceFile {
|
|
124
|
+
out := make([]*shimast.SourceFile, 0)
|
|
125
|
+
for _, f := range p.tsProgram.SourceFiles() {
|
|
126
|
+
if f == nil || f.IsDeclarationFile {
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
out = append(out, f)
|
|
130
|
+
}
|
|
131
|
+
return out
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// programDiagnostics returns the bind + semantic diagnostics for the
|
|
135
|
+
// loaded program. Same surface tsgo's CLI prints when you run a regular
|
|
136
|
+
// `tsgo --noEmit`.
|
|
137
|
+
func (p *program) programDiagnostics() []*shimast.Diagnostic {
|
|
138
|
+
if p == nil || p.tsProgram == nil {
|
|
139
|
+
return nil
|
|
140
|
+
}
|
|
141
|
+
ctx := context.Background()
|
|
142
|
+
raw := shimcompiler.GetDiagnosticsOfAnyProgram(
|
|
143
|
+
ctx,
|
|
144
|
+
p.tsProgram,
|
|
145
|
+
nil,
|
|
146
|
+
false,
|
|
147
|
+
p.tsProgram.GetBindDiagnostics,
|
|
148
|
+
p.tsProgram.GetSemanticDiagnostics,
|
|
149
|
+
)
|
|
150
|
+
return shimcompiler.SortAndDeduplicateDiagnostics(raw)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// findSourceFile locates a source file in the program by absolute path.
|
|
154
|
+
// tsgo normalizes paths to forward slashes; we do the same on our side.
|
|
155
|
+
func (p *program) findSourceFile(target string) *shimast.SourceFile {
|
|
156
|
+
want := filepath.ToSlash(target)
|
|
157
|
+
for _, file := range p.tsProgram.SourceFiles() {
|
|
158
|
+
if filepath.ToSlash(file.FileName()) == want {
|
|
159
|
+
return file
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func forceEmit(parsed *tsoptions.ParsedCommandLine) {
|
|
166
|
+
if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
options := parsed.ParsedConfig.CompilerOptions
|
|
170
|
+
options.NoEmit = shimcore.TSFalse
|
|
171
|
+
options.EmitDeclarationOnly = shimcore.TSFalse
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func forceNoEmit(parsed *tsoptions.ParsedCommandLine) {
|
|
175
|
+
if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
parsed.ParsedConfig.CompilerOptions.NoEmit = shimcore.TSTrue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func overrideOutDir(cwd string, parsed *tsoptions.ParsedCommandLine, outDir string) {
|
|
182
|
+
if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
if filepath.IsAbs(outDir) {
|
|
186
|
+
parsed.ParsedConfig.CompilerOptions.OutDir = filepath.ToSlash(outDir)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
parsed.ParsedConfig.CompilerOptions.OutDir = filepath.ToSlash(filepath.Join(cwd, outDir))
|
|
190
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package lint
|
|
2
|
+
|
|
3
|
+
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
|
+
|
|
5
|
+
// no-sparse-arrays: `[1, , 2]` — the implied undefined slot is almost
|
|
6
|
+
// always a typo.
|
|
7
|
+
// https://eslint.org/docs/latest/rules/no-sparse-arrays
|
|
8
|
+
type noSparseArrays struct{}
|
|
9
|
+
|
|
10
|
+
func (noSparseArrays) Name() string { return "no-sparse-arrays" }
|
|
11
|
+
func (noSparseArrays) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindArrayLiteralExpression} }
|
|
12
|
+
func (noSparseArrays) Check(ctx *Context, node *shimast.Node) {
|
|
13
|
+
arr := node.AsArrayLiteralExpression()
|
|
14
|
+
if arr == nil || arr.Elements == nil {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
for _, el := range arr.Elements.Nodes {
|
|
18
|
+
if el != nil && el.Kind == shimast.KindOmittedExpression {
|
|
19
|
+
ctx.Report(node, "Unexpected comma in middle of array.")
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// no-array-constructor: forbid `new Array(0)` / `Array(1, 2, 3)` (use
|
|
26
|
+
// array literals). The 1-arg numeric form is also banned because its
|
|
27
|
+
// behavior depends on the runtime — see ESLint defaults.
|
|
28
|
+
// https://eslint.org/docs/latest/rules/no-array-constructor
|
|
29
|
+
type noArrayConstructor struct{}
|
|
30
|
+
|
|
31
|
+
func (noArrayConstructor) Name() string { return "no-array-constructor" }
|
|
32
|
+
func (noArrayConstructor) Visits() []shimast.Kind {
|
|
33
|
+
return []shimast.Kind{shimast.KindNewExpression, shimast.KindCallExpression}
|
|
34
|
+
}
|
|
35
|
+
func (noArrayConstructor) Check(ctx *Context, node *shimast.Node) {
|
|
36
|
+
var callee *shimast.Node
|
|
37
|
+
var argCount int
|
|
38
|
+
switch node.Kind {
|
|
39
|
+
case shimast.KindNewExpression:
|
|
40
|
+
ne := node.AsNewExpression()
|
|
41
|
+
if ne == nil {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
if ne.TypeArguments != nil && len(ne.TypeArguments.Nodes) > 0 {
|
|
45
|
+
return // `new Array<string>()` is a typed empty array
|
|
46
|
+
}
|
|
47
|
+
callee = ne.Expression
|
|
48
|
+
if ne.Arguments != nil {
|
|
49
|
+
argCount = len(ne.Arguments.Nodes)
|
|
50
|
+
}
|
|
51
|
+
case shimast.KindCallExpression:
|
|
52
|
+
call := node.AsCallExpression()
|
|
53
|
+
if call == nil {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if call.TypeArguments != nil && len(call.TypeArguments.Nodes) > 0 {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
callee = call.Expression
|
|
60
|
+
if call.Arguments != nil {
|
|
61
|
+
argCount = len(call.Arguments.Nodes)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if identifierText(callee) != "Array" {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
// Single-arg numeric — ambiguous (length vs single element). Skip
|
|
68
|
+
// so existing patterns aren't flagged when the intent is preallocate.
|
|
69
|
+
if argCount == 1 {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
ctx.Report(node, "The array literal notation [] is preferable.")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func init() {
|
|
76
|
+
Register(noSparseArrays{})
|
|
77
|
+
Register(noArrayConstructor{})
|
|
78
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package lint
|
|
2
|
+
|
|
3
|
+
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
|
+
|
|
5
|
+
// no-console: forbid `console.*` calls. Default mode flags every method;
|
|
6
|
+
// users who want to allow specific methods can configure ESLint-style
|
|
7
|
+
// allowlists in a future iteration.
|
|
8
|
+
// https://eslint.org/docs/latest/rules/no-console
|
|
9
|
+
type noConsole struct{}
|
|
10
|
+
|
|
11
|
+
func (noConsole) Name() string { return "no-console" }
|
|
12
|
+
func (noConsole) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindCallExpression} }
|
|
13
|
+
func (noConsole) Check(ctx *Context, node *shimast.Node) {
|
|
14
|
+
call := node.AsCallExpression()
|
|
15
|
+
if call == nil || call.Expression == nil {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
if call.Expression.Kind != shimast.KindPropertyAccessExpression {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
access := call.Expression.AsPropertyAccessExpression()
|
|
22
|
+
if access == nil {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
if identifierText(access.Expression) != "console" {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
method := identifierText(access.Name())
|
|
29
|
+
if method == "" {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
ctx.Report(node, "Unexpected console statement.")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func init() {
|
|
36
|
+
Register(noConsole{})
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package lint
|
|
2
|
+
|
|
3
|
+
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
|
+
|
|
5
|
+
// no-debugger: forbid `debugger` statements.
|
|
6
|
+
// https://eslint.org/docs/latest/rules/no-debugger
|
|
7
|
+
type noDebugger struct{}
|
|
8
|
+
|
|
9
|
+
func (noDebugger) Name() string { return "no-debugger" }
|
|
10
|
+
func (noDebugger) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindDebuggerStatement} }
|
|
11
|
+
func (noDebugger) Check(ctx *Context, node *shimast.Node) {
|
|
12
|
+
ctx.Report(node, "Unexpected `debugger` statement.")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// no-with: forbid `with` statements (already disallowed in strict mode,
|
|
16
|
+
// but lint catches it before the parse error).
|
|
17
|
+
// https://eslint.org/docs/latest/rules/no-with
|
|
18
|
+
type noWith struct{}
|
|
19
|
+
|
|
20
|
+
func (noWith) Name() string { return "no-with" }
|
|
21
|
+
func (noWith) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindWithStatement} }
|
|
22
|
+
func (noWith) Check(ctx *Context, node *shimast.Node) {
|
|
23
|
+
ctx.Report(node, "Unexpected `with` statement.")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func init() {
|
|
27
|
+
Register(noDebugger{})
|
|
28
|
+
Register(noWith{})
|
|
29
|
+
}
|