@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
@@ -4,6 +4,7 @@
4
4
  package linthost
5
5
 
6
6
  import (
7
+ "encoding/json"
7
8
  "strings"
8
9
 
9
10
  shimast "github.com/microsoft/typescript-go/shim/ast"
@@ -166,68 +167,179 @@ func isNullLiteral(node *shimast.Node) bool {
166
167
  return node != nil && node.Kind == shimast.KindNullKeyword
167
168
  }
168
169
 
169
- // noExtraBind: `(function () {}).bind(this)` where `this` isn't used
170
- // only flag the case where the bind target is empty/parameterless,
171
- // keeping false-positives down.
170
+ // noExtraBind reports `.bind(thisArg)` on arrow functions and regular
171
+ // functions whose own lexical/function scope never reads `this`. Calls that
172
+ // also bind ordinary arguments are partial applications and remain untouched.
173
+ // https://eslint.org/docs/latest/rules/no-extra-bind
172
174
  type noExtraBind struct{}
173
175
 
174
176
  func (noExtraBind) Name() string { return "no-extra-bind" }
175
177
  func (noExtraBind) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindCallExpression} }
176
178
  func (noExtraBind) Check(ctx *Context, node *shimast.Node) {
177
179
  call := node.AsCallExpression()
178
- if call == nil || call.Expression == nil {
180
+ target, member, receiver, ok := noExtraBindCallParts(call)
181
+ if !ok {
179
182
  return
180
183
  }
181
- if call.Expression.Kind != shimast.KindPropertyAccessExpression {
184
+ if target.Kind == shimast.KindFunctionExpression && functionScopeReferencesThis(target) {
182
185
  return
183
186
  }
184
- access := call.Expression.AsPropertyAccessExpression()
185
- if access == nil || identifierText(access.Name()) != "bind" {
186
- return
187
+
188
+ edits := noExtraBindFixEdits(ctx, node, call, member, receiver)
189
+ if len(edits) == 0 {
190
+ ctx.Report(node, "The function binding is unnecessary.")
191
+ } else {
192
+ ctx.ReportFix(node, "The function binding is unnecessary.", edits...)
187
193
  }
188
- target := stripParens(access.Expression)
189
- if target == nil {
190
- return
194
+ }
195
+
196
+ // noExtraBindCallParts recognizes the complete syntactic bind call. Static
197
+ // computed keys and optional member/call chains are accepted, but a dynamic
198
+ // computed property is not assumed to be Function.prototype.bind. Exactly one
199
+ // non-spread argument is required: zero arguments are not the canonical bind
200
+ // shape, while later arguments perform meaningful partial application.
201
+ func noExtraBindCallParts(call *shimast.CallExpression) (
202
+ target *shimast.Node,
203
+ member *shimast.Node,
204
+ receiver *shimast.Node,
205
+ ok bool,
206
+ ) {
207
+ if call == nil || call.Expression == nil || call.Arguments == nil || len(call.Arguments.Nodes) != 1 {
208
+ return nil, nil, nil, false
191
209
  }
192
- if target.Kind != shimast.KindArrowFunction && target.Kind != shimast.KindFunctionExpression {
193
- return
210
+ receiver = call.Arguments.Nodes[0]
211
+ if receiver == nil || receiver.Kind == shimast.KindSpreadElement {
212
+ return nil, nil, nil, false
194
213
  }
195
- // Arrow functions don't have their own `this`; `.bind` is always
196
- // useless on them.
197
- if target.Kind == shimast.KindArrowFunction {
198
- ctx.Report(node, "The function binding is unnecessary.")
199
- return
214
+
215
+ member = stripParens(call.Expression)
216
+ parts, matched := referenceMemberParts(member)
217
+ if !matched || parts.private || parts.staticKey == nil || *parts.staticKey != "bind" {
218
+ return nil, nil, nil, false
200
219
  }
201
- body := target.Body()
202
- if body != nil && !bodyReferencesThis(body) {
203
- ctx.Report(node, "The function binding is unnecessary.")
220
+ target = stripParens(parts.object)
221
+ if target == nil || (target.Kind != shimast.KindArrowFunction && target.Kind != shimast.KindFunctionExpression) {
222
+ return nil, nil, nil, false
204
223
  }
224
+ return target, member, receiver, true
205
225
  }
206
226
 
207
- func bodyReferencesThis(node *shimast.Node) bool {
208
- if node == nil {
227
+ // functionScopeReferencesThis searches the bound regular function itself.
228
+ // Nested arrows inherit that function's `this`; nested regular functions and
229
+ // class-owned initializers do not. Computed class/object keys and decorators
230
+ // are evaluated outside their owning method or field, so their `this` still
231
+ // belongs to the enclosing function.
232
+ func functionScopeReferencesThis(root *shimast.Node) bool {
233
+ if root == nil {
209
234
  return false
210
235
  }
211
- if node.Kind == shimast.KindThisKeyword {
212
- return true
213
- }
214
- // Don't descend into nested function-likes — their `this` is
215
- // independent.
216
- if isFunctionLikeKind(node) && node.Parent != nil {
217
- return false
236
+ var visit func(*shimast.Node) bool
237
+ visit = func(node *shimast.Node) bool {
238
+ if node == nil {
239
+ return false
240
+ }
241
+ if node.Kind == shimast.KindThisKeyword {
242
+ return noExtraBindThisBelongsToFunction(node, root)
243
+ }
244
+ found := false
245
+ node.ForEachChild(func(child *shimast.Node) bool {
246
+ found = visit(child)
247
+ return found
248
+ })
249
+ return found
218
250
  }
219
- found := false
220
- node.ForEachChild(func(child *shimast.Node) bool {
221
- if found {
251
+ return visit(root)
252
+ }
253
+
254
+ func noExtraBindThisBelongsToFunction(node, root *shimast.Node) bool {
255
+ outerEvaluation := false
256
+ for ancestor := node.Parent; ancestor != nil; ancestor = ancestor.Parent {
257
+ if ancestor == root {
222
258
  return true
223
259
  }
224
- if bodyReferencesThis(child) {
225
- found = true
226
- return true
260
+ switch ancestor.Kind {
261
+ case shimast.KindComputedPropertyName, shimast.KindDecorator:
262
+ outerEvaluation = true
263
+ case shimast.KindClassDeclaration, shimast.KindClassExpression:
264
+ // A class decorator runs outside that class, but still inside any
265
+ // enclosing method/function. Do not carry its marker across the class
266
+ // and accidentally skip an unrelated outer method boundary.
267
+ outerEvaluation = false
268
+ case shimast.KindClassStaticBlockDeclaration:
269
+ return false
270
+ case shimast.KindPropertyDeclaration:
271
+ if outerEvaluation {
272
+ outerEvaluation = false
273
+ continue
274
+ }
275
+ return false
276
+ case shimast.KindArrowFunction:
277
+ continue
278
+ }
279
+ if !isFunctionLikeKind(ancestor) {
280
+ continue
281
+ }
282
+ switch ancestor.Kind {
283
+ case shimast.KindMethodDeclaration,
284
+ shimast.KindGetAccessor,
285
+ shimast.KindSetAccessor,
286
+ shimast.KindConstructor:
287
+ if outerEvaluation {
288
+ outerEvaluation = false
289
+ continue
290
+ }
227
291
  }
228
292
  return false
229
- })
230
- return found
293
+ }
294
+ return false
295
+ }
296
+
297
+ // noExtraBindFixEdits removes the member access and its one-argument call as
298
+ // separate ranges. Parentheses and comments before the member operator remain
299
+ // byte-for-byte intact. The fix is withheld when evaluating the receiver may
300
+ // have effects or when any comment lies inside discarded syntax.
301
+ func noExtraBindFixEdits(
302
+ ctx *Context,
303
+ callNode *shimast.Node,
304
+ call *shimast.CallExpression,
305
+ member *shimast.Node,
306
+ receiver *shimast.Node,
307
+ ) []TextEdit {
308
+ if ctx == nil || ctx.File == nil || callNode == nil || call == nil || call.Expression == nil || member == nil ||
309
+ !noExtraBindReceiverIsSideEffectFree(receiver) {
310
+ return nil
311
+ }
312
+ parts, ok := referenceMemberParts(member)
313
+ if !ok || parts.object == nil {
314
+ return nil
315
+ }
316
+
317
+ src := ctx.File.Text()
318
+ memberStart := shimscanner.SkipTrivia(src, parts.object.End())
319
+ memberEnd := member.End()
320
+ callStart := shimscanner.SkipTrivia(src, call.Expression.End())
321
+ callEnd := callNode.End()
322
+ if memberStart < 0 || memberStart >= memberEnd || memberEnd > callStart || callStart >= callEnd || callEnd > len(src) {
323
+ return nil
324
+ }
325
+ if hasCommentBetween(src, memberStart, callEnd) {
326
+ return nil
327
+ }
328
+ return []TextEdit{
329
+ {Pos: memberStart, End: memberEnd, Text: ""},
330
+ {Pos: callStart, End: callEnd, Text: ""},
331
+ }
332
+ }
333
+
334
+ func noExtraBindReceiverIsSideEffectFree(node *shimast.Node) bool {
335
+ node = stripParens(node)
336
+ if node == nil {
337
+ return false
338
+ }
339
+ return isLiteralExpression(node) ||
340
+ node.Kind == shimast.KindIdentifier ||
341
+ node.Kind == shimast.KindThisKeyword ||
342
+ node.Kind == shimast.KindFunctionExpression
231
343
  }
232
344
 
233
345
  // noLabels: labels (`outer: for (...) { break outer; }`) are
@@ -598,20 +710,21 @@ func regexHasMultipleSpaces(src string) bool {
598
710
  }
599
711
 
600
712
  // noReturnAssign: `return a = b` mixes assignment with return.
601
- type noReturnAssign struct{}
713
+ type noReturnAssign struct{ optionsRule }
602
714
 
603
715
  func (noReturnAssign) Name() string { return "no-return-assign" }
604
716
  func (noReturnAssign) Visits() []shimast.Kind {
605
717
  return []shimast.Kind{shimast.KindReturnStatement, shimast.KindArrowFunction}
606
718
  }
607
719
  func (noReturnAssign) Check(ctx *Context, node *shimast.Node) {
720
+ always := noReturnAssignAlways(ctx)
608
721
  switch node.Kind {
609
722
  case shimast.KindReturnStatement:
610
723
  ret := node.AsReturnStatement()
611
724
  if ret == nil || ret.Expression == nil {
612
725
  return
613
726
  }
614
- if isAssignmentExpression(stripParens(ret.Expression)) {
727
+ if noReturnAssignFlags(ret.Expression, always) {
615
728
  ctx.Report(node, "Return statement should not contain assignment.")
616
729
  }
617
730
  case shimast.KindArrowFunction:
@@ -619,12 +732,53 @@ func (noReturnAssign) Check(ctx *Context, node *shimast.Node) {
619
732
  if arrow == nil || arrow.Body == nil || arrow.Body.Kind == shimast.KindBlock {
620
733
  return
621
734
  }
622
- if isAssignmentExpression(stripParens(arrow.Body)) {
735
+ if noReturnAssignFlags(arrow.Body, always) {
623
736
  ctx.Report(node, "Arrow function should not return an assignment.")
624
737
  }
625
738
  }
626
739
  }
627
740
 
741
+ // noReturnAssignFlags reports whether a return/arrow operand should be flagged.
742
+ // Under the default `except-parens` an explicitly parenthesized assignment
743
+ // (`return (a = 1)`) is allowed, so the operand is tested as written: a
744
+ // wrapping ParenthesizedExpression is not a BinaryExpression and fails the
745
+ // assignment test. Under `always` the parentheses are stripped first so the
746
+ // wrapped assignment is flagged too. This matches ESLint no-return-assign's
747
+ // default and the parenthesis exemption `no-cond-assign` already applies.
748
+ func noReturnAssignFlags(expr *shimast.Node, always bool) bool {
749
+ if always {
750
+ return isAssignmentExpression(stripParens(expr))
751
+ }
752
+ return isAssignmentExpression(expr)
753
+ }
754
+
755
+ // noReturnAssignAlways reports whether the rule runs in `always` mode, where a
756
+ // parenthesized assignment is flagged as well. The single positional string
757
+ // option arrives either bare (`"always"`) or as the first slot of an array,
758
+ // mirroring the transport `resolveNoInnerDeclarationsOptions` documents.
759
+ func noReturnAssignAlways(ctx *Context) bool {
760
+ if ctx == nil || len(ctx.Options) == 0 {
761
+ return false
762
+ }
763
+ raw := strings.TrimSpace(string(ctx.Options))
764
+ if raw == "" {
765
+ return false
766
+ }
767
+ var slots []json.RawMessage
768
+ if raw[0] == '[' {
769
+ if err := json.Unmarshal([]byte(raw), &slots); err != nil {
770
+ return false
771
+ }
772
+ } else {
773
+ slots = []json.RawMessage{json.RawMessage(raw)}
774
+ }
775
+ if len(slots) == 0 {
776
+ return false
777
+ }
778
+ var mode string
779
+ return json.Unmarshal(slots[0], &mode) == nil && mode == "always"
780
+ }
781
+
628
782
  // noSequences: `(a, b)` — comma operator. Almost always a confusing
629
783
  // pattern outside of `for` headers.
630
784
  type noSequences struct{}
@@ -812,8 +966,23 @@ func needsParensForUnaryNegation(node *shimast.Node) bool {
812
966
  }
813
967
 
814
968
  // noUnusedExpressions: an expression statement whose value isn't used.
815
- // Filtered down to common patterns ESLint flags by default.
816
- type noUnusedExpressions struct{}
969
+ // Direct port of ESLint's default semantics: a disallow-list of
970
+ // side-effect-free expression shapes (unknown shapes are ignored, the way
971
+ // upstream treats unknown node types), directive prologues exempted by AST
972
+ // position rather than by recognized text, and the upstream option set
973
+ // (`allowShortCircuit`, `allowTernary`, `allowTaggedTemplates`,
974
+ // `enforceForJSX`, `ignoreDirectives`) decoded from the rule's options blob.
975
+ type noUnusedExpressions struct{ optionsRule }
976
+
977
+ // noUnusedExpressionsOptions mirrors the upstream ESLint option object;
978
+ // every flag defaults to false, matching the rule's `defaultOptions`.
979
+ type noUnusedExpressionsOptions struct {
980
+ AllowShortCircuit bool `json:"allowShortCircuit"`
981
+ AllowTernary bool `json:"allowTernary"`
982
+ AllowTaggedTemplates bool `json:"allowTaggedTemplates"`
983
+ EnforceForJSX bool `json:"enforceForJSX"`
984
+ IgnoreDirectives bool `json:"ignoreDirectives"`
985
+ }
817
986
 
818
987
  func (noUnusedExpressions) Name() string { return "no-unused-expressions" }
819
988
  func (noUnusedExpressions) Visits() []shimast.Kind {
@@ -824,61 +993,198 @@ func (noUnusedExpressions) Check(ctx *Context, node *shimast.Node) {
824
993
  if stmt == nil || stmt.Expression == nil {
825
994
  return
826
995
  }
827
- if isProductiveExpression(stmt.Expression) {
996
+ var opts noUnusedExpressionsOptions
997
+ _ = ctx.DecodeOptions(&opts)
998
+ if !noUnusedExpressionsDisallows(stmt.Expression, opts) {
999
+ return
1000
+ }
1001
+ // A directive prologue member ("use strict", "use client", …) is
1002
+ // positionally meaningful whatever its text; never report it. The
1003
+ // `ignoreDirectives` variant additionally applies upstream's loose
1004
+ // ESTree view, in which parentheses are invisible, so a parenthesized
1005
+ // string inside the leading string run is exempted too.
1006
+ if noUnusedExpressionsIsDirective(node, false) {
1007
+ return
1008
+ }
1009
+ if opts.IgnoreDirectives && noUnusedExpressionsIsDirective(node, true) {
828
1010
  return
829
1011
  }
830
1012
  ctx.Report(node, "Expected an assignment or function call and instead saw an expression.")
831
1013
  }
832
1014
 
833
- func isProductiveExpression(node *shimast.Node) bool {
834
- expr := stripParens(node)
1015
+ // noUnusedExpressionsDisallows reports whether expr is a side-effect-free
1016
+ // shape the rule must flag when it stands alone as a statement. Ports the
1017
+ // upstream Checker disallow-list: shapes not listed (calls, `new`,
1018
+ // assignments, updates, `await`, `yield`, `void`, `delete`, dynamic
1019
+ // `import()`, `satisfies`, …) are ignored, mirroring ESLint's "unknown
1020
+ // nodes are handled as false" contract. Parenthesized expressions are
1021
+ // transparent because ESTree has no parenthesized-expression node, and the
1022
+ // TypeScript wrapper expressions (`as`, angle assertions, non-null `!`,
1023
+ // instantiation expressions) inherit the classification of the expression
1024
+ // they wrap.
1025
+ func noUnusedExpressionsDisallows(expr *shimast.Node, opts noUnusedExpressionsOptions) bool {
1026
+ expr = stripParens(expr)
835
1027
  if expr == nil {
836
1028
  return false
837
1029
  }
838
1030
  switch expr.Kind {
839
- case shimast.KindCallExpression,
840
- shimast.KindNewExpression,
841
- shimast.KindAwaitExpression,
842
- shimast.KindYieldExpression,
843
- shimast.KindDeleteExpression,
844
- shimast.KindBinaryExpression,
845
- shimast.KindPrefixUnaryExpression,
846
- shimast.KindPostfixUnaryExpression,
847
- shimast.KindTaggedTemplateExpression:
848
- // Most of these kinds are unconditionally productive (call, new, await,
849
- // yield, delete, tagged template). The inner switch narrows the two kinds
850
- // that can be non-productive: a BinaryExpression is only productive when
851
- // it is an assignment, and a PrefixUnaryExpression is only productive when
852
- // it is ++ or --. KindPostfixUnaryExpression is always productive (++ and
853
- // -- are the only postfix operators). All un-matched cases reach the
854
- // outer `return true` below.
855
- switch expr.Kind {
856
- case shimast.KindBinaryExpression:
857
- bin := expr.AsBinaryExpression()
858
- if bin != nil && bin.OperatorToken != nil && isAssignmentOperator(bin.OperatorToken.Kind) {
859
- return true
860
- }
1031
+ case shimast.KindArrayLiteralExpression,
1032
+ shimast.KindObjectLiteralExpression,
1033
+ shimast.KindArrowFunction,
1034
+ shimast.KindFunctionExpression,
1035
+ shimast.KindClassExpression,
1036
+ shimast.KindIdentifier,
1037
+ shimast.KindPropertyAccessExpression,
1038
+ shimast.KindElementAccessExpression,
1039
+ shimast.KindMetaProperty,
1040
+ shimast.KindThisKeyword,
1041
+ shimast.KindStringLiteral,
1042
+ shimast.KindNumericLiteral,
1043
+ shimast.KindBigIntLiteral,
1044
+ shimast.KindRegularExpressionLiteral,
1045
+ shimast.KindTrueKeyword,
1046
+ shimast.KindFalseKeyword,
1047
+ shimast.KindNullKeyword,
1048
+ shimast.KindTemplateExpression,
1049
+ shimast.KindNoSubstitutionTemplateLiteral,
1050
+ shimast.KindTypeOfExpression:
1051
+ return true
1052
+ case shimast.KindBinaryExpression:
1053
+ bin := expr.AsBinaryExpression()
1054
+ if bin == nil || bin.OperatorToken == nil {
861
1055
  return false
862
- case shimast.KindPrefixUnaryExpression:
863
- prefix := expr.AsPrefixUnaryExpression()
864
- if prefix != nil && (prefix.Operator == shimast.KindPlusPlusToken || prefix.Operator == shimast.KindMinusMinusToken) {
865
- return true
1056
+ }
1057
+ switch bin.OperatorToken.Kind {
1058
+ case shimast.KindAmpersandAmpersandToken,
1059
+ shimast.KindBarBarToken,
1060
+ shimast.KindQuestionQuestionToken:
1061
+ // ESTree LogicalExpression: only the right operand's value goes
1062
+ // unused, so `allowShortCircuit` defers to its classification.
1063
+ if opts.AllowShortCircuit {
1064
+ return noUnusedExpressionsDisallows(bin.Right, opts)
866
1065
  }
867
- return false
868
- case shimast.KindPostfixUnaryExpression:
869
1066
  return true
870
1067
  }
1068
+ if isAssignmentOperator(bin.OperatorToken.Kind) {
1069
+ // ESTree AssignmentExpression (including `&&=`, `||=`, `??=`) —
1070
+ // productive.
1071
+ return false
1072
+ }
1073
+ // Comma sequences and ordinary binary operators (`a + b`,
1074
+ // `a === b`, `key in obj`, …) compute a value nobody reads.
871
1075
  return true
872
- case shimast.KindStringLiteral:
873
- // "use strict" prologue.
874
- text := expr.AsStringLiteral()
875
- if text != nil && (text.Text == "use strict" || text.Text == "use asm") {
1076
+ case shimast.KindPrefixUnaryExpression:
1077
+ prefix := expr.AsPrefixUnaryExpression()
1078
+ if prefix == nil {
1079
+ return false
1080
+ }
1081
+ // `++x` / `--x` are ESTree UpdateExpressions (productive); the
1082
+ // remaining prefix operators (`+`, `-`, `!`, `~`) are pure.
1083
+ return prefix.Operator != shimast.KindPlusPlusToken &&
1084
+ prefix.Operator != shimast.KindMinusMinusToken
1085
+ case shimast.KindConditionalExpression:
1086
+ cond := expr.AsConditionalExpression()
1087
+ if cond == nil {
1088
+ return false
1089
+ }
1090
+ if opts.AllowTernary {
1091
+ // Disallowed when either result branch is itself side-effect
1092
+ // free; recursion keeps nested allowances working.
1093
+ return noUnusedExpressionsDisallows(cond.WhenTrue, opts) ||
1094
+ noUnusedExpressionsDisallows(cond.WhenFalse, opts)
1095
+ }
1096
+ return true
1097
+ case shimast.KindTaggedTemplateExpression:
1098
+ return !opts.AllowTaggedTemplates
1099
+ case shimast.KindJsxElement,
1100
+ shimast.KindJsxSelfClosingElement,
1101
+ shimast.KindJsxFragment:
1102
+ return opts.EnforceForJSX
1103
+ case shimast.KindAsExpression:
1104
+ as := expr.AsAsExpression()
1105
+ return as != nil && as.Expression != nil &&
1106
+ noUnusedExpressionsDisallows(as.Expression, opts)
1107
+ case shimast.KindTypeAssertionExpression:
1108
+ assertion := expr.AsTypeAssertion()
1109
+ return assertion != nil && assertion.Expression != nil &&
1110
+ noUnusedExpressionsDisallows(assertion.Expression, opts)
1111
+ case shimast.KindNonNullExpression:
1112
+ nonNull := expr.AsNonNullExpression()
1113
+ return nonNull != nil && nonNull.Expression != nil &&
1114
+ noUnusedExpressionsDisallows(nonNull.Expression, opts)
1115
+ case shimast.KindExpressionWithTypeArguments:
1116
+ instantiation := expr.AsExpressionWithTypeArguments()
1117
+ return instantiation != nil && instantiation.Expression != nil &&
1118
+ noUnusedExpressionsDisallows(instantiation.Expression, opts)
1119
+ }
1120
+ return false
1121
+ }
1122
+
1123
+ // noUnusedExpressionsIsDirective reports whether stmt sits inside its
1124
+ // container's directive prologue — the leading run of string-literal
1125
+ // expression statements of a script or module body, a function body, or a
1126
+ // TypeScript namespace/module block
1127
+ // (https://tc39.es/ecma262/#directive-prologue). Class static blocks and
1128
+ // plain nested blocks own no prologue.
1129
+ //
1130
+ // seeThroughParens selects the `ignoreDirectives` variant: upstream ESLint
1131
+ // evaluates that option against an ESTree, which has no
1132
+ // parenthesized-expression nodes, so the loose variant lets a
1133
+ // parenthesized string participate in the leading run. The strict variant
1134
+ // matches the parser's directive notion, where `("use strict")` is not a
1135
+ // directive and terminates the prologue.
1136
+ func noUnusedExpressionsIsDirective(stmt *shimast.Node, seeThroughParens bool) bool {
1137
+ if stmt == nil || !noUnusedExpressionsCanOwnPrologue(stmt.Parent) {
1138
+ return false
1139
+ }
1140
+ for _, sibling := range parentStatements(stmt.Parent) {
1141
+ if !noUnusedExpressionsIsStringStatement(sibling, seeThroughParens) {
1142
+ return false
1143
+ }
1144
+ if nodesShareLoc(sibling, stmt) {
876
1145
  return true
877
1146
  }
878
1147
  }
879
1148
  return false
880
1149
  }
881
1150
 
1151
+ // noUnusedExpressionsCanOwnPrologue reports whether parent is a statement
1152
+ // container whose leading string statements form a directive prologue: a
1153
+ // source file (script or module), a namespace/module block, or the body
1154
+ // block of a function-like declaration. Mirrors upstream ESLint's
1155
+ // isTopLevelExpressionStatement (Program | TSModuleBlock | function body).
1156
+ func noUnusedExpressionsCanOwnPrologue(parent *shimast.Node) bool {
1157
+ if parent == nil {
1158
+ return false
1159
+ }
1160
+ switch parent.Kind {
1161
+ case shimast.KindSourceFile, shimast.KindModuleBlock:
1162
+ return true
1163
+ case shimast.KindBlock:
1164
+ return isFunctionLikeKind(parent.Parent)
1165
+ }
1166
+ return false
1167
+ }
1168
+
1169
+ // noUnusedExpressionsIsStringStatement reports whether stmt is an
1170
+ // expression statement consisting of a string literal — the shape that can
1171
+ // extend a directive prologue. With seeThroughParens the string may sit
1172
+ // behind parentheses (the ESTree view used by `ignoreDirectives`).
1173
+ func noUnusedExpressionsIsStringStatement(stmt *shimast.Node, seeThroughParens bool) bool {
1174
+ if stmt == nil || stmt.Kind != shimast.KindExpressionStatement {
1175
+ return false
1176
+ }
1177
+ inner := stmt.AsExpressionStatement()
1178
+ if inner == nil || inner.Expression == nil {
1179
+ return false
1180
+ }
1181
+ expr := inner.Expression
1182
+ if seeThroughParens {
1183
+ expr = stripParens(expr)
1184
+ }
1185
+ return expr != nil && expr.Kind == shimast.KindStringLiteral
1186
+ }
1187
+
882
1188
  // noUselessCall: `func.call(undefined, ...args)` / `func.apply(undefined, args)`
883
1189
  // — call/apply with no this binding is just a regular call.
884
1190
  type noUselessCall struct{}
@@ -1006,6 +1312,14 @@ func reportUselessRenameFix(ctx *Context, node, propertyName, name *shimast.Node
1006
1312
  ctx.Report(node, message)
1007
1313
  return
1008
1314
  }
1315
+ // A comment between the two names (`{ a as /* c */ a }`) sits inside the
1316
+ // deleted rename tail; dropping it would be silent data loss, so decline to
1317
+ // a report-only diagnostic. Mirrors ESLint no-useless-rename's
1318
+ // `commentsExistBetween` guard.
1319
+ if hasCommentBetween(ctx.File.Text(), propertyName.End(), name.End()) {
1320
+ ctx.Report(node, message)
1321
+ return
1322
+ }
1009
1323
  ctx.ReportFix(
1010
1324
  node,
1011
1325
  message,
@@ -1035,6 +1349,14 @@ func (objectShorthand) Check(ctx *Context, node *shimast.Node) {
1035
1349
  // at the end of the property name and ends at the end of the
1036
1350
  // initializer; any whitespace between `:` and the value is part of
1037
1351
  // that range.
1352
+ //
1353
+ // A comment inside that range (`{ x: /* c */ x }`) would be dropped by the
1354
+ // deletion, so decline to a report-only diagnostic. Mirrors ESLint
1355
+ // object-shorthand's `commentsExistBetween` guard.
1356
+ if hasCommentBetween(ctx.File.Text(), prop.Name().End(), prop.Initializer.End()) {
1357
+ ctx.Report(node, "Expected property shorthand.")
1358
+ return
1359
+ }
1038
1360
  ctx.ReportFix(
1039
1361
  node,
1040
1362
  "Expected property shorthand.",
@@ -1172,6 +1494,16 @@ func (preferTemplate) Check(ctx *Context, node *shimast.Node) {
1172
1494
  ctx.Report(node, message)
1173
1495
  return
1174
1496
  }
1497
+ // A comment in an operator seam (`"a" + /* c */ b`) is dropped by the
1498
+ // template rebuild, so decline to a report-only diagnostic. Mirrors ESLint
1499
+ // prefer-template's `commentsExistBetween` guard. Only the seams are scanned:
1500
+ // operand interiors are copied verbatim (their comments survive) and string
1501
+ // contents are cooked, so scanning the whole span would misread a `//` or
1502
+ // `/*` inside a string literal (`"https://" + host`) as a comment.
1503
+ if concatOperandSeamsHaveComment(src, operands) {
1504
+ ctx.Report(node, message)
1505
+ return
1506
+ }
1175
1507
  ctx.ReportFix(
1176
1508
  node,
1177
1509
  message,
@@ -1179,6 +1511,27 @@ func (preferTemplate) Check(ctx *Context, node *shimast.Node) {
1179
1511
  )
1180
1512
  }
1181
1513
 
1514
+ // concatOperandSeamsHaveComment reports whether a comment sits in any seam
1515
+ // between the flattened concat operands — the `+`-operator gaps the template
1516
+ // rebuild collapses. A leading comment before an operand is skipped by the
1517
+ // renderer (both string and expression operands begin at their trimmed token),
1518
+ // so a seam comment would be lost and the fix must decline. Operand interiors
1519
+ // are excluded because the renderer copies expression operands verbatim and
1520
+ // cooks string operands, so their contents — including a `//`-bearing URL
1521
+ // string — never trigger a false positive.
1522
+ func concatOperandSeamsHaveComment(src string, operands []*shimast.Node) bool {
1523
+ for i := 0; i+1 < len(operands); i++ {
1524
+ left, right := operands[i], operands[i+1]
1525
+ if left == nil || right == nil {
1526
+ continue
1527
+ }
1528
+ if hasCommentBetween(src, left.End(), shimscanner.SkipTrivia(src, right.Pos())) {
1529
+ return true
1530
+ }
1531
+ }
1532
+ return false
1533
+ }
1534
+
1182
1535
  func concatChainShape(node *shimast.Node) (hasString bool, hasOther bool) {
1183
1536
  if node == nil {
1184
1537
  return false, false
@@ -1198,17 +1551,24 @@ func concatChainShape(node *shimast.Node) (hasString bool, hasOther bool) {
1198
1551
  }
1199
1552
 
1200
1553
  // flattenConcatOperands walks a `+` chain left-to-right and returns each
1201
- // leaf operand in source order. Parenthesized sub-expressions are kept as
1202
- // a single operand so the rendered template literal does not lose their
1203
- // grouping. Mirrors ESLint's behavior of treating `("a" + b)` as one
1204
- // expression slot when it appears inside a larger chain.
1554
+ // leaf operand in source order. It only descends into a `+` subtree that
1555
+ // itself contains a string-like operand (see
1556
+ // concatChainContainsString): a subtree without one the `a + b` in
1557
+ // `a + b + " items"` evaluates BEFORE any string concatenation, so
1558
+ // splitting it into separate `${a}${b}` slots would change the runtime
1559
+ // value (numeric 3 becomes the digits "12"). Such a subtree stays one
1560
+ // operand and renders as a single `${a + b}` slot, mirroring upstream
1561
+ // ESLint prefer-template, which embeds non-string sub-chains as one
1562
+ // expression. Parenthesized sub-expressions are likewise kept as a
1563
+ // single operand so the rendered template literal does not lose their
1564
+ // grouping.
1205
1565
  func flattenConcatOperands(node *shimast.Node) []*shimast.Node {
1206
1566
  if node == nil {
1207
1567
  return nil
1208
1568
  }
1209
1569
  if node.Kind == shimast.KindBinaryExpression {
1210
1570
  bin := node.AsBinaryExpression()
1211
- if bin != nil && bin.OperatorToken != nil && bin.OperatorToken.Kind == shimast.KindPlusToken {
1571
+ if bin != nil && bin.OperatorToken != nil && bin.OperatorToken.Kind == shimast.KindPlusToken && concatChainContainsString(node) {
1212
1572
  out := flattenConcatOperands(bin.Left)
1213
1573
  out = append(out, flattenConcatOperands(bin.Right)...)
1214
1574
  return out
@@ -1217,17 +1577,61 @@ func flattenConcatOperands(node *shimast.Node) []*shimast.Node {
1217
1577
  return []*shimast.Node{node}
1218
1578
  }
1219
1579
 
1580
+ // concatChainContainsString reports whether a `+` chain (or a single
1581
+ // operand) contains a string-like operand: a string literal, a template
1582
+ // literal (with or without substitutions), or a nested `+` chain —
1583
+ // parenthesized or not — that itself contains one. This is the
1584
+ // flattening gate for flattenConcatOperands. Once a string-like operand
1585
+ // appears in a left-associative chain, every later `+` is string
1586
+ // concatenation, so splitting the chain into template segments preserves
1587
+ // the value; a chain with none may be numeric addition and must stay
1588
+ // whole. Parentheses are transparent to the DECISION (`("a" + b) + c`
1589
+ // is a string chain, so `c` still gets its own slot) even though the
1590
+ // flattener keeps the parenthesized operand itself as one slot. A
1591
+ // string-like node under any other operator (e.g. `a * "x"`) does NOT
1592
+ // qualify: that subexpression coerces away from string, so its chain
1593
+ // may still be numeric addition.
1594
+ func concatChainContainsString(node *shimast.Node) bool {
1595
+ node = stripParens(node)
1596
+ if node == nil {
1597
+ return false
1598
+ }
1599
+ if node.Kind == shimast.KindBinaryExpression {
1600
+ bin := node.AsBinaryExpression()
1601
+ if bin != nil && bin.OperatorToken != nil && bin.OperatorToken.Kind == shimast.KindPlusToken {
1602
+ return concatChainContainsString(bin.Left) || concatChainContainsString(bin.Right)
1603
+ }
1604
+ }
1605
+ return isStringLikeLiteral(node) || node.Kind == shimast.KindTemplateExpression
1606
+ }
1607
+
1220
1608
  // renderConcatAsTemplate renders the flattened concat operands as a single
1221
1609
  // backtick template literal. String-like literals contribute their value
1222
1610
  // directly (with template-specific escaping); any other expression becomes
1223
1611
  // a `${…}` placeholder copied verbatim from the source text. Returns ok=false
1224
1612
  // when an operand cannot be rendered (typically because its source range is
1225
1613
  // unavailable), so the caller falls back to detection-only.
1614
+ //
1615
+ // Adjacent literal operands are merged into one cooked run BEFORE
1616
+ // escaping. Escaping each literal on its own cannot see across the seam,
1617
+ // so `"$" + "{"` would emit `$` then `{` — fusing into a live `${`
1618
+ // interpolation opener in the rendered template. Escaping the merged run
1619
+ // makes escapeTemplateLiteralBody's `${` lookahead total over the body.
1620
+ // The only other seam, a literal's trailing `$` followed by an emitted
1621
+ // `${…}` placeholder, is safe: `$${expr}` parses as a literal `$` plus
1622
+ // the interpolation, which matches the original `"$" + expr` value.
1226
1623
  func renderConcatAsTemplate(src string, operands []*shimast.Node) (string, bool) {
1227
1624
  if len(operands) == 0 {
1228
1625
  return "", false
1229
1626
  }
1230
1627
  var sb strings.Builder
1628
+ var literal strings.Builder
1629
+ flushLiteral := func() {
1630
+ if literal.Len() > 0 {
1631
+ sb.WriteString(escapeTemplateLiteralBody(literal.String()))
1632
+ literal.Reset()
1633
+ }
1634
+ }
1231
1635
  sb.WriteByte('`')
1232
1636
  for _, operand := range operands {
1233
1637
  if operand == nil {
@@ -1235,7 +1639,7 @@ func renderConcatAsTemplate(src string, operands []*shimast.Node) (string, bool)
1235
1639
  }
1236
1640
  inner := stripParens(operand)
1237
1641
  if isStringLikeLiteral(inner) {
1238
- sb.WriteString(escapeTemplateLiteralBody(stringLiteralText(inner)))
1642
+ literal.WriteString(stringLiteralText(inner))
1239
1643
  continue
1240
1644
  }
1241
1645
  pos := shimscanner.SkipTrivia(src, operand.Pos())
@@ -1243,10 +1647,12 @@ func renderConcatAsTemplate(src string, operands []*shimast.Node) (string, bool)
1243
1647
  if pos < 0 || pos >= end || end > len(src) {
1244
1648
  return "", false
1245
1649
  }
1650
+ flushLiteral()
1246
1651
  sb.WriteString("${")
1247
1652
  sb.WriteString(src[pos:end])
1248
1653
  sb.WriteByte('}')
1249
1654
  }
1655
+ flushLiteral()
1250
1656
  sb.WriteByte('`')
1251
1657
  return sb.String(), true
1252
1658
  }