@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,103 +1,1059 @@
1
- // noRestrictedImports reports an `import` (or `export from`) whose
2
- // module specifier matches a project-configured deny list. The rule is
3
- // the policy hammer for forcing a module boundary: a package that has
4
- // been renamed, a workspace internal that should not leak, or a heavy
5
- // dependency that the project wants to discourage all share the same
6
- // shape — flag the import at its source so the offending line is the
7
- // one the developer fixes.
1
+ // noRestrictedImports enforces the exact paths and path patterns supplied by
2
+ // the user. With no options it is a no-op: the rule contains no project policy
3
+ // of its own.
8
4
  //
9
- // Configuration shape: a `paths` array of bare module specifiers to
10
- // flag, exact-match only. An empty (or missing) list is a no-op — the
11
- // rule contributes no findings until the project lists something to
12
- // restrict. The native engine's RuleConfig path does not carry options
13
- // to the rule, so the Go unit test exercises the rule via a hard-coded
14
- // default deny list (`lodash`, `underscore`) that fires whenever the
15
- // rule is enabled with no options blob.
5
+ // The option surface mirrors ESLint's current core rule, including positional
6
+ // path entries, the {paths, patterns} object form, import-name restrictions,
7
+ // custom messages, case sensitivity, regular-expression patterns, and
8
+ // type-only exemptions.
16
9
  // https://eslint.org/docs/latest/rules/no-restricted-imports
17
10
  package linthost
18
11
 
19
12
  import (
13
+ "bytes"
20
14
  "encoding/json"
15
+ "fmt"
16
+ "regexp"
17
+ "sort"
18
+ "strings"
19
+ "unicode"
21
20
 
22
21
  shimast "github.com/microsoft/typescript-go/shim/ast"
22
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
23
23
  )
24
24
 
25
- type noRestrictedImports struct{}
25
+ type noRestrictedImports struct{ optionsRule }
26
26
 
27
- // noRestrictedImportsOptions decodes the `paths` array from a user
28
- // `[severity, options]` tuple. Anything else on the blob is ignored so
29
- // future ESLint-compatible keys (e.g. `patterns`) can be layered in
30
- // without changing existing configs.
31
27
  type noRestrictedImportsOptions struct {
32
- Paths []string `json:"paths"`
28
+ paths []noRestrictedImportsPath
29
+ patterns []noRestrictedImportsPattern
33
30
  }
34
31
 
35
- // hardCodedRestrictedImports is the default deny list used when the
36
- // rule is enabled without an options blob. The Go-side test harness
37
- // exercises the rule through `RuleConfig` (severity-only, no options),
38
- // so the rule needs a deterministic non-empty default to be testable
39
- // at the AST layer at all. The choice mirrors a common "don't lean on
40
- // utility-belt libraries" lint setup in real projects.
41
- var hardCodedRestrictedImports = []string{"lodash", "underscore"}
32
+ type noRestrictedImportsNameControl struct {
33
+ importNames []string
34
+ hasImportNames bool
35
+ allowImportNames []string
36
+ hasAllowImportNames bool
37
+ importNamePattern *regexp.Regexp
38
+ importNamePatternText string
39
+ allowImportNamePattern *regexp.Regexp
40
+ allowImportNameText string
41
+ }
42
+
43
+ type noRestrictedImportsPath struct {
44
+ name string
45
+ message string
46
+ allowTypeImports bool
47
+ names noRestrictedImportsNameControl
48
+ }
49
+
50
+ type noRestrictedImportsPattern struct {
51
+ group []noRestrictedImportsGlob
52
+ expression *regexp.Regexp
53
+ message string
54
+ allowTypeImports bool
55
+ names noRestrictedImportsNameControl
56
+ }
57
+
58
+ type noRestrictedImportsGlob struct {
59
+ expression *regexp.Regexp
60
+ negated bool
61
+ directoryOnly bool
62
+ }
63
+
64
+ type noRestrictedImportsReference struct {
65
+ source string
66
+ sourceNode *shimast.Node
67
+ names []noRestrictedImportsImportedName
68
+ wholeTypeOnly bool
69
+ }
70
+
71
+ type noRestrictedImportsImportedName struct {
72
+ name string
73
+ node *shimast.Node
74
+ pos int
75
+ end int
76
+ typeOnly bool
77
+ }
42
78
 
43
79
  func (noRestrictedImports) Name() string { return "no-restricted-imports" }
44
80
  func (noRestrictedImports) Visits() []shimast.Kind {
45
81
  return []shimast.Kind{
46
82
  shimast.KindImportDeclaration,
47
83
  shimast.KindExportDeclaration,
84
+ shimast.KindImportEqualsDeclaration,
48
85
  }
49
86
  }
