@ttsc/lint 0.8.0-dev.20260505 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -18
- package/lib/config.d.ts +3 -0
- package/lib/config.js +3 -0
- package/lib/config.js.map +1 -0
- package/lib/structures/ITtscLintPluginConfig.d.ts +3 -2
- package/lib/structures/TtscLintConfig.d.ts +2 -3
- package/package.json +6 -2
- package/plugin/compile.go +27 -10
- package/plugin/config.go +760 -29
- package/plugin/directives.go +296 -0
- package/plugin/engine.go +29 -9
- package/plugin/eslint_runtime.go +246 -0
- package/src/config.ts +3 -0
- package/src/structures/ITtscLintPluginConfig.ts +3 -2
- package/src/structures/TtscLintConfig.ts +2 -3
package/plugin/config.go
CHANGED
|
@@ -6,6 +6,7 @@ import (
|
|
|
6
6
|
"os"
|
|
7
7
|
"os/exec"
|
|
8
8
|
"path/filepath"
|
|
9
|
+
"sort"
|
|
9
10
|
"strings"
|
|
10
11
|
)
|
|
11
12
|
|
|
@@ -71,7 +72,170 @@ func FindLintEntry(entries []PluginEntry) (*PluginEntry, error) {
|
|
|
71
72
|
// rule name (e.g. "no-var").
|
|
72
73
|
type RuleConfig map[string]Severity
|
|
73
74
|
|
|
74
|
-
//
|
|
75
|
+
// ResolvedRuleConfig is the rule map that applies to one source file.
|
|
76
|
+
// `Ignored` means an external ESLint-style ignore-only config matched the
|
|
77
|
+
// file and the engine should skip linting it entirely.
|
|
78
|
+
type ResolvedRuleConfig struct {
|
|
79
|
+
Rules RuleConfig
|
|
80
|
+
Ignored bool
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type RuleResolver interface {
|
|
84
|
+
ResolveRules(fileName string) ResolvedRuleConfig
|
|
85
|
+
ActiveRuleNames() []string
|
|
86
|
+
EnabledRuleConfig() RuleConfig
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func (c RuleConfig) ResolveRules(string) ResolvedRuleConfig {
|
|
90
|
+
return ResolvedRuleConfig{Rules: c}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func (c RuleConfig) ActiveRuleNames() []string {
|
|
94
|
+
return sortedRuleNames(c, func(sev Severity) bool { return sev != SeverityOff })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func (c RuleConfig) EnabledRuleConfig() RuleConfig {
|
|
98
|
+
out := RuleConfig{}
|
|
99
|
+
for name, sev := range c {
|
|
100
|
+
if sev != SeverityOff {
|
|
101
|
+
out[name] = sev
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type ConfigStore struct {
|
|
108
|
+
entries []ConfigEntry
|
|
109
|
+
externalConfigPath string
|
|
110
|
+
eslintRuntime bool
|
|
111
|
+
eslintRuntimeRequired bool
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type ConfigEntry struct {
|
|
115
|
+
BaseDir string
|
|
116
|
+
Files []string
|
|
117
|
+
Ignores []string
|
|
118
|
+
Rules RuleConfig
|
|
119
|
+
IgnoreOnly bool
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func (s *ConfigStore) ResolveRules(fileName string) ResolvedRuleConfig {
|
|
123
|
+
if s == nil {
|
|
124
|
+
return ResolvedRuleConfig{Rules: RuleConfig{}}
|
|
125
|
+
}
|
|
126
|
+
for _, entry := range s.entries {
|
|
127
|
+
if entry.IgnoreOnly && entry.matchesIgnores(fileName) {
|
|
128
|
+
return ResolvedRuleConfig{Rules: RuleConfig{}, Ignored: true}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
out := RuleConfig{}
|
|
132
|
+
for _, entry := range s.entries {
|
|
133
|
+
if entry.IgnoreOnly || !entry.matchesFile(fileName) {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
for name, sev := range entry.Rules {
|
|
137
|
+
out[name] = sev
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return ResolvedRuleConfig{Rules: out}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func (s *ConfigStore) ActiveRuleNames() []string {
|
|
144
|
+
if s == nil {
|
|
145
|
+
return nil
|
|
146
|
+
}
|
|
147
|
+
active := RuleConfig{}
|
|
148
|
+
for _, entry := range s.entries {
|
|
149
|
+
if entry.IgnoreOnly {
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
for name, sev := range entry.Rules {
|
|
153
|
+
if sev != SeverityOff {
|
|
154
|
+
active[name] = sev
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return sortedRuleNames(active, func(Severity) bool { return true })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func (s *ConfigStore) EnabledRuleConfig() RuleConfig {
|
|
162
|
+
out := RuleConfig{}
|
|
163
|
+
if s == nil {
|
|
164
|
+
return out
|
|
165
|
+
}
|
|
166
|
+
for _, entry := range s.entries {
|
|
167
|
+
if entry.IgnoreOnly {
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
for name, sev := range entry.Rules {
|
|
171
|
+
if sev == SeverityOff {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if out[name] != SeverityError {
|
|
175
|
+
out[name] = sev
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func (s *ConfigStore) Flatten() RuleConfig {
|
|
183
|
+
out := RuleConfig{}
|
|
184
|
+
if s == nil {
|
|
185
|
+
return out
|
|
186
|
+
}
|
|
187
|
+
for _, entry := range s.entries {
|
|
188
|
+
if entry.IgnoreOnly {
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
for name, sev := range entry.Rules {
|
|
192
|
+
out[name] = sev
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func (s *ConfigStore) ExternalConfigPath() string {
|
|
199
|
+
if s == nil {
|
|
200
|
+
return ""
|
|
201
|
+
}
|
|
202
|
+
return s.externalConfigPath
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func (s *ConfigStore) WantsESLintRuntime() bool {
|
|
206
|
+
if s == nil {
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
if s.eslintRuntime {
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
base := filepath.Base(s.externalConfigPath)
|
|
213
|
+
return strings.HasPrefix(base, "eslint.config.")
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
func (s *ConfigStore) RequiresESLintRuntime() bool {
|
|
217
|
+
if s == nil {
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
return s.eslintRuntimeRequired
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func (e ConfigEntry) matchesFile(fileName string) bool {
|
|
224
|
+
if len(e.Files) > 0 && !matchAnyPattern(e.BaseDir, e.Files, fileName) {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
if e.matchesIgnores(fileName) {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
func (e ConfigEntry) matchesIgnores(fileName string) bool {
|
|
234
|
+
return len(e.Ignores) > 0 && matchAnyPattern(e.BaseDir, e.Ignores, fileName)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ParseRules normalizes the standard native rules map from a tsconfig plugin
|
|
238
|
+
// entry.
|
|
75
239
|
//
|
|
76
240
|
// Severity values:
|
|
77
241
|
// - `"off"` → SeverityOff
|
|
@@ -99,11 +263,257 @@ func ParseRules(raw any) (RuleConfig, error) {
|
|
|
99
263
|
return out, nil
|
|
100
264
|
}
|
|
101
265
|
|
|
266
|
+
func parseExternalConfigRules(raw any) (RuleConfig, error) {
|
|
267
|
+
store, err := parseExternalConfigStore(raw, "")
|
|
268
|
+
if err != nil {
|
|
269
|
+
return nil, err
|
|
270
|
+
}
|
|
271
|
+
return store.Flatten(), nil
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func parseExternalConfigStore(raw any, configDir string) (*ConfigStore, error) {
|
|
275
|
+
return parseExternalConfigStoreWithRuntimeMode(raw, configDir, false)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
func parseExternalConfigStoreForFile(raw any, configDir string) (*ConfigStore, error) {
|
|
279
|
+
return parseExternalConfigStoreWithRuntimeMode(raw, configDir, true)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
func parseExternalConfigStoreWithRuntimeMode(raw any, configDir string, allowRuntimeOnly bool) (*ConfigStore, error) {
|
|
283
|
+
store := &ConfigStore{}
|
|
284
|
+
if err := collectExternalConfigEntries(store, raw, configDir, "config", allowRuntimeOnly); err != nil {
|
|
285
|
+
return nil, err
|
|
286
|
+
}
|
|
287
|
+
return store, nil
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
func collectExternalConfigEntries(store *ConfigStore, raw any, baseDir, path string, allowRuntimeOnly bool) error {
|
|
291
|
+
if raw == nil {
|
|
292
|
+
return nil
|
|
293
|
+
}
|
|
294
|
+
switch typed := raw.(type) {
|
|
295
|
+
case []any:
|
|
296
|
+
for i, item := range typed {
|
|
297
|
+
if err := collectExternalConfigEntries(store, item, baseDir, fmt.Sprintf("%s[%d]", path, i), allowRuntimeOnly); err != nil {
|
|
298
|
+
return err
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return nil
|
|
302
|
+
case map[string]any:
|
|
303
|
+
if isESLintConfigObject(typed) {
|
|
304
|
+
if marker, ok := typed["__ttscLintEslintRuntime"].(bool); ok && marker {
|
|
305
|
+
store.eslintRuntime = true
|
|
306
|
+
store.eslintRuntimeRequired = true
|
|
307
|
+
}
|
|
308
|
+
localBaseDir := baseDir
|
|
309
|
+
if rawBasePath, ok := typed["basePath"]; ok {
|
|
310
|
+
basePath, ok := rawBasePath.(string)
|
|
311
|
+
if !ok {
|
|
312
|
+
return fmt.Errorf("@ttsc/lint: %s.basePath must be a string, got %T", path, rawBasePath)
|
|
313
|
+
}
|
|
314
|
+
if filepath.IsAbs(basePath) {
|
|
315
|
+
localBaseDir = basePath
|
|
316
|
+
} else {
|
|
317
|
+
localBaseDir = filepath.Join(baseDir, basePath)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if hasESLintRuntimeFields(typed) {
|
|
321
|
+
store.eslintRuntime = true
|
|
322
|
+
store.eslintRuntimeRequired = true
|
|
323
|
+
}
|
|
324
|
+
if extended, ok := typed["extends"]; ok {
|
|
325
|
+
if err := collectExternalExtends(store, extended, localBaseDir, path+".extends", allowRuntimeOnly); err != nil {
|
|
326
|
+
return err
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
files, err := parsePatternList(typed["files"], path+".files")
|
|
330
|
+
if err != nil {
|
|
331
|
+
return err
|
|
332
|
+
}
|
|
333
|
+
ignores, err := parsePatternList(typed["ignores"], path+".ignores")
|
|
334
|
+
if err != nil {
|
|
335
|
+
return err
|
|
336
|
+
}
|
|
337
|
+
if rules, ok := typed["rules"]; ok {
|
|
338
|
+
parsed, err := parseExternalRuleMap(rules, path+".rules")
|
|
339
|
+
if err != nil {
|
|
340
|
+
return err
|
|
341
|
+
}
|
|
342
|
+
store.entries = append(store.entries, ConfigEntry{
|
|
343
|
+
BaseDir: localBaseDir,
|
|
344
|
+
Files: files,
|
|
345
|
+
Ignores: ignores,
|
|
346
|
+
Rules: parsed,
|
|
347
|
+
})
|
|
348
|
+
return nil
|
|
349
|
+
}
|
|
350
|
+
if len(files) == 0 && len(ignores) > 0 {
|
|
351
|
+
store.entries = append(store.entries, ConfigEntry{
|
|
352
|
+
BaseDir: localBaseDir,
|
|
353
|
+
Ignores: ignores,
|
|
354
|
+
IgnoreOnly: true,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
return nil
|
|
358
|
+
}
|
|
359
|
+
parsed, err := parseExternalRuleMap(typed, path)
|
|
360
|
+
if err != nil {
|
|
361
|
+
return err
|
|
362
|
+
}
|
|
363
|
+
store.entries = append(store.entries, ConfigEntry{
|
|
364
|
+
BaseDir: baseDir,
|
|
365
|
+
Rules: parsed,
|
|
366
|
+
})
|
|
367
|
+
return nil
|
|
368
|
+
default:
|
|
369
|
+
return fmt.Errorf("@ttsc/lint: %s must be an object or flat config array, got %T", path, raw)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
func collectExternalExtends(store *ConfigStore, raw any, baseDir, path string, allowRuntimeOnly bool) error {
|
|
374
|
+
switch typed := raw.(type) {
|
|
375
|
+
case string:
|
|
376
|
+
if allowRuntimeOnly {
|
|
377
|
+
store.eslintRuntime = true
|
|
378
|
+
store.eslintRuntimeRequired = true
|
|
379
|
+
return nil
|
|
380
|
+
}
|
|
381
|
+
return fmt.Errorf("@ttsc/lint: %s must be an object or flat config array, got %T", path, raw)
|
|
382
|
+
case []any:
|
|
383
|
+
for i, item := range typed {
|
|
384
|
+
itemPath := fmt.Sprintf("%s[%d]", path, i)
|
|
385
|
+
if _, ok := item.(string); ok && allowRuntimeOnly {
|
|
386
|
+
store.eslintRuntime = true
|
|
387
|
+
store.eslintRuntimeRequired = true
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
if err := collectExternalConfigEntries(store, item, baseDir, itemPath, allowRuntimeOnly); err != nil {
|
|
391
|
+
return err
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return nil
|
|
395
|
+
default:
|
|
396
|
+
return collectExternalConfigEntries(store, raw, baseDir, path, allowRuntimeOnly)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
func parseExternalRuleMap(raw any, path string) (RuleConfig, error) {
|
|
401
|
+
out := RuleConfig{}
|
|
402
|
+
if err := collectExternalRuleMap(out, raw, path); err != nil {
|
|
403
|
+
return nil, err
|
|
404
|
+
}
|
|
405
|
+
return out, nil
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func collectExternalRuleMap(out RuleConfig, raw any, path string) error {
|
|
409
|
+
dict, ok := raw.(map[string]any)
|
|
410
|
+
if !ok {
|
|
411
|
+
return fmt.Errorf("@ttsc/lint: %s must be a rules object, got %T", path, raw)
|
|
412
|
+
}
|
|
413
|
+
for name, value := range dict {
|
|
414
|
+
sev, err := parseExternalSeverity(value)
|
|
415
|
+
if err != nil {
|
|
416
|
+
return fmt.Errorf("@ttsc/lint: rule %q: %w", name, err)
|
|
417
|
+
}
|
|
418
|
+
out[normalizeExternalRuleName(name)] = sev
|
|
419
|
+
}
|
|
420
|
+
return nil
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
func isESLintConfigObject(value map[string]any) bool {
|
|
424
|
+
for _, key := range []string{
|
|
425
|
+
"basePath",
|
|
426
|
+
"extends",
|
|
427
|
+
"files",
|
|
428
|
+
"ignores",
|
|
429
|
+
"languageOptions",
|
|
430
|
+
"linterOptions",
|
|
431
|
+
"name",
|
|
432
|
+
"plugins",
|
|
433
|
+
"processor",
|
|
434
|
+
"rules",
|
|
435
|
+
"settings",
|
|
436
|
+
"__ttscLintEslintRuntime",
|
|
437
|
+
} {
|
|
438
|
+
if _, ok := value[key]; ok {
|
|
439
|
+
return true
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return false
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func hasESLintRuntimeFields(value map[string]any) bool {
|
|
446
|
+
for _, key := range []string{
|
|
447
|
+
"languageOptions",
|
|
448
|
+
"linterOptions",
|
|
449
|
+
"plugins",
|
|
450
|
+
"processor",
|
|
451
|
+
"settings",
|
|
452
|
+
} {
|
|
453
|
+
if _, ok := value[key]; ok {
|
|
454
|
+
return true
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return false
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
func normalizeExternalRuleName(name string) string {
|
|
461
|
+
name = strings.TrimPrefix(name, "@typescript-eslint/")
|
|
462
|
+
return strings.TrimPrefix(name, "typescript-eslint/")
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
func parsePatternList(raw any, path string) ([]string, error) {
|
|
466
|
+
if raw == nil {
|
|
467
|
+
return nil, nil
|
|
468
|
+
}
|
|
469
|
+
switch typed := raw.(type) {
|
|
470
|
+
case string:
|
|
471
|
+
if strings.TrimSpace(typed) == "" {
|
|
472
|
+
return nil, fmt.Errorf("@ttsc/lint: %s must not contain an empty pattern", path)
|
|
473
|
+
}
|
|
474
|
+
return []string{typed}, nil
|
|
475
|
+
case []any:
|
|
476
|
+
out := make([]string, 0, len(typed))
|
|
477
|
+
for i, item := range typed {
|
|
478
|
+
pattern, ok := item.(string)
|
|
479
|
+
if !ok {
|
|
480
|
+
return nil, fmt.Errorf("@ttsc/lint: %s[%d] must be a string, got %T", path, i, item)
|
|
481
|
+
}
|
|
482
|
+
if strings.TrimSpace(pattern) == "" {
|
|
483
|
+
return nil, fmt.Errorf("@ttsc/lint: %s[%d] must not be empty", path, i)
|
|
484
|
+
}
|
|
485
|
+
out = append(out, pattern)
|
|
486
|
+
}
|
|
487
|
+
return out, nil
|
|
488
|
+
default:
|
|
489
|
+
return nil, fmt.Errorf("@ttsc/lint: %s must be a string or string array, got %T", path, raw)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
102
493
|
// LoadRuleConfig resolves the lint config for one plugin entry. The only
|
|
103
494
|
// accepted lint-specific tsconfig key is `config`; it may be either an inline
|
|
104
495
|
// rules object or a string path to a standalone config file. Relative config
|
|
105
496
|
// paths are resolved from the tsconfig directory.
|
|
106
497
|
func LoadRuleConfig(entry *PluginEntry, cwd, tsconfigPath string) (RuleConfig, error) {
|
|
498
|
+
resolver, err := LoadConfigResolver(entry, cwd, tsconfigPath)
|
|
499
|
+
if err != nil {
|
|
500
|
+
return nil, err
|
|
501
|
+
}
|
|
502
|
+
switch typed := resolver.(type) {
|
|
503
|
+
case RuleConfig:
|
|
504
|
+
return typed, nil
|
|
505
|
+
case *ConfigStore:
|
|
506
|
+
return typed.Flatten(), nil
|
|
507
|
+
default:
|
|
508
|
+
return resolver.EnabledRuleConfig(), nil
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// LoadConfigResolver resolves one plugin entry into the engine-facing config
|
|
513
|
+
// model. Inline `config` remains the native flat rule map. External config
|
|
514
|
+
// files may carry ESLint flat-config-style file globs, ignores, object
|
|
515
|
+
// extends, and rule severity tuples.
|
|
516
|
+
func LoadConfigResolver(entry *PluginEntry, cwd, tsconfigPath string) (RuleResolver, error) {
|
|
107
517
|
if entry == nil {
|
|
108
518
|
return RuleConfig{}, nil
|
|
109
519
|
}
|
|
@@ -119,18 +529,22 @@ func LoadRuleConfig(entry *PluginEntry, cwd, tsconfigPath string) (RuleConfig, e
|
|
|
119
529
|
|
|
120
530
|
value, ok := inline["config"]
|
|
121
531
|
if !ok {
|
|
122
|
-
|
|
532
|
+
discovered, err := findESLintConfigFile(cwd, tsconfigPath)
|
|
533
|
+
if err != nil {
|
|
534
|
+
return nil, err
|
|
535
|
+
}
|
|
536
|
+
if discovered == "" {
|
|
537
|
+
return RuleConfig{}, nil
|
|
538
|
+
}
|
|
539
|
+
return loadExternalConfigResolver(discovered)
|
|
123
540
|
}
|
|
124
541
|
switch typed := value.(type) {
|
|
125
542
|
case string:
|
|
126
543
|
if strings.TrimSpace(typed) == "" {
|
|
127
544
|
return nil, fmt.Errorf("@ttsc/lint: \"config\" must not be empty")
|
|
128
545
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return nil, err
|
|
132
|
-
}
|
|
133
|
-
return ParseRules(rules)
|
|
546
|
+
location := resolveConfigFilePath(typed, cwd, tsconfigPath)
|
|
547
|
+
return loadExternalConfigResolver(location)
|
|
134
548
|
case map[string]any:
|
|
135
549
|
return ParseRules(typed)
|
|
136
550
|
default:
|
|
@@ -138,6 +552,57 @@ func LoadRuleConfig(entry *PluginEntry, cwd, tsconfigPath string) (RuleConfig, e
|
|
|
138
552
|
}
|
|
139
553
|
}
|
|
140
554
|
|
|
555
|
+
func loadExternalConfigResolver(location string) (RuleResolver, error) {
|
|
556
|
+
raw, err := loadConfigFile(location)
|
|
557
|
+
if err != nil {
|
|
558
|
+
return nil, err
|
|
559
|
+
}
|
|
560
|
+
store, err := parseExternalConfigStoreForFile(raw, filepath.Dir(location))
|
|
561
|
+
if err != nil {
|
|
562
|
+
return nil, err
|
|
563
|
+
}
|
|
564
|
+
store.externalConfigPath = location
|
|
565
|
+
return store, nil
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
func findESLintConfigFile(cwd, tsconfigPath string) (string, error) {
|
|
569
|
+
dir := cwd
|
|
570
|
+
if tsconfigPath != "" {
|
|
571
|
+
resolvedTsconfig := tsconfigPath
|
|
572
|
+
if !filepath.IsAbs(resolvedTsconfig) {
|
|
573
|
+
resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
|
|
574
|
+
}
|
|
575
|
+
dir = filepath.Dir(resolvedTsconfig)
|
|
576
|
+
}
|
|
577
|
+
for {
|
|
578
|
+
matches := make([]string, 0, 1)
|
|
579
|
+
for _, name := range []string{
|
|
580
|
+
"eslint.config.js",
|
|
581
|
+
"eslint.config.mjs",
|
|
582
|
+
"eslint.config.cjs",
|
|
583
|
+
"eslint.config.ts",
|
|
584
|
+
"eslint.config.mts",
|
|
585
|
+
"eslint.config.cts",
|
|
586
|
+
} {
|
|
587
|
+
candidate := filepath.Join(dir, name)
|
|
588
|
+
if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
|
|
589
|
+
matches = append(matches, candidate)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if len(matches) > 1 {
|
|
593
|
+
return "", fmt.Errorf("@ttsc/lint: multiple eslint config files found in %s; set \"config\" explicitly", dir)
|
|
594
|
+
}
|
|
595
|
+
if len(matches) == 1 {
|
|
596
|
+
return matches[0], nil
|
|
597
|
+
}
|
|
598
|
+
parent := filepath.Dir(dir)
|
|
599
|
+
if parent == dir {
|
|
600
|
+
return "", nil
|
|
601
|
+
}
|
|
602
|
+
dir = parent
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
141
606
|
func resolveConfigFilePath(configPath, cwd, tsconfigPath string) string {
|
|
142
607
|
if filepath.IsAbs(configPath) {
|
|
143
608
|
return configPath
|
|
@@ -153,7 +618,7 @@ func resolveConfigFilePath(configPath, cwd, tsconfigPath string) string {
|
|
|
153
618
|
return filepath.Join(base, configPath)
|
|
154
619
|
}
|
|
155
620
|
|
|
156
|
-
func loadConfigFile(location string) (
|
|
621
|
+
func loadConfigFile(location string) (any, error) {
|
|
157
622
|
ext := strings.ToLower(filepath.Ext(location))
|
|
158
623
|
switch ext {
|
|
159
624
|
case ".json":
|
|
@@ -167,22 +632,22 @@ func loadConfigFile(location string) (map[string]any, error) {
|
|
|
167
632
|
}
|
|
168
633
|
}
|
|
169
634
|
|
|
170
|
-
func loadJSONConfigFile(location string) (
|
|
635
|
+
func loadJSONConfigFile(location string) (any, error) {
|
|
171
636
|
body, err := os.ReadFile(location)
|
|
172
637
|
if err != nil {
|
|
173
638
|
return nil, fmt.Errorf("@ttsc/lint: read config file %s: %w", location, err)
|
|
174
639
|
}
|
|
175
|
-
var out
|
|
640
|
+
var out any
|
|
176
641
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
177
642
|
return nil, fmt.Errorf("@ttsc/lint: parse config file %s: %w", location, err)
|
|
178
643
|
}
|
|
179
|
-
if out
|
|
180
|
-
return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
|
|
644
|
+
if !isConfigContainer(out) {
|
|
645
|
+
return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object or flat config array", location)
|
|
181
646
|
}
|
|
182
647
|
return out, nil
|
|
183
648
|
}
|
|
184
649
|
|
|
185
|
-
func loadScriptConfigFile(location string) (
|
|
650
|
+
func loadScriptConfigFile(location string) (any, error) {
|
|
186
651
|
const script = `
|
|
187
652
|
const { pathToFileURL } = require("node:url");
|
|
188
653
|
|
|
@@ -190,14 +655,94 @@ const { pathToFileURL } = require("node:url");
|
|
|
190
655
|
const mod = await import(pathToFileURL(process.argv[1]).href);
|
|
191
656
|
const candidate = mod.default ?? mod.config ?? mod;
|
|
192
657
|
const value = typeof candidate === "function" ? await candidate() : candidate;
|
|
193
|
-
if (value === null || typeof value !== "object"
|
|
194
|
-
throw new Error("config file must export an object");
|
|
658
|
+
if (value === null || typeof value !== "object") {
|
|
659
|
+
throw new Error("config file must export an object or flat config array");
|
|
195
660
|
}
|
|
196
|
-
process.stdout.write(JSON.stringify(value));
|
|
661
|
+
process.stdout.write(JSON.stringify(toSerializableConfig(value)));
|
|
197
662
|
})().catch((error) => {
|
|
198
663
|
process.stderr.write(error && error.stack ? error.stack : String(error));
|
|
199
664
|
process.exit(1);
|
|
200
665
|
});
|
|
666
|
+
|
|
667
|
+
function toSerializableConfig(value) {
|
|
668
|
+
if (Array.isArray(value)) {
|
|
669
|
+
return value.map((item) => toSerializableConfig(item));
|
|
670
|
+
}
|
|
671
|
+
if (value === null || typeof value !== "object") {
|
|
672
|
+
return value;
|
|
673
|
+
}
|
|
674
|
+
if (isESLintConfigObject(value)) {
|
|
675
|
+
const out = {};
|
|
676
|
+
if (hasESLintRuntimeFields(value)) {
|
|
677
|
+
out.__ttscLintEslintRuntime = true;
|
|
678
|
+
}
|
|
679
|
+
if (Object.prototype.hasOwnProperty.call(value, "basePath")) {
|
|
680
|
+
out.basePath = value.basePath;
|
|
681
|
+
}
|
|
682
|
+
if (Object.prototype.hasOwnProperty.call(value, "extends")) {
|
|
683
|
+
out.extends = toSerializableConfig(value.extends);
|
|
684
|
+
}
|
|
685
|
+
if (Object.prototype.hasOwnProperty.call(value, "files")) {
|
|
686
|
+
out.files = toSerializablePatterns(value.files, "files");
|
|
687
|
+
}
|
|
688
|
+
if (Object.prototype.hasOwnProperty.call(value, "ignores")) {
|
|
689
|
+
out.ignores = toSerializablePatterns(value.ignores, "ignores");
|
|
690
|
+
}
|
|
691
|
+
if (Object.prototype.hasOwnProperty.call(value, "rules")) {
|
|
692
|
+
out.rules = toSerializableRules(value.rules);
|
|
693
|
+
}
|
|
694
|
+
return out;
|
|
695
|
+
}
|
|
696
|
+
return { rules: toSerializableRules(value) };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function toSerializableRules(value) {
|
|
700
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
701
|
+
throw new Error("rules must be an object");
|
|
702
|
+
}
|
|
703
|
+
return Object.fromEntries(Object.entries(value));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function toSerializablePatterns(value, key) {
|
|
707
|
+
if (typeof value === "string") {
|
|
708
|
+
return value;
|
|
709
|
+
}
|
|
710
|
+
if (Array.isArray(value)) {
|
|
711
|
+
return value.map((item, index) => {
|
|
712
|
+
if (typeof item !== "string") {
|
|
713
|
+
throw new Error(key + "[" + index + "] must be a string");
|
|
714
|
+
}
|
|
715
|
+
return item;
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
throw new Error(key + " must be a string or string array");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function isESLintConfigObject(value) {
|
|
722
|
+
return [
|
|
723
|
+
"basePath",
|
|
724
|
+
"extends",
|
|
725
|
+
"files",
|
|
726
|
+
"ignores",
|
|
727
|
+
"languageOptions",
|
|
728
|
+
"linterOptions",
|
|
729
|
+
"name",
|
|
730
|
+
"plugins",
|
|
731
|
+
"processor",
|
|
732
|
+
"rules",
|
|
733
|
+
"settings",
|
|
734
|
+
].some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function hasESLintRuntimeFields(value) {
|
|
738
|
+
return [
|
|
739
|
+
"languageOptions",
|
|
740
|
+
"linterOptions",
|
|
741
|
+
"plugins",
|
|
742
|
+
"processor",
|
|
743
|
+
"settings",
|
|
744
|
+
].some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
745
|
+
}
|
|
201
746
|
`
|
|
202
747
|
node := os.Getenv("TTSC_NODE_BINARY")
|
|
203
748
|
if node == "" {
|
|
@@ -215,17 +760,17 @@ const { pathToFileURL } = require("node:url");
|
|
|
215
760
|
}
|
|
216
761
|
return nil, fmt.Errorf("@ttsc/lint: load config file %s: %w", location, err)
|
|
217
762
|
}
|
|
218
|
-
var out
|
|
763
|
+
var out any
|
|
219
764
|
if err := json.Unmarshal(output, &out); err != nil {
|
|
220
765
|
return nil, fmt.Errorf("@ttsc/lint: parse config file %s output: %w", location, err)
|
|
221
766
|
}
|
|
222
|
-
if out
|
|
223
|
-
return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
|
|
767
|
+
if !isConfigContainer(out) {
|
|
768
|
+
return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object or flat config array", location)
|
|
224
769
|
}
|
|
225
770
|
return out, nil
|
|
226
771
|
}
|
|
227
772
|
|
|
228
|
-
func loadTypeScriptConfigFile(location string) (
|
|
773
|
+
func loadTypeScriptConfigFile(location string) (any, error) {
|
|
229
774
|
tempDir, err := os.MkdirTemp("", "ttsc-lint-config-")
|
|
230
775
|
if err != nil {
|
|
231
776
|
return nil, fmt.Errorf("@ttsc/lint: create config loader tempdir: %w", err)
|
|
@@ -276,16 +821,25 @@ func loadTypeScriptConfigFile(location string) (map[string]any, error) {
|
|
|
276
821
|
}
|
|
277
822
|
return nil, fmt.Errorf("@ttsc/lint: load TypeScript config file %s: %w", location, err)
|
|
278
823
|
}
|
|
279
|
-
var out
|
|
824
|
+
var out any
|
|
280
825
|
if err := json.Unmarshal(output, &out); err != nil {
|
|
281
826
|
return nil, fmt.Errorf("@ttsc/lint: parse TypeScript config file %s output: %w", location, err)
|
|
282
827
|
}
|
|
283
|
-
if out
|
|
284
|
-
return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object", location)
|
|
828
|
+
if !isConfigContainer(out) {
|
|
829
|
+
return nil, fmt.Errorf("@ttsc/lint: config file %s must export an object or flat config array", location)
|
|
285
830
|
}
|
|
286
831
|
return out, nil
|
|
287
832
|
}
|
|
288
833
|
|
|
834
|
+
func isConfigContainer(value any) bool {
|
|
835
|
+
switch value.(type) {
|
|
836
|
+
case []any, map[string]any:
|
|
837
|
+
return true
|
|
838
|
+
default:
|
|
839
|
+
return false
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
289
843
|
func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
290
844
|
relative, err := filepath.Rel(fromDir, location)
|
|
291
845
|
if err != nil {
|
|
@@ -309,10 +863,10 @@ declare const process: {
|
|
|
309
863
|
|
|
310
864
|
try {
|
|
311
865
|
const value = await resolveConfig(importedConfig, true);
|
|
312
|
-
if (!isObject(value)
|
|
313
|
-
throw new Error("config file must export an object");
|
|
866
|
+
if (!isObject(value)) {
|
|
867
|
+
throw new Error("config file must export an object or flat config array");
|
|
314
868
|
}
|
|
315
|
-
process.stdout.write(JSON.stringify(value));
|
|
869
|
+
process.stdout.write(JSON.stringify(toSerializableConfig(value)));
|
|
316
870
|
} catch (error) {
|
|
317
871
|
process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
|
|
318
872
|
process.exit(1);
|
|
@@ -346,6 +900,86 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
346
900
|
function hasOwn(value: Record<string, unknown>, key: string): boolean {
|
|
347
901
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
348
902
|
}
|
|
903
|
+
|
|
904
|
+
function toSerializableConfig(value: unknown): unknown {
|
|
905
|
+
if (Array.isArray(value)) {
|
|
906
|
+
return value.map((item) => toSerializableConfig(item));
|
|
907
|
+
}
|
|
908
|
+
if (!isObject(value)) {
|
|
909
|
+
return value;
|
|
910
|
+
}
|
|
911
|
+
if (isESLintConfigObject(value)) {
|
|
912
|
+
const out: Record<string, unknown> = {};
|
|
913
|
+
if (hasESLintRuntimeFields(value)) {
|
|
914
|
+
out.__ttscLintEslintRuntime = true;
|
|
915
|
+
}
|
|
916
|
+
if (hasOwn(value, "basePath")) {
|
|
917
|
+
out.basePath = value.basePath;
|
|
918
|
+
}
|
|
919
|
+
if (hasOwn(value, "extends")) {
|
|
920
|
+
out.extends = toSerializableConfig(value.extends);
|
|
921
|
+
}
|
|
922
|
+
if (hasOwn(value, "files")) {
|
|
923
|
+
out.files = toSerializablePatterns(value.files, "files");
|
|
924
|
+
}
|
|
925
|
+
if (hasOwn(value, "ignores")) {
|
|
926
|
+
out.ignores = toSerializablePatterns(value.ignores, "ignores");
|
|
927
|
+
}
|
|
928
|
+
if (hasOwn(value, "rules")) {
|
|
929
|
+
out.rules = toSerializableRules(value.rules);
|
|
930
|
+
}
|
|
931
|
+
return out;
|
|
932
|
+
}
|
|
933
|
+
return { rules: toSerializableRules(value) };
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function toSerializableRules(value: unknown): Record<string, unknown> {
|
|
937
|
+
if (!isObject(value) || Array.isArray(value)) {
|
|
938
|
+
throw new Error("rules must be an object");
|
|
939
|
+
}
|
|
940
|
+
return Object.fromEntries(Object.entries(value));
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function toSerializablePatterns(value: unknown, key: string): string | string[] {
|
|
944
|
+
if (typeof value === "string") {
|
|
945
|
+
return value;
|
|
946
|
+
}
|
|
947
|
+
if (Array.isArray(value)) {
|
|
948
|
+
return value.map((item, index) => {
|
|
949
|
+
if (typeof item !== "string") {
|
|
950
|
+
throw new Error(key + "[" + index + "] must be a string");
|
|
951
|
+
}
|
|
952
|
+
return item;
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
throw new Error(key + " must be a string or string array");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function isESLintConfigObject(value: Record<string, unknown>): boolean {
|
|
959
|
+
return [
|
|
960
|
+
"basePath",
|
|
961
|
+
"extends",
|
|
962
|
+
"files",
|
|
963
|
+
"ignores",
|
|
964
|
+
"languageOptions",
|
|
965
|
+
"linterOptions",
|
|
966
|
+
"name",
|
|
967
|
+
"plugins",
|
|
968
|
+
"processor",
|
|
969
|
+
"rules",
|
|
970
|
+
"settings",
|
|
971
|
+
].some((key) => hasOwn(value, key));
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function hasESLintRuntimeFields(value: Record<string, unknown>): boolean {
|
|
975
|
+
return [
|
|
976
|
+
"languageOptions",
|
|
977
|
+
"linterOptions",
|
|
978
|
+
"plugins",
|
|
979
|
+
"processor",
|
|
980
|
+
"settings",
|
|
981
|
+
].some((key) => hasOwn(value, key));
|
|
982
|
+
}
|
|
349
983
|
`, importLiteral)
|
|
350
984
|
}
|
|
351
985
|
|
|
@@ -451,6 +1085,16 @@ func setEnv(env []string, key, value string) []string {
|
|
|
451
1085
|
return append(env, prefix+value)
|
|
452
1086
|
}
|
|
453
1087
|
|
|
1088
|
+
func parseExternalSeverity(v any) (Severity, error) {
|
|
1089
|
+
if tuple, ok := v.([]any); ok {
|
|
1090
|
+
if len(tuple) == 0 {
|
|
1091
|
+
return SeverityOff, fmt.Errorf("severity tuple must not be empty")
|
|
1092
|
+
}
|
|
1093
|
+
return parseSeverity(tuple[0])
|
|
1094
|
+
}
|
|
1095
|
+
return parseSeverity(v)
|
|
1096
|
+
}
|
|
1097
|
+
|
|
454
1098
|
func parseSeverity(v any) (Severity, error) {
|
|
455
1099
|
switch x := v.(type) {
|
|
456
1100
|
case string:
|
|
@@ -462,7 +1106,7 @@ func parseSeverity(v any) (Severity, error) {
|
|
|
462
1106
|
case "error":
|
|
463
1107
|
return SeverityError, nil
|
|
464
1108
|
}
|
|
465
|
-
return SeverityOff, fmt.Errorf("unknown severity %q (want off | warning | error)", x)
|
|
1109
|
+
return SeverityOff, fmt.Errorf("unknown severity %q (want off | warn | warning | error)", x)
|
|
466
1110
|
case float64:
|
|
467
1111
|
switch x {
|
|
468
1112
|
case 0:
|
|
@@ -472,9 +1116,96 @@ func parseSeverity(v any) (Severity, error) {
|
|
|
472
1116
|
case 2:
|
|
473
1117
|
return SeverityError, nil
|
|
474
1118
|
}
|
|
475
|
-
return SeverityOff, fmt.Errorf("unknown severity %v (want
|
|
1119
|
+
return SeverityOff, fmt.Errorf("unknown severity %v (want 0 | 1 | 2)", x)
|
|
1120
|
+
}
|
|
1121
|
+
return SeverityOff, fmt.Errorf("severity must be one of: off | warn | warning | error | 0 | 1 | 2, got %T", v)
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
func sortedRuleNames(config RuleConfig, include func(Severity) bool) []string {
|
|
1125
|
+
names := make([]string, 0, len(config))
|
|
1126
|
+
for name, sev := range config {
|
|
1127
|
+
if include(sev) {
|
|
1128
|
+
names = append(names, name)
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
sort.Strings(names)
|
|
1132
|
+
return names
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
func matchAnyPattern(baseDir string, patterns []string, fileName string) bool {
|
|
1136
|
+
rel := filepath.ToSlash(fileName)
|
|
1137
|
+
if baseDir != "" {
|
|
1138
|
+
base := baseDir
|
|
1139
|
+
if abs, err := filepath.Abs(base); err == nil {
|
|
1140
|
+
base = abs
|
|
1141
|
+
}
|
|
1142
|
+
file := fileName
|
|
1143
|
+
if abs, err := filepath.Abs(file); err == nil {
|
|
1144
|
+
file = abs
|
|
1145
|
+
}
|
|
1146
|
+
if candidate, err := filepath.Rel(base, file); err == nil {
|
|
1147
|
+
if candidate == ".." || strings.HasPrefix(candidate, ".."+string(filepath.Separator)) {
|
|
1148
|
+
return false
|
|
1149
|
+
}
|
|
1150
|
+
rel = filepath.ToSlash(candidate)
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
rel = strings.TrimPrefix(rel, "./")
|
|
1154
|
+
for _, pattern := range patterns {
|
|
1155
|
+
if matchGlob(normalizeGlobPattern(pattern), rel) {
|
|
1156
|
+
return true
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return false
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
func normalizeGlobPattern(pattern string) string {
|
|
1163
|
+
pattern = filepath.ToSlash(pattern)
|
|
1164
|
+
pattern = strings.TrimPrefix(pattern, "./")
|
|
1165
|
+
if !strings.Contains(pattern, "/") {
|
|
1166
|
+
return "**/" + pattern
|
|
1167
|
+
}
|
|
1168
|
+
return pattern
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
func matchGlob(pattern, name string) bool {
|
|
1172
|
+
pattern = strings.Trim(pattern, "/")
|
|
1173
|
+
name = strings.Trim(name, "/")
|
|
1174
|
+
if pattern == "" {
|
|
1175
|
+
return name == ""
|
|
1176
|
+
}
|
|
1177
|
+
patternParts := strings.Split(pattern, "/")
|
|
1178
|
+
nameParts := []string{}
|
|
1179
|
+
if name != "" {
|
|
1180
|
+
nameParts = strings.Split(name, "/")
|
|
1181
|
+
}
|
|
1182
|
+
return matchGlobParts(patternParts, nameParts)
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
func matchGlobParts(patternParts, nameParts []string) bool {
|
|
1186
|
+
if len(patternParts) == 0 {
|
|
1187
|
+
return len(nameParts) == 0
|
|
1188
|
+
}
|
|
1189
|
+
head := patternParts[0]
|
|
1190
|
+
if head == "**" {
|
|
1191
|
+
if matchGlobParts(patternParts[1:], nameParts) {
|
|
1192
|
+
return true
|
|
1193
|
+
}
|
|
1194
|
+
for i := range nameParts {
|
|
1195
|
+
if matchGlobParts(patternParts[1:], nameParts[i+1:]) {
|
|
1196
|
+
return true
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return false
|
|
1200
|
+
}
|
|
1201
|
+
if len(nameParts) == 0 {
|
|
1202
|
+
return false
|
|
1203
|
+
}
|
|
1204
|
+
ok, err := filepath.Match(head, nameParts[0])
|
|
1205
|
+
if err != nil || !ok {
|
|
1206
|
+
return false
|
|
476
1207
|
}
|
|
477
|
-
return
|
|
1208
|
+
return matchGlobParts(patternParts[1:], nameParts[1:])
|
|
478
1209
|
}
|
|
479
1210
|
|
|
480
1211
|
// Severity returns the configured level for a rule, defaulting to
|