@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
@@ -15,7 +15,7 @@ import (
15
15
  // optional semicolon. Body-shaped declarations (functions, classes,
16
16
  // namespaces, enums) and control-flow statements (if/for/while/try)
17
17
  // are out of scope because they parse correctly without a terminator.
18
- type formatSemi struct{}
18
+ type formatSemi struct{ optionsRule }
19
19
 
20
20
  // formatSemiOptions is the Go mirror of `TtscLintRuleOptions.Semi`. The
21
21
  // JSON tag matches the TypeScript field name so users get the same key
@@ -135,12 +135,22 @@ func (formatSemi) Check(ctx *Context, node *shimast.Node) {
135
135
  )
136
136
  }
137
137
 
138
- // nextStatementHasASIHazard reports whether the next non-trivia byte
139
- // after `end` starts a token that would re-associate with the prior
140
- // expression if the trailing `;` is removed. Prettier handles this by
141
- // inserting a defensive leading `;` on the next line; this rule's
142
- // fixer is single-node, so the safer move is to keep the explicit
143
- // terminator.
138
+ // nextStatementHasASIHazard reports whether removing the trailing `;`
139
+ // at `end-1` could change the parse, judged by the next significant
140
+ // byte after `end` and the line structure between them.
141
+ //
142
+ // ASI only inserts a semicolon at a line terminator, end of input, or
143
+ // before `}`. So the `;` is removable in exactly two shapes:
144
+ //
145
+ // - end of input or a same-line `}` follows (ASI's closing-brace and
146
+ // end-of-input rules apply), or
147
+ // - a line terminator separates the statement from the next token AND
148
+ // that token is not a continuation hazard.
149
+ //
150
+ // Any other same-line successor (`else`, `while` of a do-loop, another
151
+ // statement after a gap comment) makes the `;` a REQUIRED separator:
152
+ // no line terminator means ASI cannot fire, so stripping would be a
153
+ // syntax error, not a style change.
144
154
  //
145
155
  // Hazard tokens per the ASI spec:
146
156
  //
