@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.
Files changed (188) hide show
  1. package/README.md +125 -43
  2. package/go.mod +5 -4
  3. package/internal/rulecode/rulecode.go +88 -0
  4. package/lib/index.d.ts +10 -1
  5. package/lib/index.js +45 -17
  6. package/lib/index.js.map +1 -1
  7. package/lib/structures/ITtscLintConfig.d.ts +3 -0
  8. package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
  9. package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
  10. package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
  11. package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
  12. package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
  13. package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
  14. package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
  15. package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
  16. package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
  17. package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
  18. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
  19. package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
  20. package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
  21. package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
  22. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
  23. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
  24. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
  25. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
  26. package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
  27. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
  28. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
  29. package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
  30. package/lib/structures/rules/index.d.ts +3 -0
  31. package/lib/structures/rules/index.js +3 -0
  32. package/lib/structures/rules/index.js.map +1 -1
  33. package/linthost/ast_helpers.go +521 -23
  34. package/linthost/ast_selector_matcher.go +1628 -0
  35. package/linthost/ast_selector_parser.go +628 -0
  36. package/linthost/comment_scan.go +154 -0
  37. package/linthost/compile.go +129 -79
  38. package/linthost/config.go +352 -88
  39. package/linthost/config_format.go +15 -1
  40. package/linthost/contrib_adapter.go +47 -8
  41. package/linthost/declaration_rules.go +12 -11
  42. package/linthost/directives.go +9 -67
  43. package/linthost/dispatch.go +37 -3
  44. package/linthost/engine.go +368 -53
  45. package/linthost/escape_string.go +77 -0
  46. package/linthost/fix.go +133 -23
  47. package/linthost/flags_gen.go +18 -17
  48. package/linthost/format.go +73 -167
  49. package/linthost/format_editor_settings.go +134 -26
  50. package/linthost/grapheme_tables_gen.go +1920 -0
  51. package/linthost/host.go +159 -61
  52. package/linthost/literal_escape_scan.go +117 -0
  53. package/linthost/lsp.go +545 -98
  54. package/linthost/numeric_literal_precision.go +228 -0
  55. package/linthost/polyfill_browserslist.go +1439 -0
  56. package/linthost/polyfill_browserslist_config.go +603 -0
  57. package/linthost/polyfill_corejs.go +750 -0
  58. package/linthost/polyfill_data.go +113 -0
  59. package/linthost/polyfill_data_gen.json +39868 -0
  60. package/linthost/print_nodes_array.go +7 -1
  61. package/linthost/print_nodes_object.go +7 -1
  62. package/linthost/project_engine.go +231 -0
  63. package/linthost/project_rules.go +78 -0
  64. package/linthost/regex_clean.go +191 -0
  65. package/linthost/regex_tree.go +1544 -0
  66. package/linthost/regex_tree_optimizer.go +1685 -0
  67. package/linthost/regexp_cache.go +44 -0
  68. package/linthost/rule_codes.go +105 -0
  69. package/linthost/rule_codes.json +745 -0
  70. package/linthost/rules_ban_ts_comment.go +312 -0
  71. package/linthost/rules_boundaries.go +128 -43
  72. package/linthost/rules_boundaries_dependencies.go +1013 -0
  73. package/linthost/rules_core_extra.go +62 -92
  74. package/linthost/rules_cypress.go +1 -1
  75. package/linthost/rules_default_case.go +87 -11
  76. package/linthost/rules_dupes.go +38 -13
  77. package/linthost/rules_empty.go +188 -20
  78. package/linthost/rules_escape.go +62 -48
  79. package/linthost/rules_format_arrow_parens.go +19 -5
  80. package/linthost/rules_format_bracket_spacing.go +1 -1
  81. package/linthost/rules_format_clause_join.go +1 -1
  82. package/linthost/rules_format_declaration_header.go +31 -19
  83. package/linthost/rules_format_indent.go +1 -1
  84. package/linthost/rules_format_jsdoc.go +1 -1
  85. package/linthost/rules_format_orphan_semi.go +1 -1
  86. package/linthost/rules_format_parameter_properties.go +17 -7
  87. package/linthost/rules_format_print_width.go +1 -1
  88. package/linthost/rules_format_quote_props.go +1 -1
  89. package/linthost/rules_format_quotes.go +1 -1
  90. package/linthost/rules_format_semi.go +106 -79
  91. package/linthost/rules_format_sort_imports.go +79 -45
  92. package/linthost/rules_format_statement_split.go +1 -1
  93. package/linthost/rules_format_ternary_nullish_parens.go +1 -1
  94. package/linthost/rules_format_trailing_comma.go +64 -1
  95. package/linthost/rules_format_whitespace.go +1 -1
  96. package/linthost/rules_functional.go +21 -22
  97. package/linthost/rules_gap.go +80 -3
  98. package/linthost/rules_grouped_accessor_pairs.go +84 -25
  99. package/linthost/rules_guard_for_in.go +64 -147
  100. package/linthost/rules_jsx_a11y.go +232 -22
  101. package/linthost/rules_logic.go +84 -38
  102. package/linthost/rules_nextjs.go +105 -16
  103. package/linthost/rules_no_class_assign.go +70 -0
  104. package/linthost/rules_no_duplicate_imports.go +266 -0
  105. package/linthost/rules_no_else_return.go +156 -38
  106. package/linthost/rules_no_extend_native.go +113 -17
  107. package/linthost/rules_no_fallthrough.go +1013 -0
  108. package/linthost/rules_no_func_assign.go +70 -0
  109. package/linthost/rules_no_import_assign.go +278 -86
  110. package/linthost/rules_no_inner_declarations.go +186 -0
  111. package/linthost/rules_no_loop_func.go +438 -53
  112. package/linthost/rules_no_mixed_operators.go +208 -52
  113. package/linthost/rules_no_param_reassign.go +376 -61
  114. package/linthost/rules_no_promise_executor_return.go +156 -0
  115. package/linthost/rules_no_restricted_imports.go +1013 -57
  116. package/linthost/rules_no_restricted_syntax.go +190 -26
  117. package/linthost/rules_no_useless_constructor.go +6 -36
  118. package/linthost/rules_problems.go +154 -311
  119. package/linthost/rules_promise.go +2578 -109
  120. package/linthost/rules_react_extras.go +71 -45
  121. package/linthost/rules_react_hooks.go +18 -2
  122. package/linthost/rules_react_perf.go +1 -0
  123. package/linthost/rules_react_refresh.go +1 -1
  124. package/linthost/rules_regexp.go +230 -14
  125. package/linthost/rules_security.go +45 -19
  126. package/linthost/rules_self.go +21 -1
  127. package/linthost/rules_solid.go +13 -7
  128. package/linthost/rules_storybook.go +1 -1
  129. package/linthost/rules_suggestions.go +490 -84
  130. package/linthost/rules_testing_library.go +12 -1
  131. package/linthost/rules_ts.go +227 -54
  132. package/linthost/rules_ts_async.go +0 -131
  133. package/linthost/rules_ts_no_base_to_string.go +3 -4
  134. package/linthost/rules_ts_no_misused_promises.go +817 -0
  135. package/linthost/rules_ts_no_restricted_types.go +320 -34
  136. package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
  137. package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
  138. package/linthost/rules_ts_require_array_sort_compare.go +5 -5
  139. package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
  140. package/linthost/rules_unicorn_better_regex.go +224 -16
  141. package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
  142. package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
  143. package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
  144. package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
  145. package/linthost/rules_unicorn_escape_case.go +30 -19
  146. package/linthost/rules_unicorn_filename_case.go +763 -16
  147. package/linthost/rules_unicorn_import_style.go +783 -17
  148. package/linthost/rules_unicorn_isolated_functions.go +934 -14
  149. package/linthost/rules_unicorn_no_hex_escape.go +26 -11
  150. package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
  151. package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
  152. package/linthost/rules_unicorn_no_unused_properties.go +756 -19
  153. package/linthost/rules_unicorn_number_literal_case.go +58 -39
  154. package/linthost/rules_unicorn_prefer_at.go +7 -3
  155. package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
  156. package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
  157. package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
  158. package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
  159. package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
  160. package/linthost/rules_unicorn_string_content.go +611 -15
  161. package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
  162. package/linthost/rules_unicorn_template_indent.go +923 -18
  163. package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
  164. package/linthost/rules_unicorn_throw_new_error.go +218 -26
  165. package/linthost/rules_var.go +829 -142
  166. package/linthost/string_length.go +222 -0
  167. package/package.json +4 -3
  168. package/rule/project.go +206 -0
  169. package/rule/rule.go +58 -1
  170. package/src/index.ts +58 -17
  171. package/src/structures/ITtscLintConfig.ts +3 -0
  172. package/src/structures/TtscLintRuleSetting.ts +3 -2
  173. package/src/structures/format/ITtscLintFormat.ts +3 -2
  174. package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
  175. package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
  176. package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
  177. package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
  178. package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
  179. package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
  180. package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
  181. package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
  182. package/src/structures/rules/ITtscLintRules.ts +7 -2
  183. package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
  184. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
  185. package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
  186. package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
  187. package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
  188. package/src/structures/rules/index.ts +3 -0
