@ttsc/lint 0.12.3 → 0.13.0

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 (74) hide show
  1. package/lib/defaultFormat.d.ts +10 -11
  2. package/lib/defaultFormat.js +10 -11
  3. package/lib/defaultFormat.js.map +1 -1
  4. package/lib/index.js +233 -135
  5. package/lib/index.js.map +1 -1
  6. package/lib/structures/ITtscLintConfig.d.ts +2 -2
  7. package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
  8. package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
  9. package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
  10. package/lib/structures/TtscLintRule.d.ts +10 -1
  11. package/lib/structures/TtscLintRuleMap.d.ts +2 -2
  12. package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
  13. package/linthost/ast_helpers.go +80 -11
  14. package/linthost/compile.go +116 -112
  15. package/linthost/config.go +647 -687
  16. package/linthost/config_format.go +272 -247
  17. package/linthost/contrib_adapter.go +7 -0
  18. package/linthost/directives.go +116 -0
  19. package/linthost/dispatch.go +33 -33
  20. package/linthost/engine.go +156 -43
  21. package/linthost/fix.go +47 -27
  22. package/linthost/flags_gen.go +31 -0
  23. package/linthost/format.go +119 -3
  24. package/linthost/host.go +115 -5
  25. package/linthost/print_dispatch.go +128 -28
  26. package/linthost/print_doc.go +23 -0
  27. package/linthost/print_engine.go +168 -4
  28. package/linthost/print_nodes_array.go +17 -7
  29. package/linthost/print_nodes_call.go +138 -20
  30. package/linthost/print_nodes_function.go +353 -0
  31. package/linthost/print_nodes_imports.go +46 -29
  32. package/linthost/print_nodes_list.go +86 -5
  33. package/linthost/print_nodes_object.go +56 -11
  34. package/linthost/rules_arrays.go +5 -2
  35. package/linthost/rules_debugger.go +3 -2
  36. package/linthost/rules_dupes.go +7 -4
  37. package/linthost/rules_empty.go +3 -2
  38. package/linthost/rules_escape.go +35 -3
  39. package/linthost/rules_eval.go +3 -0
  40. package/linthost/rules_finally.go +11 -0
  41. package/linthost/rules_format_jsdoc.go +7 -0
  42. package/linthost/rules_format_print_width.go +270 -15
  43. package/linthost/rules_format_quotes.go +3 -0
  44. package/linthost/rules_format_sort_imports.go +4 -0
  45. package/linthost/rules_gap.go +75 -3
  46. package/linthost/rules_imports.go +5 -7
  47. package/linthost/rules_logic.go +75 -5
  48. package/linthost/rules_loops.go +4 -0
  49. package/linthost/rules_misc.go +4 -2
  50. package/linthost/rules_params.go +7 -11
  51. package/linthost/rules_problems.go +89 -22
  52. package/linthost/rules_promise.go +15 -0
  53. package/linthost/rules_protos.go +3 -2
  54. package/linthost/rules_self.go +6 -0
  55. package/linthost/rules_strings.go +10 -0
  56. package/linthost/rules_suggestions.go +174 -9
  57. package/linthost/rules_throw.go +2 -0
  58. package/linthost/rules_ts.go +13 -0
  59. package/linthost/rules_ts_extra.go +25 -8
  60. package/linthost/rules_var.go +15 -2
  61. package/package.json +3 -3
  62. package/plugin/main.go +4 -4
  63. package/rule/astutil/astutil.go +12 -0
  64. package/rule/rule.go +123 -123
  65. package/src/defaultFormat.ts +10 -11
  66. package/src/index.ts +257 -168
  67. package/src/structures/ITtscLintConfig.ts +2 -2
  68. package/src/structures/ITtscLintFormatConfig.ts +53 -55
  69. package/src/structures/ITtscLintPluginConfig.ts +10 -83
  70. package/src/structures/ITtscLintPluginMeta.ts +9 -1
  71. package/src/structures/TtscLintRule.ts +10 -1
  72. package/src/structures/TtscLintRuleMap.ts +17 -18
  73. package/src/structures/TtscLintRuleOptions.ts +15 -0
  74. package/linthost/eslint_runtime.go +0 -320