87
+
88
+ func (noRestrictedImports) ValidateOptions(raw json.RawMessage) error {
89
+ _, err := parseNoRestrictedImportsOptions(raw)
90
+ return err
91
+ }
92
+
50
93
  func (noRestrictedImports) Check(ctx *Context, node *shimast.Node) {
51
- specifierNode := moduleSpecifierOf(node)
52
- if specifierNode == nil {
94
+ if ctx == nil || node == nil {
95
+ return
96
+ }
97
+ options, err := parseNoRestrictedImportsOptions(ctx.Options)
98
+ if err != nil || (len(options.paths) == 0 && len(options.patterns) == 0) {
53
99
  return
54
100
  }
55
- specifier := stringLiteralText(specifierNode)
56
- if specifier == "" {
101
+ reference, ok := noRestrictedImportsReferenceOf(ctx.File, node)
102
+ if !ok {
57
103
  return
58
104
  }
59
- denied := resolveNoRestrictedImportsPaths(ctx)
60
- for _, target := range denied {
61
- if specifier == target {
62
- ctx.Report(specifierNode, "'"+specifier+"' import is restricted from being used.")
63
- return
105
+
106
+ for _, restricted := range options.paths {
107
+ if reference.source == restricted.name {
108
+ reportNoRestrictedImportsPath(ctx, reference, restricted)
109
+ }
110
+ }
111
+ for _, restricted := range options.patterns {
112
+ if restricted.matches(reference.source) {
113
+ reportNoRestrictedImportsPattern(ctx, reference, restricted)
64
114
  }
65
115
  }
66
116
  }
67
117
 
68
- // moduleSpecifierOf returns the `from "…"` string-literal node of the
69
- // import / re-export declaration. `export { x }` (no `from`) and
70
- // every other shape returns nil so the caller short-circuits.
71
- func moduleSpecifierOf(node *shimast.Node) *shimast.Node {
72
- if node == nil {
118
+ func parseNoRestrictedImportsOptions(raw json.RawMessage) (noRestrictedImportsOptions, error) {
119
+ var options noRestrictedImportsOptions
120
+ raw = bytes.TrimSpace(raw)
121
+ if len(raw) == 0 {
122
+ return options, nil
123
+ }
124
+
125
+ switch raw[0] {
126
+ case '"':
127
+ name, err := noRestrictedImportsDecodeString(raw, "path")
128
+ if err != nil {
129
+ return options, err
130
+ }
131
+ options.paths = append(options.paths, noRestrictedImportsPath{name: name})
132
+ return options, nil
133
+ case '{':
134
+ object, err := noRestrictedImportsDecodeObject(raw, "options")
135
+ if err != nil {
136
+ return options, err
137
+ }
138
+ _, hasPaths := object["paths"]
139
+ _, hasPatterns := object["patterns"]
140
+ if len(object) == 0 || hasPaths || hasPatterns {
141
+ return parseNoRestrictedImportsObjectOptions(object)
142
+ }
143
+ path, err := parseNoRestrictedImportsPathObject(object)
144
+ if err != nil {
145
+ return options, err
146
+ }
147
+ options.paths = append(options.paths, path)
148
+ return options, nil
149
+ case '[':
150
+ entries, err := noRestrictedImportsDecodeArray(raw, "paths")
151
+ if err != nil {
152
+ return options, err
153
+ }
154
+ if err := noRestrictedImportsRequireUnique(entries, "paths"); err != nil {
155
+ return options, err
156
+ }
157
+ options.paths, err = parseNoRestrictedImportsPaths(entries)
158
+ return options, err
159
+ default:
160
+ return options, fmt.Errorf("options must be path entries or an object with %q and/or %q", "paths", "patterns")
161
+ }
162
+ }
163
+
164
+ func parseNoRestrictedImportsObjectOptions(object map[string]json.RawMessage) (noRestrictedImportsOptions, error) {
165
+ var options noRestrictedImportsOptions
166
+ if err := noRestrictedImportsRejectUnknown(object, "paths", "patterns"); err != nil {
167
+ return options, err
168
+ }
169
+ if raw, present := object["paths"]; present {
170
+ entries, err := noRestrictedImportsDecodeArray(raw, "paths")
171
+ if err != nil {
172
+ return options, err
173
+ }
174
+ if err := noRestrictedImportsRequireUnique(entries, "paths"); err != nil {
175
+ return options, err
176
+ }
177
+ options.paths, err = parseNoRestrictedImportsPaths(entries)
178
+ if err != nil {
179
+ return options, err
180
+ }
181
+ }
182
+ if raw, present := object["patterns"]; present {
183
+ entries, err := noRestrictedImportsDecodeArray(raw, "patterns")
184
+ if err != nil {
185
+ return options, err
186
+ }
187
+ if err := noRestrictedImportsRequireUnique(entries, "patterns"); err != nil {
188
+ return options, err
189
+ }
190
+ options.patterns, err = parseNoRestrictedImportsPatterns(entries)
191
+ if err != nil {
192
+ return options, err
193
+ }
194
+ }
195
+ return options, nil
196
+ }
197
+
198
+ func parseNoRestrictedImportsPaths(entries []json.RawMessage) ([]noRestrictedImportsPath, error) {
199
+ paths := make([]noRestrictedImportsPath, 0, len(entries))
200
+ for index, raw := range entries {
201
+ raw = bytes.TrimSpace(raw)
202
+ if len(raw) == 0 {
203
+ return nil, fmt.Errorf("paths[%d] must be a string or object", index)
204
+ }
205
+ switch raw[0] {
206
+ case '"':
207
+ name, err := noRestrictedImportsDecodeString(raw, fmt.Sprintf("paths[%d]", index))
208
+ if err != nil {
209
+ return nil, err
210
+ }
211
+ paths = append(paths, noRestrictedImportsPath{name: name})
212
+ case '{':
213
+ object, err := noRestrictedImportsDecodeObject(raw, fmt.Sprintf("paths[%d]", index))
214
+ if err != nil {
215
+ return nil, err
216
+ }
217
+ path, err := parseNoRestrictedImportsPathObject(object)
218
+ if err != nil {
219
+ return nil, fmt.Errorf("paths[%d]: %w", index, err)
220
+ }
221
+ paths = append(paths, path)
222
+ default:
223
+ return nil, fmt.Errorf("paths[%d] must be a string or object", index)
224
+ }
225
+ }
226
+ return paths, nil
227
+ }
228
+
229
+ func parseNoRestrictedImportsPathObject(object map[string]json.RawMessage) (noRestrictedImportsPath, error) {
230
+ var path noRestrictedImportsPath
231
+ if err := noRestrictedImportsRejectUnknown(
232
+ object,
233
+ "name",
234
+ "message",
235
+ "importNames",
236
+ "allowImportNames",
237
+ "allowTypeImports",
238
+ ); err != nil {
239
+ return path, err
240
+ }
241
+ rawName, present := object["name"]
242
+ if !present {
243
+ return path, fmt.Errorf("path object requires %q", "name")
244
+ }
245
+ var err error
246
+ if path.name, err = noRestrictedImportsDecodeString(rawName, "name"); err != nil {
247
+ return path, err
248
+ }
249
+ if raw, present := object["message"]; present {
250
+ path.message, err = noRestrictedImportsDecodeString(raw, "message")
251
+ if err != nil {
252
+ return path, err
253
+ }
254
+ if path.message == "" {
255
+ return path, fmt.Errorf("option %q must not be empty", "message")
256
+ }
257
+ }
258
+ if raw, present := object["allowTypeImports"]; present {
259
+ path.allowTypeImports, err = noRestrictedImportsDecodeBool(raw, "allowTypeImports")
260
+ if err != nil {
261
+ return path, err
262
+ }
263
+ }
264
+ if raw, present := object["importNames"]; present {
265
+ path.names.importNames, err = noRestrictedImportsStringList(raw, "importNames", false, false)
266
+ if err != nil {
267
+ return path, err
268
+ }
269
+ path.names.hasImportNames = true
270
+ }
271
+ if raw, present := object["allowImportNames"]; present {
272
+ path.names.allowImportNames, err = noRestrictedImportsStringList(raw, "allowImportNames", false, false)
273
+ if err != nil {
274
+ return path, err
275
+ }
276
+ path.names.hasAllowImportNames = true
277
+ }
278
+ if path.names.hasImportNames && path.names.hasAllowImportNames {
279
+ return path, fmt.Errorf("options %q and %q cannot be combined", "importNames", "allowImportNames")
280
+ }
281
+ return path, nil
282
+ }
283
+
284
+ func parseNoRestrictedImportsPatterns(entries []json.RawMessage) ([]noRestrictedImportsPattern, error) {
285
+ if len(entries) == 0 {
286
+ return nil, nil
287
+ }
288
+ first := bytes.TrimSpace(entries[0])
289
+ if len(first) == 0 {
290
+ return nil, fmt.Errorf("patterns[0] must be a string or object")
291
+ }
292
+
293
+ if first[0] == '"' {
294
+ group := make([]string, 0, len(entries))
295
+ for index, raw := range entries {
296
+ text, err := noRestrictedImportsDecodeString(raw, fmt.Sprintf("patterns[%d]", index))
297
+ if err != nil {
298
+ return nil, fmt.Errorf("patterns must contain only strings or only objects: %w", err)
299
+ }
300
+ group = append(group, text)
301
+ }
302
+ compiled, err := noRestrictedImportsCompileGlobGroup(group, false)
303
+ if err != nil {
304
+ return nil, err
305
+ }
306
+ return []noRestrictedImportsPattern{{group: compiled}}, nil
307
+ }
308
+ if first[0] != '{' {
309
+ return nil, fmt.Errorf("patterns[0] must be a string or object")
310
+ }
311
+
312
+ patterns := make([]noRestrictedImportsPattern, 0, len(entries))
313
+ for index, raw := range entries {
314
+ object, err := noRestrictedImportsDecodeObject(raw, fmt.Sprintf("patterns[%d]", index))
315
+ if err != nil {
316
+ return nil, fmt.Errorf("patterns must contain only strings or only objects: %w", err)
317
+ }
318
+ pattern, err := parseNoRestrictedImportsPatternObject(object)
319
+ if err != nil {
320
+ return nil, fmt.Errorf("patterns[%d]: %w", index, err)
321
+ }
322
+ patterns = append(patterns, pattern)
323
+ }
324
+ return patterns, nil
325
+ }
326
+
327
+ func parseNoRestrictedImportsPatternObject(object map[string]json.RawMessage) (noRestrictedImportsPattern, error) {
328
+ var pattern noRestrictedImportsPattern
329
+ if err := noRestrictedImportsRejectUnknown(
330
+ object,
331
+ "group",
332
+ "regex",
333
+ "message",
334
+ "caseSensitive",
335
+ "importNames",
336
+ "allowImportNames",
337
+ "importNamePattern",
338
+ "allowImportNamePattern",
339
+ "allowTypeImports",
340
+ ); err != nil {
341
+ return pattern, err
342
+ }
343
+ rawGroup, hasGroup := object["group"]
344
+ rawRegex, hasRegex := object["regex"]
345
+ if hasGroup == hasRegex {
346
+ return pattern, fmt.Errorf("pattern object must contain exactly one of %q or %q", "group", "regex")
347
+ }
348
+
349
+ var err error
350
+ caseSensitive := false
351
+ if raw, present := object["caseSensitive"]; present {
352
+ caseSensitive, err = noRestrictedImportsDecodeBool(raw, "caseSensitive")
353
+ if err != nil {
354
+ return pattern, err
355
+ }
356
+ }
357
+ if raw, present := object["allowTypeImports"]; present {
358
+ pattern.allowTypeImports, err = noRestrictedImportsDecodeBool(raw, "allowTypeImports")
359
+ if err != nil {
360
+ return pattern, err
361
+ }
362
+ }
363
+
364
+ if hasGroup {
365
+ group, err := noRestrictedImportsStringList(rawGroup, "group", true, true)
366
+ if err != nil {
367
+ return pattern, err
368
+ }
369
+ pattern.group, err = noRestrictedImportsCompileGlobGroup(group, caseSensitive)
370
+ if err != nil {
371
+ return pattern, err
372
+ }
373
+ } else {
374
+ expression, err := noRestrictedImportsDecodeString(rawRegex, "regex")
375
+ if err != nil {
376
+ return pattern, err
377
+ }
378
+ pattern.expression, err = noRestrictedImportsCompileRegexp(expression, caseSensitive)
379
+ if err != nil {
380
+ return pattern, fmt.Errorf("option %q must be a valid regular expression: %w", "regex", err)
381
+ }
382
+ }
383
+ if raw, present := object["message"]; present {
384
+ pattern.message, err = noRestrictedImportsDecodeString(raw, "message")
385
+ if err != nil {
386
+ return pattern, err
387
+ }
388
+ if pattern.message == "" {
389
+ return pattern, fmt.Errorf("option %q must not be empty", "message")
390
+ }
391
+ }
392
+ if raw, present := object["importNames"]; present {
393
+ pattern.names.importNames, err = noRestrictedImportsStringList(raw, "importNames", true, true)
394
+ if err != nil {
395
+ return pattern, err
396
+ }
397
+ pattern.names.hasImportNames = true
398
+ }
399
+ if raw, present := object["allowImportNames"]; present {
400
+ pattern.names.allowImportNames, err = noRestrictedImportsStringList(raw, "allowImportNames", true, true)
401
+ if err != nil {
402
+ return pattern, err
403
+ }
404
+ pattern.names.hasAllowImportNames = true
405
+ }
406
+ if raw, present := object["importNamePattern"]; present {
407
+ pattern.names.importNamePatternText, err = noRestrictedImportsDecodeString(raw, "importNamePattern")
408
+ if err != nil {
409
+ return pattern, err
410
+ }
411
+ pattern.names.importNamePattern, err = noRestrictedImportsCompileRegexp(pattern.names.importNamePatternText, true)
412
+ if err != nil {
413
+ return pattern, fmt.Errorf("option %q must be a valid regular expression: %w", "importNamePattern", err)
414
+ }
415
+ }
416
+ if raw, present := object["allowImportNamePattern"]; present {
417
+ pattern.names.allowImportNameText, err = noRestrictedImportsDecodeString(raw, "allowImportNamePattern")
418
+ if err != nil {
419
+ return pattern, err
420
+ }
421
+ pattern.names.allowImportNamePattern, err = noRestrictedImportsCompileRegexp(pattern.names.allowImportNameText, true)
422
+ if err != nil {
423
+ return pattern, fmt.Errorf("option %q must be a valid regular expression: %w", "allowImportNamePattern", err)
424
+ }
425
+ }
426
+ if err := pattern.names.validatePatternCombinations(); err != nil {
427
+ return pattern, err
428
+ }
429
+ return pattern, nil
430
+ }
431
+
432
+ func (control noRestrictedImportsNameControl) validatePatternCombinations() error {
433
+ if control.hasImportNames && control.hasAllowImportNames {
434
+ return fmt.Errorf("options %q and %q cannot be combined", "importNames", "allowImportNames")
435
+ }
436
+ if control.importNamePattern != nil && control.allowImportNamePattern != nil {
437
+ return fmt.Errorf("options %q and %q cannot be combined", "importNamePattern", "allowImportNamePattern")
438
+ }
439
+ if control.hasImportNames && control.allowImportNamePattern != nil {
440
+ return fmt.Errorf("options %q and %q cannot be combined", "importNames", "allowImportNamePattern")
441
+ }
442
+ if control.importNamePattern != nil && control.hasAllowImportNames {
443
+ return fmt.Errorf("options %q and %q cannot be combined", "importNamePattern", "allowImportNames")
444
+ }
445
+ if control.hasAllowImportNames && control.allowImportNamePattern != nil {
446
+ return fmt.Errorf("options %q and %q cannot be combined", "allowImportNames", "allowImportNamePattern")
447
+ }
448
+ return nil
449
+ }
450
+
451
+ func noRestrictedImportsDecodeObject(raw json.RawMessage, name string) (map[string]json.RawMessage, error) {
452
+ var object map[string]json.RawMessage
453
+ if err := json.Unmarshal(raw, &object); err != nil || object == nil {
454
+ return nil, fmt.Errorf("%s must be an object", name)
455
+ }
456
+ return object, nil
457
+ }
458
+
459
+ func noRestrictedImportsDecodeArray(raw json.RawMessage, name string) ([]json.RawMessage, error) {
460
+ var entries []json.RawMessage
461
+ if err := json.Unmarshal(raw, &entries); err != nil || entries == nil {
462
+ return nil, fmt.Errorf("option %q must be an array", name)
463
+ }
464
+ return entries, nil
465
+ }
466
+
467
+ func noRestrictedImportsDecodeString(raw json.RawMessage, name string) (string, error) {
468
+ var decoded any
469
+ if err := json.Unmarshal(raw, &decoded); err != nil {
470
+ return "", fmt.Errorf("option %q must be a string", name)
471
+ }
472
+ value, ok := decoded.(string)
473
+ if !ok {
474
+ return "", fmt.Errorf("option %q must be a string", name)
475
+ }
476
+ return value, nil
477
+ }
478
+
479
+ func noRestrictedImportsDecodeBool(raw json.RawMessage, name string) (bool, error) {
480
+ var decoded any
481
+ if err := json.Unmarshal(raw, &decoded); err != nil {
482
+ return false, fmt.Errorf("option %q must be a boolean", name)
483
+ }
484
+ value, ok := decoded.(bool)
485
+ if !ok {
486
+ return false, fmt.Errorf("option %q must be a boolean", name)
487
+ }
488
+ return value, nil
489
+ }
490
+
491
+ func noRestrictedImportsStringList(raw json.RawMessage, name string, nonEmpty, unique bool) ([]string, error) {
492
+ entries, err := noRestrictedImportsDecodeArray(raw, name)
493
+ if err != nil {
494
+ return nil, err
495
+ }
496
+ if nonEmpty && len(entries) == 0 {
497
+ return nil, fmt.Errorf("option %q must contain at least one string", name)
498
+ }
499
+ values := make([]string, 0, len(entries))
500
+ seen := make(map[string]struct{}, len(entries))
501
+ for index, rawEntry := range entries {
502
+ value, err := noRestrictedImportsDecodeString(rawEntry, fmt.Sprintf("%s[%d]", name, index))
503
+ if err != nil {
504
+ return nil, err
505
+ }
506
+ if unique {
507
+ if _, duplicate := seen[value]; duplicate {
508
+ return nil, fmt.Errorf("option %q contains duplicate value %q", name, value)
509
+ }
510
+ seen[value] = struct{}{}
511
+ }
512
+ values = append(values, value)
513
+ }
514
+ return values, nil
515
+ }
516
+
517
+ func noRestrictedImportsRejectUnknown(object map[string]json.RawMessage, allowed ...string) error {
518
+ known := make(map[string]struct{}, len(allowed))
519
+ for _, name := range allowed {
520
+ known[name] = struct{}{}
521
+ }
522
+ unknown := make([]string, 0)
523
+ for name := range object {
524
+ if _, ok := known[name]; !ok {
525
+ unknown = append(unknown, name)
526
+ }
527
+ }
528
+ if len(unknown) == 0 {
73
529
  return nil
74
530
  }
531
+ sort.Strings(unknown)
532
+ return fmt.Errorf("unknown option %q", unknown[0])
533
+ }
534
+
535
+ func noRestrictedImportsRequireUnique(entries []json.RawMessage, name string) error {
536
+ seen := make(map[string]struct{}, len(entries))
537
+ for index, raw := range entries {
538
+ var value any
539
+ if err := json.Unmarshal(raw, &value); err != nil {
540
+ return fmt.Errorf("%s[%d] must be valid JSON: %w", name, index, err)
541
+ }
542
+ canonical, err := json.Marshal(value)
543
+ if err != nil {
544
+ return fmt.Errorf("%s[%d] cannot be normalized: %w", name, index, err)
545
+ }
546
+ key := string(canonical)
547
+ if _, duplicate := seen[key]; duplicate {
548
+ return fmt.Errorf("option %q contains a duplicate entry at index %d", name, index)
549
+ }
550
+ seen[key] = struct{}{}
551
+ }
552
+ return nil
553
+ }
554
+
555
+ func noRestrictedImportsCompileRegexp(expression string, caseSensitive bool) (*regexp.Regexp, error) {
556
+ if !caseSensitive {
557
+ expression = "(?i:" + expression + ")"
558
+ }
559
+ return regexp.Compile(expression)
560
+ }
561
+
562
+ func noRestrictedImportsCompileGlobGroup(patterns []string, caseSensitive bool) ([]noRestrictedImportsGlob, error) {
563
+ group := make([]noRestrictedImportsGlob, 0, len(patterns))
564
+ for index, pattern := range patterns {
565
+ compiled, active, err := noRestrictedImportsCompileGlob(pattern, caseSensitive)
566
+ if err != nil {
567
+ return nil, fmt.Errorf("option %q[%d] is not a valid pattern: %w", "group", index, err)
568
+ }
569
+ if active {
570
+ group = append(group, compiled)
571
+ }
572
+ }
573
+ return group, nil
574
+ }
575
+
576
+ func noRestrictedImportsCompileGlob(raw string, caseSensitive bool) (noRestrictedImportsGlob, bool, error) {
577
+ var glob noRestrictedImportsGlob
578
+ pattern := strings.TrimPrefix(raw, "\uFEFF")
579
+ pattern = noRestrictedImportsTrimPattern(pattern)
580
+ if pattern == "" || strings.HasPrefix(pattern, "#") {
581
+ return glob, false, nil
582
+ }
583
+ if strings.HasPrefix(pattern, `\#`) || strings.HasPrefix(pattern, `\!`) {
584
+ pattern = pattern[1:]
585
+ } else if strings.HasPrefix(pattern, "!") {
586
+ glob.negated = true
587
+ pattern = pattern[1:]
588
+ }
589
+ if pattern == "" {
590
+ return glob, false, nil
591
+ }
592
+ glob.directoryOnly = strings.HasSuffix(pattern, "/")
593
+ pattern = strings.TrimSuffix(pattern, "/")
594
+ rooted := strings.HasPrefix(pattern, "/")
595
+ pattern = strings.TrimPrefix(pattern, "/")
596
+ if pattern == "" {
597
+ return glob, false, nil
598
+ }
599
+
600
+ body, err := noRestrictedImportsGlobRegexp(pattern, rooted)
601
+ if err != nil {
602
+ return glob, false, err
603
+ }
604
+ if !rooted && !strings.Contains(pattern, "/") {
605
+ body = `(^|/)` + body
606
+ } else {
607
+ body = `^` + body
608
+ }
609
+ body += `$`
610
+ glob.expression, err = noRestrictedImportsCompileRegexp(body, caseSensitive)
611
+ if err != nil {
612
+ return glob, false, err
613
+ }
614
+ return glob, true, nil
615
+ }
616
+
617
+ func noRestrictedImportsTrimPattern(pattern string) string {
618
+ runes := []rune(pattern)
619
+ for len(runes) > 0 && unicode.IsSpace(runes[len(runes)-1]) {
620
+ slashCount := 0
621
+ for index := len(runes) - 2; index >= 0 && runes[index] == '\\'; index-- {
622
+ slashCount++
623
+ }
624
+ if slashCount%2 == 1 {
625
+ runes = append(runes[:len(runes)-2], runes[len(runes)-1])
626
+ break
627
+ }
628
+ runes = runes[:len(runes)-1]
629
+ }
630
+ return string(runes)
631
+ }
632
+
633
+ func noRestrictedImportsGlobRegexp(pattern string, rooted bool) (string, error) {
634
+ runes := []rune(pattern)
635
+ var expression strings.Builder
636
+ for index := 0; index < len(runes); index++ {
637
+ current := runes[index]
638
+ switch current {
639
+ case '\\':
640
+ if index+1 < len(runes) {
641
+ index++
642
+ expression.WriteString(regexp.QuoteMeta(string(runes[index])))
643
+ } else {
644
+ expression.WriteString(`\\`)
645
+ }
646
+ case '*':
647
+ start := index
648
+ for index+1 < len(runes) && runes[index+1] == '*' {
649
+ index++
650
+ }
651
+ count := index - start + 1
652
+ previousSlash := start > 0 && runes[start-1] == '/'
653
+ nextSlash := index+1 < len(runes) && runes[index+1] == '/'
654
+ if count == 2 && nextSlash && (start == 0 || previousSlash) {
655
+ index++
656
+ expression.WriteString(`(?:[^/]+/)*`)
657
+ } else if count == 2 && previousSlash && index == len(runes)-1 {
658
+ expression.WriteString(`.+`)
659
+ } else if count == 1 && index == len(runes)-1 && (previousSlash || (rooted && start == 0)) {
660
+ expression.WriteString(`[^/]+`)
661
+ } else {
662
+ expression.WriteString(`[^/]*`)
663
+ }
664
+ case '?':
665
+ expression.WriteString(`[^/]`)
666
+ case '[':
667
+ closeIndex := index + 1
668
+ for closeIndex < len(runes) && runes[closeIndex] != ']' {
669
+ closeIndex++
670
+ }
671
+ if closeIndex == len(runes) {
672
+ expression.WriteString(`\[`)
673
+ continue
674
+ }
675
+ class := string(runes[index+1 : closeIndex])
676
+ if strings.HasPrefix(class, "!") {
677
+ class = "^/" + class[1:]
678
+ } else if strings.HasPrefix(class, "^") {
679
+ class = `\^` + class[1:]
680
+ }
681
+ characterClass := "[" + class + "]"
682
+ if _, err := regexp.Compile(characterClass); err != nil {
683
+ // Gitignore accepts malformed or descending ranges as patterns and
684
+ // treats an unusable range as non-matching instead of rejecting the
685
+ // entire configuration.
686
+ expression.WriteString(`(?:\b\B)`)
687
+ } else {
688
+ expression.WriteString(characterClass)
689
+ }
690
+ index = closeIndex
691
+ default:
692
+ expression.WriteString(regexp.QuoteMeta(string(current)))
693
+ }
694
+ }
695
+ return expression.String(), nil
696
+ }
697
+
698
+ func (pattern noRestrictedImportsPattern) matches(source string) bool {
699
+ if pattern.expression != nil {
700
+ return pattern.expression.MatchString(source)
701
+ }
702
+ if len(pattern.group) == 0 {
703
+ return false
704
+ }
705
+ parts := strings.Split(source, "/")
706
+ for index := range parts {
707
+ candidate := strings.Join(parts[:index+1], "/")
708
+ directory := index < len(parts)-1
709
+ ignored := false
710
+ for _, glob := range pattern.group {
711
+ if glob.directoryOnly && !directory {
712
+ continue
713
+ }
714
+ if glob.expression.MatchString(candidate) {
715
+ ignored = !glob.negated
716
+ }
717
+ }
718
+ if ignored {
719
+ return true
720
+ }
721
+ }
722
+ return false
723
+ }
724
+
725
+ func noRestrictedImportsReferenceOf(file *shimast.SourceFile, node *shimast.Node) (noRestrictedImportsReference, bool) {
726
+ var reference noRestrictedImportsReference
727
+ if node == nil {
728
+ return reference, false
729
+ }
75
730
  switch node.Kind {
76
731
  case shimast.KindImportDeclaration:
77
- if imp := node.AsImportDeclaration(); imp != nil {
78
- return imp.ModuleSpecifier
732
+ declaration := node.AsImportDeclaration()
733
+ if declaration == nil || declaration.ModuleSpecifier == nil {
734
+ return reference, false
79
735
  }
736
+ reference.sourceNode = declaration.ModuleSpecifier
737
+ reference.names, reference.wholeTypeOnly = noRestrictedImportsImportNames(declaration)
80
738
  case shimast.KindExportDeclaration:
81
- if exp := node.AsExportDeclaration(); exp != nil {
82
- return exp.ModuleSpecifier
739
+ declaration := node.AsExportDeclaration()
740
+ if declaration == nil || declaration.ModuleSpecifier == nil {
741
+ return reference, false
83
742
  }
743
+ reference.sourceNode = declaration.ModuleSpecifier
744
+ reference.names, reference.wholeTypeOnly = noRestrictedImportsExportNames(file, node, declaration)
745
+ case shimast.KindImportEqualsDeclaration:
746
+ declaration := node.AsImportEqualsDeclaration()
747
+ if declaration == nil || declaration.ModuleReference == nil ||
748
+ declaration.ModuleReference.Kind != shimast.KindExternalModuleReference {
749
+ return reference, false
750
+ }
751
+ module := declaration.ModuleReference.AsExternalModuleReference()
752
+ if module == nil || module.Expression == nil {
753
+ return reference, false
754
+ }
755
+ reference.sourceNode = module.Expression
756
+ reference.wholeTypeOnly = declaration.IsTypeOnly
757
+ default:
758
+ return reference, false
759
+ }
760
+ reference.source = stringLiteralText(reference.sourceNode)
761
+ return reference, true
762
+ }
763
+
764
+ func noRestrictedImportsImportNames(declaration *shimast.ImportDeclaration) ([]noRestrictedImportsImportedName, bool) {
765
+ if declaration == nil || declaration.ImportClause == nil {
766
+ return nil, false
767
+ }
768
+ clauseNode := declaration.ImportClause
769
+ clause := clauseNode.AsImportClause()
770
+ if clause == nil {
771
+ return nil, false
772
+ }
773
+ clauseTypeOnly := clauseNode.IsTypeOnly()
774
+ names := make([]noRestrictedImportsImportedName, 0)
775
+ if name := clause.Name(); name != nil {
776
+ names = append(names, noRestrictedImportsImportedName{name: "default", node: name, typeOnly: clauseTypeOnly})
777
+ }
778
+ if bindings := clause.NamedBindings; bindings != nil {
779
+ switch bindings.Kind {
780
+ case shimast.KindNamespaceImport:
781
+ names = append(names, noRestrictedImportsImportedName{name: "*", node: bindings, typeOnly: clauseTypeOnly})
782
+ case shimast.KindNamedImports:
783
+ named := bindings.AsNamedImports()
784
+ if named != nil && named.Elements != nil {
785
+ for _, element := range named.Elements.Nodes {
786
+ specifier := element.AsImportSpecifier()
787
+ if specifier == nil {
788
+ continue
789
+ }
790
+ imported := specifier.PropertyName
791
+ if imported == nil {
792
+ imported = specifier.Name()
793
+ }
794
+ names = append(names, noRestrictedImportsImportedName{
795
+ name: noRestrictedImportsModuleExportName(imported),
796
+ node: element,
797
+ typeOnly: clauseTypeOnly || specifier.IsTypeOnly,
798
+ })
799
+ }
800
+ }
801
+ }
802
+ }
803
+ return names, clauseTypeOnly || noRestrictedImportsAllNamesTypeOnly(names)
804
+ }
805
+
806
+ func noRestrictedImportsExportNames(
807
+ file *shimast.SourceFile,
808
+ node *shimast.Node,
809
+ declaration *shimast.ExportDeclaration,
810
+ ) ([]noRestrictedImportsImportedName, bool) {
811
+ if declaration == nil {
812
+ return nil, false
813
+ }
814
+ declarationTypeOnly := declaration.IsTypeOnly
815
+ clause := declaration.ExportClause
816
+ if clause == nil {
817
+ pos, end := noRestrictedImportsExportStarRange(file, node, declaration.ModuleSpecifier)
818
+ return []noRestrictedImportsImportedName{{name: "*", pos: pos, end: end, typeOnly: declarationTypeOnly}}, declarationTypeOnly
819
+ }
820
+ if clause.Kind == shimast.KindNamespaceExport {
821
+ pos, end := noRestrictedImportsExportStarRange(file, node, declaration.ModuleSpecifier)
822
+ return []noRestrictedImportsImportedName{{name: "*", pos: pos, end: end, typeOnly: declarationTypeOnly}}, declarationTypeOnly
823
+ }
824
+ if clause.Kind != shimast.KindNamedExports {
825
+ return nil, declarationTypeOnly
826
+ }
827
+ named := clause.AsNamedExports()
828
+ if named == nil || named.Elements == nil {
829
+ return nil, declarationTypeOnly
830
+ }
831
+ names := make([]noRestrictedImportsImportedName, 0, len(named.Elements.Nodes))
832
+ for _, element := range named.Elements.Nodes {
833
+ specifier := element.AsExportSpecifier()
834
+ if specifier == nil {
835
+ continue
836
+ }
837
+ imported := specifier.PropertyName
838
+ if imported == nil {
839
+ imported = specifier.Name()
840
+ }
841
+ names = append(names, noRestrictedImportsImportedName{
842
+ name: noRestrictedImportsModuleExportName(imported),
843
+ node: element,
844
+ typeOnly: declarationTypeOnly || specifier.IsTypeOnly,
845
+ })
846
+ }
847
+ return names, declarationTypeOnly || noRestrictedImportsAllNamesTypeOnly(names)
848
+ }
849
+
850
+ func noRestrictedImportsModuleExportName(node *shimast.Node) string {
851
+ if name := identifierText(node); name != "" {
852
+ return name
853
+ }
854
+ return stringLiteralText(node)
855
+ }
856
+
857
+ func noRestrictedImportsAllNamesTypeOnly(names []noRestrictedImportsImportedName) bool {
858
+ if len(names) == 0 {
859
+ return false
860
+ }
861
+ for _, name := range names {
862
+ if !name.typeOnly {
863
+ return false
864
+ }
865
+ }
866
+ return true
867
+ }
868
+
869
+ func noRestrictedImportsExportStarRange(
870
+ file *shimast.SourceFile,
871
+ declaration *shimast.Node,
872
+ moduleSpecifier *shimast.Node,
873
+ ) (int, int) {
874
+ if file == nil || declaration == nil {
875
+ return -1, -1
876
+ }
877
+ source := file.Text()
878
+ start, _ := tokenRange(file, declaration)
879
+ limit, _ := tokenRange(file, moduleSpecifier)
880
+ if start < 0 || limit < start || limit > len(source) {
881
+ return -1, -1
882
+ }
883
+ scanner := shimscanner.NewScanner()
884
+ scanner.SetText(source[start:limit])
885
+ scanner.SetSkipTrivia(true)
886
+ for {
887
+ switch scanner.Scan() {
888
+ case shimast.KindAsteriskToken:
889
+ return start + scanner.TokenStart(), start + scanner.TokenEnd()
890
+ case shimast.KindEndOfFile:
891
+ return -1, -1
892
+ }
893
+ }
894
+ }
895
+
896
+ func reportNoRestrictedImportsPath(ctx *Context, reference noRestrictedImportsReference, restricted noRestrictedImportsPath) {
897
+ if restricted.allowTypeImports && reference.wholeTypeOnly {
898
+ return
899
+ }
900
+ if !restricted.names.hasRestrictions() {
901
+ message := fmt.Sprintf("'%s' import is restricted from being used.", reference.source)
902
+ ctx.Report(reference.sourceNode, noRestrictedImportsCustomMessage(message, restricted.message))
903
+ return
904
+ }
905
+ reportNoRestrictedImportsNames(ctx, reference, restricted.names, restricted.message, false, restricted.allowTypeImports)
906
+ }
907
+
908
+ func reportNoRestrictedImportsPattern(ctx *Context, reference noRestrictedImportsReference, restricted noRestrictedImportsPattern) {
909
+ if restricted.allowTypeImports && reference.wholeTypeOnly {
910
+ return
911
+ }
912
+ if !restricted.names.hasRestrictions() {
913
+ message := fmt.Sprintf("'%s' import is restricted from being used by a pattern.", reference.source)
914
+ ctx.Report(reference.sourceNode, noRestrictedImportsCustomMessage(message, restricted.message))
915
+ return
916
+ }
917
+ reportNoRestrictedImportsNames(ctx, reference, restricted.names, restricted.message, true, restricted.allowTypeImports)
918
+ }
919
+
920
+ func (control noRestrictedImportsNameControl) hasRestrictions() bool {
921
+ return control.hasImportNames || control.hasAllowImportNames ||
922
+ control.importNamePattern != nil || control.allowImportNamePattern != nil
923
+ }
924
+
925
+ func reportNoRestrictedImportsNames(
926
+ ctx *Context,
927
+ reference noRestrictedImportsReference,
928
+ control noRestrictedImportsNameControl,
929
+ customMessage string,
930
+ pattern bool,
931
+ allowTypeImports bool,
932
+ ) {
933
+ for _, imported := range reference.names {
934
+ if allowTypeImports && imported.typeOnly {
935
+ continue
936
+ }
937
+ if imported.name == "*" {
938
+ message := noRestrictedImportsNamespaceMessage(reference.source, control, pattern)
939
+ imported.report(ctx, noRestrictedImportsCustomMessage(message, customMessage))
940
+ continue
941
+ }
942
+ restricted := control.hasImportNames && noRestrictedImportsContains(control.importNames, imported.name)
943
+ restricted = restricted || (control.importNamePattern != nil && control.importNamePattern.MatchString(imported.name))
944
+ if restricted {
945
+ message := fmt.Sprintf("'%s' import from '%s' is restricted.", imported.name, reference.source)
946
+ if pattern {
947
+ message = fmt.Sprintf("'%s' import from '%s' is restricted from being used by a pattern.", imported.name, reference.source)
948
+ }
949
+ imported.report(ctx, noRestrictedImportsCustomMessage(message, customMessage))
950
+ }
951
+ if control.hasAllowImportNames && !noRestrictedImportsContains(control.allowImportNames, imported.name) {
952
+ message := fmt.Sprintf(
953
+ "'%s' import from '%s' is restricted because only %s %s allowed.",
954
+ imported.name,
955
+ reference.source,
956
+ noRestrictedImportsFormatNames(control.allowImportNames),
957
+ noRestrictedImportsIsOrAre(control.allowImportNames),
958
+ )
959
+ imported.report(ctx, noRestrictedImportsCustomMessage(message, customMessage))
960
+ } else if control.allowImportNamePattern != nil && !control.allowImportNamePattern.MatchString(imported.name) {
961
+ message := fmt.Sprintf(
962
+ "'%s' import from '%s' is restricted because only imports that match the pattern '%s' are allowed from '%s'.",
963
+ imported.name,
964
+ reference.source,
965
+ control.allowImportNameText,
966
+ reference.source,
967
+ )
968
+ imported.report(ctx, noRestrictedImportsCustomMessage(message, customMessage))
969
+ }
970
+ }
971
+ }
972
+
973
+ func noRestrictedImportsNamespaceMessage(source string, control noRestrictedImportsNameControl, pattern bool) string {
974
+ if control.hasImportNames {
975
+ suffix := "restricted"
976
+ if pattern {
977
+ suffix = "restricted from being used by a pattern"
978
+ }
979
+ return fmt.Sprintf(
980
+ "* import is invalid because %s from '%s' %s %s.",
981
+ noRestrictedImportsFormatNames(control.importNames),
982
+ source,
983
+ noRestrictedImportsIsOrAre(control.importNames),
984
+ suffix,
985
+ )
986
+ }
987
+ if control.hasAllowImportNames {
988
+ return fmt.Sprintf(
989
+ "* import is invalid because only %s from '%s' %s allowed.",
990
+ noRestrictedImportsFormatNames(control.allowImportNames),
991
+ source,
992
+ noRestrictedImportsIsOrAre(control.allowImportNames),
993
+ )
994
+ }
995
+ if control.allowImportNamePattern != nil {
996
+ return fmt.Sprintf(
997
+ "* import is invalid because only imports that match the pattern '%s' from '%s' are allowed.",
998
+ control.allowImportNameText,
999
+ source,
1000
+ )
1001
+ }
1002
+ return fmt.Sprintf(
1003
+ "* import is invalid because import name matching '%s' pattern from '%s' is restricted from being used.",
1004
+ control.importNamePatternText,
1005
+ source,
1006
+ )
1007
+ }
1008
+
1009
+ func (imported noRestrictedImportsImportedName) report(ctx *Context, message string) {
1010
+ if imported.node != nil {
1011
+ ctx.Report(imported.node, message)
1012
+ return
1013
+ }
1014
+ if imported.pos >= 0 && imported.end > imported.pos {
1015
+ ctx.ReportRange(imported.pos, imported.end, message)
1016
+ }
1017
+ }
1018
+
1019
+ func noRestrictedImportsContains(values []string, value string) bool {
1020
+ for _, candidate := range values {
1021
+ if candidate == value {
1022
+ return true
1023
+ }
1024
+ }
1025
+ return false
1026
+ }
1027
+
1028
+ func noRestrictedImportsFormatNames(names []string) string {
1029
+ quoted := make([]string, len(names))
1030
+ for index, name := range names {
1031
+ quoted[index] = "'" + name + "'"
1032
+ }
1033
+ switch len(quoted) {
1034
+ case 0:
1035
+ return ""
1036
+ case 1:
1037
+ return quoted[0]
1038
+ case 2:
1039
+ return quoted[0] + " and " + quoted[1]
1040
+ default:
1041
+ return strings.Join(quoted[:len(quoted)-1], ", ") + ", and " + quoted[len(quoted)-1]
84
1042
  }
85
- return nil
86
1043
  }
87
1044
 
88
- // resolveNoRestrictedImportsPaths returns the active deny list for the
89
- // firing context. A `paths` array on the user's options blob wins; an
90
- // empty / missing blob falls back to `hardCodedRestrictedImports` so
91
- // the AST-layer test fires deterministically.
92
- func resolveNoRestrictedImportsPaths(ctx *Context) []string {
93
- if ctx == nil || len(ctx.Options) == 0 {
94
- return hardCodedRestrictedImports
1045
+ func noRestrictedImportsIsOrAre(names []string) string {
1046
+ if len(names) == 1 {
1047
+ return "is"
95
1048
  }
96
- var opts noRestrictedImportsOptions
97
- if err := json.Unmarshal(ctx.Options, &opts); err != nil || len(opts.Paths) == 0 {
98
- return hardCodedRestrictedImports
1049
+ return "are"
1050
+ }
1051
+
1052
+ func noRestrictedImportsCustomMessage(message, custom string) string {
1053
+ if custom == "" {
1054
+ return message
99
1055
  }
100
- return opts.Paths
1056
+ return message + " " + custom
101
1057
  }
102
1058
 
103
1059
  func init() {