@ttsc/lint 0.12.3 → 0.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/defaultFormat.d.ts +10 -11
- package/lib/defaultFormat.js +10 -11
- package/lib/defaultFormat.js.map +1 -1
- package/lib/index.js +10 -4
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +2 -2
- package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
- package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
- package/lib/structures/TtscLintRule.d.ts +10 -1
- package/lib/structures/TtscLintRuleMap.d.ts +2 -2
- package/linthost/ast_helpers.go +17 -0
- package/linthost/compile.go +37 -0
- package/linthost/config.go +184 -35
- package/linthost/config_format.go +257 -244
- package/linthost/directives.go +72 -0
- package/linthost/dispatch.go +33 -33
- package/linthost/engine.go +4 -0
- package/linthost/eslint_runtime.go +31 -0
- package/linthost/fix.go +29 -0
- package/linthost/format.go +23 -0
- package/linthost/host.go +11 -0
- package/linthost/print_dispatch.go +7 -5
- package/linthost/print_doc.go +4 -0
- package/linthost/print_nodes_call.go +9 -0
- package/linthost/rules_arrays.go +5 -2
- package/linthost/rules_debugger.go +3 -2
- package/linthost/rules_dupes.go +7 -4
- package/linthost/rules_empty.go +3 -2
- package/linthost/rules_escape.go +15 -0
- package/linthost/rules_eval.go +3 -0
- package/linthost/rules_finally.go +11 -0
- package/linthost/rules_format_jsdoc.go +7 -0
- package/linthost/rules_format_print_width.go +3 -0
- package/linthost/rules_format_quotes.go +3 -0
- package/linthost/rules_format_sort_imports.go +4 -0
- package/linthost/rules_gap.go +20 -0
- package/linthost/rules_imports.go +5 -7
- package/linthost/rules_logic.go +11 -0
- package/linthost/rules_loops.go +4 -0
- package/linthost/rules_misc.go +4 -2
- package/linthost/rules_params.go +7 -11
- package/linthost/rules_problems.go +24 -1
- package/linthost/rules_promise.go +12 -0
- package/linthost/rules_protos.go +3 -2
- package/linthost/rules_self.go +6 -0
- package/linthost/rules_strings.go +10 -0
- package/linthost/rules_suggestions.go +14 -4
- package/linthost/rules_throw.go +2 -0
- package/linthost/rules_ts.go +13 -0
- package/linthost/rules_ts_extra.go +25 -8
- package/linthost/rules_var.go +14 -1
- package/package.json +3 -3
- package/plugin/main.go +4 -4
- package/rule/astutil/astutil.go +12 -0
- package/rule/rule.go +123 -123
- package/src/defaultFormat.ts +10 -11
- package/src/index.ts +16 -4
- package/src/structures/ITtscLintConfig.ts +2 -2
- package/src/structures/ITtscLintFormatConfig.ts +53 -55
- package/src/structures/ITtscLintPluginMeta.ts +9 -1
- package/src/structures/TtscLintRule.ts +10 -1
- package/src/structures/TtscLintRuleMap.ts +17 -18
package/linthost/config.go
CHANGED
|
@@ -98,9 +98,20 @@ type ResolvedRuleConfig struct {
|
|
|
98
98
|
Ignored bool
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// RuleResolver is the engine-facing view of a resolved lint configuration.
|
|
102
|
+
// Implementations include RuleConfig (severity-only, no options), InlineRuleResolver
|
|
103
|
+
// (tsconfig inline rules + optional per-rule options), and *ConfigStore (external
|
|
104
|
+
// flat-config file, with per-file glob resolution and a unified options map).
|
|
101
105
|
type RuleResolver interface {
|
|
106
|
+
// ResolveRules returns the effective severity map for the given source file.
|
|
107
|
+
// Implementations that support `files`/`ignores` patterns apply them here;
|
|
108
|
+
// flat RuleConfig always returns all rules unchanged.
|
|
102
109
|
ResolveRules(fileName string) ResolvedRuleConfig
|
|
110
|
+
// ActiveRuleNames returns the sorted names of every rule that is not SeverityOff
|
|
111
|
+
// in at least one config entry. Used to build the engine's dispatch table.
|
|
103
112
|
ActiveRuleNames() []string
|
|
113
|
+
// EnabledRuleConfig returns the project-wide severity map for rules that are
|
|
114
|
+
// not SeverityOff. Where multiple entries disagree, SeverityError wins.
|
|
104
115
|
EnabledRuleConfig() RuleConfig
|
|
105
116
|
// RuleOptions returns the raw JSON options for `name`, or nil when the
|
|
106
117
|
// rule was configured with a severity alone. Returns nil for unknown
|
|
@@ -108,14 +119,20 @@ type RuleResolver interface {
|
|
|
108
119
|
RuleOptions(name string) json.RawMessage
|
|
109
120
|
}
|
|
110
121
|
|
|
122
|
+
// ResolveRules implements RuleResolver. A flat RuleConfig has no glob scoping,
|
|
123
|
+
// so every file receives the full map unchanged.
|
|
111
124
|
func (c RuleConfig) ResolveRules(string) ResolvedRuleConfig {
|
|
112
125
|
return ResolvedRuleConfig{Rules: c}
|
|
113
126
|
}
|
|
114
127
|
|
|
128
|
+
// ActiveRuleNames implements RuleResolver. Returns rule names whose severity
|
|
129
|
+
// is not SeverityOff, sorted for deterministic engine dispatch-table construction.
|
|
115
130
|
func (c RuleConfig) ActiveRuleNames() []string {
|
|
116
131
|
return sortedRuleNames(c, func(sev Severity) bool { return sev != SeverityOff })
|
|
117
132
|
}
|
|
118
133
|
|
|
134
|
+
// EnabledRuleConfig implements RuleResolver. Returns a copy containing only the
|
|
135
|
+
// non-off entries; used to populate engine state and diagnostic reporting.
|
|
119
136
|
func (c RuleConfig) EnabledRuleConfig() RuleConfig {
|
|
120
137
|
out := RuleConfig{}
|
|
121
138
|
for name, sev := range c {
|
|
@@ -139,18 +156,25 @@ type InlineRuleResolver struct {
|
|
|
139
156
|
Options RuleOptionsMap
|
|
140
157
|
}
|
|
141
158
|
|
|
159
|
+
// ResolveRules implements RuleResolver. Inline rules have no glob scoping;
|
|
160
|
+
// the full map applies to every file.
|
|
142
161
|
func (r InlineRuleResolver) ResolveRules(string) ResolvedRuleConfig {
|
|
143
162
|
return ResolvedRuleConfig{Rules: r.Rules}
|
|
144
163
|
}
|
|
145
164
|
|
|
165
|
+
// ActiveRuleNames implements RuleResolver by delegating to the inner RuleConfig.
|
|
146
166
|
func (r InlineRuleResolver) ActiveRuleNames() []string {
|
|
147
167
|
return r.Rules.ActiveRuleNames()
|
|
148
168
|
}
|
|
149
169
|
|
|
170
|
+
// EnabledRuleConfig implements RuleResolver by delegating to the inner RuleConfig.
|
|
150
171
|
func (r InlineRuleResolver) EnabledRuleConfig() RuleConfig {
|
|
151
172
|
return r.Rules.EnabledRuleConfig()
|
|
152
173
|
}
|
|
153
174
|
|
|
175
|
+
// RuleOptions implements RuleResolver. Returns the raw JSON options blob for
|
|
176
|
+
// `name`, or nil when the rule was configured without options or the name is
|
|
177
|
+
// unknown.
|
|
154
178
|
func (r InlineRuleResolver) RuleOptions(name string) json.RawMessage {
|
|
155
179
|
if r.Options == nil {
|
|
156
180
|
return nil
|
|
@@ -158,6 +182,11 @@ func (r InlineRuleResolver) RuleOptions(name string) json.RawMessage {
|
|
|
158
182
|
return r.Options[name]
|
|
159
183
|
}
|
|
160
184
|
|
|
185
|
+
// ConfigStore holds the parsed representation of an external flat-config file.
|
|
186
|
+
// It implements RuleResolver with per-file glob scoping: ResolveRules walks the
|
|
187
|
+
// entries in declaration order and the last matching entry wins. Options are
|
|
188
|
+
// intentionally NOT per-file — one project-wide options map is kept so rule
|
|
189
|
+
// behavior is uniform across the codebase even when severity varies by glob.
|
|
161
190
|
type ConfigStore struct {
|
|
162
191
|
entries []ConfigEntry
|
|
163
192
|
externalConfigPath string
|
|
@@ -179,6 +208,10 @@ func (s *ConfigStore) RuleOptions(name string) json.RawMessage {
|
|
|
179
208
|
return s.options[name]
|
|
180
209
|
}
|
|
181
210
|
|
|
211
|
+
// ConfigEntry is one block of a flat-config array. BaseDir anchors glob
|
|
212
|
+
// resolution; Files and Ignores are the ESLint-style pattern lists. IgnoreOnly
|
|
213
|
+
// marks entries that carry only `ignores` (no `files`, no `rules`) — these
|
|
214
|
+
// are evaluated first in ResolveRules and short-circuit the walk when matched.
|
|
182
215
|
type ConfigEntry struct {
|
|
183
216
|
BaseDir string
|
|
184
217
|
Files []string
|
|
@@ -187,6 +220,10 @@ type ConfigEntry struct {
|
|
|
187
220
|
IgnoreOnly bool
|
|
188
221
|
}
|
|
189
222
|
|
|
223
|
+
// ResolveRules implements RuleResolver. Ignore-only entries are checked first;
|
|
224
|
+
// if one matches, the file is marked Ignored and linting is skipped entirely.
|
|
225
|
+
// Otherwise the entries are walked in declaration order and the last matching
|
|
226
|
+
// entry wins (later entries shadow earlier ones for the same rule name).
|
|
190
227
|
func (s *ConfigStore) ResolveRules(fileName string) ResolvedRuleConfig {
|
|
191
228
|
if s == nil {
|
|
192
229
|
return ResolvedRuleConfig{Rules: RuleConfig{}}
|
|
@@ -208,6 +245,10 @@ func (s *ConfigStore) ResolveRules(fileName string) ResolvedRuleConfig {
|
|
|
208
245
|
return ResolvedRuleConfig{Rules: out}
|
|
209
246
|
}
|
|
210
247
|
|
|
248
|
+
// ActiveRuleNames implements RuleResolver. Returns the sorted union of all rule
|
|
249
|
+
// names that are not SeverityOff across every non-ignore-only config entry,
|
|
250
|
+
// regardless of which files they apply to. The engine uses this to build the
|
|
251
|
+
// per-rule dispatch table before file iteration begins.
|
|
211
252
|
func (s *ConfigStore) ActiveRuleNames() []string {
|
|
212
253
|
if s == nil {
|
|
213
254
|
return nil
|
|
@@ -226,6 +267,9 @@ func (s *ConfigStore) ActiveRuleNames() []string {
|
|
|
226
267
|
return sortedRuleNames(active, func(Severity) bool { return true })
|
|
227
268
|
}
|
|
228
269
|
|
|
270
|
+
// EnabledRuleConfig implements RuleResolver. Returns the project-wide severity
|
|
271
|
+
// map for non-off rules. Where multiple entries configure the same rule,
|
|
272
|
+
// SeverityError is sticky — it cannot be downgraded by a later warning entry.
|
|
229
273
|
func (s *ConfigStore) EnabledRuleConfig() RuleConfig {
|
|
230
274
|
out := RuleConfig{}
|
|
231
275
|
if s == nil {
|
|
@@ -247,6 +291,10 @@ func (s *ConfigStore) EnabledRuleConfig() RuleConfig {
|
|
|
247
291
|
return out
|
|
248
292
|
}
|
|
249
293
|
|
|
294
|
+
// Flatten returns the unconstrained union of all non-ignore-only entries,
|
|
295
|
+
// including SeverityOff rules. Used by LoadRuleConfig (legacy callers that
|
|
296
|
+
// expect a plain RuleConfig) and by parseExternalConfigRules. Later entries
|
|
297
|
+
// shadow earlier ones for the same rule name.
|
|
250
298
|
func (s *ConfigStore) Flatten() RuleConfig {
|
|
251
299
|
out := RuleConfig{}
|
|
252
300
|
if s == nil {
|
|
@@ -263,6 +311,9 @@ func (s *ConfigStore) Flatten() RuleConfig {
|
|
|
263
311
|
return out
|
|
264
312
|
}
|
|
265
313
|
|
|
314
|
+
// ExternalConfigPath returns the absolute path of the config file that was
|
|
315
|
+
// loaded into this store, or the empty string for stores built from inline
|
|
316
|
+
// tsconfig plugin entries.
|
|
266
317
|
func (s *ConfigStore) ExternalConfigPath() string {
|
|
267
318
|
if s == nil {
|
|
268
319
|
return ""
|
|
@@ -270,6 +321,11 @@ func (s *ConfigStore) ExternalConfigPath() string {
|
|
|
270
321
|
return s.externalConfigPath
|
|
271
322
|
}
|
|
272
323
|
|
|
324
|
+
// WantsESLintRuntime reports whether the config uses any ESLint-native feature
|
|
325
|
+
// (languageOptions, linterOptions, processor, settings, or a non-native plugin
|
|
326
|
+
// map) OR whether the config file itself is an eslint.config.* file. The latter
|
|
327
|
+
// check covers pure-ttsc ESLint-named configs that happen not to use runtime
|
|
328
|
+
// fields.
|
|
273
329
|
func (s *ConfigStore) WantsESLintRuntime() bool {
|
|
274
330
|
if s == nil {
|
|
275
331
|
return false
|
|
@@ -281,6 +337,10 @@ func (s *ConfigStore) WantsESLintRuntime() bool {
|
|
|
281
337
|
return strings.HasPrefix(base, "eslint.config.")
|
|
282
338
|
}
|
|
283
339
|
|
|
340
|
+
// RequiresESLintRuntime reports whether the config contains fields that cannot
|
|
341
|
+
// be evaluated without the JS ESLint runtime (e.g. non-native plugins, runtime
|
|
342
|
+
// language options). Unlike WantsESLintRuntime it does NOT fire for the
|
|
343
|
+
// eslint.config.* naming convention alone.
|
|
284
344
|
func (s *ConfigStore) RequiresESLintRuntime() bool {
|
|
285
345
|
if s == nil {
|
|
286
346
|
return false
|
|
@@ -386,6 +446,9 @@ func parseRuleEntry(value any) (Severity, json.RawMessage, error) {
|
|
|
386
446
|
return sev, nil, err
|
|
387
447
|
}
|
|
388
448
|
|
|
449
|
+
// parseExternalConfigRules is a convenience wrapper used by legacy callers
|
|
450
|
+
// (e.g. tests) that only need a flat RuleConfig from an already-deserialized
|
|
451
|
+
// config value. Glob scoping and options are discarded.
|
|
389
452
|
func parseExternalConfigRules(raw any) (RuleConfig, error) {
|
|
390
453
|
store, err := parseExternalConfigStore(raw, "")
|
|
391
454
|
if err != nil {
|
|
@@ -394,14 +457,24 @@ func parseExternalConfigRules(raw any) (RuleConfig, error) {
|
|
|
394
457
|
return store.Flatten(), nil
|
|
395
458
|
}
|
|
396
459
|
|
|
460
|
+
// parseExternalConfigStore parses `raw` without allowing runtime-only string
|
|
461
|
+
// extends. Used by unit tests and paths that do not load from a real file.
|
|
397
462
|
func parseExternalConfigStore(raw any, configDir string) (*ConfigStore, error) {
|
|
398
463
|
return parseExternalConfigStoreWithRuntimeMode(raw, configDir, false)
|
|
399
464
|
}
|
|
400
465
|
|
|
466
|
+
// parseExternalConfigStoreForFile parses `raw` with allowRuntimeOnly=true,
|
|
467
|
+
// which lets string-typed `extends` entries set eslintRuntimeRequired instead
|
|
468
|
+
// of returning an error. Called by loadExternalConfigResolver after a config
|
|
469
|
+
// file is read from disk.
|
|
401
470
|
func parseExternalConfigStoreForFile(raw any, configDir string) (*ConfigStore, error) {
|
|
402
471
|
return parseExternalConfigStoreWithRuntimeMode(raw, configDir, true)
|
|
403
472
|
}
|
|
404
473
|
|
|
474
|
+
// parseExternalConfigStoreWithRuntimeMode is the shared implementation for the
|
|
475
|
+
// two public parse variants. allowRuntimeOnly controls whether a bare string
|
|
476
|
+
// extends value is accepted (sets eslintRuntimeRequired) or rejected with an
|
|
477
|
+
// error.
|
|
405
478
|
func parseExternalConfigStoreWithRuntimeMode(raw any, configDir string, allowRuntimeOnly bool) (*ConfigStore, error) {
|
|
406
479
|
store := &ConfigStore{}
|
|
407
480
|
if err := collectExternalConfigEntries(store, raw, configDir, "config", allowRuntimeOnly); err != nil {
|
|
@@ -590,6 +663,9 @@ func collectExternalRuleMapWithOptions(out RuleConfig, opts RuleOptionsMap, raw
|
|
|
590
663
|
return nil
|
|
591
664
|
}
|
|
592
665
|
|
|
666
|
+
// isESLintConfigObject reports whether `value` looks like an ESLint flat-config
|
|
667
|
+
// object (has at least one recognized top-level key). Used to distinguish
|
|
668
|
+
// flat-config objects from bare rules maps during config parsing.
|
|
593
669
|
func isESLintConfigObject(value map[string]any) bool {
|
|
594
670
|
for _, key := range []string{
|
|
595
671
|
"basePath",
|
|
@@ -613,6 +689,9 @@ func isESLintConfigObject(value map[string]any) bool {
|
|
|
613
689
|
return false
|
|
614
690
|
}
|
|
615
691
|
|
|
692
|
+
// hasESLintRuntimeFields reports whether `value` contains keys that require the
|
|
693
|
+
// JS ESLint runtime to evaluate: languageOptions, linterOptions, processor,
|
|
694
|
+
// settings, or a plugins map that is not purely native contributors.
|
|
616
695
|
func hasESLintRuntimeFields(value map[string]any) bool {
|
|
617
696
|
for _, key := range []string{
|
|
618
697
|
"languageOptions",
|
|
@@ -689,11 +768,17 @@ func isNativePluginValue(entry any) bool {
|
|
|
689
768
|
}
|
|
690
769
|
}
|
|
691
770
|
|
|
771
|
+
// normalizeExternalRuleName strips the standard typescript-eslint namespace
|
|
772
|
+
// prefixes so that rules like "@typescript-eslint/no-explicit-any" and the
|
|
773
|
+
// bare "no-explicit-any" key both resolve to the same engine-internal name.
|
|
692
774
|
func normalizeExternalRuleName(name string) string {
|
|
693
775
|
name = strings.TrimPrefix(name, "@typescript-eslint/")
|
|
694
776
|
return strings.TrimPrefix(name, "typescript-eslint/")
|
|
695
777
|
}
|
|
696
778
|
|
|
779
|
+
// parsePatternList coerces a raw config value to a string slice for use as a
|
|
780
|
+
// `files` or `ignores` pattern list. Accepts a bare string (single-pattern
|
|
781
|
+
// shorthand) or a string array. Empty patterns are rejected eagerly.
|
|
697
782
|
func parsePatternList(raw any, path string) ([]string, error) {
|
|
698
783
|
if raw == nil {
|
|
699
784
|
return nil, nil
|
|
@@ -881,6 +966,9 @@ func emitLegacyConfigDeprecation() {
|
|
|
881
966
|
})
|
|
882
967
|
}
|
|
883
968
|
|
|
969
|
+
// loadExternalConfigResolver loads and parses an external config file at
|
|
970
|
+
// `location` into a *ConfigStore and returns it as a RuleResolver. The store's
|
|
971
|
+
// externalConfigPath is set so callers can query WantsESLintRuntime.
|
|
884
972
|
func loadExternalConfigResolver(location string) (RuleResolver, error) {
|
|
885
973
|
raw, err := loadConfigFile(location)
|
|
886
974
|
if err != nil {
|
|
@@ -943,6 +1031,9 @@ func findLintConfigFile(cwd, tsconfigPath string) (string, error) {
|
|
|
943
1031
|
}
|
|
944
1032
|
}
|
|
945
1033
|
|
|
1034
|
+
// resolveConfigFilePath resolves a user-supplied config path to an absolute
|
|
1035
|
+
// path. Absolute paths are returned unchanged; relative paths are joined to the
|
|
1036
|
+
// tsconfig directory (or cwd when no tsconfig is set).
|
|
946
1037
|
func resolveConfigFilePath(configPath, cwd, tsconfigPath string) string {
|
|
947
1038
|
if filepath.IsAbs(configPath) {
|
|
948
1039
|
return configPath
|
|
@@ -950,6 +1041,10 @@ func resolveConfigFilePath(configPath, cwd, tsconfigPath string) string {
|
|
|
950
1041
|
return filepath.Join(tsconfigBaseDir(cwd, tsconfigPath), configPath)
|
|
951
1042
|
}
|
|
952
1043
|
|
|
1044
|
+
// discoveryConfigBaseDir returns the directory from which auto-discovery walks
|
|
1045
|
+
// upward when no explicit config path is provided. Prefer the tsconfig
|
|
1046
|
+
// directory over cwd so that nested package configs are found relative to the
|
|
1047
|
+
// tsconfig that triggered the lint run.
|
|
953
1048
|
func discoveryConfigBaseDir(cwd, tsconfigPath string) string {
|
|
954
1049
|
if tsconfigPath != "" {
|
|
955
1050
|
resolvedTsconfig := tsconfigPath
|
|
@@ -961,6 +1056,9 @@ func discoveryConfigBaseDir(cwd, tsconfigPath string) string {
|
|
|
961
1056
|
return cwd
|
|
962
1057
|
}
|
|
963
1058
|
|
|
1059
|
+
// tsconfigBaseDir returns the directory that contains the tsconfig file, or
|
|
1060
|
+
// cwd when tsconfigPath is empty. Used as the base for relative config paths
|
|
1061
|
+
// supplied in the tsconfig plugin entry.
|
|
964
1062
|
func tsconfigBaseDir(cwd, tsconfigPath string) string {
|
|
965
1063
|
if tsconfigPath == "" {
|
|
966
1064
|
return cwd
|
|
@@ -972,6 +1070,9 @@ func tsconfigBaseDir(cwd, tsconfigPath string) string {
|
|
|
972
1070
|
return filepath.Dir(resolvedTsconfig)
|
|
973
1071
|
}
|
|
974
1072
|
|
|
1073
|
+
// loadConfigFile loads and deserializes a lint config file at `location`.
|
|
1074
|
+
// The file format is determined by extension: .json is parsed natively;
|
|
1075
|
+
// .js/.cjs/.mjs run through a Node subprocess; .ts/.cts/.mts run through ttsx.
|
|
975
1076
|
func loadConfigFile(location string) (any, error) {
|
|
976
1077
|
ext := strings.ToLower(filepath.Ext(location))
|
|
977
1078
|
switch ext {
|
|
@@ -986,6 +1087,8 @@ func loadConfigFile(location string) (any, error) {
|
|
|
986
1087
|
}
|
|
987
1088
|
}
|
|
988
1089
|
|
|
1090
|
+
// loadJSONConfigFile reads and JSON-parses a lint config file. A leading UTF-8
|
|
1091
|
+
// BOM is stripped before parsing so files saved by Windows editors are accepted.
|
|
989
1092
|
func loadJSONConfigFile(location string) (any, error) {
|
|
990
1093
|
body, err := os.ReadFile(location)
|
|
991
1094
|
if err != nil {
|
|
@@ -1006,6 +1109,11 @@ func loadJSONConfigFile(location string) (any, error) {
|
|
|
1006
1109
|
return out, nil
|
|
1007
1110
|
}
|
|
1008
1111
|
|
|
1112
|
+
// loadScriptConfigFile evaluates a .js/.cjs/.mjs config file by running a
|
|
1113
|
+
// Node subprocess that dynamic-imports the file, resolves the exported config
|
|
1114
|
+
// through the same 8-hop default/config unwrap used by the TS loader, and
|
|
1115
|
+
// serializes the result as JSON to stdout. The subprocess has a
|
|
1116
|
+
// configLoaderTimeout deadline to prevent user code from hanging indefinitely.
|
|
1009
1117
|
func loadScriptConfigFile(location string) (any, error) {
|
|
1010
1118
|
const script = `
|
|
1011
1119
|
const { pathToFileURL } = require("node:url");
|
|
@@ -1182,6 +1290,11 @@ function isNativePluginValue(entry) {
|
|
|
1182
1290
|
return out, nil
|
|
1183
1291
|
}
|
|
1184
1292
|
|
|
1293
|
+
// loadTypeScriptConfigFile evaluates a .ts/.cts/.mts config file by writing
|
|
1294
|
+
// an ephemeral loader script and tsconfig into a temp directory, symlinking the
|
|
1295
|
+
// nearest node_modules, then running `ttsx` with a configLoaderTimeout deadline.
|
|
1296
|
+
// The loader script imports the config file, resolves it through the same
|
|
1297
|
+
// unwrap chain used by loadScriptConfigFile, and writes JSON to stdout.
|
|
1185
1298
|
func loadTypeScriptConfigFile(location string) (any, error) {
|
|
1186
1299
|
tempDir, err := os.MkdirTemp("", "ttsc-lint-config-")
|
|
1187
1300
|
if err != nil {
|
|
@@ -1248,6 +1361,9 @@ func loadTypeScriptConfigFile(location string) (any, error) {
|
|
|
1248
1361
|
return out, nil
|
|
1249
1362
|
}
|
|
1250
1363
|
|
|
1364
|
+
// isConfigContainer reports whether `value` is a top-level config container
|
|
1365
|
+
// (an object or flat-config array). Scalar values are rejected so users get a
|
|
1366
|
+
// clear error instead of an opaque parse failure downstream.
|
|
1251
1367
|
func isConfigContainer(value any) bool {
|
|
1252
1368
|
switch value.(type) {
|
|
1253
1369
|
case []any, map[string]any:
|
|
@@ -1257,6 +1373,10 @@ func isConfigContainer(value any) bool {
|
|
|
1257
1373
|
}
|
|
1258
1374
|
}
|
|
1259
1375
|
|
|
1376
|
+
// relativeImportSpecifier computes the ESM import specifier for `location`
|
|
1377
|
+
// relative to `fromDir`. The result always starts with "./" or "../" so it is
|
|
1378
|
+
// treated as a relative path by the ESM loader rather than as a bare package
|
|
1379
|
+
// name.
|
|
1260
1380
|
func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
1261
1381
|
relative, err := filepath.Rel(fromDir, location)
|
|
1262
1382
|
if err != nil {
|
|
@@ -1269,6 +1389,11 @@ func relativeImportSpecifier(fromDir, location string) (string, error) {
|
|
|
1269
1389
|
return "./" + relative, nil
|
|
1270
1390
|
}
|
|
1271
1391
|
|
|
1392
|
+
// typeScriptConfigLoaderSource returns the TypeScript source of the ephemeral
|
|
1393
|
+
// loader script that ttsx executes to evaluate a TypeScript lint config file.
|
|
1394
|
+
// `importLiteral` is a JSON-encoded relative import path (e.g. `"./lint.config.ts"`)
|
|
1395
|
+
// that is spliced directly into the `import * as` statement, so it must
|
|
1396
|
+
// already be a valid JSON string (produced by json.Marshal).
|
|
1272
1397
|
func typeScriptConfigLoaderSource(importLiteral string) string {
|
|
1273
1398
|
return fmt.Sprintf(`import * as importedConfig from %s;
|
|
1274
1399
|
|
|
@@ -1442,6 +1567,10 @@ function isNativePluginValue(entry: unknown): boolean {
|
|
|
1442
1567
|
`, importLiteral)
|
|
1443
1568
|
}
|
|
1444
1569
|
|
|
1570
|
+
// typeScriptConfigLoaderTsconfig generates the JSON content of the ephemeral
|
|
1571
|
+
// tsconfig that compiles the loader script. Settings mirror the JS-factory
|
|
1572
|
+
// loader's lenient baseline so identical user configs evaluate the same way
|
|
1573
|
+
// from both the JS and Go sides.
|
|
1445
1574
|
func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
1446
1575
|
// Mirror the JS-factory loader's lenient settings (see the matching
|
|
1447
1576
|
// tsconfig synthesis in `packages/lint/src/index.ts::readTtsxConfigPlugins`).
|
|
@@ -1478,6 +1607,8 @@ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
|
|
|
1478
1607
|
return string(body)
|
|
1479
1608
|
}
|
|
1480
1609
|
|
|
1610
|
+
// ttsxCommand returns a ttsx exec.Cmd bound to a background context. Use
|
|
1611
|
+
// ttsxCommandContext when a deadline is needed (e.g. config file loading).
|
|
1481
1612
|
func ttsxCommand(args ...string) *exec.Cmd {
|
|
1482
1613
|
return ttsxCommandContext(context.Background(), args...)
|
|
1483
1614
|
}
|
|
@@ -1501,6 +1632,9 @@ func ttsxCommandContext(ctx context.Context, args ...string) *exec.Cmd {
|
|
|
1501
1632
|
return exec.CommandContext(ctx, ttsx, args...)
|
|
1502
1633
|
}
|
|
1503
1634
|
|
|
1635
|
+
// shouldRunTtsxThroughNode reports whether the resolved ttsx binary is a
|
|
1636
|
+
// script (JS or TS extension) rather than a compiled native executable.
|
|
1637
|
+
// Scripts must be executed via `node <binary> <args>` instead of directly.
|
|
1504
1638
|
func shouldRunTtsxThroughNode(binary string) bool {
|
|
1505
1639
|
switch strings.ToLower(filepath.Ext(binary)) {
|
|
1506
1640
|
case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
|
|
@@ -1510,6 +1644,10 @@ func shouldRunTtsxThroughNode(binary string) bool {
|
|
|
1510
1644
|
}
|
|
1511
1645
|
}
|
|
1512
1646
|
|
|
1647
|
+
// nodeConfigLoaderEnv builds the environment for a Node.js config-loader
|
|
1648
|
+
// subprocess. It prepends the nearest node_modules directory to NODE_PATH so
|
|
1649
|
+
// that imports in .js/.cjs/.mjs config files resolve correctly even when the
|
|
1650
|
+
// subprocess's cwd differs from the config file's location.
|
|
1513
1651
|
func nodeConfigLoaderEnv(location string) []string {
|
|
1514
1652
|
env := os.Environ()
|
|
1515
1653
|
parts := make([]string, 0, 2)
|
|
@@ -1525,6 +1663,11 @@ func nodeConfigLoaderEnv(location string) []string {
|
|
|
1525
1663
|
return setEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
|
|
1526
1664
|
}
|
|
1527
1665
|
|
|
1666
|
+
// linkNearestNodeModules creates a node_modules symlink (or Windows junction)
|
|
1667
|
+
// inside `tempDir` that points at the nearest node_modules directory found
|
|
1668
|
+
// upward from `sourceDir`. This lets the TypeScript config loader resolve
|
|
1669
|
+
// imports from the user's project without copying the entire module tree.
|
|
1670
|
+
// If no node_modules directory exists, the function is a no-op.
|
|
1528
1671
|
func linkNearestNodeModules(tempDir, sourceDir string) error {
|
|
1529
1672
|
nodeModules := findNearestNodeModules(sourceDir)
|
|
1530
1673
|
if nodeModules == "" {
|
|
@@ -1550,6 +1693,10 @@ func linkNearestNodeModules(tempDir, sourceDir string) error {
|
|
|
1550
1693
|
return fmt.Errorf("@ttsc/lint: link config node_modules %s: %w", nodeModules, err)
|
|
1551
1694
|
}
|
|
1552
1695
|
|
|
1696
|
+
// createWindowsJunction creates a directory junction at `link` pointing at
|
|
1697
|
+
// `target` using `cmd /c mklink /J`. Junctions do not require elevated
|
|
1698
|
+
// privileges (unlike symlinks on Windows), making them the right fallback when
|
|
1699
|
+
// os.Symlink fails.
|
|
1553
1700
|
func createWindowsJunction(link, target string) error {
|
|
1554
1701
|
// `cmd /c mklink /J link target` is the standard recipe and works
|
|
1555
1702
|
// without elevated privileges. Both arguments must be absolute paths
|
|
@@ -1561,6 +1708,9 @@ func createWindowsJunction(link, target string) error {
|
|
|
1561
1708
|
return nil
|
|
1562
1709
|
}
|
|
1563
1710
|
|
|
1711
|
+
// findNearestNodeModules walks upward from `start` and returns the first
|
|
1712
|
+
// node_modules directory found, or the empty string if the filesystem root is
|
|
1713
|
+
// reached without a match.
|
|
1564
1714
|
func findNearestNodeModules(start string) string {
|
|
1565
1715
|
dir := filepath.Clean(start)
|
|
1566
1716
|
for {
|
|
@@ -1576,6 +1726,9 @@ func findNearestNodeModules(start string) string {
|
|
|
1576
1726
|
}
|
|
1577
1727
|
}
|
|
1578
1728
|
|
|
1729
|
+
// setEnv updates an existing key=value entry in `env` (in-place) or appends
|
|
1730
|
+
// a new one. It is intentionally a pure-slice helper — no os.Setenv side
|
|
1731
|
+
// effects — so callers can pass it directly to exec.Cmd.Env.
|
|
1579
1732
|
func setEnv(env []string, key, value string) []string {
|
|
1580
1733
|
prefix := key + "="
|
|
1581
1734
|
for i, entry := range env {
|
|
@@ -1587,44 +1740,19 @@ func setEnv(env []string, key, value string) []string {
|
|
|
1587
1740
|
return append(env, prefix+value)
|
|
1588
1741
|
}
|
|
1589
1742
|
|
|
1590
|
-
// parseExternalRuleEntry
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
// JSON array would land in `DecodeOptions` and fall back to defaults
|
|
1596
|
-
// — a silent fallback the standard parser deliberately avoids.
|
|
1743
|
+
// parseExternalRuleEntry delegates to parseRuleEntry. Both the inline
|
|
1744
|
+
// (tsconfig) and external (flat-config file) paths accept exactly the
|
|
1745
|
+
// same severity-tuple grammar, so a single implementation suffices.
|
|
1746
|
+
// The name is preserved because test files in the same package call it
|
|
1747
|
+
// directly.
|
|
1597
1748
|
func parseExternalRuleEntry(v any) (Severity, json.RawMessage, error) {
|
|
1598
|
-
|
|
1599
|
-
if len(tuple) == 0 {
|
|
1600
|
-
return SeverityOff, nil, fmt.Errorf("severity tuple must not be empty")
|
|
1601
|
-
}
|
|
1602
|
-
sev, err := parseSeverity(tuple[0])
|
|
1603
|
-
if err != nil {
|
|
1604
|
-
return SeverityOff, nil, err
|
|
1605
|
-
}
|
|
1606
|
-
if len(tuple) == 1 {
|
|
1607
|
-
return sev, nil, nil
|
|
1608
|
-
}
|
|
1609
|
-
if len(tuple) > 2 {
|
|
1610
|
-
return SeverityOff, nil, fmt.Errorf("severity tuple must be [severity] or [severity, options], got %d elements", len(tuple))
|
|
1611
|
-
}
|
|
1612
|
-
if tuple[1] == nil {
|
|
1613
|
-
return sev, nil, nil
|
|
1614
|
-
}
|
|
1615
|
-
if _, ok := tuple[1].(map[string]any); !ok {
|
|
1616
|
-
return SeverityOff, nil, fmt.Errorf("severity tuple's options slot must be an object, got %T", tuple[1])
|
|
1617
|
-
}
|
|
1618
|
-
encoded, err := json.Marshal(tuple[1])
|
|
1619
|
-
if err != nil {
|
|
1620
|
-
return SeverityOff, nil, fmt.Errorf("encode options: %w", err)
|
|
1621
|
-
}
|
|
1622
|
-
return sev, encoded, nil
|
|
1623
|
-
}
|
|
1624
|
-
sev, err := parseSeverity(v)
|
|
1625
|
-
return sev, nil, err
|
|
1749
|
+
return parseRuleEntry(v)
|
|
1626
1750
|
}
|
|
1627
1751
|
|
|
1752
|
+
// parseSeverity converts a raw config value to a Severity. Accepts the string
|
|
1753
|
+
// literals "off", "warn"/"warning", "error" and the numeric equivalents 0, 1,
|
|
1754
|
+
// 2 (the ESLint convention). Any other value is a hard error — there is no
|
|
1755
|
+
// silent fallback so typos are surfaced immediately.
|
|
1628
1756
|
func parseSeverity(v any) (Severity, error) {
|
|
1629
1757
|
switch x := v.(type) {
|
|
1630
1758
|
case string:
|
|
@@ -1651,6 +1779,9 @@ func parseSeverity(v any) (Severity, error) {
|
|
|
1651
1779
|
return SeverityOff, fmt.Errorf("severity must be one of: off | warn | warning | error | 0 | 1 | 2, got %T", v)
|
|
1652
1780
|
}
|
|
1653
1781
|
|
|
1782
|
+
// sortedRuleNames returns the sorted slice of rule names from `config` for
|
|
1783
|
+
// which `include` returns true. Sorting ensures deterministic dispatch-table
|
|
1784
|
+
// ordering so test output and diagnostic ordering are stable across runs.
|
|
1654
1785
|
func sortedRuleNames(config RuleConfig, include func(Severity) bool) []string {
|
|
1655
1786
|
names := make([]string, 0, len(config))
|
|
1656
1787
|
for name, sev := range config {
|
|
@@ -1662,6 +1793,12 @@ func sortedRuleNames(config RuleConfig, include func(Severity) bool) []string {
|
|
|
1662
1793
|
return names
|
|
1663
1794
|
}
|
|
1664
1795
|
|
|
1796
|
+
// matchAnyPattern reports whether `fileName` matches at least one of the
|
|
1797
|
+
// provided glob patterns. If baseDir is non-empty, both paths are made
|
|
1798
|
+
// absolute before computing a relative path so that glob patterns rooted at
|
|
1799
|
+
// the config file's directory match correctly regardless of the process cwd.
|
|
1800
|
+
// Files outside the base directory never match (the relative path would start
|
|
1801
|
+
// with "..").
|
|
1665
1802
|
func matchAnyPattern(baseDir string, patterns []string, fileName string) bool {
|
|
1666
1803
|
rel := filepath.ToSlash(fileName)
|
|
1667
1804
|
if baseDir != "" {
|
|
@@ -1689,6 +1826,10 @@ func matchAnyPattern(baseDir string, patterns []string, fileName string) bool {
|
|
|
1689
1826
|
return false
|
|
1690
1827
|
}
|
|
1691
1828
|
|
|
1829
|
+
// normalizeGlobPattern normalizes a user-supplied glob pattern to forward
|
|
1830
|
+
// slashes and strips a leading "./". Patterns that contain no slash are treated
|
|
1831
|
+
// as basename-only globs by prepending "**/" so that `*.ts` matches any
|
|
1832
|
+
// TypeScript file regardless of directory depth, matching ESLint's behavior.
|
|
1692
1833
|
func normalizeGlobPattern(pattern string) string {
|
|
1693
1834
|
pattern = filepath.ToSlash(pattern)
|
|
1694
1835
|
pattern = strings.TrimPrefix(pattern, "./")
|
|
@@ -1698,6 +1839,10 @@ func normalizeGlobPattern(pattern string) string {
|
|
|
1698
1839
|
return pattern
|
|
1699
1840
|
}
|
|
1700
1841
|
|
|
1842
|
+
// matchGlob tests whether `name` matches `pattern` using the ESLint-compatible
|
|
1843
|
+
// glob semantics implemented by matchGlobParts. Both strings are trimmed of
|
|
1844
|
+
// leading/trailing slashes before splitting on "/" so that empty segments do
|
|
1845
|
+
// not appear in the part slices.
|
|
1701
1846
|
func matchGlob(pattern, name string) bool {
|
|
1702
1847
|
pattern = strings.Trim(pattern, "/")
|
|
1703
1848
|
name = strings.Trim(name, "/")
|
|
@@ -1712,6 +1857,10 @@ func matchGlob(pattern, name string) bool {
|
|
|
1712
1857
|
return matchGlobParts(patternParts, nameParts)
|
|
1713
1858
|
}
|
|
1714
1859
|
|
|
1860
|
+
// matchGlobParts recursively matches path segments against pattern segments.
|
|
1861
|
+
// A "**" segment matches zero or more path segments (greedy: tries zero first,
|
|
1862
|
+
// then each successive prefix) so that `**/*.ts` matches both `a.ts` and
|
|
1863
|
+
// `dir/a.ts`.
|
|
1715
1864
|
func matchGlobParts(patternParts, nameParts []string) bool {
|
|
1716
1865
|
if len(patternParts) == 0 {
|
|
1717
1866
|
return len(nameParts) == 0
|