@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.
- package/README.md +125 -43
- package/go.mod +5 -4
- package/internal/rulecode/rulecode.go +88 -0
- package/lib/index.d.ts +10 -1
- package/lib/index.js +45 -17
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +3 -0
- package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
- package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
- package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
- package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
- package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
- package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
- package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
- package/lib/structures/rules/index.d.ts +3 -0
- package/lib/structures/rules/index.js +3 -0
- package/lib/structures/rules/index.js.map +1 -1
- package/linthost/ast_helpers.go +521 -23
- package/linthost/ast_selector_matcher.go +1628 -0
- package/linthost/ast_selector_parser.go +628 -0
- package/linthost/comment_scan.go +154 -0
- package/linthost/compile.go +129 -79
- package/linthost/config.go +352 -88
- package/linthost/config_format.go +15 -1
- package/linthost/contrib_adapter.go +47 -8
- package/linthost/declaration_rules.go +12 -11
- package/linthost/directives.go +9 -67
- package/linthost/dispatch.go +37 -3
- package/linthost/engine.go +368 -53
- package/linthost/escape_string.go +77 -0
- package/linthost/fix.go +133 -23
- package/linthost/flags_gen.go +18 -17
- package/linthost/format.go +73 -167
- package/linthost/format_editor_settings.go +134 -26
- package/linthost/grapheme_tables_gen.go +1920 -0
- package/linthost/host.go +159 -61
- package/linthost/literal_escape_scan.go +117 -0
- package/linthost/lsp.go +545 -98
- package/linthost/numeric_literal_precision.go +228 -0
- package/linthost/polyfill_browserslist.go +1439 -0
- package/linthost/polyfill_browserslist_config.go +603 -0
- package/linthost/polyfill_corejs.go +750 -0
- package/linthost/polyfill_data.go +113 -0
- package/linthost/polyfill_data_gen.json +39868 -0
- package/linthost/print_nodes_array.go +7 -1
- package/linthost/print_nodes_object.go +7 -1
- package/linthost/project_engine.go +231 -0
- package/linthost/project_rules.go +78 -0
- package/linthost/regex_clean.go +191 -0
- package/linthost/regex_tree.go +1544 -0
- package/linthost/regex_tree_optimizer.go +1685 -0
- package/linthost/regexp_cache.go +44 -0
- package/linthost/rule_codes.go +105 -0
- package/linthost/rule_codes.json +745 -0
- package/linthost/rules_ban_ts_comment.go +312 -0
- package/linthost/rules_boundaries.go +128 -43
- package/linthost/rules_boundaries_dependencies.go +1013 -0
- package/linthost/rules_core_extra.go +62 -92
- package/linthost/rules_cypress.go +1 -1
- package/linthost/rules_default_case.go +87 -11
- package/linthost/rules_dupes.go +38 -13
- package/linthost/rules_empty.go +188 -20
- package/linthost/rules_escape.go +62 -48
- package/linthost/rules_format_arrow_parens.go +19 -5
- package/linthost/rules_format_bracket_spacing.go +1 -1
- package/linthost/rules_format_clause_join.go +1 -1
- package/linthost/rules_format_declaration_header.go +31 -19
- package/linthost/rules_format_indent.go +1 -1
- package/linthost/rules_format_jsdoc.go +1 -1
- package/linthost/rules_format_orphan_semi.go +1 -1
- package/linthost/rules_format_parameter_properties.go +17 -7
- package/linthost/rules_format_print_width.go +1 -1
- package/linthost/rules_format_quote_props.go +1 -1
- package/linthost/rules_format_quotes.go +1 -1
- package/linthost/rules_format_semi.go +106 -79
- package/linthost/rules_format_sort_imports.go +79 -45
- package/linthost/rules_format_statement_split.go +1 -1
- package/linthost/rules_format_ternary_nullish_parens.go +1 -1
- package/linthost/rules_format_trailing_comma.go +64 -1
- package/linthost/rules_format_whitespace.go +1 -1
- package/linthost/rules_functional.go +21 -22
- package/linthost/rules_gap.go +80 -3
- package/linthost/rules_grouped_accessor_pairs.go +84 -25
- package/linthost/rules_guard_for_in.go +64 -147
- package/linthost/rules_jsx_a11y.go +232 -22
- package/linthost/rules_logic.go +84 -38
- package/linthost/rules_nextjs.go +105 -16
- package/linthost/rules_no_class_assign.go +70 -0
- package/linthost/rules_no_duplicate_imports.go +266 -0
- package/linthost/rules_no_else_return.go +156 -38
- package/linthost/rules_no_extend_native.go +113 -17
- package/linthost/rules_no_fallthrough.go +1013 -0
- package/linthost/rules_no_func_assign.go +70 -0
- package/linthost/rules_no_import_assign.go +278 -86
- package/linthost/rules_no_inner_declarations.go +186 -0
- package/linthost/rules_no_loop_func.go +438 -53
- package/linthost/rules_no_mixed_operators.go +208 -52
- package/linthost/rules_no_param_reassign.go +376 -61
- package/linthost/rules_no_promise_executor_return.go +156 -0
- package/linthost/rules_no_restricted_imports.go +1013 -57
- package/linthost/rules_no_restricted_syntax.go +190 -26
- package/linthost/rules_no_useless_constructor.go +6 -36
- package/linthost/rules_problems.go +154 -311
- package/linthost/rules_promise.go +2578 -109
- package/linthost/rules_react_extras.go +71 -45
- package/linthost/rules_react_hooks.go +18 -2
- package/linthost/rules_react_perf.go +1 -0
- package/linthost/rules_react_refresh.go +1 -1
- package/linthost/rules_regexp.go +230 -14
- package/linthost/rules_security.go +45 -19
- package/linthost/rules_self.go +21 -1
- package/linthost/rules_solid.go +13 -7
- package/linthost/rules_storybook.go +1 -1
- package/linthost/rules_suggestions.go +490 -84
- package/linthost/rules_testing_library.go +12 -1
- package/linthost/rules_ts.go +227 -54
- package/linthost/rules_ts_async.go +0 -131
- package/linthost/rules_ts_no_base_to_string.go +3 -4
- package/linthost/rules_ts_no_misused_promises.go +817 -0
- package/linthost/rules_ts_no_restricted_types.go +320 -34
- package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
- package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
- package/linthost/rules_ts_require_array_sort_compare.go +5 -5
- package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
- package/linthost/rules_unicorn_better_regex.go +224 -16
- package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
- package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
- package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
- package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
- package/linthost/rules_unicorn_escape_case.go +30 -19
- package/linthost/rules_unicorn_filename_case.go +763 -16
- package/linthost/rules_unicorn_import_style.go +783 -17
- package/linthost/rules_unicorn_isolated_functions.go +934 -14
- package/linthost/rules_unicorn_no_hex_escape.go +26 -11
- package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
- package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
- package/linthost/rules_unicorn_no_unused_properties.go +756 -19
- package/linthost/rules_unicorn_number_literal_case.go +58 -39
- package/linthost/rules_unicorn_prefer_at.go +7 -3
- package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
- package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
- package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
- package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
- package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
- package/linthost/rules_unicorn_string_content.go +611 -15
- package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
- package/linthost/rules_unicorn_template_indent.go +923 -18
- package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
- package/linthost/rules_unicorn_throw_new_error.go +218 -26
- package/linthost/rules_var.go +829 -142
- package/linthost/string_length.go +222 -0
- package/package.json +4 -3
- package/rule/project.go +206 -0
- package/rule/rule.go +58 -1
- package/src/index.ts +58 -17
- package/src/structures/ITtscLintConfig.ts +3 -0
- package/src/structures/TtscLintRuleSetting.ts +3 -2
- package/src/structures/format/ITtscLintFormat.ts +3 -2
- package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
- package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
- package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
- package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
- package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
- package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
- package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
- package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
- package/src/structures/rules/ITtscLintRules.ts +7 -2
- package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
- package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
- package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
- 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
|
|
170
|
-
//
|
|
171
|
-
//
|
|
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
|
-
|
|
180
|
+
target, member, receiver, ok := noExtraBindCallParts(call)
|
|
181
|
+
if !ok {
|
|
179
182
|
return
|
|
180
183
|
}
|
|
181
|
-
if
|
|
184
|
+
if target.Kind == shimast.KindFunctionExpression && functionScopeReferencesThis(target) {
|
|
182
185
|
return
|
|
183
186
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
210
|
+
receiver = call.Arguments.Nodes[0]
|
|
211
|
+
if receiver == nil || receiver.Kind == shimast.KindSpreadElement {
|
|
212
|
+
return nil, nil, nil, false
|
|
194
213
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
if
|
|
203
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
816
|
-
|
|
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
|
-
|
|
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
|
-
|
|
834
|
-
|
|
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.
|
|
840
|
-
shimast.
|
|
841
|
-
shimast.
|
|
842
|
-
shimast.
|
|
843
|
-
shimast.
|
|
844
|
-
shimast.
|
|
845
|
-
shimast.
|
|
846
|
-
shimast.
|
|
847
|
-
shimast.
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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.
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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.
|
|
1202
|
-
// a
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
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
|
-
|
|
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
|
}
|