@@ -1,26 +1,792 @@
1
- // unicorn/import-style: forbids named or default imports from configured
2
- // modules so that, for example, `path` is always imported as the
3
- // namespace `path` and never destructured. The rule's behavior is
4
- // entirely driven by a per-module configuration map
5
- // (`styles: { foo: { default: false, namespace: true } }`). Without that
6
- // configuration plumbing landing in the host's rule-options pipeline,
7
- // the rule has no defaults to enforce, so it is registered as a no-op
8
- // stub to keep `TestTypedKeysMatchRegisteredRules` aligned with the
9
- // typed `ITtscLintUnicornRules` surface.
1
+ // unicorn/import-style: enforce specific import styles per module. A
2
+ // module's allowed styles (`unassigned`, `default`, `namespace`,
3
+ // `named`) come from the merged default table (`chalk`/`path` are
4
+ // default-only, `util` is named-only) and the user's `styles` option;
5
+ // static imports, dynamic imports, `export from`, and `require`
6
+ // calls are classified into actual styles and reported when any of
7
+ // them falls outside the module's allowed set. `node:`-prefixed
8
+ // specifiers inherit the bare module's configuration while diagnostics
9
+ // keep the spelling the source used.
10
10
  //
11
- // The matching fixture under
12
- // `tests/test-lint/src/cases/unicorn-import-style.ts` keeps its
13
- // `@ttsc-corpus-skip` directive until the configuration plumbing lands.
11
+ // Mirrors upstream's ESTree listeners: `import` declarations and
12
+ // unassigned dynamic imports / `require` statements are checked
13
+ // directly, while `const = await import(…)` and `const … =
14
+ // require(…)` classify the declarator's binding target. Module names
15
+ // resolve through a static-string evaluator covering literals,
16
+ // parenthesized expressions, `+` concatenation, and template literals
17
+ // whose substitutions are themselves static strings; upstream's
18
+ // scope-based constant folding is intentionally out of scope, matching
19
+ // the literal-only convention of the other import rules in this host.
14
20
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/import-style.md
15
21
  package linthost
