@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
@@ -0,0 +1,603 @@
1
+ // Config and custom-stats discovery for the browserslist port
2
+ // (`polyfill_browserslist.go`): a transliteration of browserslist 4.28.6
3
+ // `node.js` — `loadConfig`, `findConfig(File)`, `parseConfig`,
4
+ // `parsePackage`, `pickEnv`, `getStat`, and `normalizeStats` — minus the
5
+ // process-level caches (every lookup re-reads the filesystem so long-lived
6
+ // hosts never serve stale config) and minus `extends` / regional loaders,
7
+ // which need a JS runtime or data ttsc does not embed.
8
+ package linthost
9
+
10
+ import (
11
+ "encoding/json"
12
+ "fmt"
13
+ "os"
14
+ "path/filepath"
15
+ "regexp"
16
+ "sort"
17
+ "strings"
18
+ )
19
+
20
+ // browserslistSectionValue is one config section: either a query array or the
21
+ // raw string form package.json allows (`"browserslist": "query"`), whose JS
22
+ // falsiness when empty decides fallbacks.
23
+ type browserslistSectionValue struct {
24
+ queries []string
25
+ isString bool
26
+ str string
27
+ }
28
+
29
+ // browserslistPackageConfig is a parsed config file: env sections plus the
30
+ // `invalid` marker for truthy non-config `browserslist` values, which
31
+ // upstream carries through and fails on only when the config is used.
32
+ type browserslistPackageConfig struct {
33
+ sections map[string]browserslistSectionValue
34
+ invalid bool
35
+ }
36
+
37
+ var browserslistSectionPattern = regexp.MustCompile(`^\s*\[(.+)]\s*$`)
38
+ var browserslistCommentPattern = regexp.MustCompile(`#[^\n]*`)
39
+
40
+ // browserslistLoadConfig mirrors loadConfig: the BROWSERSLIST /
41
+ // BROWSERSLIST_CONFIG environment overrides, then config discovery from the
42
+ // path. `found` reports a JS-truthy config (an empty query array is still
43
+ // truthy, and resolves to an empty browser list).
44
+ func browserslistLoadConfig(opts browserslistOpts) (queries []string, found bool, err error) {
45
+ if value := os.Getenv("BROWSERSLIST"); value != "" {
46
+ return []string{value}, true, nil
47
+ }
48
+ if file := os.Getenv("BROWSERSLIST_CONFIG"); file != "" {
49
+ config, parseErr := browserslistParsePackageOrReadConfig(file)
50
+ if parseErr != nil {
51
+ return nil, false, parseErr
52
+ }
53
+ return browserslistPickEnv(config, opts)
54
+ }
55
+ if opts.path != "" {
56
+ config, findErr := browserslistFindConfig(opts.path)
57
+ if findErr != nil {
58
+ return nil, false, findErr
59
+ }
60
+ return browserslistPickEnv(config, opts)
61
+ }
62
+ return nil, false, nil
63
+ }
64
+
65
+ // browserslistPickEnv mirrors pickEnv over the parsed sections, using JS
66
+ // truthiness: a section holding an empty string falls through to `defaults`,
67
+ // and an empty-string result reads as "no config".
68
+ func browserslistPickEnv(config *browserslistPackageConfig, opts browserslistOpts) ([]string, bool, error) {
69
+ if config == nil {
70
+ return nil, false, nil
71
+ }
72
+ if config.invalid {
73
+ return nil, false, newBrowserslistError(
74
+ "Browserslist config should be a string or an array of strings with browser queries")
75
+ }
76
+ name := opts.env
77
+ if name == "" {
78
+ name = os.Getenv("BROWSERSLIST_ENV")
79
+ }
80
+ if name == "" {
81
+ name = os.Getenv("NODE_ENV")
82
+ }
83
+ if name == "" {
84
+ name = "production"
85
+ }
86
+ section, ok := config.sections[name]
87
+ if !ok || browserslistSectionFalsy(section) {
88
+ section, ok = config.sections["defaults"]
89
+ if !ok || browserslistSectionFalsy(section) {
90
+ return nil, false, nil
91
+ }
92
+ }
93
+ if section.isString {
94
+ return []string{section.str}, true, nil
95
+ }
96
+ if section.queries == nil {
97
+ return []string{}, true, nil
98
+ }
99
+ return section.queries, true, nil
100
+ }
101
+
102
+ // browserslistSectionFalsy reports the JS falsiness of a section value: only
103
+ // the empty string is falsy (an empty array is truthy).
104
+ func browserslistSectionFalsy(section browserslistSectionValue) bool {
105
+ return section.isString && section.str == ""
106
+ }
107
+
108
+ func browserslistParsePackageOrReadConfig(file string) (*browserslistPackageConfig, error) {
109
+ if filepath.Base(file) == "package.json" {
110
+ return browserslistParsePackage(file)
111
+ }
112
+ return browserslistReadConfig(file)
113
+ }
114
+
115
+ // browserslistParsePackage mirrors parsePackage. Config-shape problems come
116
+ // back as *browserslistConfigError (findConfigFile re-throws those) while
117
+ // unreadable / unparsable JSON comes back as a generic error (findConfigFile
118
+ // treats the directory as config-free, like upstream's warn-and-ignore).
119
+ func browserslistParsePackage(file string) (*browserslistPackageConfig, error) {
120
+ text, err := os.ReadFile(file)
121
+ if err != nil {
122
+ return nil, fmt.Errorf("could not read %s: %w", file, err)
123
+ }
124
+ body := strings.TrimPrefix(string(text), "\uFEFF")
125
+ if strings.Contains(body, `"browserslist"`) {
126
+ var parsed struct {
127
+ Browserslist json.RawMessage `json:"browserslist"`
128
+ }
129
+ if jsonErr := json.Unmarshal([]byte(body), &parsed); jsonErr != nil {
130
+ return nil, fmt.Errorf("could not parse %s: %w", file, jsonErr)
131
+ }
132
+ return browserslistNormalizePackageValue(parsed.Browserslist)
133
+ }
134
+ if strings.Contains(body, `"browserlist"`) {
135
+ var parsed struct {
136
+ Browserlist json.RawMessage `json:"browserlist"`
137
+ }
138
+ if jsonErr := json.Unmarshal([]byte(body), &parsed); jsonErr != nil {
139
+ return nil, fmt.Errorf("could not parse %s: %w", file, jsonErr)
140
+ }
141
+ if browserslistJSONTruthy(parsed.Browserlist) {
142
+ return nil, newBrowserslistError(
143
+ "`browserlist` key instead of `browserslist` in %s", file)
144
+ }
145
+ }
146
+ return nil, nil
147
+ }
148
+
149
+ // browserslistNormalizePackageValue turns the raw `browserslist` package.json
150
+ // value into sections, mirroring the string/array wrapping and the `check()`
151
+ // validation. Truthy scalars become the deferred `invalid` config; falsy ones
152
+ // read as "no config here".
153
+ func browserslistNormalizePackageValue(raw json.RawMessage) (*browserslistPackageConfig, error) {
154
+ trimmed := strings.TrimSpace(string(raw))
155
+ if trimmed == "" || trimmed == "null" {
156
+ return nil, nil
157
+ }
158
+ switch trimmed[0] {
159
+ case '"':
160
+ var value string
161
+ if err := json.Unmarshal(raw, &value); err != nil {
162
+ return nil, fmt.Errorf("invalid browserslist string: %w", err)
163
+ }
164
+ return &browserslistPackageConfig{
165
+ sections: map[string]browserslistSectionValue{
166
+ "defaults": {isString: true, str: value},
167
+ },
168
+ }, nil
169
+ case '[':
170
+ queries, err := browserslistDecodeQueryArray(raw)
171
+ if err != nil {
172
+ return nil, err
173
+ }
174
+ return &browserslistPackageConfig{
175
+ sections: map[string]browserslistSectionValue{"defaults": {queries: queries}},
176
+ }, nil
177
+ case '{':
178
+ var sections map[string]json.RawMessage
179
+ if err := json.Unmarshal(raw, &sections); err != nil {
180
+ return nil, fmt.Errorf("invalid browserslist sections: %w", err)
181
+ }
182
+ config := &browserslistPackageConfig{sections: map[string]browserslistSectionValue{}}
183
+ for name, value := range sections {
184
+ valueTrimmed := strings.TrimSpace(string(value))
185
+ if strings.HasPrefix(valueTrimmed, `"`) {
186
+ var str string
187
+ if err := json.Unmarshal(value, &str); err != nil {
188
+ return nil, fmt.Errorf("invalid browserslist section: %w", err)
189
+ }
190
+ config.sections[name] = browserslistSectionValue{isString: true, str: str}
191
+ continue
192
+ }
193
+ queries, err := browserslistDecodeQueryArray(value)
194
+ if err != nil {
195
+ return nil, err
196
+ }
197
+ config.sections[name] = browserslistSectionValue{queries: queries}
198
+ }
199
+ return config, nil
200
+ default:
201
+ // Scalar: JS keeps falsy values as "no config" and carries truthy ones
202
+ // until `pickEnv` / query checking fails on them.
203
+ if trimmed == "false" || trimmed == "0" {
204
+ return nil, nil
205
+ }
206
+ return &browserslistPackageConfig{invalid: true}, nil
207
+ }
208
+ }
209
+
210
+ // browserslistDecodeQueryArray mirrors check(): a section must be an array
211
+ // whose every element is a string (the string form is handled by callers).
212
+ func browserslistDecodeQueryArray(raw json.RawMessage) ([]string, error) {
213
+ var elements []json.RawMessage
214
+ if !strings.HasPrefix(strings.TrimSpace(string(raw)), "[") {
215
+ return nil, newBrowserslistError(
216
+ "Browserslist config should be a string or an array of strings with browser queries")
217
+ }
218
+ if err := json.Unmarshal(raw, &elements); err != nil {
219
+ return nil, newBrowserslistError(
220
+ "Browserslist config should be a string or an array of strings with browser queries")
221
+ }
222
+ queries := make([]string, 0, len(elements))
223
+ for _, element := range elements {
224
+ var query string
225
+ if err := json.Unmarshal(element, &query); err != nil {
226
+ return nil, newBrowserslistError(
227
+ "Browserslist config should be a string or an array of strings with browser queries")
228
+ }
229
+ queries = append(queries, query)
230
+ }
231
+ return queries, nil
232
+ }
233
+
234
+ func browserslistJSONTruthy(raw json.RawMessage) bool {
235
+ trimmed := strings.TrimSpace(string(raw))
236
+ switch trimmed {
237
+ case "", "null", "false", "0", `""`:
238
+ return false
239
+ }
240
+ return true
241
+ }
242
+
243
+ // browserslistParseConfig mirrors parseConfig for `browserslist` /
244
+ // `.browserslistrc` files: strip `#` comments, split lines on newlines AND
245
+ // commas, and route queries into `[section]` groups.
246
+ func browserslistParseConfig(text string) (*browserslistPackageConfig, error) {
247
+ config := &browserslistPackageConfig{
248
+ sections: map[string]browserslistSectionValue{"defaults": {queries: []string{}}},
249
+ }
250
+ sections := []string{"defaults"}
251
+ body := browserslistCommentPattern.ReplaceAllString(text, "")
252
+ for _, line := range strings.FieldsFunc(body, func(r rune) bool { return r == '\n' || r == ',' }) {
253
+ line = strings.TrimSpace(line)
254
+ if line == "" {
255
+ continue
256
+ }
257
+ if match := browserslistSectionPattern.FindStringSubmatch(line); match != nil {
258
+ sections = strings.Split(strings.TrimSpace(match[1]), " ")
259
+ for _, section := range sections {
260
+ if _, duplicate := config.sections[section]; duplicate {
261
+ return nil, newBrowserslistError(
262
+ "Duplicate section %s in Browserslist config", section)
263
+ }
264
+ config.sections[section] = browserslistSectionValue{queries: []string{}}
265
+ }
266
+ continue
267
+ }
268
+ for _, section := range sections {
269
+ value := config.sections[section]
270
+ value.queries = append(value.queries, line)
271
+ config.sections[section] = value
272
+ }
273
+ }
274
+ return config, nil
275
+ }
276
+
277
+ func browserslistReadConfig(file string) (*browserslistPackageConfig, error) {
278
+ if !browserslistIsFile(file) {
279
+ return nil, newBrowserslistError("Can't read %s config", file)
280
+ }
281
+ text, err := os.ReadFile(file)
282
+ if err != nil {
283
+ return nil, newBrowserslistError("Can't read %s config", file)
284
+ }
285
+ return browserslistParseConfig(string(text))
286
+ }
287
+
288
+ func browserslistIsFile(path string) bool {
289
+ stat, err := os.Stat(path)
290
+ return err == nil && stat.Mode().IsRegular()
291
+ }
292
+
293
+ func browserslistIsDirectory(path string) bool {
294
+ stat, err := os.Stat(path)
295
+ return err == nil && stat.IsDir()
296
+ }
297
+
298
+ // browserslistPathInRoot mirrors pathInRoot: BROWSERSLIST_ROOT_PATH bounds
299
+ // upward walks.
300
+ func browserslistPathInRoot(p string) bool {
301
+ root := os.Getenv("BROWSERSLIST_ROOT_PATH")
302
+ if root == "" {
303
+ return true
304
+ }
305
+ rootAbs, err := filepath.Abs(root)
306
+ if err != nil {
307
+ return true
308
+ }
309
+ relative, err := filepath.Rel(rootAbs, p)
310
+ if err != nil {
311
+ // Different volumes: node's path.relative returns the target itself,
312
+ // which never starts with "..".
313
+ return true
314
+ }
315
+ return !strings.HasPrefix(relative, "..")
316
+ }
317
+
318
+ // browserslistEachParent mirrors eachParent without the process cache: walk
319
+ // from `file` to the filesystem root, invoking the callback on directories.
320
+ func browserslistEachParent(file string, callback func(dir string) (string, bool, error)) (string, error) {
321
+ loc, err := filepath.Abs(file)
322
+ if err != nil {
323
+ return "", nil
324
+ }
325
+ loc = filepath.Clean(loc)
326
+ for {
327
+ if !browserslistPathInRoot(loc) {
328
+ return "", nil
329
+ }
330
+ if browserslistIsDirectory(loc) {
331
+ result, done, callbackErr := callback(loc)
332
+ if callbackErr != nil {
333
+ return "", callbackErr
334
+ }
335
+ if done {
336
+ return result, nil
337
+ }
338
+ }
339
+ parent := filepath.Dir(loc)
340
+ if parent == loc {
341
+ return "", nil
342
+ }
343
+ loc = parent
344
+ }
345
+ }
346
+
347
+ // browserslistFindConfigFile mirrors findConfigFile, including the
348
+ // conflicting-config errors and the "ignore unparsable package.json" rule.
349
+ func browserslistFindConfigFile(from string) (string, error) {
350
+ return browserslistEachParent(from, func(dir string) (string, bool, error) {
351
+ config := filepath.Join(dir, "browserslist")
352
+ pkg := filepath.Join(dir, "package.json")
353
+ rc := filepath.Join(dir, ".browserslistrc")
354
+
355
+ var pkgBrowserslist *browserslistPackageConfig
356
+ if browserslistIsFile(pkg) {
357
+ parsed, err := browserslistParsePackage(pkg)
358
+ if err != nil {
359
+ if _, isBrowserslistError := err.(*browserslistConfigError); isBrowserslistError {
360
+ return "", false, err
361
+ }
362
+ // Upstream warns and ignores package.json files it cannot parse.
363
+ } else {
364
+ pkgBrowserslist = parsed
365
+ }
366
+ }
367
+
368
+ configIsFile := browserslistIsFile(config)
369
+ rcIsFile := browserslistIsFile(rc)
370
+ switch {
371
+ case configIsFile && pkgBrowserslist != nil:
372
+ return "", false, newBrowserslistError(
373
+ "%s contains both browserslist and package.json with browsers", dir)
374
+ case rcIsFile && pkgBrowserslist != nil:
375
+ return "", false, newBrowserslistError(
376
+ "%s contains both .browserslistrc and package.json with browsers", dir)
377
+ case configIsFile && rcIsFile:
378
+ return "", false, newBrowserslistError(
379
+ "%s contains both .browserslistrc and browserslist", dir)
380
+ case configIsFile:
381
+ return config, true, nil
382
+ case rcIsFile:
383
+ return rc, true, nil
384
+ case pkgBrowserslist != nil:
385
+ return pkg, true, nil
386
+ }
387
+ return "", false, nil
388
+ })
389
+ }
390
+
391
+ func browserslistFindConfig(from string) (*browserslistPackageConfig, error) {
392
+ configFile, err := browserslistFindConfigFile(from)
393
+ if err != nil || configFile == "" {
394
+ return nil, err
395
+ }
396
+ return browserslistParsePackageOrReadConfig(configFile)
397
+ }
398
+
399
+ // --- custom usage stats -------------------------------------------------------
400
+
401
+ // browserslistGetStat mirrors getStat + normalizeStats + fillUsage: locate a
402
+ // browserslist-stats.json (env override or upward walk), then flatten it into
403
+ // "browser version" usage keys preserving JS object iteration order. JSON
404
+ // null usage values are tracked separately because they skip percentage
405
+ // filters yet still occupy coverage slots.
406
+ func browserslistGetStat(opts browserslistOpts) (map[string]float64, []string, map[string]bool, error) {
407
+ statsPath := os.Getenv("BROWSERSLIST_STATS")
408
+ if statsPath == "" && opts.path != "" {
409
+ found, err := browserslistEachParent(opts.path, func(dir string) (string, bool, error) {
410
+ candidate := filepath.Join(dir, "browserslist-stats.json")
411
+ if browserslistIsFile(candidate) {
412
+ return candidate, true, nil
413
+ }
414
+ return "", false, nil
415
+ })
416
+ if err != nil {
417
+ return nil, nil, nil, err
418
+ }
419
+ statsPath = found
420
+ }
421
+ if statsPath == "" {
422
+ return nil, nil, nil, nil
423
+ }
424
+ content, err := os.ReadFile(statsPath)
425
+ if err != nil {
426
+ return nil, nil, nil, newBrowserslistError("Can't read %s", statsPath)
427
+ }
428
+ root, err := browserslistParseOrderedJSON(content)
429
+ if err != nil {
430
+ return nil, nil, nil, newBrowserslistError("Can't read %s", statsPath)
431
+ }
432
+ stats, ok := root.(*browserslistOrderedObject)
433
+ if !ok {
434
+ // `'dataByBrowser' in stats` throws on JSON null / scalars upstream.
435
+ return nil, nil, nil, newBrowserslistError("Can't read %s", statsPath)
436
+ }
437
+ if inner, has := stats.get("dataByBrowser"); has {
438
+ innerObject, isObject := inner.(*browserslistOrderedObject)
439
+ if !isObject {
440
+ return nil, nil, nil, nil
441
+ }
442
+ stats = innerObject
443
+ }
444
+
445
+ data := browserslistData()
446
+ values := map[string]float64{}
447
+ var order []string
448
+ nulls := map[string]bool{}
449
+ for _, browser := range stats.keys {
450
+ browserStats, isObject := stats.values[browser].(*browserslistOrderedObject)
451
+ if !isObject {
452
+ continue
453
+ }
454
+ versions := browserStats.keys
455
+ // normalizeStats: a one-version stat for a one-version browser is
456
+ // rekeyed onto the browser's canonical version.
457
+ if len(versions) == 1 {
458
+ if agent, known := data.data[browser]; known && len(agent.versions) == 1 {
459
+ key := browser + " " + agent.versions[0]
460
+ browserslistAppendStat(values, &order, nulls, key, browserStats.values[versions[0]])
461
+ continue
462
+ }
463
+ }
464
+ for _, version := range versions {
465
+ key := browser + " " + version
466
+ browserslistAppendStat(values, &order, nulls, key, browserStats.values[version])
467
+ }
468
+ }
469
+ return values, order, nulls, nil
470
+ }
471
+
472
+ func browserslistAppendStat(values map[string]float64, order *[]string, nulls map[string]bool, key string, value interface{}) {
473
+ if _, dup := values[key]; dup {
474
+ // fillUsage overwrites, keeping first key position.
475
+ switch typed := value.(type) {
476
+ case float64:
477
+ values[key] = typed
478
+ delete(nulls, key)
479
+ case nil:
480
+ values[key] = 0
481
+ nulls[key] = true
482
+ }
483
+ return
484
+ }
485
+ switch typed := value.(type) {
486
+ case float64:
487
+ values[key] = typed
488
+ case nil:
489
+ values[key] = 0
490
+ nulls[key] = true
491
+ default:
492
+ // Non-numeric usage values behave like null in the percentage filters.
493
+ values[key] = 0
494
+ nulls[key] = true
495
+ }
496
+ *order = append(*order, key)
497
+ }
498
+
499
+ // --- ordered JSON --------------------------------------------------------------
500
+
501
+ // browserslistOrderedObject preserves JS object key semantics for
502
+ // runtime-parsed JSON: canonical array indexes first in ascending order, then
503
+ // the remaining keys in document order.
504
+ type browserslistOrderedObject struct {
505
+ keys []string
506
+ values map[string]interface{}
507
+ }
508
+
509
+ func (o *browserslistOrderedObject) get(key string) (interface{}, bool) {
510
+ value, ok := o.values[key]
511
+ return value, ok
512
+ }
513
+
514
+ var browserslistArrayIndexPattern = regexp.MustCompile(`^(0|[1-9][0-9]*)$`)
515
+
516
+ // browserslistParseOrderedJSON decodes JSON keeping object key order.
517
+ func browserslistParseOrderedJSON(content []byte) (interface{}, error) {
518
+ decoder := json.NewDecoder(strings.NewReader(string(content)))
519
+ decoder.UseNumber()
520
+ value, err := browserslistDecodeOrderedValue(decoder)
521
+ if err != nil {
522
+ return nil, err
523
+ }
524
+ if decoder.More() {
525
+ return nil, newBrowserslistError("trailing JSON content")
526
+ }
527
+ return value, nil
528
+ }
529
+
530
+ func browserslistDecodeOrderedValue(decoder *json.Decoder) (interface{}, error) {
531
+ token, err := decoder.Token()
532
+ if err != nil {
533
+ return nil, err
534
+ }
535
+ switch typed := token.(type) {
536
+ case json.Delim:
537
+ switch typed {
538
+ case '{':
539
+ object := &browserslistOrderedObject{values: map[string]interface{}{}}
540
+ for decoder.More() {
541
+ keyToken, keyErr := decoder.Token()
542
+ if keyErr != nil {
543
+ return nil, keyErr
544
+ }
545
+ key := keyToken.(string)
546
+ value, valueErr := browserslistDecodeOrderedValue(decoder)
547
+ if valueErr != nil {
548
+ return nil, valueErr
549
+ }
550
+ if _, dup := object.values[key]; !dup {
551
+ object.keys = append(object.keys, key)
552
+ }
553
+ object.values[key] = value
554
+ }
555
+ if _, err := decoder.Token(); err != nil { // consume '}'
556
+ return nil, err
557
+ }
558
+ browserslistApplyJSKeyOrder(object)
559
+ return object, nil
560
+ case '[':
561
+ var array []interface{}
562
+ for decoder.More() {
563
+ value, valueErr := browserslistDecodeOrderedValue(decoder)
564
+ if valueErr != nil {
565
+ return nil, valueErr
566
+ }
567
+ array = append(array, value)
568
+ }
569
+ if _, err := decoder.Token(); err != nil { // consume ']'
570
+ return nil, err
571
+ }
572
+ return array, nil
573
+ }
574
+ return nil, newBrowserslistError("unexpected JSON delimiter")
575
+ case json.Number:
576
+ parsed, parseErr := typed.Float64()
577
+ if parseErr != nil {
578
+ return nil, parseErr
579
+ }
580
+ return parsed, nil
581
+ default:
582
+ return token, nil // string, bool, nil
583
+ }
584
+ }
585
+
586
+ // browserslistApplyJSKeyOrder reorders object keys the way JS property
587
+ // enumeration does: integer-like keys ascending before insertion-ordered
588
+ // string keys.
589
+ func browserslistApplyJSKeyOrder(object *browserslistOrderedObject) {
590
+ var indexKeys []string
591
+ var stringKeys []string
592
+ for _, key := range object.keys {
593
+ if browserslistArrayIndexPattern.MatchString(key) && len(key) <= 10 {
594
+ indexKeys = append(indexKeys, key)
595
+ } else {
596
+ stringKeys = append(stringKeys, key)
597
+ }
598
+ }
599
+ sort.Slice(indexKeys, func(i, j int) bool {
600
+ return jsParseInt(indexKeys[i]) < jsParseInt(indexKeys[j])
601
+ })
602
+ object.keys = append(indexKeys, stringKeys...)
603
+ }