@ttsc/lint 0.7.1 → 0.7.3
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 +62 -24
- package/lib/index.js.map +1 -1
- package/package.json +3 -3
- package/plugin/ast_helpers.go +158 -158
- package/plugin/compile.go +303 -303
- package/plugin/config.go +322 -322
- package/plugin/engine.go +152 -153
- package/plugin/host.go +128 -128
- package/plugin/main.go +21 -21
- package/plugin/rules_arrays.go +51 -51
- package/plugin/rules_console.go +22 -23
- package/plugin/rules_debugger.go +4 -4
- package/plugin/rules_dupes.go +134 -134
- package/plugin/rules_empty.go +68 -68
- package/plugin/rules_eval.go +28 -28
- package/plugin/rules_finally.go +91 -91
- package/plugin/rules_gap.go +188 -188
- package/plugin/rules_logic.go +268 -268
- package/plugin/rules_loops.go +89 -89
- package/plugin/rules_misc.go +43 -43
- package/plugin/rules_problems.go +396 -396
- package/plugin/rules_protos.go +17 -17
- package/plugin/rules_self.go +56 -56
- package/plugin/rules_strings.go +65 -65
- package/plugin/rules_suggestions.go +863 -863
- package/plugin/rules_throw.go +16 -16
- package/plugin/rules_ts.go +161 -161
- package/plugin/rules_ts_extra.go +467 -467
- package/plugin/rules_var.go +86 -86
- package/src/index.ts +1 -4
package/plugin/config.go
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
package main
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
// Severity is the `error | warning | off` ladder.
|
|
13
13
|
type Severity int
|
|
14
14
|
|
|
15
15
|
const (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
SeverityOff Severity = iota
|
|
17
|
+
SeverityWarn
|
|
18
|
+
SeverityError
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
func (s Severity) String() string {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
switch s {
|
|
23
|
+
case SeverityError:
|
|
24
|
+
return "error"
|
|
25
|
+
case SeverityWarn:
|
|
26
|
+
return "warning"
|
|
27
|
+
case SeverityOff:
|
|
28
|
+
return "off"
|
|
29
|
+
}
|
|
30
|
+
return "unknown"
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// PluginEntry mirrors the shape ttsc serializes into `--plugins-json`.
|
|
@@ -35,36 +35,36 @@ func (s Severity) String() string {
|
|
|
35
35
|
// `Config` carries the original tsconfig plugin entry. `Name` and `Stage`
|
|
36
36
|
// come from the JS plugin descriptor returned to the ttsc host.
|
|
37
37
|
type PluginEntry struct {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
Config map[string]any `json:"config"`
|
|
39
|
+
Name string `json:"name"`
|
|
40
|
+
Stage string `json:"stage"`
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// ParsePlugins decodes the `--plugins-json` payload.
|
|
44
44
|
func ParsePlugins(text string) ([]PluginEntry, error) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
if strings.TrimSpace(text) == "" {
|
|
46
|
+
return nil, nil
|
|
47
|
+
}
|
|
48
|
+
var entries []PluginEntry
|
|
49
|
+
if err := json.Unmarshal([]byte(text), &entries); err != nil {
|
|
50
|
+
return nil, fmt.Errorf("@ttsc/lint: invalid --plugins-json: %w", err)
|
|
51
|
+
}
|
|
52
|
+
return entries, nil
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// FindLintEntry returns the lint entry only when it is the first active
|
|
56
56
|
// plugin. Linting after a source-transforming plugin would inspect mutated
|
|
57
57
|
// source, which is not a meaningful user-code lint result.
|
|
58
58
|
func FindLintEntry(entries []PluginEntry) (*PluginEntry, error) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
for i := range entries {
|
|
60
|
+
if entries[i].Name == "@ttsc/lint" {
|
|
61
|
+
if i != 0 {
|
|
62
|
+
return nil, fmt.Errorf("@ttsc/lint must be the first active compilerOptions.plugins entry")
|
|
63
|
+
}
|
|
64
|
+
return &entries[i], nil
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return nil, nil
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// RuleConfig captures the resolved per-rule severity. The map is keyed by
|
|
@@ -81,22 +81,22 @@ type RuleConfig map[string]Severity
|
|
|
81
81
|
// Anything else returns an error (no silent fallback — typos in a rule
|
|
82
82
|
// severity should be loud).
|
|
83
83
|
func ParseRules(raw any) (RuleConfig, error) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
if raw == nil {
|
|
85
|
+
return RuleConfig{}, nil
|
|
86
|
+
}
|
|
87
|
+
dict, ok := raw.(map[string]any)
|
|
88
|
+
if !ok {
|
|
89
|
+
return nil, fmt.Errorf("@ttsc/lint: \"config\" must be an object, got %T", raw)
|
|
90
|
+
}
|
|
91
|
+
out := make(RuleConfig, len(dict))
|
|
92
|
+
for name, value := range dict {
|
|
93
|
+
sev, err := parseSeverity(value)
|
|
94
|
+
if err != nil {
|
|
95
|
+
return nil, fmt.Errorf("@ttsc/lint: rule %q: %w", name, err)
|
|
96
|
+
}
|
|
97
|
+
out[name] = sev
|
|
98
|
+
}
|
|
99
|
+
return out, nil
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// LoadRuleConfig resolves the lint config for one plugin entry. The only
|
|
@@ -104,86 +104,86 @@ func ParseRules(raw any) (RuleConfig, error) {
|
|
|
104
104
|
// rules object or a string path to a standalone config file. Relative config
|
|
105
105
|
// paths are resolved from the tsconfig directory.
|
|
106
106
|
func LoadRuleConfig(entry *PluginEntry, cwd, tsconfigPath string) (RuleConfig, error) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
107
|
+
if entry == nil {
|
|
108
|
+
return RuleConfig{}, nil
|
|
109
|
+
}
|
|
110
|
+
inline := entry.Config
|
|
111
|
+
if inline == nil {
|
|
112
|
+
inline = map[string]any{}
|
|
113
|
+
}
|
|
114
|
+
for _, key := range []string{"rules", "configFile", "configPath"} {
|
|
115
|
+
if _, ok := inline[key]; ok {
|
|
116
|
+
return nil, fmt.Errorf("@ttsc/lint: %q is not supported; use \"config\"", key)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
value, ok := inline["config"]
|
|
121
|
+
if !ok {
|
|
122
|
+
return RuleConfig{}, nil
|
|
123
|
+
}
|
|
124
|
+
switch typed := value.(type) {
|
|
125
|
+
case string:
|
|
126
|
+
if strings.TrimSpace(typed) == "" {
|
|
127
|
+
return nil, fmt.Errorf("@ttsc/lint: \"config\" must not be empty")
|
|
128
|
+
}
|
|
129
|
+
rules, err := loadConfigFile(resolveConfigFilePath(typed, cwd, tsconfigPath))
|
|
130
|
+
if err != nil {
|
|
131
|
+
return nil, err
|
|
132
|
+
}
|
|
133
|
+
return ParseRules(rules)
|
|
134
|
+
case map[string]any:
|
|
135
|
+
return ParseRules(typed)
|
|
136
|
+
default:
|
|
137
|
+
return nil, fmt.Errorf("@ttsc/lint: \"config\" must be a string path or object, got %T", value)
|
|
138
|
+
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
func resolveConfigFilePath(configPath, cwd, tsconfigPath string) string {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
142
|
+
if filepath.IsAbs(configPath) {
|
|
143
|
+
return configPath
|
|
144
|
+
}
|
|
145
|
+
base := cwd
|
|
146
|
+
if tsconfigPath != "" {
|
|
147
|
+
resolvedTsconfig := tsconfigPath
|
|
148
|
+
if !filepath.IsAbs(resolvedTsconfig) {
|
|
149
|
+
resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
|
|
150
|
+
}
|
|
151
|
+
base = filepath.Dir(resolvedTsconfig)
|
|
152
|
+
}
|
|
153
|
+
return filepath.Join(base, configPath)
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
func loadConfigFile(location string) (map[string]any, error) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
157
|
+
ext := strings.ToLower(filepath.Ext(location))
|
|
158
|
+
switch ext {
|
|
159
|
+
case ".json":
|
|
160
|
+
return loadJSONConfigFile(location)
|
|
161
|
+
case ".js", ".cjs", ".mjs":
|
|
162
|
+
return loadScriptConfigFile(location)
|
|
163
|
+
case ".ts", ".cts", ".mts":
|
|
164
|
+
return loadTypeScriptConfigFile(location)
|
|
165
|
+
default:
|
|
166
|
+
return nil, fmt.Errorf("@ttsc/lint: unsupported config file extension %q for %s", ext, location)
|
|
167
|
+
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
func loadJSONConfigFile(location string) (map[string]any, error) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
171
|
+
body, err := os.ReadFile(location)
|
|
172
|
+
if err != nil {
|
|
173
|
+
return nil, fmt.Errorf("@ttsc/lint: read config file %s: %w", location, err)
|
|
174
|
+
}
|
|
175
|
+
var out map[string]any
|
|
176
|
+
if err := json.Unmarshal(body, &out); err != nil {
|
|
177
|
+
return nil, fmt.Errorf("@ttsc/lint: parse config file %s: %w", location, err)
|
|
178
|
+
}
|
|
179
|
+
if out == nil {
|
|
180
|
+
return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
|
|
181
|
+
}
|
|
182
|
+
return out, nil
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
func loadScriptConfigFile(location string) (map[string]any, error) {
|
|
186
|
-
|
|
186
|
+
const script = `
|
|
187
187
|
const { pathToFileURL } = require("node:url");
|
|
188
188
|
|
|
189
189
|
(async () => {
|
|
@@ -199,107 +199,107 @@ const { pathToFileURL } = require("node:url");
|
|
|
199
199
|
process.exit(1);
|
|
200
200
|
});
|
|
201
201
|
`
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
202
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
203
|
+
if node == "" {
|
|
204
|
+
node = "node"
|
|
205
|
+
}
|
|
206
|
+
cmd := exec.Command(node, "-e", script, location)
|
|
207
|
+
output, err := cmd.Output()
|
|
208
|
+
if err != nil {
|
|
209
|
+
stderr := ""
|
|
210
|
+
if exit, ok := err.(*exec.ExitError); ok {
|
|
211
|
+
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
212
|
+
}
|
|
213
|
+
if stderr != "" {
|
|
214
|
+
return nil, fmt.Errorf("@ttsc/lint: load config file %s: %s", location, stderr)
|
|
215
|
+
}
|
|
216
|
+
return nil, fmt.Errorf("@ttsc/lint: load config file %s: %w", location, err)
|
|
217
|
+
}
|
|
218
|
+
var out map[string]any
|
|
219
|
+
if err := json.Unmarshal(output, &out); err != nil {
|
|
220
|
+
return nil, fmt.Errorf("@ttsc/lint: parse config file %s output: %w", location, err)
|
|
221
|
+
}
|
|
222
|
+
if out == nil {
|
|
223
|
+
return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
|
|
224
|
+
}
|
|
225
|
+
return out, nil
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
func loadTypeScriptConfigFile(location string) (map[string]any, error) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
229
|
+
tempDir, err := os.MkdirTemp("", "ttsc-lint-config-")
|
|
230
|
+
if err != nil {
|
|
231
|
+
return nil, fmt.Errorf("@ttsc/lint: create config loader tempdir: %w", err)
|
|
232
|
+
}
|
|
233
|
+
defer os.RemoveAll(tempDir)
|
|
234
|
+
|
|
235
|
+
if err := linkNearestNodeModules(tempDir, filepath.Dir(location)); err != nil {
|
|
236
|
+
return nil, err
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
loader := filepath.Join(tempDir, "loader.mts")
|
|
240
|
+
tsconfig := filepath.Join(tempDir, "tsconfig.json")
|
|
241
|
+
importSpecifier, err := relativeImportSpecifier(tempDir, location)
|
|
242
|
+
if err != nil {
|
|
243
|
+
return nil, err
|
|
244
|
+
}
|
|
245
|
+
importLiteral, err := json.Marshal(importSpecifier)
|
|
246
|
+
if err != nil {
|
|
247
|
+
return nil, fmt.Errorf("@ttsc/lint: encode config import %s: %w", location, err)
|
|
248
|
+
}
|
|
249
|
+
if err := os.WriteFile(loader, []byte(typeScriptConfigLoaderSource(string(importLiteral))), 0o644); err != nil {
|
|
250
|
+
return nil, fmt.Errorf("@ttsc/lint: write config loader: %w", err)
|
|
251
|
+
}
|
|
252
|
+
if err := os.WriteFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
|
|
253
|
+
return nil, fmt.Errorf("@ttsc/lint: write config loader tsconfig: %w", err)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
args := []string{
|
|
257
|
+
"--project", tsconfig,
|
|
258
|
+
"--cwd", tempDir,
|
|
259
|
+
"--cache-dir", filepath.Join(tempDir, "cache"),
|
|
260
|
+
}
|
|
261
|
+
if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
|
|
262
|
+
args = append(args, "--binary", tsgo)
|
|
263
|
+
}
|
|
264
|
+
args = append(args, loader)
|
|
265
|
+
|
|
266
|
+
cmd := ttsxCommand(args...)
|
|
267
|
+
cmd.Env = nodeConfigLoaderEnv(location)
|
|
268
|
+
output, err := cmd.Output()
|
|
269
|
+
if err != nil {
|
|
270
|
+
stderr := ""
|
|
271
|
+
if exit, ok := err.(*exec.ExitError); ok {
|
|
272
|
+
stderr = strings.TrimSpace(string(exit.Stderr))
|
|
273
|
+
}
|
|
274
|
+
if stderr != "" {
|
|
275
|
+
return nil, fmt.Errorf("@ttsc/lint: load TypeScript config file %s: %s", location, stderr)
|
|
276
|
+
}
|
|
277
|
+
return nil, fmt.Errorf("@ttsc/lint: load TypeScript config file %s: %w", location, err)
|
|
278
|
+
}
|
|
279
|
+
var out map[string]any
|
|
280
|
+
if err := json.Unmarshal(output, &out); err != nil {
|
|
281
|
+
return nil, fmt.Errorf("@ttsc/lint: parse TypeScript config file %s output: %w", location, err)
|
|
282
|
+
}
|
|
283
|
+
if out == nil {
|
|
284
|
+
return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
|
|
285
|
+
}
|
|
286
|
+
return out, nil
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
290
|
+
relative, err := filepath.Rel(fromDir, location)
|
|
291
|
+
if err != nil {
|
|
292
|
+
return "", fmt.Errorf("@ttsc/lint: resolve relative config import %s: %w", location, err)
|
|
293
|
+
}
|
|
294
|
+
relative = filepath.ToSlash(relative)
|
|
295
|
+
if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
|
|
296
|
+
return relative, nil
|
|
297
|
+
}
|
|
298
|
+
return "./" + relative, nil
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
func typeScriptConfigLoaderSource(importLiteral string) string {
|
|
302
|
-
|
|
302
|
+
return fmt.Sprintf(`import * as importedConfig from %s;
|
|
303
303
|
|
|
304
304
|
declare const process: {
|
|
305
305
|
stdout: { write(value: string): void };
|
|
@@ -350,138 +350,138 @@ function hasOwn(value: Record<string, unknown>, key: string): boolean {
|
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
353
|
+
content := map[string]any{
|
|
354
|
+
"compilerOptions": map[string]any{
|
|
355
|
+
"allowImportingTsExtensions": true,
|
|
356
|
+
"module": "ESNext",
|
|
357
|
+
"moduleResolution": "bundler",
|
|
358
|
+
"outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
|
|
359
|
+
"rewriteRelativeImportExtensions": true,
|
|
360
|
+
"rootDir": "/",
|
|
361
|
+
"skipLibCheck": true,
|
|
362
|
+
"strict": true,
|
|
363
|
+
"target": "ES2022",
|
|
364
|
+
},
|
|
365
|
+
"files": []string{
|
|
366
|
+
filepath.ToSlash(loader),
|
|
367
|
+
filepath.ToSlash(location),
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
body, err := json.MarshalIndent(content, "", " ")
|
|
371
|
+
if err != nil {
|
|
372
|
+
panic(err)
|
|
373
|
+
}
|
|
374
|
+
return string(body)
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
func ttsxCommand(args ...string) *exec.Cmd {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
378
|
+
ttsx := os.Getenv("TTSC_TTSX_BINARY")
|
|
379
|
+
if ttsx == "" {
|
|
380
|
+
ttsx = "ttsx"
|
|
381
|
+
}
|
|
382
|
+
if shouldRunTtsxThroughNode(ttsx) {
|
|
383
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
384
|
+
if node == "" {
|
|
385
|
+
node = "node"
|
|
386
|
+
}
|
|
387
|
+
return exec.Command(node, append([]string{ttsx}, args...)...)
|
|
388
|
+
}
|
|
389
|
+
return exec.Command(ttsx, args...)
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
func shouldRunTtsxThroughNode(binary string) bool {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
393
|
+
switch strings.ToLower(filepath.Ext(binary)) {
|
|
394
|
+
case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
|
|
395
|
+
return true
|
|
396
|
+
default:
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
func nodeConfigLoaderEnv(location string) []string {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
402
|
+
env := os.Environ()
|
|
403
|
+
parts := make([]string, 0, 2)
|
|
404
|
+
if nodeModules := findNearestNodeModules(filepath.Dir(location)); nodeModules != "" {
|
|
405
|
+
parts = append(parts, nodeModules)
|
|
406
|
+
}
|
|
407
|
+
if existing := os.Getenv("NODE_PATH"); existing != "" {
|
|
408
|
+
parts = append(parts, existing)
|
|
409
|
+
}
|
|
410
|
+
if len(parts) == 0 {
|
|
411
|
+
return env
|
|
412
|
+
}
|
|
413
|
+
return setEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
func linkNearestNodeModules(tempDir, sourceDir string) error {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
417
|
+
nodeModules := findNearestNodeModules(sourceDir)
|
|
418
|
+
if nodeModules == "" {
|
|
419
|
+
return nil
|
|
420
|
+
}
|
|
421
|
+
link := filepath.Join(tempDir, "node_modules")
|
|
422
|
+
if err := os.Symlink(nodeModules, link); err != nil {
|
|
423
|
+
return fmt.Errorf("@ttsc/lint: link config node_modules %s: %w", nodeModules, err)
|
|
424
|
+
}
|
|
425
|
+
return nil
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
func findNearestNodeModules(start string) string {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
429
|
+
dir := filepath.Clean(start)
|
|
430
|
+
for {
|
|
431
|
+
candidate := filepath.Join(dir, "node_modules")
|
|
432
|
+
if stat, err := os.Stat(candidate); err == nil && stat.IsDir() {
|
|
433
|
+
return candidate
|
|
434
|
+
}
|
|
435
|
+
parent := filepath.Dir(dir)
|
|
436
|
+
if parent == dir {
|
|
437
|
+
return ""
|
|
438
|
+
}
|
|
439
|
+
dir = parent
|
|
440
|
+
}
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
func setEnv(env []string, key, value string) []string {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
444
|
+
prefix := key + "="
|
|
445
|
+
for i, entry := range env {
|
|
446
|
+
if strings.HasPrefix(entry, prefix) {
|
|
447
|
+
env[i] = prefix + value
|
|
448
|
+
return env
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return append(env, prefix+value)
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
func parseSeverity(v any) (Severity, error) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
455
|
+
switch x := v.(type) {
|
|
456
|
+
case string:
|
|
457
|
+
switch x {
|
|
458
|
+
case "off":
|
|
459
|
+
return SeverityOff, nil
|
|
460
|
+
case "warning", "warn":
|
|
461
|
+
return SeverityWarn, nil
|
|
462
|
+
case "error":
|
|
463
|
+
return SeverityError, nil
|
|
464
|
+
}
|
|
465
|
+
return SeverityOff, fmt.Errorf("unknown severity %q (want off | warning | error)", x)
|
|
466
|
+
case float64:
|
|
467
|
+
switch x {
|
|
468
|
+
case 0:
|
|
469
|
+
return SeverityOff, nil
|
|
470
|
+
case 1:
|
|
471
|
+
return SeverityWarn, nil
|
|
472
|
+
case 2:
|
|
473
|
+
return SeverityError, nil
|
|
474
|
+
}
|
|
475
|
+
return SeverityOff, fmt.Errorf("unknown severity %v (want off | warning | error)", x)
|
|
476
|
+
}
|
|
477
|
+
return SeverityOff, fmt.Errorf("severity must be one of: off | warning | error, got %T", v)
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
// Severity returns the configured level for a rule, defaulting to
|
|
481
481
|
// `SeverityOff`. Rules opt in explicitly — silent on missing entries.
|
|
482
482
|
func (c RuleConfig) Severity(name string) Severity {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
483
|
+
if c == nil {
|
|
484
|
+
return SeverityOff
|
|
485
|
+
}
|
|
486
|
+
return c[name]
|
|
487
487
|
}
|