@ttsc/lint 0.10.2 → 0.11.0-dev.20260517
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 +172 -33
- package/lib/defineConfig.d.ts +55 -0
- package/lib/defineConfig.js +39 -0
- package/lib/defineConfig.js.map +1 -0
- package/lib/index.d.ts +25 -1
- package/lib/index.js +622 -2
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPlugin.d.ts +53 -0
- package/lib/structures/ITtscLintPlugin.js +3 -0
- package/lib/structures/ITtscLintPlugin.js.map +1 -0
- package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
- package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
- package/lib/structures/ITtscLintPluginMeta.js +3 -0
- package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
- package/lib/structures/PluginRuleNames.d.ts +14 -0
- package/lib/structures/PluginRuleNames.js +3 -0
- package/lib/structures/PluginRuleNames.js.map +1 -0
- package/lib/structures/TtscLintConfig.d.ts +24 -8
- package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
- package/lib/structures/TtscLintConfigEntry.js +3 -0
- package/lib/structures/TtscLintConfigEntry.js.map +1 -0
- package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
- package/lib/structures/TtscLintFormatConfig.js +3 -0
- package/lib/structures/TtscLintFormatConfig.js.map +1 -0
- package/lib/structures/TtscLintPlugins.d.ts +9 -0
- package/lib/structures/TtscLintPlugins.js +3 -0
- package/lib/structures/TtscLintPlugins.js.map +1 -0
- package/lib/structures/TtscLintRule.d.ts +1 -1
- package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
- package/lib/structures/TtscLintRuleEntry.js +3 -0
- package/lib/structures/TtscLintRuleEntry.js.map +1 -0
- package/lib/structures/TtscLintRuleMap.d.ts +44 -0
- package/lib/structures/TtscLintRuleMap.js +3 -0
- package/lib/structures/TtscLintRuleMap.js.map +1 -0
- package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
- package/lib/structures/TtscLintRuleOptions.js +3 -0
- package/lib/structures/TtscLintRuleOptions.js.map +1 -0
- package/lib/structures/defaultFormat.d.ts +31 -0
- package/lib/structures/defaultFormat.js +35 -0
- package/lib/structures/defaultFormat.js.map +1 -0
- package/lib/structures/index.d.ts +10 -0
- package/lib/structures/index.js +10 -0
- package/lib/structures/index.js.map +1 -1
- package/{plugin → linthost}/ast_helpers.go +92 -1
- package/{plugin → linthost}/compile.go +1 -1
- package/{plugin → linthost}/config.go +583 -66
- package/linthost/config_format.go +360 -0
- package/linthost/contrib_adapter.go +134 -0
- package/{plugin → linthost}/directives.go +1 -1
- package/linthost/dispatch.go +62 -0
- package/{plugin → linthost}/engine.go +156 -11
- package/{plugin → linthost}/eslint_runtime.go +76 -2
- package/linthost/fix.go +249 -0
- package/linthost/format.go +107 -0
- package/{plugin → linthost}/host.go +1 -1
- package/linthost/print_dispatch.go +118 -0
- package/linthost/print_doc.go +168 -0
- package/linthost/print_engine.go +343 -0
- package/linthost/print_nodes_array.go +46 -0
- package/linthost/print_nodes_call.go +180 -0
- package/linthost/print_nodes_imports.go +171 -0
- package/linthost/print_nodes_list.go +64 -0
- package/linthost/print_nodes_object.go +51 -0
- package/{plugin → linthost}/rules_arrays.go +1 -1
- package/{plugin → linthost}/rules_console.go +1 -1
- package/{plugin → linthost}/rules_debugger.go +1 -1
- package/{plugin → linthost}/rules_dupes.go +1 -1
- package/{plugin → linthost}/rules_empty.go +1 -1
- package/linthost/rules_escape.go +238 -0
- package/{plugin → linthost}/rules_eval.go +1 -1
- package/{plugin → linthost}/rules_finally.go +1 -1
- package/linthost/rules_format_jsdoc.go +230 -0
- package/linthost/rules_format_print_width.go +332 -0
- package/linthost/rules_format_quotes.go +219 -0
- package/linthost/rules_format_semi.go +190 -0
- package/linthost/rules_format_sort_imports.go +462 -0
- package/linthost/rules_format_trailing_comma.go +460 -0
- package/{plugin → linthost}/rules_gap.go +101 -6
- package/linthost/rules_imports.go +101 -0
- package/{plugin → linthost}/rules_logic.go +55 -3
- package/{plugin → linthost}/rules_loops.go +1 -1
- package/{plugin → linthost}/rules_misc.go +1 -1
- package/linthost/rules_params.go +64 -0
- package/{plugin → linthost}/rules_problems.go +10 -2
- package/linthost/rules_promise.go +111 -0
- package/{plugin → linthost}/rules_protos.go +1 -1
- package/{plugin → linthost}/rules_self.go +1 -1
- package/{plugin → linthost}/rules_strings.go +1 -1
- package/{plugin → linthost}/rules_suggestions.go +51 -9
- package/{plugin → linthost}/rules_throw.go +1 -1
- package/{plugin → linthost}/rules_ts.go +12 -2
- package/{plugin → linthost}/rules_ts_extra.go +90 -4
- package/{plugin → linthost}/rules_var.go +36 -5
- package/package.json +8 -6
- package/plugin/main.go +14 -27
- package/rule/astutil/astutil.go +148 -0
- package/rule/rule.go +291 -0
- package/src/defineConfig.ts +69 -0
- package/src/index.ts +807 -3
- package/src/structures/ITtscLintPlugin.ts +56 -0
- package/src/structures/ITtscLintPluginConfig.ts +86 -5
- package/src/structures/ITtscLintPluginMeta.ts +14 -0
- package/src/structures/PluginRuleNames.ts +19 -0
- package/src/structures/TtscLintConfig.ts +27 -8
- package/src/structures/TtscLintConfigEntry.ts +50 -0
- package/src/structures/TtscLintFormatConfig.ts +169 -0
- package/src/structures/TtscLintPlugins.ts +10 -0
- package/src/structures/TtscLintRule.ts +14 -1
- package/src/structures/TtscLintRuleEntry.ts +28 -0
- package/src/structures/TtscLintRuleMap.ts +52 -0
- package/src/structures/TtscLintRuleOptions.ts +162 -0
- package/src/structures/defaultFormat.ts +33 -0
- package/src/structures/index.ts +10 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
// printNamedImports renders `{ a, b, c }` inside an import declaration.
|
|
8
|
+
// Each specifier flows through PrintNode, so `a as b` shapes that the
|
|
9
|
+
// dispatcher does not understand yet still print verbatim.
|
|
10
|
+
//
|
|
11
|
+
// Flat: `{ a, b, c }`
|
|
12
|
+
//
|
|
13
|
+
// Broken: `{
|
|
14
|
+
// a,
|
|
15
|
+
// b,
|
|
16
|
+
// c,
|
|
17
|
+
// }`
|
|
18
|
+
//
|
|
19
|
+
// The open/close braces always carry a single space inside in flat
|
|
20
|
+
// mode, matching Prettier's `bracketSpacing: true` default for import
|
|
21
|
+
// declarations (which is hard-coded; Prettier ignores `bracketSpacing`
|
|
22
|
+
// for imports). Empty named imports `{}` collapse cleanly.
|
|
23
|
+
func printNamedImports(ctx *PrintContext, node *shimast.Node) Doc {
|
|
24
|
+
if node == nil {
|
|
25
|
+
return Doc{}
|
|
26
|
+
}
|
|
27
|
+
ni := node.AsNamedImports()
|
|
28
|
+
if ni == nil || ni.Elements == nil {
|
|
29
|
+
return verbatim(ctx, node)
|
|
30
|
+
}
|
|
31
|
+
items := make([]Doc, 0, len(ni.Elements.Nodes))
|
|
32
|
+
for _, spec := range ni.Elements.Nodes {
|
|
33
|
+
if spec == nil {
|
|
34
|
+
return verbatim(ctx, node)
|
|
35
|
+
}
|
|
36
|
+
doc, _ := PrintNode(ctx, spec)
|
|
37
|
+
items = append(items, doc)
|
|
38
|
+
}
|
|
39
|
+
return printList(ctx, listShape{
|
|
40
|
+
OpenTok: "{",
|
|
41
|
+
CloseTok: "}",
|
|
42
|
+
Items: items,
|
|
43
|
+
Space: true,
|
|
44
|
+
AddComma: true,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// printNamedExports renders `export { a, b }`. The shape is identical
|
|
49
|
+
// to NamedImports; only the surrounding declaration differs.
|
|
50
|
+
func printNamedExports(ctx *PrintContext, node *shimast.Node) Doc {
|
|
51
|
+
if node == nil {
|
|
52
|
+
return Doc{}
|
|
53
|
+
}
|
|
54
|
+
ne := node.AsNamedExports()
|
|
55
|
+
if ne == nil || ne.Elements == nil {
|
|
56
|
+
return verbatim(ctx, node)
|
|
57
|
+
}
|
|
58
|
+
items := make([]Doc, 0, len(ne.Elements.Nodes))
|
|
59
|
+
for _, spec := range ne.Elements.Nodes {
|
|
60
|
+
if spec == nil {
|
|
61
|
+
return verbatim(ctx, node)
|
|
62
|
+
}
|
|
63
|
+
doc, _ := PrintNode(ctx, spec)
|
|
64
|
+
items = append(items, doc)
|
|
65
|
+
}
|
|
66
|
+
return printList(ctx, listShape{
|
|
67
|
+
OpenTok: "{",
|
|
68
|
+
CloseTok: "}",
|
|
69
|
+
Items: items,
|
|
70
|
+
Space: true,
|
|
71
|
+
AddComma: true,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// printImportDeclaration renders the surrounding `import … from "x";`.
|
|
76
|
+
// The default specifier, namespace import, named-imports clause, and
|
|
77
|
+
// module specifier are all individually dispatched so the named-imports
|
|
78
|
+
// portion can reflow while the rest stays verbatim.
|
|
79
|
+
//
|
|
80
|
+
// The dispatcher hands off to the per-clause printers; the top-level
|
|
81
|
+
// frame stitches them together with the keywords and `from` token.
|
|
82
|
+
func printImportDeclaration(ctx *PrintContext, node *shimast.Node) Doc {
|
|
83
|
+
if node == nil {
|
|
84
|
+
return Doc{}
|
|
85
|
+
}
|
|
86
|
+
imp := node.AsImportDeclaration()
|
|
87
|
+
if imp == nil {
|
|
88
|
+
return verbatim(ctx, node)
|
|
89
|
+
}
|
|
90
|
+
// If the declaration uses anything other than a vanilla
|
|
91
|
+
// `import { ... } from "x"` shape (default specifier, namespace
|
|
92
|
+
// alias, attributes clause, etc.), fall back to verbatim. The
|
|
93
|
+
// canonical reflow target is the named-import body.
|
|
94
|
+
clause := imp.ImportClause
|
|
95
|
+
if clause == nil {
|
|
96
|
+
return verbatim(ctx, node)
|
|
97
|
+
}
|
|
98
|
+
clauseData := clause.AsImportClause()
|
|
99
|
+
if clauseData == nil || clauseData.NamedBindings == nil {
|
|
100
|
+
return verbatim(ctx, node)
|
|
101
|
+
}
|
|
102
|
+
if clauseData.NamedBindings.Kind != shimast.KindNamedImports {
|
|
103
|
+
// Namespace imports (`import * as ns from "x"`) have no
|
|
104
|
+
// reflow surface; leave them alone.
|
|
105
|
+
return verbatim(ctx, node)
|
|
106
|
+
}
|
|
107
|
+
if clause.Name() != nil {
|
|
108
|
+
// `import Default, { … } from "x"` — keep verbatim for v1.
|
|
109
|
+
return verbatim(ctx, node)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Bracketed clause prefix: `import ` (and optional `type `).
|
|
113
|
+
prefix := "import "
|
|
114
|
+
if clause.IsTypeOnly() {
|
|
115
|
+
prefix = "import type "
|
|
116
|
+
}
|
|
117
|
+
named, _ := PrintNode(ctx, clauseData.NamedBindings)
|
|
118
|
+
moduleSpec := verbatim(ctx, imp.ModuleSpecifier)
|
|
119
|
+
parts := []Doc{Text(prefix), named, Text(" from "), moduleSpec}
|
|
120
|
+
if sourceHasStatementTerminator(ctx.Source, node.End()) {
|
|
121
|
+
// Preserve the user's terminator decision. Emitting `;`
|
|
122
|
+
// unconditionally would collide with `format/semi`'s
|
|
123
|
+
// zero-width insert on the same cascade pass and produce
|
|
124
|
+
// `;;` — `format/semi` owns terminator placement.
|
|
125
|
+
parts = append(parts, Text(";"))
|
|
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)
|
|
132
|
+
}
|
|
133
|
+
return Concat(parts...)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// sourceHasStatementTerminator reports whether the last non-trivia
|
|
137
|
+
// byte at or before `end` is `;`. TypeScript-Go's parser folds an
|
|
138
|
+
// optional trailing semicolon into the statement's End(), so this
|
|
139
|
+
// check tells the printer whether the user wrote one.
|
|
140
|
+
//
|
|
141
|
+
// Trailing block comments (`import { … } from "x" /* tail */;`) sit
|
|
142
|
+
// between the module specifier and the `;`. The backward walk must
|
|
143
|
+
// step past them; otherwise the helper sees `/` from `*/` and
|
|
144
|
+
// concludes "no terminator".
|
|
145
|
+
func sourceHasStatementTerminator(src string, end int) bool {
|
|
146
|
+
i := end - 1
|
|
147
|
+
for i >= 0 {
|
|
148
|
+
c := src[i]
|
|
149
|
+
if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
|
|
150
|
+
i--
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if c == ';' {
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
// Skip a trailing block comment: walk back past `*/`...`/*`.
|
|
157
|
+
if c == '/' && i-1 >= 0 && src[i-1] == '*' {
|
|
158
|
+
j := i - 2
|
|
159
|
+
for j-1 >= 0 && !(src[j-1] == '/' && src[j] == '*') {
|
|
160
|
+
j--
|
|
161
|
+
}
|
|
162
|
+
if j-1 < 0 {
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
i = j - 2
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
// Shared helpers for comma-separated list printers.
|
|
4
|
+
//
|
|
5
|
+
// Object literals, array literals, call arguments, parameter lists,
|
|
6
|
+
// named imports, named exports, tuple types, type-parameter lists —
|
|
7
|
+
// they all share the same broken/flat shape:
|
|
8
|
+
//
|
|
9
|
+
// flat: OPEN child, child, child CLOSE
|
|
10
|
+
// broken: OPEN
|
|
11
|
+
// child,
|
|
12
|
+
// child,
|
|
13
|
+
// child, // optional trailing comma
|
|
14
|
+
// CLOSE
|
|
15
|
+
//
|
|
16
|
+
// printList captures that shape in one place so every per-node printer
|
|
17
|
+
// can stay tiny.
|
|
18
|
+
//
|
|
19
|
+
// `openTok` and `closeTok` are the literal punctuation strings; the
|
|
20
|
+
// helper does not look them up from the source because that lookup
|
|
21
|
+
// would couple every list to its own scanner. `space` toggles the
|
|
22
|
+
// "single space between OPEN and the first child" used by object
|
|
23
|
+
// literals (`{ a }`) but not arrays (`[a]`).
|
|
24
|
+
type listShape struct {
|
|
25
|
+
OpenTok string
|
|
26
|
+
CloseTok string
|
|
27
|
+
Items []Doc
|
|
28
|
+
Space bool // emit a space after OPEN / before CLOSE in flat mode
|
|
29
|
+
AddComma bool // emit a trailing comma in broken mode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 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.
|
|
35
|
+
func printList(ctx *PrintContext, shape listShape) Doc {
|
|
36
|
+
if len(shape.Items) == 0 {
|
|
37
|
+
return Text(shape.OpenTok + shape.CloseTok)
|
|
38
|
+
}
|
|
39
|
+
sep := Concat(Text(","), Line())
|
|
40
|
+
body := Join(sep, shape.Items)
|
|
41
|
+
|
|
42
|
+
flatPad := Doc{Kind: docNil}
|
|
43
|
+
if shape.Space {
|
|
44
|
+
flatPad = Text(" ")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
trailing := Doc{Kind: docNil}
|
|
48
|
+
if shape.AddComma {
|
|
49
|
+
trailing = IfBreak(Text(","), Doc{Kind: docNil})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
openTok := Text(shape.OpenTok)
|
|
53
|
+
closeTok := Text(shape.CloseTok)
|
|
54
|
+
|
|
55
|
+
// 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.
|
|
58
|
+
leadingSep := IfBreak(Hardline(), flatPad)
|
|
59
|
+
trailingSep := IfBreak(Hardline(), flatPad)
|
|
60
|
+
|
|
61
|
+
bodyBlock := Indent(ctx.indentUnit(), leadingSep, body, trailing)
|
|
62
|
+
doc := Concat(openTok, bodyBlock, trailingSep, closeTok)
|
|
63
|
+
return Group(doc)
|
|
64
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
// printObjectLiteral renders an ObjectLiteralExpression with width-aware
|
|
8
|
+
// fit-or-break. Members are emitted verbatim from the source — only
|
|
9
|
+
// the outer braces and the comma layout participate in reflow. That
|
|
10
|
+
// keeps the per-member behavior compatible with shapes the dispatcher
|
|
11
|
+
// has not yet learned (computed property keys, accessor declarations,
|
|
12
|
+
// spread elements, etc.), while still solving the headline use case:
|
|
13
|
+
// long member lists that need to break across lines.
|
|
14
|
+
//
|
|
15
|
+
// Flat: `{ a: 1, b: 2 }`
|
|
16
|
+
//
|
|
17
|
+
// Broken: `{
|
|
18
|
+
// a: 1,
|
|
19
|
+
// b: 2,
|
|
20
|
+
// }`
|
|
21
|
+
//
|
|
22
|
+
// The flat form uses a single space inside the braces, matching
|
|
23
|
+
// Prettier's `bracketSpacing: true` default. Empty object literals
|
|
24
|
+
// collapse to `{}` with no inner space, matching every formatter.
|
|
25
|
+
func printObjectLiteral(ctx *PrintContext, node *shimast.Node) Doc {
|
|
26
|
+
if node == nil {
|
|
27
|
+
return Doc{}
|
|
28
|
+
}
|
|
29
|
+
obj := node.AsObjectLiteralExpression()
|
|
30
|
+
if obj == nil || obj.Properties == nil {
|
|
31
|
+
return verbatim(ctx, node)
|
|
32
|
+
}
|
|
33
|
+
items := make([]Doc, 0, len(obj.Properties.Nodes))
|
|
34
|
+
for _, prop := range obj.Properties.Nodes {
|
|
35
|
+
if prop == nil {
|
|
36
|
+
// A nil child entry would render as an empty Doc and surface
|
|
37
|
+
// as `a, , b` in the output. Bail to verbatim so the source
|
|
38
|
+
// bytes round-trip unchanged.
|
|
39
|
+
return verbatim(ctx, node)
|
|
40
|
+
}
|
|
41
|
+
doc, _ := PrintNode(ctx, prop)
|
|
42
|
+
items = append(items, doc)
|
|
43
|
+
}
|
|
44
|
+
return printList(ctx, listShape{
|
|
45
|
+
OpenTok: "{",
|
|
46
|
+
CloseTok: "}",
|
|
47
|
+
Items: items,
|
|
48
|
+
Space: true,
|
|
49
|
+
AddComma: true,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// no-useless-escape: flag backslashes that escape characters which do not
|
|
10
|
+
// require escaping. Inside string/template literals the meaningful
|
|
11
|
+
// escapes are limited to a fixed set; everywhere else the backslash is
|
|
12
|
+
// noise from a copy/paste accident or a confused author. ESLint
|
|
13
|
+
// canonical: https://eslint.org/docs/latest/rules/no-useless-escape
|
|
14
|
+
//
|
|
15
|
+
// The rule is `eslint:recommended` and ships a one-byte autofix that
|
|
16
|
+
// deletes the redundant backslash. We mirror that fixer when the byte
|
|
17
|
+
// after the backslash is ASCII; multi-byte UTF-8 sequences are left
|
|
18
|
+
// detection-only because byte-deletion in the middle of a code point
|
|
19
|
+
// would corrupt the file.
|
|
20
|
+
type noUselessEscape struct{}
|
|
21
|
+
|
|
22
|
+
func (noUselessEscape) Name() string { return "no-useless-escape" }
|
|
23
|
+
func (noUselessEscape) Visits() []shimast.Kind {
|
|
24
|
+
return []shimast.Kind{
|
|
25
|
+
shimast.KindStringLiteral,
|
|
26
|
+
shimast.KindNoSubstitutionTemplateLiteral,
|
|
27
|
+
shimast.KindTemplateHead,
|
|
28
|
+
shimast.KindTemplateMiddle,
|
|
29
|
+
shimast.KindTemplateTail,
|
|
30
|
+
shimast.KindRegularExpressionLiteral,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
func (noUselessEscape) Check(ctx *Context, node *shimast.Node) {
|
|
34
|
+
if ctx.File == nil {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
// Tagged templates expose the raw bytes of the template to the tag
|
|
38
|
+
// function (`String.raw`, `dedent`, `gql`, `css`, …), so a backslash
|
|
39
|
+
// that looks redundant to the JS lexer is meaningful at the tag
|
|
40
|
+
// boundary. ESLint canonical skips tagged templates entirely.
|
|
41
|
+
if isInsideTaggedTemplate(node) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
src := ctx.File.Text()
|
|
45
|
+
pos := node.Pos()
|
|
46
|
+
end := node.End()
|
|
47
|
+
if pos < 0 || end > len(src) || pos >= end {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
raw := src[pos:end]
|
|
51
|
+
// Determine quote/scan delimiters based on node kind so the
|
|
52
|
+
// single-char escape whitelist matches ESLint per-context.
|
|
53
|
+
switch node.Kind {
|
|
54
|
+
case shimast.KindStringLiteral:
|
|
55
|
+
reportStringEscapes(ctx, raw, pos, stringValidEscapes)
|
|
56
|
+
case shimast.KindNoSubstitutionTemplateLiteral,
|
|
57
|
+
shimast.KindTemplateHead,
|
|
58
|
+
shimast.KindTemplateMiddle,
|
|
59
|
+
shimast.KindTemplateTail:
|
|
60
|
+
reportStringEscapes(ctx, raw, pos, templateValidEscapes)
|
|
61
|
+
case shimast.KindRegularExpressionLiteral:
|
|
62
|
+
reportRegexEscapes(ctx, raw, pos)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stringValidEscapes = "'\"\\bfnrtv0xuU\n\r"
|
|
67
|
+
const templateValidEscapes = "`'\"\\bfnrtv0xuU$\n\r"
|
|
68
|
+
|
|
69
|
+
// regexValidEscapes covers characters that are *always* meaningful when
|
|
70
|
+
// preceded by `\` in a regex pattern. Character classes and group syntax
|
|
71
|
+
// inside a regex add more legitimate escapes (`\d`, `\w`, …); we handle
|
|
72
|
+
// those by allowing every ASCII letter (which makes the fix conservative
|
|
73
|
+
// — it never deletes a backslash whose meaning could be context-sensitive).
|
|
74
|
+
const regexValidEscapes = "^$\\.*+?()[]{}|/-\n\r"
|
|
75
|
+
|
|
76
|
+
func reportStringEscapes(ctx *Context, raw string, base int, whitelist string) {
|
|
77
|
+
if len(raw) < 2 {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
// Strip the enclosing quotes; first and last bytes are quote / backtick / `${` / `}`.
|
|
81
|
+
// For TemplateHead/Middle/Tail the head/tail use backtick + `${`, but
|
|
82
|
+
// every quoted form starts with one ASCII byte and ends with one or
|
|
83
|
+
// two ASCII bytes — neither contains a meaningful backslash, so just
|
|
84
|
+
// skip the first byte and stop one byte before the end.
|
|
85
|
+
startSkip := 1
|
|
86
|
+
endSkip := 1
|
|
87
|
+
if raw[len(raw)-2] == '$' && raw[len(raw)-1] == '{' {
|
|
88
|
+
endSkip = 2
|
|
89
|
+
}
|
|
90
|
+
for i := startSkip; i < len(raw)-endSkip; i++ {
|
|
91
|
+
if raw[i] != '\\' {
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if i+1 >= len(raw)-endSkip {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
next := raw[i+1]
|
|
98
|
+
if isUselessStringEscape(next, whitelist) {
|
|
99
|
+
// Only emit a fix when both surrounding bytes are plain ASCII so
|
|
100
|
+
// deleting one byte cannot corrupt a multi-byte sequence.
|
|
101
|
+
if next < 0x80 {
|
|
102
|
+
ctx.ReportRangeFix(
|
|
103
|
+
base+i,
|
|
104
|
+
base+i+1,
|
|
105
|
+
"Unnecessary escape character: \\"+string(next)+".",
|
|
106
|
+
TextEdit{Pos: base + i, End: base + i + 1, Text: ""},
|
|
107
|
+
)
|
|
108
|
+
} else {
|
|
109
|
+
ctx.ReportRange(
|
|
110
|
+
base+i,
|
|
111
|
+
base+i+1,
|
|
112
|
+
"Unnecessary escape character: \\"+string(next)+".",
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
i++ // skip the escaped char so `\\\\` doesn't double-report.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func reportRegexEscapes(ctx *Context, raw string, base int) {
|
|
121
|
+
if len(raw) < 3 || raw[0] != '/' {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
// The regex pattern is between the leading `/` and the trailing
|
|
125
|
+
// `/<flags>`; locate the closing slash by scanning right-to-left.
|
|
126
|
+
closing := strings.LastIndexByte(raw, '/')
|
|
127
|
+
if closing <= 0 {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
body := raw[1:closing]
|
|
131
|
+
inClass := false
|
|
132
|
+
for i := 0; i < len(body); i++ {
|
|
133
|
+
ch := body[i]
|
|
134
|
+
if ch == '[' && !inClass {
|
|
135
|
+
inClass = true
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if ch == ']' && inClass {
|
|
139
|
+
inClass = false
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
if ch != '\\' || i+1 >= len(body) {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
next := body[i+1]
|
|
146
|
+
if isUselessRegexEscape(next, inClass) {
|
|
147
|
+
// base + 1 (for the leading `/`) + i is the byte offset of the
|
|
148
|
+
// backslash in the source.
|
|
149
|
+
pos := base + 1 + i
|
|
150
|
+
if next < 0x80 {
|
|
151
|
+
ctx.ReportRangeFix(
|
|
152
|
+
pos,
|
|
153
|
+
pos+1,
|
|
154
|
+
"Unnecessary escape character: \\"+string(next)+".",
|
|
155
|
+
TextEdit{Pos: pos, End: pos + 1, Text: ""},
|
|
156
|
+
)
|
|
157
|
+
} else {
|
|
158
|
+
ctx.ReportRange(
|
|
159
|
+
pos,
|
|
160
|
+
pos+1,
|
|
161
|
+
"Unnecessary escape character: \\"+string(next)+".",
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
i++ // consume the escaped character.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func isUselessStringEscape(ch byte, whitelist string) bool {
|
|
170
|
+
// Whitespace + control chars are escape sequences too.
|
|
171
|
+
if ch < 0x20 {
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
if strings.IndexByte(whitelist, ch) >= 0 {
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
// ASCII letters that aren't in the whitelist are user-error escapes
|
|
178
|
+
// like `\a`, `\m`. Digits 1-9 are octal-shaped (`no-octal-escape`
|
|
179
|
+
// owns those). `0` is in the whitelist. Punctuation that isn't in the
|
|
180
|
+
// whitelist (e.g., `\.`) is also redundant.
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func isUselessRegexEscape(ch byte, inClass bool) bool {
|
|
185
|
+
if ch < 0x20 {
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
// Regex meta-chars and their friends.
|
|
189
|
+
if strings.IndexByte(regexValidEscapes, ch) >= 0 {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
// Inside a character class `]`, `\`, and `-` are still meaningful
|
|
193
|
+
// beyond the always-list; everything else stays useless.
|
|
194
|
+
if inClass {
|
|
195
|
+
switch ch {
|
|
196
|
+
case ']', '\\', '-', 'b':
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Common regex shorthand: \d \D \w \W \s \S \b \B \f \n \r \t \v \0
|
|
201
|
+
switch ch {
|
|
202
|
+
case 'd', 'D', 'w', 'W', 's', 'S', 'b', 'B', 'f', 'n', 'r', 't', 'v', '0',
|
|
203
|
+
'x', 'u', 'c', 'p', 'P', 'k', 'q':
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// isInsideTaggedTemplate reports whether `node` is the template payload of
|
|
210
|
+
// a TaggedTemplateExpression. Two AST shapes reach here:
|
|
211
|
+
//
|
|
212
|
+
// - `KindNoSubstitutionTemplateLiteral` — the direct child of a
|
|
213
|
+
// TaggedTemplateExpression's Template slot.
|
|
214
|
+
// - `KindTemplateHead/Middle/Tail` — wrapped in TemplateSpan and
|
|
215
|
+
// TemplateExpression nodes; the TemplateExpression's parent is the
|
|
216
|
+
// TaggedTemplateExpression.
|
|
217
|
+
func isInsideTaggedTemplate(node *shimast.Node) bool {
|
|
218
|
+
if node == nil || node.Parent == nil {
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
parent := node.Parent
|
|
222
|
+
if parent.Kind == shimast.KindTaggedTemplateExpression {
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
// Walk up at most two more hops to reach the TaggedTemplateExpression
|
|
226
|
+
// for the spans family.
|
|
227
|
+
for i := 0; i < 2 && parent.Parent != nil; i++ {
|
|
228
|
+
parent = parent.Parent
|
|
229
|
+
if parent.Kind == shimast.KindTaggedTemplateExpression {
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func init() {
|
|
237
|
+
Register(noUselessEscape{})
|
|
238
|
+
}
|