@ttsc/lint 0.18.4 → 0.19.1
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 +125 -43
- package/go.mod +5 -4
- package/internal/rulecode/rulecode.go +88 -0
- package/lib/index.d.ts +10 -1
- package/lib/index.js +45 -17
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +3 -0
- package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
- package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
- package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
- package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
- package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
- package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
- package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
- package/lib/structures/rules/index.d.ts +3 -0
- package/lib/structures/rules/index.js +3 -0
- package/lib/structures/rules/index.js.map +1 -1
- package/linthost/ast_helpers.go +521 -23
- package/linthost/ast_selector_matcher.go +1628 -0
- package/linthost/ast_selector_parser.go +628 -0
- package/linthost/comment_scan.go +154 -0
- package/linthost/compile.go +129 -79
- package/linthost/config.go +352 -88
- package/linthost/config_format.go +15 -1
- package/linthost/contrib_adapter.go +47 -8
- package/linthost/declaration_rules.go +12 -11
- package/linthost/directives.go +9 -67
- package/linthost/dispatch.go +37 -3
- package/linthost/engine.go +368 -53
- package/linthost/escape_string.go +77 -0
- package/linthost/fix.go +133 -23
- package/linthost/flags_gen.go +18 -17
- package/linthost/format.go +73 -167
- package/linthost/format_editor_settings.go +134 -26
- package/linthost/grapheme_tables_gen.go +1920 -0
- package/linthost/host.go +159 -61
- package/linthost/literal_escape_scan.go +117 -0
- package/linthost/lsp.go +545 -98
- package/linthost/numeric_literal_precision.go +228 -0
- package/linthost/polyfill_browserslist.go +1439 -0
- package/linthost/polyfill_browserslist_config.go +603 -0
- package/linthost/polyfill_corejs.go +750 -0
- package/linthost/polyfill_data.go +113 -0
- package/linthost/polyfill_data_gen.json +39868 -0
- package/linthost/print_nodes_array.go +7 -1
- package/linthost/print_nodes_object.go +7 -1
- package/linthost/project_engine.go +231 -0
- package/linthost/project_rules.go +78 -0
- package/linthost/regex_clean.go +191 -0
- package/linthost/regex_tree.go +1544 -0
- package/linthost/regex_tree_optimizer.go +1685 -0
- package/linthost/regexp_cache.go +44 -0
- package/linthost/rule_codes.go +105 -0
- package/linthost/rule_codes.json +745 -0
- package/linthost/rules_ban_ts_comment.go +312 -0
- package/linthost/rules_boundaries.go +128 -43
- package/linthost/rules_boundaries_dependencies.go +1013 -0
- package/linthost/rules_core_extra.go +62 -92
- package/linthost/rules_cypress.go +1 -1
- package/linthost/rules_default_case.go +87 -11
- package/linthost/rules_dupes.go +38 -13
- package/linthost/rules_empty.go +188 -20
- package/linthost/rules_escape.go +62 -48
- package/linthost/rules_format_arrow_parens.go +19 -5
- package/linthost/rules_format_bracket_spacing.go +1 -1
- package/linthost/rules_format_clause_join.go +1 -1
- package/linthost/rules_format_declaration_header.go +31 -19
- package/linthost/rules_format_indent.go +1 -1
- package/linthost/rules_format_jsdoc.go +1 -1
- package/linthost/rules_format_orphan_semi.go +1 -1
- package/linthost/rules_format_parameter_properties.go +17 -7
- package/linthost/rules_format_print_width.go +1 -1
- package/linthost/rules_format_quote_props.go +1 -1
- package/linthost/rules_format_quotes.go +1 -1
- package/linthost/rules_format_semi.go +106 -79
- package/linthost/rules_format_sort_imports.go +79 -45
- package/linthost/rules_format_statement_split.go +1 -1
- package/linthost/rules_format_ternary_nullish_parens.go +1 -1
- package/linthost/rules_format_trailing_comma.go +64 -1
- package/linthost/rules_format_whitespace.go +1 -1
- package/linthost/rules_functional.go +21 -22
- package/linthost/rules_gap.go +80 -3
- package/linthost/rules_grouped_accessor_pairs.go +84 -25
- package/linthost/rules_guard_for_in.go +64 -147
- package/linthost/rules_jsx_a11y.go +232 -22
- package/linthost/rules_logic.go +84 -38
- package/linthost/rules_nextjs.go +105 -16
- package/linthost/rules_no_class_assign.go +70 -0
- package/linthost/rules_no_duplicate_imports.go +266 -0
- package/linthost/rules_no_else_return.go +156 -38
- package/linthost/rules_no_extend_native.go +113 -17
- package/linthost/rules_no_fallthrough.go +1013 -0
- package/linthost/rules_no_func_assign.go +70 -0
- package/linthost/rules_no_import_assign.go +278 -86
- package/linthost/rules_no_inner_declarations.go +186 -0
- package/linthost/rules_no_loop_func.go +438 -53
- package/linthost/rules_no_mixed_operators.go +208 -52
- package/linthost/rules_no_param_reassign.go +376 -61
- package/linthost/rules_no_promise_executor_return.go +156 -0
- package/linthost/rules_no_restricted_imports.go +1013 -57
- package/linthost/rules_no_restricted_syntax.go +190 -26
- package/linthost/rules_no_useless_constructor.go +6 -36
- package/linthost/rules_problems.go +154 -311
- package/linthost/rules_promise.go +2578 -109
- package/linthost/rules_react_extras.go +71 -45
- package/linthost/rules_react_hooks.go +18 -2
- package/linthost/rules_react_perf.go +1 -0
- package/linthost/rules_react_refresh.go +1 -1
- package/linthost/rules_regexp.go +230 -14
- package/linthost/rules_security.go +45 -19
- package/linthost/rules_self.go +21 -1
- package/linthost/rules_solid.go +13 -7
- package/linthost/rules_storybook.go +1 -1
- package/linthost/rules_suggestions.go +490 -84
- package/linthost/rules_testing_library.go +12 -1
- package/linthost/rules_ts.go +227 -54
- package/linthost/rules_ts_async.go +0 -131
- package/linthost/rules_ts_no_base_to_string.go +3 -4
- package/linthost/rules_ts_no_misused_promises.go +817 -0
- package/linthost/rules_ts_no_restricted_types.go +320 -34
- package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
- package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
- package/linthost/rules_ts_require_array_sort_compare.go +5 -5
- package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
- package/linthost/rules_unicorn_better_regex.go +224 -16
- package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
- package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
- package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
- package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
- package/linthost/rules_unicorn_escape_case.go +30 -19
- package/linthost/rules_unicorn_filename_case.go +763 -16
- package/linthost/rules_unicorn_import_style.go +783 -17
- package/linthost/rules_unicorn_isolated_functions.go +934 -14
- package/linthost/rules_unicorn_no_hex_escape.go +26 -11
- package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
- package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
- package/linthost/rules_unicorn_no_unused_properties.go +756 -19
- package/linthost/rules_unicorn_number_literal_case.go +58 -39
- package/linthost/rules_unicorn_prefer_at.go +7 -3
- package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
- package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
- package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
- package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
- package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
- package/linthost/rules_unicorn_string_content.go +611 -15
- package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
- package/linthost/rules_unicorn_template_indent.go +923 -18
- package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
- package/linthost/rules_unicorn_throw_new_error.go +218 -26
- package/linthost/rules_var.go +829 -142
- package/linthost/string_length.go +222 -0
- package/package.json +4 -3
- package/rule/project.go +206 -0
- package/rule/rule.go +58 -1
- package/src/index.ts +58 -17
- package/src/structures/ITtscLintConfig.ts +3 -0
- package/src/structures/TtscLintRuleSetting.ts +3 -2
- package/src/structures/format/ITtscLintFormat.ts +3 -2
- package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
- package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
- package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
- package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
- package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
- package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
- package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
- package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
- package/src/structures/rules/ITtscLintRules.ts +7 -2
- package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
- package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
- package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
- package/src/structures/rules/index.ts +3 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// escape_string.go ports eslint-plugin-unicorn's shared `escapeString` helper
|
|
2
|
+
// (`rules/utils/escape-string.js`), which delegates to the `quote-js-string`
|
|
3
|
+
// package. Every unicorn rule that rewrites a string literal from its cooked
|
|
4
|
+
// value routes the replacement through it — `unicorn/better-regex`'s
|
|
5
|
+
// `new RegExp("pattern", "flags")` branch and `unicorn/string-content`'s
|
|
6
|
+
// literal branch — so the two share one port here rather than drifting apart.
|
|
7
|
+
//
|
|
8
|
+
// quote-js-string source: https://github.com/sindresorhus/quote-js-string
|
|
9
|
+
package linthost
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"strconv"
|
|
13
|
+
"strings"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// escapeStringIsUnsafeRune reports the code points quote-js-string treats as
|
|
17
|
+
// unsafe to write raw into a JavaScript string literal: the C0 control range
|
|
18
|
+
// (<= U+001F), DEL (U+007F), the U+2028 / U+2029 line and paragraph separators
|
|
19
|
+
// (legal raw since ES2019, but line-oriented tooling still renders the literal
|
|
20
|
+
// as broken across lines), and lone surrogates (no valid UTF-8 encoding).
|
|
21
|
+
func escapeStringIsUnsafeRune(codePoint rune) bool {
|
|
22
|
+
return codePoint <= 0x1F ||
|
|
23
|
+
codePoint == 0x7F ||
|
|
24
|
+
codePoint == 0x2028 ||
|
|
25
|
+
codePoint == 0x2029 ||
|
|
26
|
+
(codePoint >= 0xD800 && codePoint <= 0xDFFF)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// escapeString wraps `value` in `quote` (`'` or `"`) the way upstream's
|
|
30
|
+
// escapeString helper does. It escapes the backslash, the delimiter quote, and
|
|
31
|
+
// every unsafe code point — writing the named escapes `\n \r \t \b \f \v` where
|
|
32
|
+
// they apply and a braced ES6 code-point escape `\u{HEX}` (lowercase hex,
|
|
33
|
+
// `\u{0}` for NUL) for the rest. Escaping the line terminators is what keeps a
|
|
34
|
+
// cooked LF or CR from closing the literal early and corrupting the file.
|
|
35
|
+
//
|
|
36
|
+
// All other text passes through raw, exactly as upstream writes it: the
|
|
37
|
+
// non-delimiter quote characters, C1 controls above DEL, non-ASCII letters,
|
|
38
|
+
// exotic Unicode whitespace such as NBSP and the U+2000-U+200A group, and
|
|
39
|
+
// astral symbols. Lone surrogates cannot reach this function — the tsgo scanner
|
|
40
|
+
// cooks them to U+FFFD, which is safe and passes through like any other text.
|
|
41
|
+
func escapeString(value string, quote byte) string {
|
|
42
|
+
var out strings.Builder
|
|
43
|
+
out.Grow(len(value) + 2)
|
|
44
|
+
out.WriteByte(quote)
|
|
45
|
+
for _, character := range value {
|
|
46
|
+
switch {
|
|
47
|
+
case character == '\\':
|
|
48
|
+
out.WriteString(`\\`)
|
|
49
|
+
case character == rune(quote):
|
|
50
|
+
out.WriteByte('\\')
|
|
51
|
+
out.WriteByte(quote)
|
|
52
|
+
case escapeStringIsUnsafeRune(character):
|
|
53
|
+
switch character {
|
|
54
|
+
case '\n':
|
|
55
|
+
out.WriteString(`\n`)
|
|
56
|
+
case '\r':
|
|
57
|
+
out.WriteString(`\r`)
|
|
58
|
+
case '\t':
|
|
59
|
+
out.WriteString(`\t`)
|
|
60
|
+
case '\b':
|
|
61
|
+
out.WriteString(`\b`)
|
|
62
|
+
case '\f':
|
|
63
|
+
out.WriteString(`\f`)
|
|
64
|
+
case '\v':
|
|
65
|
+
out.WriteString(`\v`)
|
|
66
|
+
default:
|
|
67
|
+
out.WriteString(`\u{`)
|
|
68
|
+
out.WriteString(strconv.FormatInt(int64(character), 16))
|
|
69
|
+
out.WriteString(`}`)
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
out.WriteRune(character)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
out.WriteByte(quote)
|
|
76
|
+
return out.String()
|
|
77
|
+
}
|
package/linthost/fix.go
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Autofix orchestration for the `@ttsc/lint fix` subcommand.
|
|
2
2
|
//
|
|
3
3
|
// RunFix drives the fix cascade: it repeatedly runs the native lint engine
|
|
4
|
-
// and applies any emitted TextEdit
|
|
4
|
+
// and applies any emitted automatic TextEdit fixes until no more fixable
|
|
5
5
|
// findings remain or maxFixPasses is reached. After the cascade settles, it
|
|
6
6
|
// runs a final diagnostic pass so remaining issues are surfaced in the
|
|
7
7
|
// normal error stream.
|
|
@@ -43,14 +43,24 @@ func runFix(opts *subcommandOpts) int {
|
|
|
43
43
|
fmt.Fprintln(os.Stderr, err)
|
|
44
44
|
return 2
|
|
45
45
|
}
|
|
46
|
-
// `ttsc fix`
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
46
|
+
// `ttsc fix` runs lint autofixes AND format-rule edits in one pass. Formatting
|
|
47
|
+
// is configured solely through the `format` block (a format/* key in `rules`
|
|
48
|
+
// is dropped), so wrap the resolver in formatCommandResolver to force-activate
|
|
49
|
+
// the format rules that block configured — the same promotion `ttsc format`
|
|
50
|
+
// applies to a configured block.
|
|
51
|
+
//
|
|
52
|
+
// Deliberately NOT newFormatCommandResolver: fix supplies no defaultOptions, so
|
|
53
|
+
// a project with no `format` block enables no format rules and fix stays a pure
|
|
54
|
+
// lint pass. Loading the always-on default formatter is `ttsc format`'s job;
|
|
55
|
+
// doing it here would reformat every file during a plain lint fix. This
|
|
56
|
+
// divergence is the contract pinned by
|
|
57
|
+
// command_fix_skips_default_formatting_that_format_applies_test.go and the
|
|
58
|
+
// website lint docs.
|
|
53
59
|
engine := NewEngineWithResolver(formatCommandResolver{inner: rules})
|
|
60
|
+
if err := engine.ConfigError(); err != nil {
|
|
61
|
+
fmt.Fprintln(os.Stderr, err)
|
|
62
|
+
return 2
|
|
63
|
+
}
|
|
54
64
|
engine.SetSerial(opts.singleThreaded)
|
|
55
65
|
needsRuleChecker := engine.NeedsTypeChecker()
|
|
56
66
|
|
|
@@ -73,7 +83,7 @@ func runFix(opts *subcommandOpts) int {
|
|
|
73
83
|
// kinds of findings in one pass — no filtering needed here.
|
|
74
84
|
cascadeConverged := false
|
|
75
85
|
for pass := 0; pass < maxFixPasses; pass++ {
|
|
76
|
-
findings :=
|
|
86
|
+
findings := prog.runLintCycle(engine)
|
|
77
87
|
fixed, err := applyFindingFixes(opts.cwd, findings)
|
|
78
88
|
if err != nil {
|
|
79
89
|
fmt.Fprintln(os.Stderr, err)
|
|
@@ -133,6 +143,7 @@ func loadFixProgram(opts *subcommandOpts, needsRuleChecker bool) (*program, int)
|
|
|
133
143
|
singleThreaded: opts.singleThreaded,
|
|
134
144
|
checkers: opts.checkers,
|
|
135
145
|
tsgoArgs: opts.tsgoArgs,
|
|
146
|
+
projectIdentity: opts.projectIdentity,
|
|
136
147
|
})
|
|
137
148
|
if err != nil {
|
|
138
149
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
|
|
@@ -155,13 +166,14 @@ func reloadFixProgram(current *program, opts *subcommandOpts, needsRuleChecker b
|
|
|
155
166
|
return loadFixProgram(opts, needsRuleChecker)
|
|
156
167
|
}
|
|
157
168
|
|
|
158
|
-
// fileFixes groups all pending TextEdit
|
|
159
|
-
//
|
|
160
|
-
//
|
|
169
|
+
// fileFixes groups all pending automatic TextEdit fixes for a single file,
|
|
170
|
+
// one edit group per finding so a multi-edit atomic fix stays all-or-nothing
|
|
171
|
+
// during selection. `text` is the source content at the time the findings were
|
|
172
|
+
// collected; byte offsets in `groups` are relative to this snapshot.
|
|
161
173
|
type fileFixes struct {
|
|
162
|
-
path
|
|
163
|
-
text
|
|
164
|
-
|
|
174
|
+
path string
|
|
175
|
+
text string
|
|
176
|
+
groups [][]TextEdit
|
|
165
177
|
}
|
|
166
178
|
|
|
167
179
|
// applyFindingFixes groups all fixable findings by file, resolves each
|
|
@@ -189,7 +201,7 @@ func applyFindingFixes(cwd string, findings []*Finding) (int, error) {
|
|
|
189
201
|
bucket = &fileFixes{path: path, text: finding.File.Text()}
|
|
190
202
|
byFile[path] = bucket
|
|
191
203
|
}
|
|
192
|
-
bucket.
|
|
204
|
+
bucket.groups = append(bucket.groups, finding.Fix)
|
|
193
205
|
}
|
|
194
206
|
|
|
195
207
|
paths := make([]string, 0, len(byFile))
|
|
@@ -200,7 +212,7 @@ func applyFindingFixes(cwd string, findings []*Finding) (int, error) {
|
|
|
200
212
|
total := 0
|
|
201
213
|
for _, p := range paths {
|
|
202
214
|
bucket := byFile[p]
|
|
203
|
-
fixed, err := applyTextEditsToFile(bucket.path, bucket.text, bucket.
|
|
215
|
+
fixed, err := applyTextEditsToFile(bucket.path, bucket.text, bucket.groups)
|
|
204
216
|
if err != nil {
|
|
205
217
|
return total, err
|
|
206
218
|
}
|
|
@@ -209,12 +221,13 @@ func applyFindingFixes(cwd string, findings []*Finding) (int, error) {
|
|
|
209
221
|
return total, nil
|
|
210
222
|
}
|
|
211
223
|
|
|
212
|
-
// applyTextEditsToFile selects
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// applied, or 0 when no edits
|
|
216
|
-
|
|
217
|
-
|
|
224
|
+
// applyTextEditsToFile selects a non-overlapping, per-finding-atomic set of
|
|
225
|
+
// edits from `groups` (one group per finding), applies them to `source` in
|
|
226
|
+
// reverse order (right-to-left) to preserve earlier offsets, and writes the
|
|
227
|
+
// result to `path`. Returns the number of edits applied, or 0 when no edits
|
|
228
|
+
// survive selection.
|
|
229
|
+
func applyTextEditsToFile(path, source string, groups [][]TextEdit) (int, error) {
|
|
230
|
+
selected := selectTextEditGroups(len(source), groups)
|
|
218
231
|
if len(selected) == 0 {
|
|
219
232
|
return 0, nil
|
|
220
233
|
}
|
|
@@ -292,3 +305,100 @@ func selectTextEdits(sourceLen int, edits []TextEdit) []TextEdit {
|
|
|
292
305
|
}
|
|
293
306
|
return selected
|
|
294
307
|
}
|
|
308
|
+
|
|
309
|
+
// selectTextEditGroups selects a non-overlapping application sequence from
|
|
310
|
+
// per-finding edit GROUPS, applying each group all-or-nothing. Unlike
|
|
311
|
+
// selectTextEdits, which resolves conflicts edit-by-edit, this keeps a
|
|
312
|
+
// finding's multi-edit fix atomic: a fix such as noImportTypeSideEffects emits
|
|
313
|
+
// an `import type` insert paired with an inline `type` deletion, and applying
|
|
314
|
+
// only one member emits invalid code (`import type { type A }`). Groups are
|
|
315
|
+
// considered earliest-edit-first, and a group is accepted only when every one
|
|
316
|
+
// of its edits coexists — under the same disjointness rules as selectTextEdits
|
|
317
|
+
// — with the edits already accepted from earlier groups. If any member would be
|
|
318
|
+
// dropped (it overlaps an already-selected group, duplicates an already-
|
|
319
|
+
// selected edit, is a coincident zero-width insert, or is out of bounds), the
|
|
320
|
+
// WHOLE group is skipped so the owning finding re-fires on the next cascade
|
|
321
|
+
// pass rather than half-applying (samchon/ttsc#605).
|
|
322
|
+
func selectTextEditGroups(sourceLen int, groups [][]TextEdit) []TextEdit {
|
|
323
|
+
order := make([]int, 0, len(groups))
|
|
324
|
+
for i := range groups {
|
|
325
|
+
if len(groups[i]) > 0 {
|
|
326
|
+
order = append(order, i)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
sort.SliceStable(order, func(a, b int) bool {
|
|
330
|
+
return textEditGroupLess(groups[order[a]], groups[order[b]])
|
|
331
|
+
})
|
|
332
|
+
selected := make([]TextEdit, 0)
|
|
333
|
+
for _, index := range order {
|
|
334
|
+
group := dedupeTextEdits(groups[index])
|
|
335
|
+
candidate := make([]TextEdit, 0, len(selected)+len(group))
|
|
336
|
+
candidate = append(candidate, selected...)
|
|
337
|
+
candidate = append(candidate, group...)
|
|
338
|
+
kept := selectTextEdits(sourceLen, candidate)
|
|
339
|
+
// Accept the group only if nothing was dropped: a shorter result means one
|
|
340
|
+
// of its edits collided with an already-selected edit (or a sibling edit of
|
|
341
|
+
// its own group), so the group cannot apply atomically here.
|
|
342
|
+
if len(kept) == len(candidate) {
|
|
343
|
+
selected = kept
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return selected
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// dedupeTextEdits removes exact-duplicate edits while preserving order. A
|
|
350
|
+
// finding that emits the same edit twice still applies its distinct edits;
|
|
351
|
+
// deduping here keeps such a finding from being rejected as "internally
|
|
352
|
+
// inconsistent" by selectTextEditGroups, since an exact duplicate is not a
|
|
353
|
+
// conflict the way an overlap is.
|
|
354
|
+
func dedupeTextEdits(edits []TextEdit) []TextEdit {
|
|
355
|
+
if len(edits) == 0 {
|
|
356
|
+
return nil
|
|
357
|
+
}
|
|
358
|
+
seen := make(map[TextEdit]struct{}, len(edits))
|
|
359
|
+
out := make([]TextEdit, 0, len(edits))
|
|
360
|
+
for _, edit := range edits {
|
|
361
|
+
if _, ok := seen[edit]; ok {
|
|
362
|
+
continue
|
|
363
|
+
}
|
|
364
|
+
seen[edit] = struct{}{}
|
|
365
|
+
out = append(out, edit)
|
|
366
|
+
}
|
|
367
|
+
return out
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// textEditGroupLess orders edit groups by their earliest member under the same
|
|
371
|
+
// (Pos, End, Text) key selectTextEdits sorts by, so selection is deterministic
|
|
372
|
+
// and the earliest-starting finding wins a contested range.
|
|
373
|
+
func textEditGroupLess(a, b []TextEdit) bool {
|
|
374
|
+
ea, eb := minTextEdit(a), minTextEdit(b)
|
|
375
|
+
if ea.Pos != eb.Pos {
|
|
376
|
+
return ea.Pos < eb.Pos
|
|
377
|
+
}
|
|
378
|
+
if ea.End != eb.End {
|
|
379
|
+
return ea.End < eb.End
|
|
380
|
+
}
|
|
381
|
+
return ea.Text < eb.Text
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// minTextEdit returns the least edit of a non-empty group under the
|
|
385
|
+
// (Pos, End, Text) order. Panics on an empty slice; callers filter empties out.
|
|
386
|
+
func minTextEdit(edits []TextEdit) TextEdit {
|
|
387
|
+
least := edits[0]
|
|
388
|
+
for _, edit := range edits[1:] {
|
|
389
|
+
if textEditLess(edit, least) {
|
|
390
|
+
least = edit
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return least
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
func textEditLess(a, b TextEdit) bool {
|
|
397
|
+
if a.Pos != b.Pos {
|
|
398
|
+
return a.Pos < b.Pos
|
|
399
|
+
}
|
|
400
|
+
if a.End != b.End {
|
|
401
|
+
return a.End < b.End
|
|
402
|
+
}
|
|
403
|
+
return a.Text < b.Text
|
|
404
|
+
}
|
package/linthost/flags_gen.go
CHANGED
|
@@ -13,21 +13,22 @@ package linthost
|
|
|
13
13
|
// Generated from packages/ttsc/src/flags/schema.ts. Edit the schema, not
|
|
14
14
|
// this file.
|
|
15
15
|
var LintFlagAllowList = map[string]bool{
|
|
16
|
-
"checkers":
|
|
17
|
-
"cwd":
|
|
18
|
-
"diagnostics":
|
|
19
|
-
"emit":
|
|
20
|
-
"extendedDiagnostics":
|
|
21
|
-
"file":
|
|
22
|
-
"noEmit":
|
|
23
|
-
"out":
|
|
24
|
-
"outDir":
|
|
25
|
-
"p":
|
|
26
|
-
"plugins-json":
|
|
27
|
-
"project":
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
16
|
+
"checkers": true,
|
|
17
|
+
"cwd": true,
|
|
18
|
+
"diagnostics": false,
|
|
19
|
+
"emit": false,
|
|
20
|
+
"extendedDiagnostics": false,
|
|
21
|
+
"file": true,
|
|
22
|
+
"noEmit": false,
|
|
23
|
+
"out": true,
|
|
24
|
+
"outDir": true,
|
|
25
|
+
"p": true,
|
|
26
|
+
"plugins-json": true,
|
|
27
|
+
"project": true,
|
|
28
|
+
"project-context-json": true,
|
|
29
|
+
"quiet": false,
|
|
30
|
+
"singleThreaded": false,
|
|
31
|
+
"tsconfig": true,
|
|
32
|
+
"tsgo-args": true,
|
|
33
|
+
"verbose": false,
|
|
33
34
|
}
|
package/linthost/format.go
CHANGED
|
@@ -48,6 +48,10 @@ func runFormat(opts *subcommandOpts) int {
|
|
|
48
48
|
return 2
|
|
49
49
|
}
|
|
50
50
|
engine := NewEngineWithResolver(resolver)
|
|
51
|
+
if err := engine.ConfigError(); err != nil {
|
|
52
|
+
fmt.Fprintln(os.Stderr, err)
|
|
53
|
+
return 2
|
|
54
|
+
}
|
|
51
55
|
engine.SetSerial(opts.singleThreaded)
|
|
52
56
|
needsRuleChecker := engine.NeedsTypeChecker()
|
|
53
57
|
|
|
@@ -64,7 +68,7 @@ func runFormat(opts *subcommandOpts) int {
|
|
|
64
68
|
totalFixes := 0
|
|
65
69
|
cascadeConverged := false
|
|
66
70
|
for pass := 0; pass < maxFormatPasses; pass++ {
|
|
67
|
-
findings :=
|
|
71
|
+
findings := prog.runLintCycle(engine)
|
|
68
72
|
fixed, err := applyFindingFixes(opts.cwd, filterFormatFindings(findings))
|
|
69
73
|
if err != nil {
|
|
70
74
|
fmt.Fprintln(os.Stderr, err)
|
|
@@ -115,15 +119,6 @@ type formatCommandResolver struct {
|
|
|
115
119
|
// around as a value) shares the same underlying cache rather than copying a
|
|
116
120
|
// sync.Once lock.
|
|
117
121
|
ruleNames *formatRuleNamesCache
|
|
118
|
-
// entryDecisions memoizes the per-fileName ignored/matches decision that
|
|
119
|
-
// ResolveRules derives from fileIsIgnoredByEntry and fileMatchesAnyEntry.
|
|
120
|
-
// Both walk store.entries with glob matching and are recomputed identically
|
|
121
|
-
// for the same fileName on every format pass, so the result is cached once
|
|
122
|
-
// per file and reused. The field is a pointer (a *sync.Map) so the nil zero
|
|
123
|
-
// value stays valid for construction sites that omit it, and copying the
|
|
124
|
-
// resolver by value shares the same underlying map. sync.Map handles the
|
|
125
|
-
// engine's concurrent per-file ResolveRules calls without an extra lock.
|
|
126
|
-
entryDecisions *sync.Map
|
|
127
122
|
// defaultOptions holds the always-on format rules' options (from
|
|
128
123
|
// expandFormatBlock) to apply when the project config declares no `format`
|
|
129
124
|
// block — no block in lint.config.*, or no config file at all. nil when a
|
|
@@ -142,9 +137,8 @@ type formatCommandResolver struct {
|
|
|
142
137
|
// defaultOptions nil so the block stays authoritative.
|
|
143
138
|
func newFormatCommandResolver(inner RuleResolver, startDir string, language string) (formatCommandResolver, error) {
|
|
144
139
|
r := formatCommandResolver{
|
|
145
|
-
inner:
|
|
146
|
-
ruleNames:
|
|
147
|
-
entryDecisions: &sync.Map{},
|
|
140
|
+
inner: inner,
|
|
141
|
+
ruleNames: &formatRuleNamesCache{},
|
|
148
142
|
}
|
|
149
143
|
if !hasInnerFormatRules(inner) {
|
|
150
144
|
opts, err := defaultFormatOptions(editorFormatOverrides(startDir, language))
|
|
@@ -204,176 +198,52 @@ type formatRuleNamesCache struct {
|
|
|
204
198
|
names []string
|
|
205
199
|
}
|
|
206
200
|
|
|
207
|
-
// formatEntryDecision is the cached per-fileName outcome of the two entry-scope
|
|
208
|
-
// checks ResolveRules runs before applying its format-rule upgrade.
|
|
209
|
-
type formatEntryDecision struct {
|
|
210
|
-
ignoredByEntry bool
|
|
211
|
-
matchesEntry bool
|
|
212
|
-
}
|
|
213
|
-
|
|
214
201
|
// ResolveRules implements RuleResolver. It delegates to the inner resolver
|
|
215
202
|
// and then upgrades format-rule entries from off to warn so they are applied
|
|
216
203
|
// even when the project config omits them.
|
|
217
204
|
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// asked the lint config to ignore — the engine's lint walk would skip the
|
|
224
|
-
// file but the formatter would still touch it.
|
|
225
|
-
//
|
|
226
|
-
// The symmetric guard applies to `files`: if every non-IgnoreOnly entry
|
|
227
|
-
// carries a `files` filter and `fileName` matches none of them, the engine
|
|
228
|
-
// would not run any rules on the file (`ConfigEntry.matchesFile` returns
|
|
229
|
-
// false for every entry). The format command must skip its blanket
|
|
230
|
-
// format-rule upgrade for the same reason — otherwise `ttsc format` would
|
|
231
|
-
// rewrite files that fall outside every entry's scope, e.g. a `.json`
|
|
232
|
-
// resolved into the program via `resolveJsonModule` when the only entry
|
|
233
|
-
// targets `src/**/*.ts`.
|
|
205
|
+
// ConfigStore resolves entry applicability together with rules and options.
|
|
206
|
+
// A global ignore skips the file, while OutOfScope skips only files rejected
|
|
207
|
+
// by every rule-bearing entry. An ignore on one entry therefore cannot erase a
|
|
208
|
+
// separate matching entry's contribution. The flags also survive resolver
|
|
209
|
+
// wrappers, unlike inspecting a concrete ConfigStore from this outer layer.
|
|
234
210
|
func (r formatCommandResolver) ResolveRules(fileName string) ResolvedRuleConfig {
|
|
235
211
|
resolved := r.inner.ResolveRules(fileName)
|
|
236
|
-
if resolved.Ignored {
|
|
237
|
-
return resolved
|
|
238
|
-
}
|
|
239
|
-
decision := r.entryDecision(fileName)
|
|
240
|
-
if decision.ignoredByEntry {
|
|
241
|
-
return resolved
|
|
242
|
-
}
|
|
243
|
-
if !decision.matchesEntry {
|
|
212
|
+
if resolved.Ignored || resolved.OutOfScope {
|
|
244
213
|
return resolved
|
|
245
214
|
}
|
|
246
215
|
if resolved.Rules == nil {
|
|
247
216
|
resolved.Rules = RuleConfig{}
|
|
248
217
|
}
|
|
218
|
+
if resolved.Options == nil {
|
|
219
|
+
resolved.Options = RuleOptionsMap{}
|
|
220
|
+
}
|
|
249
221
|
for _, name := range r.formatOptionRuleNames() {
|
|
222
|
+
_, declaredForFile := resolved.Rules[normalizeBuiltinRuleName(name)]
|
|
223
|
+
if r.defaultOptions == nil && !declaredForFile {
|
|
224
|
+
// A configured format block is still a normal config entry: its files
|
|
225
|
+
// and ignores selectors scope the complete rule setting. Only the
|
|
226
|
+
// synthetic default set is allowed to introduce an undeclared rule.
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
250
229
|
if resolved.Rules.Severity(name) == SeverityOff {
|
|
251
230
|
resolved.Rules[name] = SeverityWarn
|
|
252
231
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return r.computeEntryDecision(fileName)
|
|
266
|
-
}
|
|
267
|
-
if cached, ok := r.entryDecisions.Load(fileName); ok {
|
|
268
|
-
return cached.(formatEntryDecision)
|
|
269
|
-
}
|
|
270
|
-
decision := r.computeEntryDecision(fileName)
|
|
271
|
-
// LoadOrStore keeps the first writer's value so concurrent passes over the
|
|
272
|
-
// same file agree; the decision is a pure function of fileName, so either
|
|
273
|
-
// value is correct.
|
|
274
|
-
actual, _ := r.entryDecisions.LoadOrStore(fileName, decision)
|
|
275
|
-
return actual.(formatEntryDecision)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// computeEntryDecision runs the two uncached entry-scope checks behind
|
|
279
|
-
// entryDecision.
|
|
280
|
-
func (r formatCommandResolver) computeEntryDecision(fileName string) formatEntryDecision {
|
|
281
|
-
return formatEntryDecision{
|
|
282
|
-
ignoredByEntry: r.fileIsIgnoredByEntry(fileName),
|
|
283
|
-
matchesEntry: r.fileMatchesAnyEntry(fileName),
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// fileIsIgnoredByEntry reports whether any non-IgnoreOnly entry in the inner
|
|
288
|
-
// ConfigStore has an `ignores` glob that matches `fileName`. IgnoreOnly
|
|
289
|
-
// entries are already handled by ResolvedRuleConfig.Ignored — they are
|
|
290
|
-
// checked first by ConfigStore.ResolveRules and short-circuit the walk. This
|
|
291
|
-
// helper covers the complementary case: an entry that carries both `rules`
|
|
292
|
-
// and `ignores`, where the engine skips the entry's rule contributions via
|
|
293
|
-
// `ConfigEntry.matchesFile` but the format command must learn the same fact
|
|
294
|
-
// independently because its job is to upgrade format rules to `warn`, not to
|
|
295
|
-
// read the engine's resolved severity map.
|
|
296
|
-
func (r formatCommandResolver) fileIsIgnoredByEntry(fileName string) bool {
|
|
297
|
-
matched, _ := r.anyNonIgnoreEntry(func(entry *ConfigEntry) bool {
|
|
298
|
-
return entry.matchesIgnores(fileName)
|
|
299
|
-
})
|
|
300
|
-
return matched
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// anyNonIgnoreEntry resolves the inner resolver to its concrete *ConfigStore
|
|
304
|
-
// once and reports whether any non-IgnoreOnly entry satisfies `pred`. The
|
|
305
|
-
// second return value is false when the inner resolver is not a *ConfigStore
|
|
306
|
-
// (or is a nil one), letting each caller pick its own non-store default. This
|
|
307
|
-
// collapses the shared store-resolution and entry-walk that fileIsIgnoredByEntry
|
|
308
|
-
// and fileMatchesAnyEntry would otherwise duplicate; the per-caller default and
|
|
309
|
-
// the empty-entries base case stay in the callers where they differ.
|
|
310
|
-
func (r formatCommandResolver) anyNonIgnoreEntry(pred func(*ConfigEntry) bool) (matched bool, hasStore bool) {
|
|
311
|
-
store, ok := r.inner.(*ConfigStore)
|
|
312
|
-
if !ok || store == nil {
|
|
313
|
-
return false, false
|
|
314
|
-
}
|
|
315
|
-
for i := range store.entries {
|
|
316
|
-
entry := &store.entries[i]
|
|
317
|
-
if entry.IgnoreOnly {
|
|
318
|
-
continue
|
|
232
|
+
if len(resolved.RuleOptions(name)) == 0 {
|
|
233
|
+
var raw json.RawMessage
|
|
234
|
+
if resolved.OptionsResolved {
|
|
235
|
+
raw = r.defaultOptions[name]
|
|
236
|
+
} else {
|
|
237
|
+
// Legacy custom resolvers have no authoritative per-file options, so
|
|
238
|
+
// retain their file-agnostic override before consulting defaults.
|
|
239
|
+
raw = r.RuleOptions(name)
|
|
240
|
+
}
|
|
241
|
+
if len(raw) > 0 {
|
|
242
|
+
resolved.Options[name] = append(json.RawMessage(nil), raw...)
|
|
243
|
+
}
|
|
319
244
|
}
|
|
320
|
-
if pred(entry) {
|
|
321
|
-
return true, true
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return false, true
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// fileMatchesAnyEntry reports whether `fileName` falls inside the `files`
|
|
328
|
-
// scope of at least one non-IgnoreOnly entry. An entry without an explicit
|
|
329
|
-
// `files` list matches every file by definition (eslint flat-config
|
|
330
|
-
// semantics), so a config with any unrestricted non-IgnoreOnly entry returns
|
|
331
|
-
// true for every file. The format command treats a `false` result the same
|
|
332
|
-
// way the engine does: no entry contributes rules, so the blanket
|
|
333
|
-
// format-rule upgrade must be skipped.
|
|
334
|
-
//
|
|
335
|
-
// Base cases:
|
|
336
|
-
//
|
|
337
|
-
// - Empty entries slice: returns `true`. A store with no entries cannot
|
|
338
|
-
// skip a file by scope (the engine has nothing to walk), so format
|
|
339
|
-
// mode must be allowed to apply its default upgrade. Matching engine
|
|
340
|
-
// behavior at `ConfigStore.ResolveRules` for the same input
|
|
341
|
-
// (`Ignored = false`, empty rule map).
|
|
342
|
-
// - All entries are IgnoreOnly: returns `false`. No non-IgnoreOnly
|
|
343
|
-
// entry contributes a `files` scope, so the file is out-of-scope by
|
|
344
|
-
// construction.
|
|
345
|
-
// - Inner resolver is not a *ConfigStore: returns `true` (conservative
|
|
346
|
-
// default). The `files` concept is store-specific; an in-process
|
|
347
|
-
// custom resolver that does not surface a scope cannot have its
|
|
348
|
-
// rule-eligibility reasoned about from the outside, so the format
|
|
349
|
-
// upgrade applies the same as it does for an entry without `files`.
|
|
350
|
-
func (r formatCommandResolver) fileMatchesAnyEntry(fileName string) bool {
|
|
351
|
-
matched, hasStore := r.anyNonIgnoreEntry(func(entry *ConfigEntry) bool {
|
|
352
|
-
return entry.matchesFile(fileName)
|
|
353
|
-
})
|
|
354
|
-
if !hasStore {
|
|
355
|
-
// Not a *ConfigStore: conservative default, see the doc comment above.
|
|
356
|
-
return true
|
|
357
|
-
}
|
|
358
|
-
// A store with no entries (or only IgnoreOnly ones that the walk skipped)
|
|
359
|
-
// produces matched == false; an empty entries slice must still return true
|
|
360
|
-
// so format mode applies its default upgrade, matching ConfigStore behavior.
|
|
361
|
-
// The all-IgnoreOnly case is distinguished by a non-empty entries slice.
|
|
362
|
-
if !matched && len(r.storeEntries()) == 0 {
|
|
363
|
-
return true
|
|
364
|
-
}
|
|
365
|
-
return matched
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// storeEntries returns the inner resolver's config entries, or nil when the
|
|
369
|
-
// inner resolver is not a *ConfigStore. Used by fileMatchesAnyEntry to tell the
|
|
370
|
-
// empty-entries base case (return true) apart from the all-IgnoreOnly case
|
|
371
|
-
// (return false) after anyNonIgnoreEntry reports no match.
|
|
372
|
-
func (r formatCommandResolver) storeEntries() []ConfigEntry {
|
|
373
|
-
if store, ok := r.inner.(*ConfigStore); ok && store != nil {
|
|
374
|
-
return store.entries
|
|
375
245
|
}
|
|
376
|
-
return
|
|
246
|
+
return resolved
|
|
377
247
|
}
|
|
378
248
|
|
|
379
249
|
// ActiveRuleNames implements RuleResolver. Returns the union of the inner
|
|
@@ -420,6 +290,40 @@ func (r formatCommandResolver) RuleOptions(name string) json.RawMessage {
|
|
|
420
290
|
return nil
|
|
421
291
|
}
|
|
422
292
|
|
|
293
|
+
func (r formatCommandResolver) RuleOptionsVariants(name string) []json.RawMessage {
|
|
294
|
+
variants := resolvedRuleOptionsVariants(r.inner, name)
|
|
295
|
+
defaultOptions := r.defaultOptions[name]
|
|
296
|
+
if len(defaultOptions) == 0 {
|
|
297
|
+
return variants
|
|
298
|
+
}
|
|
299
|
+
effective := make([]json.RawMessage, 0, len(variants)+1)
|
|
300
|
+
defaultIncluded := false
|
|
301
|
+
for _, raw := range variants {
|
|
302
|
+
if len(raw) == 0 {
|
|
303
|
+
raw = defaultOptions
|
|
304
|
+
defaultIncluded = true
|
|
305
|
+
} else if string(raw) == string(defaultOptions) {
|
|
306
|
+
defaultIncluded = true
|
|
307
|
+
}
|
|
308
|
+
effective = append(effective, append(json.RawMessage(nil), raw...))
|
|
309
|
+
}
|
|
310
|
+
if !defaultIncluded {
|
|
311
|
+
// A scoped custom resolver may enumerate only its explicit tuples even
|
|
312
|
+
// though the default remains reachable for every other file.
|
|
313
|
+
effective = append(effective, append(json.RawMessage(nil), defaultOptions...))
|
|
314
|
+
}
|
|
315
|
+
return effective
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ResolveProjectRules forwards project declarations unchanged. Format defaults
|
|
319
|
+
// are file rules and cannot create or scope project-rule state.
|
|
320
|
+
func (r formatCommandResolver) ResolveProjectRules(names []string) (map[string]ProjectRuleSetting, error) {
|
|
321
|
+
if r.inner == nil {
|
|
322
|
+
return RuleConfig{}.ResolveProjectRules(names)
|
|
323
|
+
}
|
|
324
|
+
return r.inner.ResolveProjectRules(names)
|
|
325
|
+
}
|
|
326
|
+
|
|
423
327
|
// formatOptionRuleNames returns the sorted list of rule names from the inner
|
|
424
328
|
// resolver's options that are registered as format rules. These are the rules
|
|
425
329
|
// that formatCommandResolver promotes from off to warn.
|
|
@@ -464,10 +368,12 @@ func (r formatCommandResolver) computeFormatOptionRuleNames() []string {
|
|
|
464
368
|
// options (e.g. bare RuleConfig).
|
|
465
369
|
func resolverOptions(resolver RuleResolver) RuleOptionsMap {
|
|
466
370
|
switch r := resolver.(type) {
|
|
371
|
+
case boundProjectRuleResolver:
|
|
372
|
+
return resolverOptions(r.RuleResolver)
|
|
467
373
|
case InlineRuleResolver:
|
|
468
374
|
return r.Options
|
|
469
375
|
case *ConfigStore:
|
|
470
|
-
return r.
|
|
376
|
+
return r.flattenOptions()
|
|
471
377
|
default:
|
|
472
378
|
return nil
|
|
473
379
|
}
|