@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
@@ -2,6 +2,7 @@ package linthost
2
2
 
3
3
  import (
4
4
  "strings"
5
+ "unicode/utf8"
5
6
 
6
7
  shimast "github.com/microsoft/typescript-go/shim/ast"
7
8
  )
@@ -37,8 +38,11 @@ func (noUselessEscape) Check(ctx *Context, node *shimast.Node) {
37
38
  // Tagged templates expose the raw bytes of the template to the tag
38
39
  // function (`String.raw`, `dedent`, `gql`, `css`, …), so a backslash
39
40
  // that looks redundant to the JS lexer is meaningful at the tag
40
- // boundary. ESLint canonical skips tagged templates entirely.
41
- if isInsideTaggedTemplate(node) {
41
+ // boundary. ESLint canonical skips a tagged template's own quasis, but a
42
+ // literal merely nested inside a tag's substitution is not itself tagged
43
+ // and stays checked — so this consults the literal's OWN enclosing
44
+ // template, not any ancestor tag.
45
+ if isTaggedTemplateElement(node) {
42
46
  return
43
47
  }
44
48
  // tsgo's `node.Pos()` points at the start of leading trivia; the regex
@@ -82,6 +86,16 @@ const templateValidEscapes = "`'\"\\bfnrtv0xuU$\n\r"
82
86
  const regexNonClassValidEscapes = "^$\\.*+?()[]{}|/-\n\r"
83
87
  const regexClassValidEscapes = "\\]-^\n\r"
84
88
 
89
+ // regexClassSetValidEscapes is the in-class allowlist for a `v`-flag
90
+ // (unicodeSets) regex. There the ClassSetSyntaxCharacters `( ) [ ] { } / | -`
91
+ // stay meaningful inside `[...]`, so a backslash before them is load-bearing:
92
+ // stripping it (e.g. `/[\(]/v` → `/[(]/v`) turns valid source into a
93
+ // `SyntaxError: Invalid character in character class`. Mirrors ESLint's
94
+ // REGEX_CLASSSET_CHARACTER_ESCAPES; its `q` (from `\q{…}`) is handled with the
95
+ // other shorthand letters in `isUselessRegexEscape`. `]` and `-` already sit
96
+ // in the base set above.
97
+ const regexClassSetValidEscapes = regexClassValidEscapes + "()[{}|/"
98
+
85
99
  // reportStringEscapes walks the raw source bytes of a string or template
86
100
  // literal and reports each backslash whose following character is not in
87
101
  // `whitelist`. `base` is the source offset of `raw[0]` so reported ranges
@@ -128,21 +142,18 @@ func reportStringEscapes(ctx *Context, raw string, base int, whitelist string, i
128
142
  continue
129
143
  }
130
144
  if isUselessStringEscape(next, whitelist) {
145
+ message := uselessEscapeMessage(raw[i+1:])
131
146
  // Only emit a fix when both surrounding bytes are plain ASCII so
132
147
  // deleting one byte cannot corrupt a multi-byte sequence.
133
148
  if next < 0x80 {
134
149
  ctx.ReportRangeFix(
135
150
  base+i,
136
151
  base+i+1,
137
- "Unnecessary escape character: \\"+string(next)+".",
152
+ message,
138
153
  TextEdit{Pos: base + i, End: base + i + 1, Text: ""},
139
154
  )
140
155
  } else {
141
- ctx.ReportRange(
142
- base+i,
143
- base+i+1,
144
- "Unnecessary escape character: \\"+string(next)+".",
145
- )
156
+ ctx.ReportRange(base+i, base+i+1, message)
146
157
  }
147
158
  }
148
159
  i++ // skip the escaped char so `\\\\` doesn't double-report.
@@ -151,7 +162,9 @@ func reportStringEscapes(ctx *Context, raw string, base int, whitelist string, i
151
162
 
152
163
  // reportRegexEscapes walks the pattern body of a regex literal and reports
153
164
  // backslashes that escape non-special characters. `base` is the source offset
154
- // of `raw[0]`. Character-class context (`[…]`) widens the legal set slightly.
165
+ // of `raw[0]`. Character-class context (`[…]`) widens the legal set slightly,
166
+ // and the trailing `v` (unicodeSets) flag widens it further because more
167
+ // punctuation is meaningful inside a class in that mode.
155
168
  func reportRegexEscapes(ctx *Context, raw string, base int) {
156
169
  if len(raw) < 3 || raw[0] != '/' {
157
170
  return
@@ -163,6 +176,12 @@ func reportRegexEscapes(ctx *Context, raw string, base int) {
163
176
  return
164
177
  }
165
178
  body := raw[1:closing]
179
+ // Flags follow the closing slash. The `v` (unicodeSets) flag promotes the
180
+ // ClassSetSyntaxCharacters to meaningful members of a character class, so
181
+ // their escapes must be preserved there; deleting one corrupts the source.
182
+ // Mirrors ESLint selecting REGEX_CLASSSET_CHARACTER_ESCAPES over
183
+ // REGEX_GENERAL_ESCAPES on that flag.
184
+ unicodeSets := strings.IndexByte(raw[closing+1:], 'v') >= 0
166
185
  inClass := false
167
186
  for i := 0; i < len(body); i++ {
168
187
  ch := body[i]
@@ -178,29 +197,39 @@ func reportRegexEscapes(ctx *Context, raw string, base int) {
178
197
  continue
179
198
  }
180
199
  next := body[i+1]
181
- if isUselessRegexEscape(next, inClass) {
200
+ if isUselessRegexEscape(next, inClass, unicodeSets) {
182
201
  // base + 1 (for the leading `/`) + i is the byte offset of the
183
202
  // backslash in the source.
184
203
  pos := base + 1 + i
204
+ message := uselessEscapeMessage(body[i+1:])
185
205
  if next < 0x80 {
186
206
  ctx.ReportRangeFix(
187
207
  pos,
188
208
  pos+1,
189
- "Unnecessary escape character: \\"+string(next)+".",
209
+ message,
190
210
  TextEdit{Pos: pos, End: pos + 1, Text: ""},
191
211
  )
192
212
  } else {
193
- ctx.ReportRange(
194
- pos,
195
- pos+1,
196
- "Unnecessary escape character: \\"+string(next)+".",
197
- )
213
+ ctx.ReportRange(pos, pos+1, message)
198
214
  }
199
215
  }
200
216
  i++ // consume the escaped character.
201
217
  }
202
218
  }
203
219
 
220
+ // uselessEscapeMessage renders the diagnostic for one redundant backslash.
221
+ // `rest` is the raw source starting at the escaped character, of which only
222
+ // the leading UTF-8 rune is named. Decoding is required because the escape
223
+ // target may be multi-byte: converting the lone lead byte would reinterpret it
224
+ // as the code point of the same numeric value, so `\你` (lead byte 0xE4) would
225
+ // accuse `ä` (U+00E4). ESLint canonical names the whole code point as well.
226
+ // A byte that is not valid UTF-8 decodes to U+FFFD, which keeps the message
227
+ // itself well-formed.
228
+ func uselessEscapeMessage(rest string) string {
229
+ escaped, _ := utf8.DecodeRuneInString(rest)
230
+ return "Unnecessary escape character: \\" + string(escaped) + "."
231
+ }
232
+
204
233
  // isUselessStringEscape reports whether a backslash before `ch` is redundant
205
234
  // inside a string or template literal. The `whitelist` contains the characters
206
235
  // that are valid escape targets for the specific literal kind (string vs template).
@@ -212,10 +241,16 @@ func isUselessStringEscape(ch byte, whitelist string) bool {
212
241
  if strings.IndexByte(whitelist, ch) >= 0 {
213
242
  return false
214
243
  }
244
+ // Digits 1-9 are octal-shaped (`no-octal-escape` owns those; `0` is in
245
+ // the whitelist). Deleting the backslash of `\1`…`\7` changes the cooked
246
+ // string value, and upstream ESLint skips backslash-digit entirely, so
247
+ // `\8`/`\9` are exempt too for oracle parity.
248
+ if ch >= '1' && ch <= '9' {
249
+ return false
250
+ }
215
251
  // ASCII letters that aren't in the whitelist are user-error escapes
216
- // like `\a`, `\m`. Digits 1-9 are octal-shaped (`no-octal-escape`
217
- // owns those). `0` is in the whitelist. Punctuation that isn't in the
218
- // whitelist (e.g., `\.`) is also redundant.
252
+ // like `\a`, `\m`. Punctuation that isn't in the whitelist (e.g., `\.`)
253
+ // is also redundant.
219
254
  return true
220
255
  }
221
256
 
@@ -224,12 +259,18 @@ func isUselessStringEscape(ch byte, whitelist string) bool {
224
259
  // character class, which narrows the set of meaningful meta-char escapes:
225
260
  // most regex meta-chars (`.`, `*`, `+`, `?`, `(`, `)`, `{`, `}`, `|`, `^`,
226
261
  // `$`, `/`) are literal inside a class, so escaping them there is noise.
227
- func isUselessRegexEscape(ch byte, inClass bool) bool {
262
+ // `unicodeSets` is true for a `v`-flag regex, where the ClassSetSyntaxCharacters
263
+ // regain meaning inside a class and their escapes are therefore required.
264
+ func isUselessRegexEscape(ch byte, inClass, unicodeSets bool) bool {
228
265
  if ch < 0x20 {
229
266
  return false
230
267
  }
231
268
  if inClass {
232
- if strings.IndexByte(regexClassValidEscapes, ch) >= 0 {
269
+ classEscapes := regexClassValidEscapes
270
+ if unicodeSets {
271
+ classEscapes = regexClassSetValidEscapes
272
+ }
273
+ if strings.IndexByte(classEscapes, ch) >= 0 {
233
274
  return false
234
275
  }
235
276
  } else if strings.IndexByte(regexNonClassValidEscapes, ch) >= 0 {
@@ -258,33 +299,6 @@ func isUselessRegexEscape(ch byte, inClass bool) bool {
258
299
  return true
259
300
  }
260
301
 
261
- // isInsideTaggedTemplate reports whether `node` is the template payload of
262
- // a TaggedTemplateExpression. Two AST shapes reach here:
263
- //
264
- // - `KindNoSubstitutionTemplateLiteral` — the direct child of a
265
- // TaggedTemplateExpression's Template slot.
266
- // - `KindTemplateHead/Middle/Tail` — wrapped in TemplateSpan and
267
- // TemplateExpression nodes; the TemplateExpression's parent is the
268
- // TaggedTemplateExpression.
269
- func isInsideTaggedTemplate(node *shimast.Node) bool {
270
- if node == nil || node.Parent == nil {
271
- return false
272
- }
273
- parent := node.Parent
274
- if parent.Kind == shimast.KindTaggedTemplateExpression {
275
- return true
276
- }
277
- // Walk up at most two more hops to reach the TaggedTemplateExpression
278
- // for the spans family.
279
- for i := 0; i < 2 && parent.Parent != nil; i++ {
280
- parent = parent.Parent
281
- if parent.Kind == shimast.KindTaggedTemplateExpression {
282
- return true
283
- }
284
- }
285
- return false
286
- }
287
-
288
302
  func init() {
289
303
  Register(noUselessEscape{})
290
304
  }
@@ -17,13 +17,16 @@ import (
17
17
  // other shape (`(x: T)`, `({ x })`, `(...x)`, `(x = 1)`, `(x?)`, `(x, y)`,
18
18
  // `()`) keeps its parentheses in both modes, exactly as Prettier does.
19
19
  // `async x => x` is handled too: the modifier sits before the parameter, so
20
- // the parameter-name span is unaffected.
20
+ // the parameter-name span is unaffected. A legal trailing comma (`(x,) => x`)
21
+ // counts as already parenthesized — a trailing comma can only exist inside a
22
+ // parameter-list paren pair — so "always" leaves it alone and "avoid" deletes
23
+ // the comma together with the parens (`x => x`, matching Prettier).
21
24
  //
22
25
  // The rule rewrites only the parameter span (`x` <-> `(x)`), never the body
23
26
  // or the `=>`, so a chained arrow `a => b => …` has each eligible arm
24
27
  // normalized independently. Idempotent: a parameter already in the
25
28
  // preferred shape compares equal and emits nothing.
26
- type formatArrowParens struct{}
29
+ type formatArrowParens struct{ optionsRule }
27
30
 
28
31
  type formatArrowParensOptions struct {
29
32
  Prefer string `json:"prefer"`
@@ -90,9 +93,13 @@ func (formatArrowParens) Check(ctx *Context, node *shimast.Node) {
90
93
 
91
94
  // Is the parameter already wrapped in `(` … `)`? Scan over whitespace on
92
95
  // each side; the sole parameter of an arrow is delimited by the
93
- // parameter-list parens when present.
96
+ // parameter-list parens when present. The forward scan starts at the
97
+ // parameter *list*'s end, not the name's: the list span covers a legal
98
+ // trailing comma (`(x,)` — that is how the AST records HasTrailingComma),
99
+ // so a trailing-comma list is classified as wrapped instead of the `,`
100
+ // byte aborting the scan and mis-reporting a bare name.
94
101
  openParen := scanBackForByte(src, nameStart, '(')
95
- closeParen := scanForwardForByte(src, nameEnd, ')')
102
+ closeParen := scanForwardForByte(src, arrow.Parameters.End(), ')')
96
103
  wrapped := openParen >= 0 && closeParen >= 0
97
104
 
98
105
  switch prefer {
@@ -189,7 +196,10 @@ func arrowParamRegionHasComment(src string, paramPos, nameStart, nameEnd int) bo
189
196
  // paren so a comment between `)` and `=>` (`(x) /* c */ => x`) — which in
190
197
  // "avoid" mode would otherwise be stranded when the parens are stripped — is
191
198
  // also detected (Prettier keeps the parens of an arrow with such a dangling
192
- // comment).
199
+ // comment). Likewise tolerate one trailing comma before the paren so a
200
+ // comment on either side of it (`(x /* c */,) => x`, `(x, /* c */) => x`) —
201
+ // which the "avoid" deletion would destroy — is also detected.
202
+ sawComma := false
193
203
  sawParen := false
194
204
  for i := nameEnd; i+1 < len(src); i++ {
195
205
  c := src[i]
@@ -199,6 +209,10 @@ func arrowParamRegionHasComment(src string, paramPos, nameStart, nameEnd int) bo
199
209
  if c == '/' && (src[i+1] == '*' || src[i+1] == '/') {
200
210
  return true
201
211
  }
212
+ if c == ',' && !sawComma && !sawParen {
213
+ sawComma = true
214
+ continue
215
+ }
202
216
  if c == ')' && !sawParen {
203
217
  sawParen = true
204
218
  continue
@@ -22,7 +22,7 @@ import (
22
22
  // and an empty `{}` has no interior to pad. It rewrites just the whitespace
23
23
  // run immediately inside each brace, so it never disturbs the contents.
24
24
  // Idempotent: a pair already in the preferred shape compares equal.
25
- type formatBracketSpacing struct{}
25
+ type formatBracketSpacing struct{ optionsRule }
26
26
 
27
27
  type formatBracketSpacingOptions struct {
28
28
  Spacing *bool `json:"spacing"`
@@ -23,7 +23,7 @@ import (
23
23
  // `format/indent` (leading whitespace of a line) or `format/print-width`
24
24
  // (interior reflow). Idempotent: once joined the gap holds no newline
25
25
  // and the rule abstains.
26
- type formatClauseJoin struct{}
26
+ type formatClauseJoin struct{ optionsRule }
27
27
 
28
28
  // formatClauseJoinOptions mirrors the printWidth/indent keys the rule
29
29
  // needs to decide whether the joined line fits. The config layer mirrors
@@ -46,7 +46,7 @@ import (
46
46
  // breaking heritage clause at once), a header carrying comments, or a
47
47
  // type/parameter that is itself multi-line makes the rule ABSTAIN, so it
48
48
  // never emits a header it cannot reproduce exactly.
49
- type formatDeclarationHeader struct{}
49
+ type formatDeclarationHeader struct{ optionsRule }
50
50
 
51
51
  type formatDeclarationHeaderOptions struct {
52
52
  PrintWidth *int `json:"printWidth"`
@@ -172,7 +172,7 @@ func (formatDeclarationHeader) Check(ctx *Context, node *shimast.Node) {
172
172
  // with a non-empty body; an interface (any body) and an empty body
173
173
  // keep `{` glued to the last header line (so an empty body reads
174
174
  // `… {}`). isClass && !emptyBody captures that.
175
- brace := headerBrace(isClass, emptyBody, base)
175
+ brace := headerBrace(isClass, emptyBody, base, layout.eol)
176
176
  target, ok = brokenDeclarationHeader(src, base, prefix, typeParams, paramTexts, clauses, layout, brace, emptyBody, isClass)
177
177
  if !ok {
178
178
  return // unverified combination: abstain
@@ -194,6 +194,11 @@ type declarationHeaderLayout struct {
194
194
  printWidth int
195
195
  tabWidth int
196
196
  oneLevel string
197
+ // eol is the newline the reflow builders synthesize. It mirrors
198
+ // loadFormatLayout: `"\n"` by default, `"\r\n"` under endOfLine:"crlf",
199
+ // so a reflowed CRLF header stays consistently CRLF instead of gaining
200
+ // lone LFs.
201
+ eol string
197
202
  }
198
203
 
199
204
  func (l declarationHeaderLayout) indent(base string, depth int) string {
@@ -203,7 +208,7 @@ func (l declarationHeaderLayout) indent(base string, depth int) string {
203
208
  func loadDeclarationHeaderLayout(ctx *Context) declarationHeaderLayout {
204
209
  var opts formatDeclarationHeaderOptions
205
210
  _ = ctx.DecodeOptions(&opts)
206
- l := declarationHeaderLayout{printWidth: 80, tabWidth: 2, oneLevel: " "}
211
+ l := declarationHeaderLayout{printWidth: 80, tabWidth: 2, oneLevel: " ", eol: "\n"}
207
212
  if opts.PrintWidth != nil && *opts.PrintWidth > 0 {
208
213
  l.printWidth = *opts.PrintWidth
209
214
  }
@@ -215,6 +220,9 @@ func loadDeclarationHeaderLayout(ctx *Context) declarationHeaderLayout {
215
220
  } else if opts.TabWidth != nil && *opts.TabWidth > 0 {
216
221
  l.oneLevel = strings.Repeat(" ", *opts.TabWidth)
217
222
  }
223
+ if opts.EndOfLine != nil && *opts.EndOfLine == "crlf" {
224
+ l.eol = "\r\n"
225
+ }
218
226
  return l
219
227
  }
220
228
 
@@ -392,11 +400,11 @@ func multiClauseHeader(prefix string, clauses []heritageClauseText, layout decla
392
400
  indent1 := layout.indent(base, 1)
393
401
  indent2 := layout.indent(base, 2)
394
402
  for ci, c := range clauses {
395
- b.WriteString("\n")
403
+ b.WriteString(layout.eol)
396
404
  b.WriteString(indent1)
397
405
  b.WriteString(c.keyword)
398
406
  inlineWidth := visualWidth(indent1+c.keyword+" "+strings.Join(c.types, ", "), layout.tabWidth)
399
- if ci == len(clauses)-1 && !strings.HasPrefix(brace, "\n") {
407
+ if ci == len(clauses)-1 && !strings.HasPrefix(brace, layout.eol) {
400
408
  inlineWidth += visualWidth(brace, layout.tabWidth)
401
409
  }
402
410
  if inlineWidth <= layout.printWidth {
@@ -405,7 +413,7 @@ func multiClauseHeader(prefix string, clauses []heritageClauseText, layout decla
405
413
  continue
406
414
  }
407
415
  for i, t := range c.types {
408
- b.WriteString("\n")
416
+ b.WriteString(layout.eol)
409
417
  b.WriteString(indent2)
410
418
  b.WriteString(t)
411
419
  if i < len(c.types)-1 {
@@ -427,7 +435,7 @@ func multiTypeHeader(prefix string, clause heritageClauseText, layout declaratio
427
435
  indent1 := layout.indent(base, 1)
428
436
  inline := indent1 + clause.keyword + " " + strings.Join(clause.types, ", ")
429
437
  inlineWidth := visualWidth(inline, layout.tabWidth)
430
- if !strings.HasPrefix(brace, "\n") {
438
+ if !strings.HasPrefix(brace, layout.eol) {
431
439
  inlineWidth += visualWidth(brace, layout.tabWidth)
432
440
  }
433
441
  if emptyBody {
@@ -437,16 +445,16 @@ func multiTypeHeader(prefix string, clause heritageClauseText, layout declaratio
437
445
  inlineWidth++
438
446
  }
439
447
  if inlineWidth <= layout.printWidth {
440
- return prefix + "\n" + inline + brace
448
+ return prefix + layout.eol + inline + brace
441
449
  }
442
450
 
443
451
  var b strings.Builder
444
452
  b.WriteString(prefix)
445
- b.WriteString("\n")
453
+ b.WriteString(layout.eol)
446
454
  b.WriteString(indent1)
447
455
  b.WriteString(clause.keyword)
448
456
  for i, t := range clause.types {
449
- b.WriteString("\n")
457
+ b.WriteString(layout.eol)
450
458
  b.WriteString(layout.indent(base, 2))
451
459
  b.WriteString(t)
452
460
  if i < len(clause.types)-1 {
@@ -524,14 +532,15 @@ func singleGenericHeritageHeader(src, base, prefix string, typeParams, heritage
524
532
  b.WriteString(keyword)
525
533
  b.WriteString(" ")
526
534
  b.WriteString(typeName)
527
- b.WriteString("<\n")
535
+ b.WriteString("<")
536
+ b.WriteString(layout.eol)
528
537
  for i, a := range args {
529
538
  b.WriteString(layout.indent(base, 1))
530
539
  b.WriteString(a)
531
540
  if i < len(args)-1 {
532
541
  b.WriteString(",")
533
542
  }
534
- b.WriteString("\n")
543
+ b.WriteString(layout.eol)
535
544
  }
536
545
  b.WriteString(base)
537
546
  b.WriteString("> {")
@@ -541,10 +550,11 @@ func singleGenericHeritageHeader(src, base, prefix string, typeParams, heritage
541
550
  // headerBrace returns the opening-brace fragment that closes a broken
542
551
  // header. Prettier puts `{` on its own line only for a class with a
543
552
  // non-empty body; an interface and any empty body keep it glued to the
544
- // last header line.
545
- func headerBrace(isClass, emptyBody bool, base string) string {
553
+ // last header line. `eol` is the synthesized newline (LF or CRLF) so the
554
+ // own-line brace matches the file's line endings.
555
+ func headerBrace(isClass, emptyBody bool, base, eol string) string {
546
556
  if isClass && !emptyBody {
547
- return "\n" + base + "{"
557
+ return eol + base + "{"
548
558
  }
549
559
  return " {"
550
560
  }
@@ -579,11 +589,13 @@ func typeParamExplodeHeader(
579
589
  ) string {
580
590
  var b strings.Builder
581
591
  b.WriteString(prefix)
582
- b.WriteString("<\n")
592
+ b.WriteString("<")
593
+ b.WriteString(layout.eol)
583
594
  paramIndent := layout.indent(base, 1)
584
595
  for _, p := range typeParams.Nodes {
585
596
  b.WriteString(renderExplodedTypeParam(src, p, layout, paramIndent))
586
- b.WriteString(",\n")
597
+ b.WriteString(",")
598
+ b.WriteString(layout.eol)
587
599
  }
588
600
  b.WriteString(base)
589
601
  b.WriteString(">")
@@ -594,7 +606,7 @@ func typeParamExplodeHeader(
594
606
  // >
595
607
  // extends IPage.IRequest {
596
608
  c := clauses[0]
597
- b.WriteString("\n")
609
+ b.WriteString(layout.eol)
598
610
  b.WriteString(layout.indent(base, 1))
599
611
  b.WriteString(c.keyword)
600
612
  b.WriteString(" ")
@@ -628,7 +640,7 @@ func renderExplodedTypeParam(src string, p *shimast.Node, layout declarationHead
628
640
  head := strings.TrimRight(src[start:defStart], " \t") // ends in `=`
629
641
  def := strings.TrimSpace(src[defStart:end])
630
642
  if head != "" && def != "" {
631
- return paramIndent + head + "\n" + paramIndent + layout.oneLevel + def
643
+ return paramIndent + head + layout.eol + paramIndent + layout.oneLevel + def
632
644
  }
633
645
  }
634
646
  }
@@ -31,7 +31,7 @@ import (
31
31
  //
32
32
  // Idempotent: a correctly-indented statement compares equal in step 3
33
33
  // and emits nothing.
34
- type formatIndent struct{}
34
+ type formatIndent struct{ optionsRule }
35
35
 
36
36
  func (formatIndent) Name() string { return "format/indent" }
37
37
  func (formatIndent) IsFormat() bool { return true }
@@ -25,7 +25,7 @@ import (
25
25
  // `/** ... */` blocks; it deliberately avoids relying on the JSDoc AST
26
26
  // because comment attachment is a moving target across TypeScript
27
27
  // versions.
28
- type formatJSDoc struct{}
28
+ type formatJSDoc struct{ optionsRule }
29
29
 
30
30
  // formatJSDocOptions mirrors `TtscLintRuleOptions.JSDoc`. The
31
31
  // `tagSynonyms` map layers on top of the built-in synonym table so
@@ -24,7 +24,7 @@ import (
24
24
  // is left alone, dropping a redundant `;` safely depends on the
25
25
  // surrounding semicolon policy and is out of scope here. Idempotent:
26
26
  // once merged the gap holds no newline, so the rule finds nothing to do.
27
- type formatOrphanSemi struct{}
27
+ type formatOrphanSemi struct{ optionsRule }
28
28
 
29
29
  // formatOrphanSemiOptions carries the effective `semi` setting, mirrored
30
30
  // from format.semi by the config layer. Defaults to true (semicolons),
@@ -25,13 +25,15 @@ import (
25
25
  // region and emits no trailing comma, format/trailing-comma adds it on
26
26
  // the now-multi-line list, so the two rules stay disjoint. Idempotent:
27
27
  // an already-broken list contains a newline and is skipped.
28
- type formatParameterProperties struct{}
28
+ type formatParameterProperties struct{ optionsRule }
29
29
 
30
- // formatParameterPropertiesOptions carries the indentation settings the
31
- // rewrite needs. The config layer mirrors format.tabWidth/useTabs in.
30
+ // formatParameterPropertiesOptions carries the indentation + EOL settings
31
+ // the rewrite needs. The config layer mirrors
32
+ // format.tabWidth/useTabs/endOfLine in.
32
33
  type formatParameterPropertiesOptions struct {
33
- TabWidth *int `json:"tabWidth"`
34
- UseTabs *bool `json:"useTabs"`
34
+ TabWidth *int `json:"tabWidth"`
35
+ UseTabs *bool `json:"useTabs"`
36
+ EndOfLine *string `json:"endOfLine"`
35
37
  }
36
38
 
37
39
  func (formatParameterProperties) Name() string { return "format/parameter-properties" }
@@ -111,10 +113,18 @@ func (formatParameterProperties) Check(ctx *Context, node *shimast.Node) {
111
113
  } else if opts.TabWidth != nil && *opts.TabWidth > 0 {
112
114
  oneLevel = strings.Repeat(" ", *opts.TabWidth)
113
115
  }
116
+ // The synthesized break mirrors loadFormatLayout: `"\n"` by default,
117
+ // `"\r\n"` under endOfLine:"crlf", so a broken CRLF constructor keeps
118
+ // consistent line endings instead of gaining lone LFs.
119
+ eol := "\n"
120
+ if opts.EndOfLine != nil && *opts.EndOfLine == "crlf" {
121
+ eol = "\r\n"
122
+ }
114
123
  inner := indent + oneLevel
115
124
 
116
125
  var b strings.Builder
117
- b.WriteString("(\n")
126
+ b.WriteString("(")
127
+ b.WriteString(eol)
118
128
  for i, p := range params {
119
129
  if p == nil {
120
130
  return
@@ -129,7 +139,7 @@ func (formatParameterProperties) Check(ctx *Context, node *shimast.Node) {
129
139
  if i < len(params)-1 {
130
140
  b.WriteByte(',')
131
141
  }
132
- b.WriteByte('\n')
142
+ b.WriteString(eol)
133
143
  }
134
144
  b.WriteString(indent)
135
145
  b.WriteByte(')')
@@ -49,7 +49,7 @@ import (
49
49
  // configured severities. Reuses the `error` severity caveat from
50
50
  // other format rules: only set `error` once the full reflow coverage
51
51
  // is mature.
52
- type formatPrintWidth struct{}
52
+ type formatPrintWidth struct{ optionsRule }
53
53
 
54
54
  // formatPrintWidthOptions mirrors `TtscLintRuleOptions.PrintWidth`.
55
55
  //
@@ -20,7 +20,7 @@ import (
20
20
  // the first) with no escapes. A key with escapes, unicode, a leading digit
21
21
  // (numeric-looking), or any non-identifier byte is left quoted in every
22
22
  // mode, so the rule can never produce an invalid key. Idempotent.
23
- type formatQuoteProps struct{}
23
+ type formatQuoteProps struct{ optionsRule }
24
24
 
25
25
  type formatQuotePropsOptions struct {
26
26
  Mode string `json:"mode"`
@@ -23,7 +23,7 @@ import (
23
23
  // quotes corrupts working code. Template literals, no-substitution
24
24
  // template literals, and JSX text nodes use distinct AST kinds and are
25
25
  // also intentionally out of scope.
26
- type formatQuotes struct{}
26
+ type formatQuotes struct{ optionsRule }
27
27
 
28
28
  // formatQuotesOptions mirrors `TtscLintRuleOptions.Quotes`. Prefer accepts
29
29
  // `"double"` (the default, enforces double quotes) or `"single"` (enforces