@@ -20,56 +20,70 @@ import (
20
20
  // mode, matching Prettier's `bracketSpacing: true` default for import
21
21
  // declarations (which is hard-coded; Prettier ignores `bracketSpacing`
22
22
  // for imports). Empty named imports `{}` collapse cleanly.
23
- func printNamedImports(ctx *PrintContext, node *shimast.Node) Doc {
23
+ //
24
+ // The second return value is the `covered` flag: see PrintNode.
25
+ func printNamedImports(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
24
26
  if node == nil {
25
- return Doc{}
27
+ return Doc{}, true
26
28
  }
27
29
  ni := node.AsNamedImports()
28
30
  if ni == nil || ni.Elements == nil {
29
- return verbatim(ctx, node)
31
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
30
32
  }
31
33
  items := make([]Doc, 0, len(ni.Elements.Nodes))
34
+ covered := true
32
35
  for _, spec := range ni.Elements.Nodes {
33
36
  if spec == nil {
34
- return verbatim(ctx, node)
37
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
35
38
  }
36
- doc, _ := PrintNode(ctx, spec)
39
+ doc, childCovered := PrintNode(ctx, spec)
40
+ covered = covered && childCovered
37
41
  items = append(items, doc)
38
42
  }
43
+ // AddComma honors `format.trailingComma`: named imports are an
44
+ // ES5-permitted comma position, so "all" and "es5" both keep the
45
+ // trailing comma; only "none" drops it.
39
46
  return printList(ctx, listShape{
40
47
  OpenTok: "{",
41
48
  CloseTok: "}",
42
49
  Items: items,
43
50
  Space: true,
44
- AddComma: true,
45
- })
51
+ AddComma: ctx.allowsEs5TrailingComma(),
52
+ }), covered
46
53
  }
47
54
 
48
55
  // printNamedExports renders `export { a, b }`. The shape is identical
49
56
  // to NamedImports; only the surrounding declaration differs.
50
- func printNamedExports(ctx *PrintContext, node *shimast.Node) Doc {
57
+ //
58
+ // The second return value is the `covered` flag: see PrintNode.
59
+ func printNamedExports(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
51
60
  if node == nil {
52
- return Doc{}
61
+ return Doc{}, true
53
62
  }
54
63
  ne := node.AsNamedExports()
55
64
  if ne == nil || ne.Elements == nil {
56
- return verbatim(ctx, node)
65
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
57
66
  }
58
67
  items := make([]Doc, 0, len(ne.Elements.Nodes))
68
+ covered := true
59
69
  for _, spec := range ne.Elements.Nodes {
60
70
  if spec == nil {
61
- return verbatim(ctx, node)
71
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
62
72
  }
63
- doc, _ := PrintNode(ctx, spec)
73
+ doc, childCovered := PrintNode(ctx, spec)
74
+ covered = covered && childCovered
64
75
  items = append(items, doc)
65
76
  }
77
+ // AddComma honors `format.trailingComma`: named exports are an
78
+ // ES5-permitted comma position, so "all" and "es5" both keep the
79
+ // trailing comma; only "none" drops it.
66
80
  return printList(ctx, listShape{
67
81
  OpenTok: "{",
68
82
  CloseTok: "}",
69
83
  Items: items,
70
84
  Space: true,
71
- AddComma: true,
72
- })
85
+ AddComma: ctx.allowsEs5TrailingComma(),
86
+ }), covered
73
87
  }
74
88
 
75
89
  // printImportDeclaration renders the surrounding `import … from "x";`.