@@ -149,18 +159,72 @@ func (formatSemi) Check(ctx *Context, node *shimast.Node) {
149
159
  // - “ ` “: tagged template literal continues
150
160
  // - `+`, `-`, `*`: binary operator continues
151
161
  // - `,`: comma operator continues
152
- // - `/`: division operator or regex literal continues; handled by
153
- // the comment-or-regex branch below (a leading `//` or `/*` is not a
154
- // hazard, a bare `/` is), so it is absent from the token switch.
162
+ // - `/`: division operator or regex literal continues (a leading `//`
163
+ // or `/*` is trivia consumed by scanPastTrivia; a bare `/` is a
164
+ // hazard).
155
165
  func nextStatementHasASIHazard(src string, end int) bool {
156
- for i := end; i < len(src); i++ {
166
+ i, sawNewline := scanPastTrivia(src, end)
167
+ if i >= len(src) {
168
+ return false
169
+ }
170
+ c := src[i]
171
+ if c == '}' {
172
+ // ASI applies before a closing brace regardless of line
173
+ // structure: `{ a(); }` → `{ a() }` stays valid.
174
+ return false
175
+ }
176
+ if !sawNewline {
177
+ // Next token on the same line: ASI cannot fire without a line
178
+ // terminator, so the `;` separates the two constructs
179
+ // (`if (a) b(); else c();`, `do f(); while (x);`,
180
+ // `a = 1; /* note */ b = 2`). Keep it.
181
+ return true
182
+ }
183
+ if c == '/' {
184
+ // bare `/` starts a regex literal or division, hazard.
185
+ return true
186
+ }
187
+ switch c {
188
+ // If the next significant byte is one of these, dropping the terminator
189
+ // could let the following line re-associate with the prior expression.
190
+ // `( [`, a unary `+ -`, and a tagged-template backtick are the cases
191
+ // actually reachable from a valid statement start; `<` matters in .tsx
192
+ // (a leading `<` opens a JSX element). The remaining infix bytes cannot
193
+ // begin a valid statement on their own, but are listed defensively so
194
+ // the strip always cedes rather than risk a parse-changing edit.
195
+ case '[', '(', '`', '+', '-', '*', ',', '.', '<', '>', '=', '?', '%', '&', '|', '^':
196
+ return true
197
+ }
198
+ return false
199
+ }
200
+
201
+ // scanPastTrivia advances from `pos` past whitespace and comments,
202
+ // returning the index of the next significant byte (len(src) at end of
203
+ // input) and whether a line terminator was crossed on the way. Both
204
+ // semicolon scanners (statement and member) share it so the ASI line
205
+ // rules cannot drift apart.
206
+ //
207
+ // A block comment that spans lines counts as a crossed line: per
208
+ // ECMA-262 (Comments), a multi-line comment containing a line
209
+ // terminator is treated as a line terminator for ASI, so the decision
210
+ // keys on comment content, not comment kind. `\r` counts as a line
211
+ // terminator on its own, which also covers CRLF sources.
212
+ func scanPastTrivia(src string, pos int) (next int, sawNewline bool) {
213
+ i := pos
214
+ for i < len(src) {
157
215
  c := src[i]
158
- if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
216
+ if c == '\n' || c == '\r' {
217
+ sawNewline = true
218
+ i++
219
+ continue
220
+ }
221
+ if c == ' ' || c == '\t' {
222
+ i++
159
223
  continue
160
224
  }
161
225
  if c == '/' && i+1 < len(src) {
162
226
  if src[i+1] == '/' {
163
- for i < len(src) && src[i] != '\n' {
227
+ for i < len(src) && src[i] != '\n' && src[i] != '\r' {
164
228
  i++
165
229
  }
166
230
  continue
@@ -168,30 +232,22 @@ func nextStatementHasASIHazard(src string, end int) bool {
168
232
  if src[i+1] == '*' {
169
233
  i += 2
170
234
  for i+1 < len(src) && !(src[i] == '*' && src[i+1] == '/') {
235
+ if src[i] == '\n' || src[i] == '\r' {
236
+ sawNewline = true
237
+ }
171
238
  i++
172
239
  }
173
240
  if i+1 < len(src) {
174
- i++ // step past '*/'
241
+ i += 2 // step past '*/'
242
+ } else {
243
+ i = len(src) // unterminated block comment swallows the rest
175
244
  }
176
245
  continue
177
246
  }
178
- // bare `/` starts a regex literal or division, hazard.
179
- return true
180
- }
181
- switch c {
182
- // If the next significant byte is one of these, dropping the terminator
183
- // could let the following line re-associate with the prior expression.
184
- // `( [`, a unary `+ -`, and a tagged-template backtick are the cases
185
- // actually reachable from a valid statement start; `<` matters in .tsx
186
- // (a leading `<` opens a JSX element). The remaining infix bytes cannot
187
- // begin a valid statement on their own, but are listed defensively so
188
- // the strip always cedes rather than risk a parse-changing edit.
189
- case '[', '(', '`', '+', '-', '*', ',', '.', '<', '>', '=', '?', '%', '&', '|', '^':
190
- return true
191
247
  }
192
- return false
248
+ return i, sawNewline
193
249
  }
194
- return false
250
+ return len(src), sawNewline
195
251
  }
196
252
 
197
253
  // preferNeverSafeKind reports whether stripping the trailing semicolon
@@ -291,8 +347,9 @@ func stripMemberSemicolon(ctx *Context, src string, node *shimast.Node, isClassF
291
347
 
292
348
  // memberSemicolonRedundant reports whether the member terminator `;`
293
349
  // whose following byte is at `after` can be dropped without changing the
294
- // parse. It scans past trivia (whitespace + comments) to the next
295
- // significant byte and applies Prettier's semi:false member rules:
350
+ // parse. It scans past trivia (whitespace + comments, via
351
+ // scanPastTrivia) to the next significant byte and applies Prettier's
352
+ // semi:false member rules:
296
353
  //
297
354
  // - The closing `}` (or end of file) always makes the `;` redundant.
298
355
  // - A next member on the SAME line (no newline crossed) keeps the `;`
@@ -305,57 +362,27 @@ func stripMemberSemicolon(ctx *Context, src string, node *shimast.Node, isClassF
305
362
  // call/construct/generic signature (`(` / `<`) for type members
306
363
  // (a leading `[` is an index signature there, not a continuation).
307
364
  func memberSemicolonRedundant(src string, after int, isClassField bool) bool {
308
- sawNewline := false
309
- for i := after; i < len(src); {
310
- c := src[i]
311
- if c == '\n' {
312
- sawNewline = true
313
- i++
314
- continue
315
- }
316
- if c == ' ' || c == '\t' || c == '\r' {
317
- i++
318
- continue
319
- }
320
- if c == '/' && i+1 < len(src) {
321
- if src[i+1] == '/' {
322
- for i < len(src) && src[i] != '\n' {
323
- i++
324
- }
325
- continue
326
- }
327
- if src[i+1] == '*' {
328
- i += 2
329
- for i+1 < len(src) && !(src[i] == '*' && src[i+1] == '/') {
330
- if src[i] == '\n' {
331
- sawNewline = true
332
- }
333
- i++
334
- }
335
- if i+1 < len(src) {
336
- i += 2
337
- }
338
- continue
339
- }
340
- }
341
- if c == '}' {
342
- return true
343
- }
344
- if !sawNewline {
365
+ i, sawNewline := scanPastTrivia(src, after)
366
+ if i >= len(src) {
367
+ return true
368
+ }
369
+ c := src[i]
370
+ if c == '}' {
371
+ return true
372
+ }
373
+ if !sawNewline {
374
+ return false
375
+ }
376
+ if isClassField {
377
+ switch c {
378
+ case '[', '(', '`', '+', '-', '*', '/', ',':
345
379
  return false
346
380
  }
347
- if isClassField {
348
- switch c {
349
- case '[', '(', '`', '+', '-', '*', '/', ',':
350
- return false
351
- }
352
- } else {
353
- switch c {
354
- case '(', '<':
355
- return false
356
- }
381
+ } else {
382
+ switch c {
383
+ case '(', '<':
384
+ return false
357
385
  }
358
- return true
359
386
  }
360
387
  return true
361
388
  }
@@ -27,11 +27,15 @@ const (
27
27
  separatorPlaceholder = ""
28
28
  )
29
29
 
30
- // formatSortImportsOptions mirrors `ITtscLintFormatSortImports`.
30
+ // formatSortImportsOptions mirrors `ITtscLintFormatSortImports`, plus the
31
+ // top-level `endOfLine` the config layer threads in so the rebuilt block
32
+ // joins declarations with the file's line ending.
31
33
  type formatSortImportsOptions struct {
32
- Order []string `json:"order"`
33
- CaseSensitive bool `json:"caseSensitive"`
34
- CombineTypeAndValue bool `json:"combineTypeAndValue"`
34
+ Order []string `json:"order"`
35
+ CaseSensitive bool `json:"caseSensitive"`
36
+ CombineTypeAndValue bool `json:"combineTypeAndValue"`
37
+ UnsafeSortRuntimeImports bool `json:"unsafeSortRuntimeImports"`
38
+ EndOfLine *string `json:"endOfLine"`
35
39
  }
36
40
 
37
41
  // defaultImportOrder is used when the user supplies no `order`: Node built-ins,
@@ -44,12 +48,13 @@ var defaultImportOrder = []string{
44
48
  `^[.]`,
45
49
  }
46
50
 
47
- // formatSortImports orders the file's top-level import declarations into
48
- // canonical groups, alphabetizes each group, merges duplicate imports of the
49
- // same module, and (when `combineTypeAndValue` is on) folds a type-only
50
- // import into a value import of the same module. Groups are user-configurable
51
- // via the `order` option; when omitted, the rule falls back to {@link
52
- // defaultImportOrder}.
51
+ // formatSortImports safely sorts named specifiers and erased type-only import
52
+ // blocks. Runtime-bearing declarations retain source order unless the user
53
+ // explicitly enables `unsafeSortRuntimeImports`; only then are they grouped,
54
+ // alphabetized, and merged. When that unsafe mode and `combineTypeAndValue`
55
+ // are both on, a type-only import may fold into a value import of the same
56
+ // module. Groups are user-configurable via the `order` option; when omitted,
57
+ // the rule falls back to {@link defaultImportOrder}.
53
58
  //
54
59
  // Within each group, declarations are sorted by their module-specifier text
55
60
  // (ASCII order, or case-insensitive unless `caseSensitive: true`). Named
@@ -58,10 +63,10 @@ var defaultImportOrder = []string{
58
63
  // Safety policy: if any byte between the contiguous imports is not
59
64
  // whitespace, the rule bails. Comments anchored to specific imports would
60
65
  // otherwise move with the wrong declaration, which is a strictly worse
61
- // outcome than declining to sort. Side-effect imports (`import "foo"`) also
62
- // bail the whole block because their evaluation order can carry meaning the
63
- // rule cannot reason about.
64
- type formatSortImports struct{}
66
+ // outcome than declining to sort. Every import other than `import type` can
67
+ // evaluate a module, including default, namespace, and named binding imports,
68
+ // so declaration-level rewriting is disabled for those blocks by default.
69
+ type formatSortImports struct{ optionsRule }
65
70
 
66
71
  func (formatSortImports) Name() string { return "format/sort-imports" }
67
72
  func (formatSortImports) IsFormat() bool { return true }
@@ -80,19 +85,15 @@ func (formatSortImports) Check(ctx *Context, node *shimast.Node) {
80
85
  return
81
86
  }
82
87
  imports := collectLeadingImports(statements.Nodes)
83
- // Block-level reorder runs only when the rule can do it safely:
84
- // two or more contiguous imports with no comment trivia between
85
- // them (comments anchor to specific imports and moving them would
86
- // mis-attach the user's intent), AND no side-effect-only imports
87
- // in the block. A side-effect import (`import "./polyfill"`) runs
88
- // its module's top-level code for its observable effect; sorting it
89
- // across a sibling import that depends on the polyfill being
90
- // initialized first would silently change runtime behavior. The
91
- // rule conservatively refuses to reorder the entire block in that
92
- // case.
88
+ // Block-level rewriting runs only for erased type-only imports or after the
89
+ // caller explicitly accepts runtime reordering. Binding imports execute
90
+ // their dependencies just like bare imports, so preserving only
91
+ // `import "./polyfill"` is insufficient. Comment trivia remains an
92
+ // unconditional barrier even in unsafe mode because the rebuilder cannot
93
+ // preserve a comment's declaration attachment.
93
94
  if len(imports) >= 2 &&
94
95
  leadingTriviaIsAllWhitespace(src, imports) &&
95
- !containsSideEffectImport(imports) {
96
+ (opts.unsafeSortRuntimeImports || !importsHaveRuntimeEvaluation(imports)) {
96
97
  first := imports[0]
97
98
  last := imports[len(imports)-1]
98
99
  replaceStart := shimscanner.SkipTrivia(src, first.Pos())
@@ -118,9 +119,14 @@ func (formatSortImports) Check(ctx *Context, node *shimast.Node) {
118
119
  // during one Check call. All option defaults are applied here so the
119
120
  // rest of the rule code does not branch on nil-ness.
120
121
  type resolvedSortImportsOptions struct {
121
- groups []sortImportsGroup
122
- caseSensitive bool
123
- combineTypeAndValue bool
122
+ groups []sortImportsGroup
123
+ caseSensitive bool
124
+ combineTypeAndValue bool
125
+ unsafeSortRuntimeImports bool
126
+ // eol is the newline joined between rebuilt declarations: `"\n"` by
127
+ // default, `"\r\n"` under endOfLine:"crlf". Verbatim declaration text is
128
+ // preserved as-is; only the synthesized inter-declaration joins use it.
129
+ eol string
124
130
  }
125
131
 
126
132
  // sortImportsGroup is one resolved entry of the `order` array. A separator
@@ -143,10 +149,16 @@ func loadSortImportsOptions(ctx *Context) resolvedSortImportsOptions {
143
149
  order = defaultImportOrder
144
150
  }
145
151
  groups := parseImportOrder(order)
152
+ eol := "\n"
153
+ if raw.EndOfLine != nil && *raw.EndOfLine == "crlf" {
154
+ eol = "\r\n"
155
+ }
146
156
  return resolvedSortImportsOptions{
147
- groups: groups,
148
- caseSensitive: raw.CaseSensitive,
149
- combineTypeAndValue: raw.CombineTypeAndValue,
157
+ groups: groups,
158
+ caseSensitive: raw.CaseSensitive,
159
+ combineTypeAndValue: raw.CombineTypeAndValue,
160
+ unsafeSortRuntimeImports: raw.UnsafeSortRuntimeImports,
161
+ eol: eol,
150
162
  }
151
163
  }
152
164
 
@@ -298,21 +310,22 @@ func specifierListHasCommentTrivia(src string, specifiers []*shimast.Node) bool
298
310
  return false
299
311
  }
300
312
 
301
- // containsSideEffectImport reports whether any import in the contiguous
302
- // block has no import clause (i.e. is a side-effect-only `import "x"`).
303
- // These imports are evaluated for their top-level effect; their order
304
- // relative to other imports may carry meaning the rule cannot reason
305
- // about, so the safety policy is to refuse to sort.
306
- func containsSideEffectImport(imports []*shimast.Node) bool {
313
+ // importsHaveRuntimeEvaluation reports whether a contiguous block contains an
314
+ // import that survives as a module dependency. Bare, default, namespace,
315
+ // named, and deferred imports can all trigger top-level evaluation; only a
316
+ // clause explicitly marked `type` is erased. Unexpected AST shapes are treated
317
+ // as runtime-bearing so a parser change cannot silently weaken the guard.
318
+ func importsHaveRuntimeEvaluation(imports []*shimast.Node) bool {
307
319
  for _, decl := range imports {
308
320
  if decl == nil {
309
- continue
321
+ return true
310
322
  }
311
323
  imp := decl.AsImportDeclaration()
312
- if imp == nil {
313
- continue
324
+ if imp == nil || imp.ImportClause == nil {
325
+ return true
314
326
  }
315
- if imp.ImportClause == nil {
327
+ clause := imp.ImportClause.AsImportClause()
328
+ if clause == nil || clause.PhaseModifier != shimast.KindTypeKeyword {
316
329
  return true
317
330
  }
318
331
  }
@@ -420,9 +433,11 @@ func buildSortedImportBlock(src string, imports []*shimast.Node, opts resolvedSo
420
433
  for i, e := range entries {
421
434
  if i > 0 {
422
435
  if e.group != prevGroup && separatorBetween(opts.groups, prevGroup, e.group) {
423
- b.WriteString("\n\n")
436
+ // A blank line between groups is two line endings.
437
+ b.WriteString(opts.eol)
438
+ b.WriteString(opts.eol)
424
439
  } else {
425
- b.WriteString("\n")
440
+ b.WriteString(opts.eol)
426
441
  }
427
442
  }
428
443
  b.WriteString(e.text)
@@ -594,14 +609,25 @@ func originalEntry(d siDecl) siEntry {
594
609
  // renderMergedDecl folds a bucket of mergeable declarations into one statement.
595
610
  // It returns the rendered text, whether the merged result is a type-only import,
596
611
  // and an `ok` flag that is false when the declarations cannot be merged
597
- // (conflicting default names, or a type-only default that cannot survive in a
598
- // mixed value import).
612
+ // (conflicting default names, a type-only default that cannot survive in a
613
+ // mixed value import, a type-only default alongside named bindings, or a
614
+ // bucket containing a namespace or comment-bearing declaration).
599
615
  func renderMergedDecl(group []siDecl, opts resolvedSortImportsOptions) (string, bool, bool) {
600
616
  mergedTypeOnly := true
601
617
  for _, d := range group {
602
618
  if !d.typeOnly {
603
619
  mergedTypeOnly = false
604
620
  }
621
+ // Namespace bindings and comment-bearing declarations are never mergeable:
622
+ // the rebuilt statement carries no `* as ns` slot and would drop comment
623
+ // bytes. mergeKey normally isolates them in per-declaration buckets, but
624
+ // its key embeds the original text, so byte-identical declarations (a
625
+ // duplicate-binding error TypeScript reports later, which the parse-level
626
+ // formatter still sees) collide into one bucket. Refuse here so the
627
+ // originals are re-emitted verbatim instead of silently losing bindings.
628
+ if d.namespace || d.hasSpecComment {
629
+ return "", false, false
630
+ }
605
631
  }
606
632
  defaultName := ""
607
633
  defaultTypeOnly := false
@@ -624,6 +650,14 @@ func renderMergedDecl(group []siDecl, opts resolvedSortImportsOptions) (string,
624
650
  if defaultName == "" && len(specs) == 0 {
625
651
  return "", false, false
626
652
  }
653
+ // A type-only import cannot carry both a default binding and named bindings:
654
+ // `import type D, { A } from "m"` is a syntax error (TS1363). Refuse the
655
+ // merge so the declarations stay separate. (A type-only default with an
656
+ // empty merged spec list still folds to `import type D from "m"`, which is
657
+ // legal.)
658
+ if mergedTypeOnly && defaultName != "" && len(specs) > 0 {
659
+ return "", false, false
660
+ }
627
661
 
628
662
  var b strings.Builder
629
663
  b.WriteString("import ")
@@ -36,7 +36,7 @@ import (
36
36
  //
37
37
  // Idempotent: once each statement is alone on its line, step 2 abstains
38
38
  // for all of them and the rule emits nothing.
39
- type formatStatementSplit struct{}
39
+ type formatStatementSplit struct{ optionsRule }
40
40
 
41
41
  // formatStatementSplitOptions carries the indentation + EOL settings the
42
42
  // rule needs to synthesize the inserted line break. The JSON tags match
@@ -19,7 +19,7 @@ import (
19
19
  // so it is a normalization rule rather than part of the print-width
20
20
  // reflow. Idempotent: a wrapped operand parses as a parenthesized
21
21
  // expression, not a bare `??`, so the next pass leaves it alone.
22
- type formatTernaryNullishParens struct{}
22
+ type formatTernaryNullishParens struct{ optionsRule }
23
23
 
24
24
  func (formatTernaryNullishParens) Name() string { return "format/ternary-nullish-parens" }
25
25
  func (formatTernaryNullishParens) IsFormat() bool { return true }
@@ -35,12 +35,22 @@ import (
35
35
  // element. The same restriction applies to rest binding patterns, which
36
36
  // the rule does not visit at all.
37
37
  //
38
+ // Destructuring assignment TARGETS are the one array/object-literal shape
39
+ // where the same rest restriction bites: `({ a, ...rest } = obj)` and
40
+ // `[a, ...rest] = arr` parse as ObjectLiteralExpression /
41
+ // ArrayLiteralExpression (not binding patterns) and are therefore visited,
42
+ // yet a trailing comma after their `AssignmentRestProperty` /
43
+ // `AssignmentRestElement` is a syntax error. `isRestAssignmentTargetLiteral`
44
+ // suppresses the insert for exactly those; a real value literal with a
45
+ // trailing spread (`{ a, ...o }`) and a non-rest target (`{ a, b } = obj`)
46
+ // both legally keep the comma.
47
+ //
38
48
  // Unparenthesized arrow parameters (`a => …`) are also skipped: there is
39
49
  // no parameter-list paren to anchor the comma against, and ECMAScript has
40
50
  // no place to insert one. `findCloseTokenAfter` bails on the first
41
51
  // non-trivia byte after the parameter's `End()` (the `=>` token), so the
42
52
  // rule abstains without emitting an edit.
43
- type formatTrailingComma struct{}
53
+ type formatTrailingComma struct{ optionsRule }
44
54
 
45
55
  // formatTrailingCommaOptions mirrors `TtscLintRuleOptions.TrailingComma`.
46
56
  type formatTrailingCommaOptions struct {
@@ -94,12 +104,18 @@ func (formatTrailingComma) Check(ctx *Context, node *shimast.Node) {
94
104
  if arr == nil {
95
105
  return
96
106
  }
107
+ if isRestAssignmentTargetLiteral(node) {
108
+ return
109
+ }
97
110
  considerTrailingComma(ctx, arr.Elements, node.End()-1)
98
111
  case shimast.KindObjectLiteralExpression:
99
112
  obj := node.AsObjectLiteralExpression()
100
113
  if obj == nil {
101
114
  return
102
115
  }
116
+ if isRestAssignmentTargetLiteral(node) {
117
+ return
118
+ }
103
119
  considerTrailingComma(ctx, obj.Properties, node.End()-1)
104
120
  case shimast.KindCallExpression:
105
121
  if mode == "es5" {
@@ -351,6 +367,53 @@ func considerFunctionParameterComma(ctx *Context, list *shimast.NodeList) {
351
367
  considerTrailingComma(ctx, list, closePos)
352
368
  }
353
369
 
370
+ // isRestAssignmentTargetLiteral reports whether node is an object- or
371
+ // array-literal used as a destructuring ASSIGNMENT TARGET whose last
372
+ // element is a rest (`...x`). ECMAScript forbids a trailing comma after an
373
+ // `AssignmentRestElement` / `AssignmentRestProperty`, so neither this rule
374
+ // nor the print-width printer (which shares this helper) may add one there.
375
+ //
376
+ // The two-pronged guard is what keeps it from over-suppressing. Only a
377
+ // destructuring assignment target that ENDS in a rest is illegal:
378
+ //
379
+ // - A real value literal with a trailing spread (`{ a, ...o }`,
380
+ // `[a, ...rest]`) is not a target, so isDestructuringAssignmentTarget
381
+ // returns false and the comma stays legal.
382
+ // - A non-rest assignment target (`{ a, b } = obj`) fails the
383
+ // last-element-is-rest check and keeps its comma.
384
+ //
385
+ // It handles nested targets (`[{ a, ...rest }] = arr`,
386
+ // `({ x: [a, ...rest] } = obj)`) and for-of/for-in assignment initializers
387
+ // through isDestructuringAssignmentTarget's ancestor walk.
388
+ func isRestAssignmentTargetLiteral(node *shimast.Node) bool {
389
+ if node == nil {
390
+ return false
391
+ }
392
+ switch node.Kind {
393
+ case shimast.KindArrayLiteralExpression:
394
+ arr := node.AsArrayLiteralExpression()
395
+ if arr == nil || arr.Elements == nil || len(arr.Elements.Nodes) == 0 {
396
+ return false
397
+ }
398
+ last := arr.Elements.Nodes[len(arr.Elements.Nodes)-1]
399
+ if last == nil || last.Kind != shimast.KindSpreadElement {
400
+ return false
401
+ }
402
+ case shimast.KindObjectLiteralExpression:
403
+ obj := node.AsObjectLiteralExpression()
404
+ if obj == nil || obj.Properties == nil || len(obj.Properties.Nodes) == 0 {
405
+ return false
406
+ }
407
+ last := obj.Properties.Nodes[len(obj.Properties.Nodes)-1]
408
+ if last == nil || last.Kind != shimast.KindSpreadAssignment {
409
+ return false
410
+ }
411
+ default:
412
+ return false
413
+ }
414
+ return isDestructuringAssignmentTarget(node)
415
+ }
416
+
354
417
  // lastParameterIsRest reports whether the parameter list ends with a
355
418
  // rest element (`...rest`). ECMAScript syntax disallows a trailing
356
419
  // comma after a rest element; the rule must not insert one.
@@ -34,7 +34,7 @@ import (
34
34
  // Idempotent: a file that already has no trailing whitespace, at most
35
35
  // one consecutive blank line, no leading/trailing blank lines, and a
36
36
  // single final EOL produces no edits.
37
- type formatWhitespace struct{}
37
+ type formatWhitespace struct{ optionsRule }
38
38
 
39
39
  // formatWhitespaceOptions carries only the EOL setting; the rule needs
40
40
  // it to synthesize the file's final newline. Indentation is irrelevant
@@ -1,32 +1,31 @@
1
1
  package linthost
2
2
 
3
3
  import (
4
- "regexp"
5
4
  "strings"
6
5
 
7
6
  shimast "github.com/microsoft/typescript-go/shim/ast"
8
7
  )
9
8
 
10
- type functionalParameters struct{}
11
- type functionalImmutableData struct{}
12
- type functionalNoClassInheritance struct{}
13
- type functionalNoClasses struct{}
14
- type functionalNoConditionalStatements struct{}
15
- type functionalNoExpressionStatements struct{}
16
- type functionalNoLet struct{}
17
- type functionalNoLoopStatements struct{}
18
- type functionalNoMixedTypes struct{}
19
- type functionalNoPromiseReject struct{}
20
- type functionalNoReturnVoid struct{}
21
- type functionalNoThisExpressions struct{}
22
- type functionalNoThrowStatements struct{}
23
- type functionalNoTryStatements struct{}
24
- type functionalPreferImmutableTypes struct{}
25
- type functionalPreferPropertySignatures struct{}
26
- type functionalPreferReadonlyType struct{}
27
- type functionalPreferTacit struct{}
28
- type functionalReadonlyType struct{}
29
- type functionalTypeDeclarationImmutability struct{}
9
+ type functionalParameters struct{ optionsRule }
10
+ type functionalImmutableData struct{ optionsRule }
11
+ type functionalNoClassInheritance struct{ optionsRule }
12
+ type functionalNoClasses struct{ optionsRule }
13
+ type functionalNoConditionalStatements struct{ optionsRule }
14
+ type functionalNoExpressionStatements struct{ optionsRule }
15
+ type functionalNoLet struct{ optionsRule }
16
+ type functionalNoLoopStatements struct{ optionsRule }
17
+ type functionalNoMixedTypes struct{ optionsRule }
18
+ type functionalNoPromiseReject struct{ optionsRule }
19
+ type functionalNoReturnVoid struct{ optionsRule }
20
+ type functionalNoThisExpressions struct{ optionsRule }
21
+ type functionalNoThrowStatements struct{ optionsRule }
22
+ type functionalNoTryStatements struct{ optionsRule }
23
+ type functionalPreferImmutableTypes struct{ optionsRule }
24
+ type functionalPreferPropertySignatures struct{ optionsRule }
25
+ type functionalPreferReadonlyType struct{ optionsRule }
26
+ type functionalPreferTacit struct{ optionsRule }
27
+ type functionalReadonlyType struct{ optionsRule }
28
+ type functionalTypeDeclarationImmutability struct{ optionsRule }
30
29
 
31
30
  func (functionalParameters) Name() string { return "functional/functional-parameters" }
32
31
  func (functionalImmutableData) Name() string { return "functional/immutable-data" }
@@ -921,7 +920,7 @@ func functionalPatternMatches(pattern, name string) bool {
921
920
  if pattern == name {
922
921
  return true
923
922
  }
924
- re, err := regexp.Compile(pattern)
923
+ re, err := compileUserPattern(pattern)
925
924
  return err == nil && re.MatchString(name)
926
925
  }
927
926