16
22
 
17
- import shimast "github.com/microsoft/typescript-go/shim/ast"
23
+ import (
24
+ "bytes"
25
+ "encoding/json"
26
+ "errors"
27
+ "fmt"
28
+ "strings"
18
29
 
19
- type unicornImportStyle struct{}
30
+ shimast "github.com/microsoft/typescript-go/shim/ast"
31
+ )
20
32
 
21
- func (unicornImportStyle) Name() string { return "unicorn/import-style" }
22
- func (unicornImportStyle) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
23
- func (unicornImportStyle) Check(_ *Context, _ *shimast.Node) {
33
+ type unicornImportStyle struct{ optionsRule }
34
+
35
+ const (
36
+ unicornImportStyleUnassigned = "unassigned"
37
+ unicornImportStyleDefault = "default"
38
+ unicornImportStyleNamespace = "namespace"
39
+ unicornImportStyleNamed = "named"
40
+ )
41
+
42
+ // unicornImportStyleCanonicalStyles lists the four style names a module
43
+ // configuration must set to `false`, one and all, to count as banned.
44
+ var unicornImportStyleCanonicalStyles = []string{
45
+ unicornImportStyleUnassigned,
46
+ unicornImportStyleDefault,
47
+ unicornImportStyleNamespace,
48
+ unicornImportStyleNamed,
49
+ }
50
+
51
+ // unicornImportStyleStylePair is one `style: allowed` entry of a module
52
+ // configuration. Order is preserved because the diagnostic lists the
53
+ // allowed styles in configuration order, exactly like upstream's
54
+ // JavaScript object spread.
55
+ type unicornImportStyleStylePair struct {
56
+ style string
57
+ allowed bool
58
+ }
59
+
60
+ // unicornImportStyleDefaultStyles mirrors upstream's built-in table.
61
+ // Keep this alphabetically sorted for easier maintenance.
62
+ var unicornImportStyleDefaultStyles = map[string][]unicornImportStyleStylePair{
63
+ "chalk": {{style: unicornImportStyleDefault, allowed: true}},
64
+ "path": {{style: unicornImportStyleDefault, allowed: true}},
65
+ "util": {{style: unicornImportStyleNamed, allowed: true}},
66
+ }
67
+
68
+ var unicornImportStyleDefaultStyleNames = []string{"chalk", "path", "util"}
69
+
70
+ // unicornImportStyleModule is one module's resolved policy: the ordered
71
+ // allowed styles and whether every canonical style was explicitly
72
+ // disabled (the misuse upstream reports with a dedicated message).
73
+ type unicornImportStyleModule struct {
74
+ allowed []string
75
+ banned bool
76
+ }
77
+
78
+ type unicornImportStyleOptions struct {
79
+ checkImport bool
80
+ checkDynamicImport bool
81
+ checkExportFrom bool
82
+ checkRequire bool
83
+ styles map[string]unicornImportStyleModule
84
+ }
85
+
86
+ // unicornImportStyleUserModule captures one raw `styles` entry before
87
+ // merging: `false` disables the module's restrictions, an object lists
88
+ // per-style booleans in declaration order.
89
+ type unicornImportStyleUserModule struct {
90
+ disabled bool
91
+ pairs []unicornImportStyleStylePair
92
+ }
93
+
94
+ func (unicornImportStyle) Name() string { return "unicorn/import-style" }
95
+ func (unicornImportStyle) Visits() []shimast.Kind {
96
+ return []shimast.Kind{
97
+ shimast.KindImportDeclaration,
98
+ shimast.KindExportDeclaration,
99
+ shimast.KindCallExpression,
100
+ shimast.KindVariableDeclaration,
101
+ }
102
+ }
103
+
104
+ func (unicornImportStyle) ValidateOptions(raw json.RawMessage) error {
105
+ _, err := parseUnicornImportStyleOptions(raw)
106
+ return err
107
+ }
108
+
109
+ func (unicornImportStyle) Check(ctx *Context, node *shimast.Node) {
110
+ if ctx == nil || node == nil {
111
+ return
112
+ }
113
+ options, err := parseUnicornImportStyleOptions(ctx.Options)
114
+ if err != nil || len(options.styles) == 0 {
115
+ return
116
+ }
117
+ switch node.Kind {
118
+ case shimast.KindImportDeclaration:
119
+ if options.checkImport {
120
+ checkUnicornImportStyleImportDeclaration(ctx, node, options)
121
+ }
122
+ case shimast.KindExportDeclaration:
123
+ if options.checkExportFrom {
124
+ checkUnicornImportStyleExportDeclaration(ctx, node, options)
125
+ }
126
+ case shimast.KindCallExpression:
127
+ checkUnicornImportStyleCallExpression(ctx, node, options)
128
+ case shimast.KindVariableDeclaration:
129
+ checkUnicornImportStyleVariableDeclaration(ctx, node, options)
130
+ }
131
+ }
132
+
133
+ func parseUnicornImportStyleOptions(raw json.RawMessage) (unicornImportStyleOptions, error) {
134
+ options := unicornImportStyleOptions{
135
+ checkImport: true,
136
+ checkDynamicImport: true,
137
+ checkRequire: true,
138
+ }
139
+ extendDefaultStyles := true
140
+ var userStyles map[string]unicornImportStyleUserModule
141
+ trimmed := bytes.TrimSpace(raw)
142
+ if len(trimmed) != 0 {
143
+ if trimmed[0] != '{' {
144
+ return options, errors.New("options must be an object")
145
+ }
146
+ var fields map[string]json.RawMessage
147
+ if err := json.Unmarshal(trimmed, &fields); err != nil {
148
+ return options, fmt.Errorf("options must be an object: %w", err)
149
+ }
150
+ for name, value := range fields {
151
+ var err error
152
+ switch name {
153
+ case "checkImport":
154
+ options.checkImport, err = unicornImportStyleDecodeBool(value, name)
155
+ case "checkDynamicImport":
156
+ options.checkDynamicImport, err = unicornImportStyleDecodeBool(value, name)
157
+ case "checkExportFrom":
158
+ options.checkExportFrom, err = unicornImportStyleDecodeBool(value, name)
159
+ case "checkRequire":
160
+ options.checkRequire, err = unicornImportStyleDecodeBool(value, name)
161
+ case "extendDefaultStyles":
162
+ extendDefaultStyles, err = unicornImportStyleDecodeBool(value, name)
163
+ case "styles":
164
+ userStyles, err = parseUnicornImportStyleStyles(value)
165
+ default:
166
+ err = fmt.Errorf("unknown option %q", name)
167
+ }
168
+ if err != nil {
169
+ return options, err
170
+ }
171
+ }
172
+ }
173
+ options.styles = resolveUnicornImportStyleStyles(userStyles, extendDefaultStyles)
174
+ return options, nil
175
+ }
176
+
177
+ func unicornImportStyleDecodeBool(raw json.RawMessage, name string) (bool, error) {
178
+ var decoded any
179
+ if err := json.Unmarshal(raw, &decoded); err != nil {
180
+ return false, fmt.Errorf("option %q must be a boolean", name)
181
+ }
182
+ value, ok := decoded.(bool)
183
+ if !ok {
184
+ return false, fmt.Errorf("option %q must be a boolean", name)
185
+ }
186
+ return value, nil
187
+ }
188
+
189
+ func parseUnicornImportStyleStyles(raw json.RawMessage) (map[string]unicornImportStyleUserModule, error) {
190
+ trimmed := bytes.TrimSpace(raw)
191
+ if len(trimmed) == 0 || trimmed[0] != '{' {
192
+ return nil, fmt.Errorf("option %q must be an object", "styles")
193
+ }
194
+ var modules map[string]json.RawMessage
195
+ if err := json.Unmarshal(trimmed, &modules); err != nil {
196
+ return nil, fmt.Errorf("option %q must be an object", "styles")
197
+ }
198
+ parsed := make(map[string]unicornImportStyleUserModule, len(modules))
199
+ for name, rawModule := range modules {
200
+ value := bytes.TrimSpace(rawModule)
201
+ if bytes.Equal(value, []byte("false")) {
202
+ parsed[name] = unicornImportStyleUserModule{disabled: true}
203
+ continue
204
+ }
205
+ if len(value) == 0 || value[0] != '{' {
206
+ return nil, fmt.Errorf("styles entry %q must be false or an object of booleans", name)
207
+ }
208
+ pairs, err := unicornImportStyleDecodeOrderedBooleans(value, name)
209
+ if err != nil {
210
+ return nil, err
211
+ }
212
+ parsed[name] = unicornImportStyleUserModule{pairs: pairs}
213
+ }
214
+ return parsed, nil
215
+ }
216
+
217
+ // unicornImportStyleDecodeOrderedBooleans reads one module's style
218
+ // object while preserving key order. encoding/json maps are unordered,
219
+ // but the diagnostic message lists allowed styles in declaration order,
220
+ // so the pairs are decoded token by token. A duplicated key keeps its
221
+ // first position and takes the last value, mirroring JSON.parse.
222
+ func unicornImportStyleDecodeOrderedBooleans(raw []byte, moduleName string) ([]unicornImportStyleStylePair, error) {
223
+ decoder := json.NewDecoder(bytes.NewReader(raw))
224
+ if token, err := decoder.Token(); err != nil || token != json.Delim('{') {
225
+ return nil, fmt.Errorf("styles entry %q must be false or an object of booleans", moduleName)
226
+ }
227
+ var pairs []unicornImportStyleStylePair
228
+ for decoder.More() {
229
+ keyToken, err := decoder.Token()
230
+ if err != nil {
231
+ return nil, fmt.Errorf("styles entry %q must be false or an object of booleans", moduleName)
232
+ }
233
+ key, ok := keyToken.(string)
234
+ if !ok {
235
+ return nil, fmt.Errorf("styles entry %q must be false or an object of booleans", moduleName)
236
+ }
237
+ var decoded any
238
+ if err := decoder.Decode(&decoded); err != nil {
239
+ return nil, fmt.Errorf("style %q of module %q must be a boolean", key, moduleName)
240
+ }
241
+ value, isBool := decoded.(bool)
242
+ if !isBool {
243
+ return nil, fmt.Errorf("style %q of module %q must be a boolean", key, moduleName)
244
+ }
245
+ pairs = unicornImportStyleUpsertPair(pairs, key, value)
246
+ }
247
+ if token, err := decoder.Token(); err != nil || token != json.Delim('}') {
248
+ return nil, fmt.Errorf("styles entry %q must be false or an object of booleans", moduleName)
249
+ }
250
+ return pairs, nil
251
+ }
252
+
253
+ func unicornImportStyleUpsertPair(
254
+ pairs []unicornImportStyleStylePair,
255
+ style string,
256
+ allowed bool,
257
+ ) []unicornImportStyleStylePair {
258
+ for index := range pairs {
259
+ if pairs[index].style == style {
260
+ pairs[index].allowed = allowed
261
+ return pairs
262
+ }
263
+ }
264
+ return append(pairs, unicornImportStyleStylePair{style: style, allowed: allowed})
265
+ }
266
+
267
+ // resolveUnicornImportStyleStyles folds the default table and the
268
+ // user's `styles` into the final per-module policy, mirroring
269
+ // upstream's object spread: with `extendDefaultStyles` the defaults
270
+ // seed each shared module and user entries override style by style,
271
+ // while `moduleName: false` clears the module's restrictions entirely.
272
+ func resolveUnicornImportStyleStyles(
273
+ userStyles map[string]unicornImportStyleUserModule,
274
+ extendDefaultStyles bool,
275
+ ) map[string]unicornImportStyleModule {
276
+ names := make([]string, 0, len(unicornImportStyleDefaultStyleNames)+len(userStyles))
277
+ if extendDefaultStyles {
278
+ names = append(names, unicornImportStyleDefaultStyleNames...)
279
+ }
280
+ for name := range userStyles {
281
+ if !extendDefaultStyles || unicornImportStyleDefaultStyles[name] == nil {
282
+ names = append(names, name)
283
+ }
284
+ }
285
+ resolved := make(map[string]unicornImportStyleModule, len(names))
286
+ for _, name := range names {
287
+ user, hasUser := userStyles[name]
288
+ if hasUser && user.disabled {
289
+ resolved[name] = unicornImportStyleModule{}
290
+ continue
291
+ }
292
+ var pairs []unicornImportStyleStylePair
293
+ if extendDefaultStyles {
294
+ pairs = append(pairs, unicornImportStyleDefaultStyles[name]...)
295
+ }
296
+ for _, pair := range user.pairs {
297
+ pairs = unicornImportStyleUpsertPair(pairs, pair.style, pair.allowed)
298
+ }
299
+ resolved[name] = unicornImportStyleFinalizeModule(pairs)
300
+ }
301
+ return resolved
302
+ }
303
+
304
+ func unicornImportStyleFinalizeModule(pairs []unicornImportStyleStylePair) unicornImportStyleModule {
305
+ module := unicornImportStyleModule{}
306
+ for _, pair := range pairs {
307
+ if pair.allowed {
308
+ module.allowed = append(module.allowed, pair.style)
309
+ }
310
+ }
311
+ banned := true
312
+ for _, style := range unicornImportStyleCanonicalStyles {
313
+ explicitlyFalse := false
314
+ for _, pair := range pairs {
315
+ if pair.style == style {
316
+ explicitlyFalse = !pair.allowed
317
+ break
318
+ }
319
+ }
320
+ if !explicitlyFalse {
321
+ banned = false
322
+ break
323
+ }
324
+ }
325
+ module.banned = banned
326
+ return module
327
+ }
328
+
329
+ func checkUnicornImportStyleImportDeclaration(ctx *Context, node *shimast.Node, options unicornImportStyleOptions) {
330
+ declaration := node.AsImportDeclaration()
331
+ if declaration == nil {
332
+ return
333
+ }
334
+ moduleName, ok := unicornImportStyleStaticString(declaration.ModuleSpecifier)
335
+ if !ok {
336
+ return
337
+ }
338
+ styles := unicornImportStyleImportDeclarationStyles(declaration)
339
+ reportUnicornImportStyle(ctx, node, moduleName, styles, false, options)
340
+ }
341
+
342
+ // unicornImportStyleImportDeclarationStyles classifies one static
343
+ // import the way upstream reads ESTree specifiers: no specifiers is
344
+ // the unassigned style, a default binding or a named `default` alias
345
+ // is the default style, `* as ns` is namespace, and every other named
346
+ // specifier is named.
347
+ func unicornImportStyleImportDeclarationStyles(declaration *shimast.ImportDeclaration) []string {
348
+ clauseNode := declaration.ImportClause
349
+ if clauseNode == nil {
350
+ return []string{unicornImportStyleUnassigned}
351
+ }
352
+ clause := clauseNode.AsImportClause()
353
+ if clause == nil {
354
+ return []string{unicornImportStyleUnassigned}
355
+ }
356
+ var styles []string
357
+ if clause.Name() != nil {
358
+ styles = unicornImportStyleAppend(styles, unicornImportStyleDefault)
359
+ }
360
+ if bindings := clause.NamedBindings; bindings != nil {
361
+ switch bindings.Kind {
362
+ case shimast.KindNamespaceImport:
363
+ styles = unicornImportStyleAppend(styles, unicornImportStyleNamespace)
364
+ case shimast.KindNamedImports:
365
+ if named := bindings.AsNamedImports(); named != nil && named.Elements != nil {
366
+ for _, element := range named.Elements.Nodes {
367
+ specifier := element.AsImportSpecifier()
368
+ if specifier == nil {
369
+ continue
370
+ }
371
+ imported := specifier.PropertyName
372
+ if imported == nil {
373
+ imported = specifier.Name()
374
+ }
375
+ if identifierText(imported) == "default" {
376
+ styles = unicornImportStyleAppend(styles, unicornImportStyleDefault)
377
+ } else {
378
+ styles = unicornImportStyleAppend(styles, unicornImportStyleNamed)
379
+ }
380
+ }
381
+ }
382
+ }
383
+ }
384
+ if len(styles) == 0 {
385
+ return []string{unicornImportStyleUnassigned}
386
+ }
387
+ return styles
388
+ }
389
+
390
+ func checkUnicornImportStyleExportDeclaration(ctx *Context, node *shimast.Node, options unicornImportStyleOptions) {
391
+ declaration := node.AsExportDeclaration()
392
+ if declaration == nil || declaration.ModuleSpecifier == nil {
393
+ return
394
+ }
395
+ moduleName, ok := unicornImportStyleStaticString(declaration.ModuleSpecifier)
396
+ if !ok {
397
+ return
398
+ }
399
+ var styles []string
400
+ clause := declaration.ExportClause
401
+ switch {
402
+ case clause == nil || clause.Kind == shimast.KindNamespaceExport:
403
+ // `export * from` and `export * as ns from` are upstream's
404
+ // ExportAllDeclaration: always the namespace style.
405
+ styles = []string{unicornImportStyleNamespace}
406
+ case clause.Kind == shimast.KindNamedExports:
407
+ named := clause.AsNamedExports()
408
+ if named == nil || named.Elements == nil || len(named.Elements.Nodes) == 0 {
409
+ styles = []string{unicornImportStyleUnassigned}
410
+ break
411
+ }
412
+ for _, element := range named.Elements.Nodes {
413
+ specifier := element.AsExportSpecifier()
414
+ if specifier == nil {
415
+ continue
416
+ }
417
+ // Upstream classifies by the exported (outer) name, so
418
+ // `export {x as default} from "m"` is the default style.
419
+ if identifierText(specifier.Name()) == "default" {
420
+ styles = unicornImportStyleAppend(styles, unicornImportStyleDefault)
421
+ } else {
422
+ styles = unicornImportStyleAppend(styles, unicornImportStyleNamed)
423
+ }
424
+ }
425
+ default:
426
+ return
427
+ }
428
+ reportUnicornImportStyle(ctx, node, moduleName, styles, false, options)
429
+ }
430
+
431
+ func checkUnicornImportStyleCallExpression(ctx *Context, node *shimast.Node, options unicornImportStyleOptions) {
432
+ call := node.AsCallExpression()
433
+ if call == nil || call.Expression == nil {
434
+ return
435
+ }
436
+ if call.Expression.Kind == shimast.KindImportKeyword {
437
+ if !options.checkDynamicImport || unicornImportStyleIsAssignedDynamicImport(node) {
438
+ return
439
+ }
440
+ moduleName, ok := unicornImportStyleStaticString(unicornImportStyleFirstArgument(call))
441
+ if !ok {
442
+ return
443
+ }
444
+ reportUnicornImportStyle(ctx, node, moduleName, []string{unicornImportStyleUnassigned}, false, options)
445
+ return
446
+ }
447
+ if !options.checkRequire {
448
+ return
449
+ }
450
+ // Upstream's unassigned-require listener matches the exact
451
+ // statement-level shape: a non-optional bare `require` call with one
452
+ // argument whose parent is the expression statement itself.
453
+ if call.QuestionDotToken != nil || identifierText(stripParens(call.Expression)) != "require" {
454
+ return
455
+ }
456
+ if call.Arguments == nil || len(call.Arguments.Nodes) != 1 {
457
+ return
458
+ }
459
+ parent, child := unicornImportStyleEffectiveParent(node)
460
+ if parent == nil || parent.Kind != shimast.KindExpressionStatement {
461
+ return
462
+ }
463
+ statement := parent.AsExpressionStatement()
464
+ if statement == nil || statement.Expression != child {
465
+ return
466
+ }
467
+ moduleName, ok := unicornImportStyleStaticString(call.Arguments.Nodes[0])
468
+ if !ok {
469
+ return
470
+ }
471
+ reportUnicornImportStyle(ctx, node, moduleName, []string{unicornImportStyleUnassigned}, true, options)
472
+ }
473
+
474
+ func checkUnicornImportStyleVariableDeclaration(ctx *Context, node *shimast.Node, options unicornImportStyleOptions) {
475
+ declaration := node.AsVariableDeclaration()
476
+ if declaration == nil || declaration.Initializer == nil {
477
+ return
478
+ }
479
+ initializer := stripParens(declaration.Initializer)
480
+ if initializer == nil {
481
+ return
482
+ }
483
+ if initializer.Kind == shimast.KindAwaitExpression {
484
+ if !options.checkDynamicImport {
485
+ return
486
+ }
487
+ await := initializer.AsAwaitExpression()
488
+ if await == nil {
489
+ return
490
+ }
491
+ call := unicornImportStyleDynamicImportCall(await.Expression)
492
+ if call == nil {
493
+ return
494
+ }
495
+ moduleName, ok := unicornImportStyleStaticString(unicornImportStyleFirstArgument(call))
496
+ if !ok || moduleName == "" {
497
+ return
498
+ }
499
+ styles := unicornImportStyleAssignmentTargetStyles(declaration.Name())
500
+ reportUnicornImportStyle(ctx, node, moduleName, styles, false, options)
501
+ return
502
+ }
503
+ if !options.checkRequire || initializer.Kind != shimast.KindCallExpression {
504
+ return
505
+ }
506
+ call := initializer.AsCallExpression()
507
+ if call == nil || identifierText(stripParens(call.Expression)) != "require" {
508
+ return
509
+ }
510
+ // ESTree wraps `require?.(…)` in a ChainExpression, so upstream's
511
+ // `init.type === 'CallExpression'` listener never classifies an
512
+ // optional require declarator. QuestionDotToken is the tsgo AST's
513
+ // equivalent of that wrapper.
514
+ if call.QuestionDotToken != nil {
515
+ return
516
+ }
517
+ moduleName, ok := unicornImportStyleStaticString(unicornImportStyleFirstArgument(call))
518
+ if !ok || moduleName == "" {
519
+ return
520
+ }
521
+ styles := unicornImportStyleAssignmentTargetStyles(declaration.Name())
522
+ reportUnicornImportStyle(ctx, node, moduleName, styles, true, options)
523
+ }
524
+
525
+ // unicornImportStyleDynamicImportCall returns the `import(…)` call
526
+ // behind `expression`, or nil when the expression is anything else.
527
+ func unicornImportStyleDynamicImportCall(expression *shimast.Node) *shimast.CallExpression {
528
+ expression = stripParens(expression)
529
+ if expression == nil || expression.Kind != shimast.KindCallExpression {
530
+ return nil
531
+ }
532
+ call := expression.AsCallExpression()
533
+ if call == nil || call.Expression == nil || call.Expression.Kind != shimast.KindImportKeyword {
534
+ return nil
535
+ }
536
+ return call
537
+ }
538
+
539
+ func unicornImportStyleFirstArgument(call *shimast.CallExpression) *shimast.Node {
540
+ if call == nil || call.Arguments == nil || len(call.Arguments.Nodes) == 0 {
541
+ return nil
542
+ }
543
+ return call.Arguments.Nodes[0]
544
+ }
545
+
546
+ // unicornImportStyleIsAssignedDynamicImport mirrors upstream's
547
+ // isAssignedDynamicImport: `const x = await import("m")` is handled by
548
+ // the declarator path, so the bare dynamic-import listener skips an
549
+ // import expression awaited directly inside a declarator initializer.
550
+ func unicornImportStyleIsAssignedDynamicImport(importCall *shimast.Node) bool {
551
+ parent, child := unicornImportStyleEffectiveParent(importCall)
552
+ if parent == nil || parent.Kind != shimast.KindAwaitExpression {
553
+ return false
554
+ }
555
+ await := parent.AsAwaitExpression()
556
+ if await == nil || await.Expression != child {
557
+ return false
558
+ }
559
+ grandparent, awaited := unicornImportStyleEffectiveParent(parent)
560
+ if grandparent == nil || grandparent.Kind != shimast.KindVariableDeclaration {
561
+ return false
562
+ }
563
+ declaration := grandparent.AsVariableDeclaration()
564
+ return declaration != nil && stripParens(declaration.Initializer) == stripParens(awaited)
565
+ }
566
+
567
+ // unicornImportStyleEffectiveParent walks upward through parenthesized
568
+ // expressions and returns the first structural ancestor plus the child
569
+ // slot it holds, because ESTree (upstream's AST) has no parenthesized
570
+ // nodes at all.
571
+ func unicornImportStyleEffectiveParent(node *shimast.Node) (*shimast.Node, *shimast.Node) {
572
+ current := node
573
+ parent := node.Parent
574
+ for parent != nil && parent.Kind == shimast.KindParenthesizedExpression {
575
+ wrapper := parent.AsParenthesizedExpression()
576
+ if wrapper == nil || wrapper.Expression != current {
577
+ break
578
+ }
579
+ current = parent
580
+ parent = parent.Parent
581
+ }
582
+ return parent, current
583
+ }
584
+
585
+ // unicornImportStyleAssignmentTargetStyles classifies the binding
586
+ // target of an awaited dynamic import or a `require` declarator:
587
+ // identifiers and array patterns take the whole namespace, an empty
588
+ // object pattern is unassigned, and object-pattern properties are the
589
+ // default style for `default` keys, named otherwise (rest elements
590
+ // included).
591
+ func unicornImportStyleAssignmentTargetStyles(target *shimast.Node) []string {
592
+ if target == nil {
593
+ return nil
594
+ }
595
+ switch target.Kind {
596
+ case shimast.KindIdentifier, shimast.KindArrayBindingPattern:
597
+ return []string{unicornImportStyleNamespace}
598
+ case shimast.KindObjectBindingPattern:
599
+ pattern := target.AsBindingPattern()
600
+ if pattern == nil || pattern.Elements == nil || len(pattern.Elements.Nodes) == 0 {
601
+ return []string{unicornImportStyleUnassigned}
602
+ }
603
+ var styles []string
604
+ for _, elementNode := range pattern.Elements.Nodes {
605
+ element := elementNode.AsBindingElement()
606
+ if element == nil {
607
+ continue
608
+ }
609
+ if element.DotDotDotToken != nil {
610
+ styles = unicornImportStyleAppend(styles, unicornImportStyleNamed)
611
+ continue
612
+ }
613
+ key := element.PropertyName
614
+ if key == nil {
615
+ key = element.Name()
616
+ }
617
+ if key != nil && key.Kind == shimast.KindComputedPropertyName {
618
+ if computed := key.AsComputedPropertyName(); computed != nil {
619
+ key = stripParens(computed.Expression)
620
+ }
621
+ }
622
+ if key == nil || key.Kind != shimast.KindIdentifier {
623
+ // Upstream only classifies identifier keys; literal keys such
624
+ // as `{"x": y}` contribute no style at all.
625
+ continue
626
+ }
627
+ if identifierText(key) == "default" {
628
+ styles = unicornImportStyleAppend(styles, unicornImportStyleDefault)
629
+ } else {
630
+ styles = unicornImportStyleAppend(styles, unicornImportStyleNamed)
631
+ }
632
+ }
633
+ return styles
634
+ }
635
+ return nil
636
+ }
637
+
638
+ func unicornImportStyleAppend(styles []string, style string) []string {
639
+ for _, existing := range styles {
640
+ if existing == style {
641
+ return styles
642
+ }
643
+ }
644
+ return append(styles, style)
645
+ }
646
+
647
+ // unicornImportStyleStaticString evaluates a compile-time-constant
648
+ // string expression: string literals, no-substitution templates,
649
+ // parenthesized expressions, `+` concatenations of static strings, and
650
+ // template literals whose substitutions are static strings.
651
+ func unicornImportStyleStaticString(node *shimast.Node) (string, bool) {
652
+ node = stripParens(node)
653
+ if node == nil {
654
+ return "", false
655
+ }
656
+ switch node.Kind {
657
+ case shimast.KindStringLiteral:
658
+ if literal := node.AsStringLiteral(); literal != nil {
659
+ return literal.Text, true
660
+ }
661
+ case shimast.KindNoSubstitutionTemplateLiteral:
662
+ if literal := node.AsNoSubstitutionTemplateLiteral(); literal != nil {
663
+ return literal.Text, true
664
+ }
665
+ case shimast.KindTemplateExpression:
666
+ template := node.AsTemplateExpression()
667
+ if template == nil || template.Head == nil || template.TemplateSpans == nil {
668
+ return "", false
669
+ }
670
+ head := template.Head.AsTemplateHead()
671
+ if head == nil {
672
+ return "", false
673
+ }
674
+ var builder strings.Builder
675
+ builder.WriteString(head.Text)
676
+ for _, spanNode := range template.TemplateSpans.Nodes {
677
+ span := spanNode.AsTemplateSpan()
678
+ if span == nil {
679
+ return "", false
680
+ }
681
+ value, ok := unicornImportStyleStaticString(span.Expression)
682
+ if !ok || span.Literal == nil {
683
+ return "", false
684
+ }
685
+ builder.WriteString(value)
686
+ switch span.Literal.Kind {
687
+ case shimast.KindTemplateMiddle:
688
+ middle := span.Literal.AsTemplateMiddle()
689
+ if middle == nil {
690
+ return "", false
691
+ }
692
+ builder.WriteString(middle.Text)
693
+ case shimast.KindTemplateTail:
694
+ tail := span.Literal.AsTemplateTail()
695
+ if tail == nil {
696
+ return "", false
697
+ }
698
+ builder.WriteString(tail.Text)
699
+ default:
700
+ return "", false
701
+ }
702
+ }
703
+ return builder.String(), true
704
+ case shimast.KindBinaryExpression:
705
+ binary := node.AsBinaryExpression()
706
+ if binary == nil || binary.OperatorToken == nil ||
707
+ binary.OperatorToken.Kind != shimast.KindPlusToken {
708
+ return "", false
709
+ }
710
+ left, ok := unicornImportStyleStaticString(binary.Left)
711
+ if !ok {
712
+ return "", false
713
+ }
714
+ right, ok := unicornImportStyleStaticString(binary.Right)
715
+ if !ok {
716
+ return "", false
717
+ }
718
+ return left + right, true
719
+ }
720
+ return "", false
721
+ }
722
+
723
+ // reportUnicornImportStyle applies one module's policy to the actual
724
+ // styles of a reference. `node:`-prefixed specifiers resolve to the
725
+ // bare module's configuration; the diagnostic keeps the original
726
+ // spelling. For `require`, an allowed `default` style implies
727
+ // `namespace` because CommonJS interop cannot distinguish them.
728
+ func reportUnicornImportStyle(
729
+ ctx *Context,
730
+ node *shimast.Node,
731
+ moduleName string,
732
+ actualStyles []string,
733
+ isRequire bool,
734
+ options unicornImportStyleOptions,
735
+ ) {
736
+ module, exists := options.styles[strings.TrimPrefix(moduleName, "node:")]
737
+ if !exists {
738
+ return
739
+ }
740
+ if len(module.allowed) == 0 {
741
+ if module.banned {
742
+ ctx.Report(node, fmt.Sprintf(
743
+ "All import styles are disabled for module `%s`. Use the `no-restricted-imports` rule to disallow a module.",
744
+ moduleName,
745
+ ))
746
+ }
747
+ return
748
+ }
749
+ effective := module.allowed
750
+ if isRequire &&
751
+ unicornImportStyleContains(effective, unicornImportStyleDefault) &&
752
+ !unicornImportStyleContains(effective, unicornImportStyleNamespace) {
753
+ effective = append(append([]string(nil), module.allowed...), unicornImportStyleNamespace)
754
+ }
755
+ for _, style := range actualStyles {
756
+ if !unicornImportStyleContains(effective, style) {
757
+ ctx.Report(node, fmt.Sprintf(
758
+ "Use %s import for module `%s`.",
759
+ unicornImportStyleDisjunction(module.allowed),
760
+ moduleName,
761
+ ))
762
+ return
763
+ }
764
+ }
765
+ }
766
+
767
+ func unicornImportStyleContains(styles []string, style string) bool {
768
+ for _, candidate := range styles {
769
+ if candidate == style {
770
+ return true
771
+ }
772
+ }
773
+ return false
774
+ }
775
+
776
+ // unicornImportStyleDisjunction renders the allowed styles the way
777
+ // upstream's `Intl.ListFormat("en-US", {type: "disjunction"})` does:
778
+ // "a", "a or b", "a, b, or c".
779
+ func unicornImportStyleDisjunction(items []string) string {
780
+ switch len(items) {
781
+ case 0:
782
+ return ""
783
+ case 1:
784
+ return items[0]
785
+ case 2:
786
+ return items[0] + " or " + items[1]
787
+ default:
788
+ return strings.Join(items[:len(items)-1], ", ") + ", or " + items[len(items)-1]
789
+ }
24
790
  }
25
791
 
26
792
  func init() {