@@ -79,13 +93,15 @@ func printNamedExports(ctx *PrintContext, node *shimast.Node) Doc {
79
93
  //
80
94
  // The dispatcher hands off to the per-clause printers; the top-level
81
95
  // frame stitches them together with the keywords and `from` token.
82
- func printImportDeclaration(ctx *PrintContext, node *shimast.Node) Doc {
96
+ //
97
+ // The second return value is the `covered` flag: see PrintNode.
98
+ func printImportDeclaration(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
83
99
  if node == nil {
84
- return Doc{}
100
+ return Doc{}, true
85
101
  }
86
102
  imp := node.AsImportDeclaration()
87
103
  if imp == nil {
88
- return verbatim(ctx, node)
104
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
89
105
  }
90
106
  // If the declaration uses anything other than a vanilla
91
107
  // `import { ... } from "x"` shape (default specifier, namespace
@@ -93,20 +109,26 @@ func printImportDeclaration(ctx *PrintContext, node *shimast.Node) Doc {
93
109
  // canonical reflow target is the named-import body.
94
110
  clause := imp.ImportClause
95
111
  if clause == nil {
96
- return verbatim(ctx, node)
112
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
97
113
  }
98
114
  clauseData := clause.AsImportClause()
99
115
  if clauseData == nil || clauseData.NamedBindings == nil {
100
- return verbatim(ctx, node)
116
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
101
117
  }
102
118
  if clauseData.NamedBindings.Kind != shimast.KindNamedImports {
103
119
  // Namespace imports (`import * as ns from "x"`) have no
104
120
  // reflow surface; leave them alone.
105
- return verbatim(ctx, node)
121
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
106
122
  }
107
123
  if clause.Name() != nil {
108
124
  // `import Default, { … } from "x"` — keep verbatim for v1.
109
- return verbatim(ctx, node)
125
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
126
+ }
127
+ // AttributeClause (`with { ... }` / `assert { ... }`) lives after
128
+ // the module specifier. Fall back to verbatim when present so we
129
+ // don't drop attributes silently.
130
+ if imp.Attributes != nil {
131
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
110
132
  }
111
133
 
112
134
  // Bracketed clause prefix: `import ` (and optional `type `).
@@ -114,8 +136,9 @@ func printImportDeclaration(ctx *PrintContext, node *shimast.Node) Doc {
114
136
  if clause.IsTypeOnly() {
115
137
  prefix = "import type "
116
138
  }
117
- named, _ := PrintNode(ctx, clauseData.NamedBindings)
139
+ named, covered := PrintNode(ctx, clauseData.NamedBindings)
118
140
  moduleSpec := verbatim(ctx, imp.ModuleSpecifier)
141
+ covered = covered && !nodeSpansMultipleLines(ctx, imp.ModuleSpecifier)
119
142
  parts := []Doc{Text(prefix), named, Text(" from "), moduleSpec}
120
143
  if sourceHasStatementTerminator(ctx.Source, node.End()) {
121
144
  // Preserve the user's terminator decision. Emitting `;`
@@ -124,13 +147,7 @@ func printImportDeclaration(ctx *PrintContext, node *shimast.Node) Doc {
124
147
  // `;;` — `format/semi` owns terminator placement.
125
148
  parts = append(parts, Text(";"))
126
149
  }
127
- // AttributeClause (`with { ... }` / `assert { ... }`) lives after
128
- // the module specifier. Fall back to verbatim when present so we
129
- // don't drop attributes silently.
130
- if imp.Attributes != nil {
131
- return verbatim(ctx, node)
132
- }
133
- return Concat(parts...)
150
+ return Concat(parts...), covered
134
151
  }
135
152
 
136
153
  // sourceHasStatementTerminator reports whether the last non-trivia
@@ -27,15 +27,53 @@ type listShape struct {
27
27
  Items []Doc
28
28
  Space bool // emit a space after OPEN / before CLOSE in flat mode
29
29
  AddComma bool // emit a trailing comma in broken mode
30
+ // HugLast keeps the final item attached to the parens instead of
31
+ // exploding the whole list. It is set by the call/new argument
32
+ // printer when the last argument is a callback or object literal —
33
+ // see printArgList. When true, the last item is emitted directly
34
+ // against OPEN…CLOSE with no leading/trailing soft break, so a
35
+ // multi-line callback body does not force every preceding argument
36
+ // onto its own line.
37
+ HugLast bool
38
+ // ForceBreak commits the list to its broken, one-item-per-line shape
39
+ // even when it would fit flat. The object-literal printer sets it to
40
+ // mirror Prettier's objectWrap:"preserve" — an object the source
41
+ // wrote with a newline after `{` stays expanded.
42
+ ForceBreak bool
30
43
  }
31
44
 
32
45
  // printList renders the list shape as a Doc tree. Empty lists collapse
33
- // to `OPENCLOSE`. Single-element lists still wrap so trailing-comma
34
- // logic stays uniform.
46
+ // to `OPENCLOSE`. A HugLast list becomes a ConditionalGroup of up to
47
+ // three shapes; every other list is the plain fit-or-break Group.
35
48
  func printList(ctx *PrintContext, shape listShape) Doc {
36
49
  if len(shape.Items) == 0 {
37
50
  return Text(shape.OpenTok + shape.CloseTok)
38
51
  }
52
+ plain := printListPlain(ctx, shape)
53
+ if !shape.HugLast {
54
+ return plain
55
+ }
56
+ // A HugLast list offers the engine up to three shapes, in preference
57
+ // order:
58
+ // 1. allFlat — every item on one line, chosen when it fits;
59
+ // 2. hugged — leading items inline, the final callback or object
60
+ // committed to its multi-line shape, chosen when its opening
61
+ // line fits but the all-flat form does not;
62
+ // 3. plain — every item exploded onto its own indented line.
63
+ // The all-flat option is dropped when the list cannot render flat
64
+ // (a block-bodied callback argument carries hard line breaks).
65
+ hugged := printListHuggingLast(ctx, shape)
66
+ if allFlat, ok := flatten(plain); ok {
67
+ return ConditionalGroup(allFlat, hugged, plain)
68
+ }
69
+ return ConditionalGroup(hugged, plain)
70
+ }
71
+
72
+ // printListPlain renders the open-comma-close list as a single
73
+ // fit-or-break Group: flat (`OPEN a, b CLOSE`) when it fits the width
74
+ // budget, one item per indented line — with an optional trailing
75
+ // comma — when it does not.
76
+ func printListPlain(ctx *PrintContext, shape listShape) Doc {
39
77
  sep := Concat(Text(","), Line())
40
78
  body := Join(sep, shape.Items)
41
79
 
@@ -53,12 +91,55 @@ func printList(ctx *PrintContext, shape listShape) Doc {
53
91
  closeTok := Text(shape.CloseTok)
54
92
 
55
93
  // In flat mode: OPEN [pad] body [pad] CLOSE
56
- // In broken mode: OPEN \n body, \n CLOSE — using Softline so the
57
- // flat form collapses cleanly.
94
+ // In broken mode: OPEN \n body, \n CLOSE.
58
95
  leadingSep := IfBreak(Hardline(), flatPad)
59
96
  trailingSep := IfBreak(Hardline(), flatPad)
60
97
 
61
98
  bodyBlock := Indent(ctx.indentUnit(), leadingSep, body, trailing)
62
99
  doc := Concat(openTok, bodyBlock, trailingSep, closeTok)
63
- return Group(doc)
100
+ group := Group(doc)
101
+ // ForceBreak (object-literal newline preservation) commits the group
102
+ // to its broken shape regardless of fit.
103
+ group.Break = shape.ForceBreak
104
+ return group
105
+ }
106
+
107
+ // printListHuggingLast renders the "last-argument hugging" shape that
108
+ // Prettier uses for `foo(a, b, () => { … })`: the leading items flow
109
+ // comma-separated and the final item stays attached to the closing
110
+ // paren instead of being pushed onto its own indented line.
111
+ //
112
+ // hugged: OPEN a, b, OPEN-of-last … CLOSE-of-last CLOSE
113
+ //
114
+ // The result is a plain Concat, not a Group: the hugged last argument
115
+ // is a callback or object literal whose own printer carries the
116
+ // fit-or-break decision for its body, so wrapping here would let a
117
+ // multi-line body force the `a, b,` prefix to break. printList offers
118
+ // this Concat as a ConditionalGroup option; when its opening line would
119
+ // overflow printWidth the engine falls back to the plain exploded list.
120
+ //
121
+ // The hugged final item is forced broken — via forceBreakFirstGroup —
122
+ // so the hugged option is genuinely multi-line and distinct from the
123
+ // all-flat option. A flat hugged object would otherwise be
124
+ // byte-identical to all-flat yet escape its width check. The break
125
+ // reaches the first Group in the item's subtree, so an object or array
126
+ // nested inside an arrow body (`(x) => ({ … })`) breaks too.
127
+ func printListHuggingLast(ctx *PrintContext, shape listShape) Doc {
128
+ last := shape.Items[len(shape.Items)-1]
129
+ lead := shape.Items[:len(shape.Items)-1]
130
+ // Force the hugged item's first Group broken only when the item has
131
+ // no hard line breaks of its own. A block-bodied callback already
132
+ // renders multi-line through its block's hard breaks; descending into
133
+ // it with forceBreakFirstGroup would instead force some unrelated
134
+ // nested Group — a plain statement's call-argument list — broken.
135
+ if _, flat := flatten(last); flat {
136
+ last, _ = forceBreakFirstGroup(last)
137
+ }
138
+
139
+ parts := []Doc{Text(shape.OpenTok)}
140
+ for _, item := range lead {
141
+ parts = append(parts, item, Text(", "))
142
+ }
143
+ parts = append(parts, last, Text(shape.CloseTok))
144
+ return Concat(parts...)
64
145
  }
@@ -2,6 +2,7 @@ package linthost
2
2
 
3
3
  import (
4
4
  shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
5
6
  )
6
7
 
7
8
  // printObjectLiteral renders an ObjectLiteralExpression with width-aware
@@ -22,30 +23,74 @@ import (
22
23
  // The flat form uses a single space inside the braces, matching
23
24
  // Prettier's `bracketSpacing: true` default. Empty object literals
24
25
  // collapse to `{}` with no inner space, matching every formatter.
25
- func printObjectLiteral(ctx *PrintContext, node *shimast.Node) Doc {
26
+ //
27
+ // The second return value is the `covered` flag: see PrintNode. It is
28
+ // the AND of every property's coverage — one multi-line verbatim
29
+ // member taints the whole literal.
30
+ func printObjectLiteral(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
26
31
  if node == nil {
27
- return Doc{}
32
+ return Doc{}, true
28
33
  }
29
34
  obj := node.AsObjectLiteralExpression()
30
35
  if obj == nil || obj.Properties == nil {
31
- return verbatim(ctx, node)
36
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
32
37
  }
33
38
  items := make([]Doc, 0, len(obj.Properties.Nodes))
39
+ covered := true
34
40
  for _, prop := range obj.Properties.Nodes {
35
41
  if prop == nil {
36
42
  // A nil child entry would render as an empty Doc and surface
37
43
  // as `a, , b` in the output. Bail to verbatim so the source
38
44
  // bytes round-trip unchanged.
39
- return verbatim(ctx, node)
45
+ return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
40
46
  }
41
- doc, _ := PrintNode(ctx, prop)
47
+ doc, childCovered := PrintNode(ctx, prop)
48
+ covered = covered && childCovered
42
49
  items = append(items, doc)
43
50
  }
51
+ // objectWrap:"preserve" — keep a non-empty object expanded when the
52
+ // source wrote a newline after `{`. An empty literal has no first
53
+ // property to anchor the check and never preserves.
54
+ forceBreak := false
55
+ if len(obj.Properties.Nodes) > 0 {
56
+ forceBreak = objectHasNewlineAfterBrace(ctx.Source, node, obj.Properties.Nodes[0])
57
+ }
58
+ // AddComma honors `format.trailingComma`: object literals accept
59
+ // trailing commas in ES5 so both "all" and "es5" keep them; only
60
+ // "none" suppresses. Pairs with the call/array branches so the printer
61
+ // never disagrees with the trailing-comma rule on the same setting.
44
62
  return printList(ctx, listShape{
45
- OpenTok: "{",
46
- CloseTok: "}",
47
- Items: items,
48
- Space: true,
49
- AddComma: true,
50
- })
63
+ OpenTok: "{",
64
+ CloseTok: "}",
65
+ Items: items,
66
+ Space: true,
67
+ AddComma: ctx.allowsEs5TrailingComma(),
68
+ ForceBreak: forceBreak,
69
+ }), covered
70
+ }
71
+
72
+ // objectHasNewlineAfterBrace reports whether the source places a
73
+ // newline between the object literal's `{` and its first property.
74
+ //
75
+ // Prettier's objectWrap:"preserve" default keeps such an object
76
+ // expanded even when it would fit flat, treating the author's line
77
+ // break as intentional structure. format/print-width mirrors that:
78
+ // without it a deliberately multi-line object that happens to fit
79
+ // would be silently collapsed onto one line, which Prettier never
80
+ // does.
81
+ func objectHasNewlineAfterBrace(src string, node *shimast.Node, firstProp *shimast.Node) bool {
82
+ if node == nil || firstProp == nil {
83
+ return false
84
+ }
85
+ brace := shimscanner.SkipTrivia(src, node.Pos())
86
+ propStart := shimscanner.SkipTrivia(src, firstProp.Pos())
87
+ if brace < 0 || propStart <= brace || propStart > len(src) {
88
+ return false
89
+ }
90
+ for i := brace; i < propStart; i++ {
91
+ if src[i] == '\n' {
92
+ return true
93
+ }
94
+ }
95
+ return false
51
96
  }
@@ -25,8 +25,11 @@ func (noSparseArrays) Check(ctx *Context, node *shimast.Node) {
25
25
  }
26
26
 
27
27
  // no-array-constructor: forbid `new Array(0)` / `Array(1, 2, 3)` (use
28
- // array literals). The 1-arg numeric form is also banned because its
29
- // behavior depends on the runtime see ESLint defaults.
28
+ // array literals). The single-argument form is intentionally excluded from
29
+ // the ban: `Array(n)` is commonly used to pre-allocate a sparse array by
30
+ // length, and banning it would generate noise on existing idiomatic code.
31
+ // The typed form `new Array<string>()` is also excluded — a type argument
32
+ // signals intentional use and the caller takes responsibility.
30
33
  // https://eslint.org/docs/latest/rules/no-array-constructor
31
34
  type noArrayConstructor struct{}
32
35
 
@@ -12,8 +12,9 @@ func (noDebugger) Check(ctx *Context, node *shimast.Node) {
12
12
  ctx.Report(node, "Unexpected `debugger` statement.")
13
13
  }
14
14
 
15
- // no-with: forbid `with` statements (already disallowed in strict mode,
16
- // but lint catches it before the parse error).
15
+ // no-with: forbid `with` statements. `with` is disallowed in strict mode
16
+ // at the parser level, but TypeScript source files may not use strict mode
17
+ // explicitly; the lint rule catches it uniformly regardless of mode.
17
18
  // https://eslint.org/docs/latest/rules/no-with
18
19
  type noWith struct{}
19
20
 
@@ -49,18 +49,17 @@ func (noDupeKeys) Check(ctx *Context, node *shimast.Node) {
49
49
  if obj == nil || obj.Properties == nil {
50
50
  return
51
51
  }
52
- seen := make(map[string]*shimast.Node, len(obj.Properties.Nodes))
52
+ seen := make(map[string]bool, len(obj.Properties.Nodes))
53
53
  for _, prop := range obj.Properties.Nodes {
54
54
  key := propertyKey(ctx.File, prop)
55
55
  if key == "" {
56
56
  continue
57
57
  }
58
- if first, ok := seen[key]; ok {
58
+ if seen[key] {
59
59
  ctx.Report(prop, "Duplicate key '"+key+"'.")
60
- _ = first
61
60
  continue
62
61
  }
63
- seen[key] = prop
62
+ seen[key] = true
64
63
  }
65
64
  }
66
65
 
@@ -153,6 +152,10 @@ func propertyKey(file *shimast.SourceFile, prop *shimast.Node) string {
153
152
  return ""
154
153
  }
155
154
 
155
+ // staticPropertyKey extracts a comparable string key from a property name node.
156
+ // Identifiers, string/numeric literals, and computed names with a literal
157
+ // payload all produce a stable string. Other computed names (dynamic
158
+ // expressions) return "" so the caller skips them without false positives.
156
159
  func staticPropertyKey(file *shimast.SourceFile, name *shimast.Node) string {
157
160
  if name == nil {
158
161
  return ""
@@ -83,8 +83,9 @@ func (noEmptyPattern) Check(ctx *Context, node *shimast.Node) {
83
83
  }
84
84
  }
85
85
 
86
- // isFunctionLikeKind reports whether the node represents a function-like
87
- // host whose body is the relevant scope for the empty check.
86
+ // isFunctionLikeKind reports whether n represents a function-like AST node
87
+ // (declaration, expression, arrow, method, accessor, or constructor). Used
88
+ // to detect scope boundaries by rules_empty, rules_finally, and others.
88
89
  func isFunctionLikeKind(n *shimast.Node) bool {
89
90
  if n == nil {
90
91
  return false
@@ -52,12 +52,12 @@ func (noUselessEscape) Check(ctx *Context, node *shimast.Node) {
52
52
  // single-char escape whitelist matches ESLint per-context.
53
53
  switch node.Kind {
54
54
  case shimast.KindStringLiteral:
55
- reportStringEscapes(ctx, raw, pos, stringValidEscapes)
55
+ reportStringEscapes(ctx, raw, pos, stringValidEscapes, false)
56
56
  case shimast.KindNoSubstitutionTemplateLiteral,
57
57
  shimast.KindTemplateHead,
58
58
  shimast.KindTemplateMiddle,
59
59
  shimast.KindTemplateTail:
60
- reportStringEscapes(ctx, raw, pos, templateValidEscapes)
60
+ reportStringEscapes(ctx, raw, pos, templateValidEscapes, true)
61
61
  case shimast.KindRegularExpressionLiteral:
62
62
  reportRegexEscapes(ctx, raw, pos)
63
63
  }
@@ -73,7 +73,22 @@ const templateValidEscapes = "`'\"\\bfnrtv0xuU$\n\r"
73
73
  // — it never deletes a backslash whose meaning could be context-sensitive).
74
74
  const regexValidEscapes = "^$\\.*+?()[]{}|/-\n\r"
75
75
 
76
- func reportStringEscapes(ctx *Context, raw string, base int, whitelist string) {
76
+ // reportStringEscapes walks the raw source bytes of a string or template
77
+ // literal and reports each backslash whose following character is not in
78
+ // `whitelist`. `base` is the source offset of `raw[0]` so reported ranges
79
+ // translate to absolute file positions. The function issues an autofix
80
+ // (delete the backslash) for ASCII escapes; multi-byte sequences are
81
+ // reported without a fix to avoid corrupting UTF-8.
82
+ //
83
+ // `isTemplate` is true for `NoSubstitutionTemplateLiteral` and
84
+ // `TemplateHead`/`Middle`/`Tail` payloads. Inside a template, `\${` escapes
85
+ // the interpolation opener: stripping the backslash from `\${expr}` would
86
+ // either turn the literal text into an interpolation (corrupting the
87
+ // program) or — when the surrounding template already contains a real
88
+ // `${expr}` — produce TS syntax that no longer parses. The explicit guard
89
+ // here pins that exception so future tightening of `templateValidEscapes`
90
+ // cannot regress the corruption.
91
+ func reportStringEscapes(ctx *Context, raw string, base int, whitelist string, isTemplate bool) {
77
92
  if len(raw) < 2 {
78
93
  return
79
94
  }
@@ -95,6 +110,14 @@ func reportStringEscapes(ctx *Context, raw string, base int, whitelist string) {
95
110
  return
96
111
  }
97
112
  next := raw[i+1]
113
+ // Template-literal exception: `\${` escapes the interpolation
114
+ // opener. Without the backslash the next two bytes would either
115
+ // start an interpolation or trigger a parse error, so the escape is
116
+ // load-bearing even though `\$` looks redundant in isolation.
117
+ if isTemplate && next == '$' && i+2 < len(raw) && raw[i+2] == '{' {
118
+ i++ // consume the `$` so the `{` is not re-examined as a fresh char.
119
+ continue
120
+ }
98
121
  if isUselessStringEscape(next, whitelist) {
99
122
  // Only emit a fix when both surrounding bytes are plain ASCII so
100
123
  // deleting one byte cannot corrupt a multi-byte sequence.
@@ -117,6 +140,9 @@ func reportStringEscapes(ctx *Context, raw string, base int, whitelist string) {
117
140
  }
118
141
  }
119
142
 
143
+ // reportRegexEscapes walks the pattern body of a regex literal and reports
144
+ // backslashes that escape non-special characters. `base` is the source offset
145
+ // of `raw[0]`. Character-class context (`[…]`) widens the legal set slightly.
120
146
  func reportRegexEscapes(ctx *Context, raw string, base int) {
121
147
  if len(raw) < 3 || raw[0] != '/' {
122
148
  return
@@ -166,6 +192,9 @@ func reportRegexEscapes(ctx *Context, raw string, base int) {
166
192
  }
167
193
  }
168
194
 
195
+ // isUselessStringEscape reports whether a backslash before `ch` is redundant
196
+ // inside a string or template literal. The `whitelist` contains the characters
197
+ // that are valid escape targets for the specific literal kind (string vs template).
169
198
  func isUselessStringEscape(ch byte, whitelist string) bool {
170
199
  // Whitespace + control chars are escape sequences too.
171
200
  if ch < 0x20 {
@@ -181,6 +210,9 @@ func isUselessStringEscape(ch byte, whitelist string) bool {
181
210
  return true
182
211
  }
183
212
 
213
+ // isUselessRegexEscape reports whether a backslash before `ch` is redundant
214
+ // in a regex pattern. `inClass` is true when the escape occurs inside a `[…]`
215
+ // character class, which widens the set of meaningful escapes.
184
216
  func isUselessRegexEscape(ch byte, inClass bool) bool {
185
217
  if ch < 0x20 {
186
218
  return false
@@ -36,6 +36,9 @@ func (noScriptURL) Check(ctx *Context, node *shimast.Node) {
36
36
  }
37
37
  }
38
38
 
39
+ // isJavaScriptURL reports whether text starts with the "javascript:" scheme
40
+ // (case-insensitively, ASCII only). The manual loop avoids importing strings
41
+ // just for strings.ToLower and keeps the hot path allocation-free.
39
42
  func isJavaScriptURL(text string) bool {
40
43
  const prefix = "javascript:"
41
44
  if len(text) < len(prefix) {
@@ -25,6 +25,15 @@ func (noUnsafeFinally) Check(ctx *Context, node *shimast.Node) {
25
25
  ctx.Report(node, "Unsafe usage of "+keyword+".")
26
26
  }
27
27
 
28
+ // walkToFinally walks the parent chain from node upward looking for a
29
+ // `finally` block. It returns the Block node that IS the finally clause when
30
+ // found, or nil when the search exits through a function boundary (making any
31
+ // control-flow transfer target something outside the finally block) or when no
32
+ // finally block is found at all.
33
+ //
34
+ // A `break` or `continue` that targets an inner loop or switch INSIDE the
35
+ // finally block is safe — it does not escape the finally — so the walk stops
36
+ // early and returns nil in that case.
28
37
  func walkToFinally(node *shimast.Node) *shimast.Node {
29
38
  cur := node.Parent
30
39
  for cur != nil {
@@ -58,6 +67,8 @@ func walkToFinally(node *shimast.Node) *shimast.Node {
58
67
  return nil
59
68
  }
60
69
 
70
+ // keywordOfControl returns the control-flow keyword string for the given
71
+ // statement node, used to build the diagnostic message text.
61
72
  func keywordOfControl(node *shimast.Node) string {
62
73
  switch node.Kind {
63
74
  case shimast.KindReturnStatement:
@@ -136,6 +136,10 @@ func findJSDocBlocks(src string) []jsdocBlock {
136
136
  return out
137
137
  }
138
138
 
139
+ // rewriteJSDocTags scans one JSDoc block and emits a fix for each tag that has
140
+ // a canonical synonym. Tags preceded by a byte other than `*`, whitespace, or a
141
+ // newline are treated as inline `@foo` references (not top-level tags) and are
142
+ // left alone. Tags inside `@example` bodies are also skipped.
139
143
  func rewriteJSDocTags(ctx *Context, src string, block jsdocBlock, synonyms map[string]string) {
140
144
  for i := block.bodyStart; i < block.bodyEnd; i++ {
141
145
  if src[i] != '@' {
@@ -205,6 +209,9 @@ func endOfJSDocExampleBody(src string, block jsdocBlock, start int) int {
205
209
  return block.bodyEnd
206
210
  }
207
211
 
212
+ // isJSDocTagByte reports whether `b` is an ASCII letter that may appear in a
213
+ // JSDoc tag name. Tags are purely alphabetic: digits, hyphens, and underscores
214
+ // terminate a tag name.
208
215
  func isJSDocTagByte(b byte) bool {
209
216
  return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
210
217
  }