@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.
- package/lib/defaultFormat.d.ts +10 -11
- package/lib/defaultFormat.js +10 -11
- package/lib/defaultFormat.js.map +1 -1
- package/lib/index.js +233 -135
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +2 -2
- package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
- package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
- package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
- package/lib/structures/TtscLintRule.d.ts +10 -1
- package/lib/structures/TtscLintRuleMap.d.ts +2 -2
- package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
- package/linthost/ast_helpers.go +80 -11
- package/linthost/compile.go +116 -112
- package/linthost/config.go +647 -687
- package/linthost/config_format.go +272 -247
- package/linthost/contrib_adapter.go +7 -0
- package/linthost/directives.go +116 -0
- package/linthost/dispatch.go +33 -33
- package/linthost/engine.go +156 -43
- package/linthost/fix.go +47 -27
- package/linthost/flags_gen.go +31 -0
- package/linthost/format.go +119 -3
- package/linthost/host.go +115 -5
- package/linthost/print_dispatch.go +128 -28
- package/linthost/print_doc.go +23 -0
- package/linthost/print_engine.go +168 -4
- package/linthost/print_nodes_array.go +17 -7
- package/linthost/print_nodes_call.go +138 -20
- package/linthost/print_nodes_function.go +353 -0
- package/linthost/print_nodes_imports.go +46 -29
- package/linthost/print_nodes_list.go +86 -5
- package/linthost/print_nodes_object.go +56 -11
- package/linthost/rules_arrays.go +5 -2
- package/linthost/rules_debugger.go +3 -2
- package/linthost/rules_dupes.go +7 -4
- package/linthost/rules_empty.go +3 -2
- package/linthost/rules_escape.go +35 -3
- package/linthost/rules_eval.go +3 -0
- package/linthost/rules_finally.go +11 -0
- package/linthost/rules_format_jsdoc.go +7 -0
- package/linthost/rules_format_print_width.go +270 -15
- package/linthost/rules_format_quotes.go +3 -0
- package/linthost/rules_format_sort_imports.go +4 -0
- package/linthost/rules_gap.go +75 -3
- package/linthost/rules_imports.go +5 -7
- package/linthost/rules_logic.go +75 -5
- package/linthost/rules_loops.go +4 -0
- package/linthost/rules_misc.go +4 -2
- package/linthost/rules_params.go +7 -11
- package/linthost/rules_problems.go +89 -22
- package/linthost/rules_promise.go +15 -0
- package/linthost/rules_protos.go +3 -2
- package/linthost/rules_self.go +6 -0
- package/linthost/rules_strings.go +10 -0
- package/linthost/rules_suggestions.go +174 -9
- package/linthost/rules_throw.go +2 -0
- package/linthost/rules_ts.go +13 -0
- package/linthost/rules_ts_extra.go +25 -8
- package/linthost/rules_var.go +15 -2
- package/package.json +3 -3
- package/plugin/main.go +4 -4
- package/rule/astutil/astutil.go +12 -0
- package/rule/rule.go +123 -123
- package/src/defaultFormat.ts +10 -11
- package/src/index.ts +257 -168
- package/src/structures/ITtscLintConfig.ts +2 -2
- package/src/structures/ITtscLintFormatConfig.ts +53 -55
- package/src/structures/ITtscLintPluginConfig.ts +10 -83
- package/src/structures/ITtscLintPluginMeta.ts +9 -1
- package/src/structures/TtscLintRule.ts +10 -1
- package/src/structures/TtscLintRuleMap.ts +17 -18
- package/src/structures/TtscLintRuleOptions.ts +15 -0
- package/linthost/eslint_runtime.go +0 -320
package/linthost/rules_misc.go
CHANGED
|
@@ -2,8 +2,10 @@ package linthost
|
|
|
2
2
|
|
|
3
3
|
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
4
|
|
|
5
|
-
// radix: `parseInt(x)` without an explicit radix can
|
|
6
|
-
//
|
|
5
|
+
// radix: `parseInt(x)` without an explicit radix argument can trigger
|
|
6
|
+
// implementation-defined octal parsing in older environments. ESLint's
|
|
7
|
+
// default "always" mode requires the second argument and rejects values
|
|
8
|
+
// outside the valid bases {2, 8, 10, 16}.
|
|
7
9
|
// https://eslint.org/docs/latest/rules/radix
|
|
8
10
|
type radix struct{}
|
|
9
11
|
|
package/linthost/rules_params.go
CHANGED
|
@@ -24,9 +24,12 @@ func (defaultParamLast) Check(ctx *Context, node *shimast.Node) {
|
|
|
24
24
|
if len(params) == 0 {
|
|
25
25
|
return
|
|
26
26
|
}
|
|
27
|
-
// Walk right-to-left
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
// Walk right-to-left: the first non-default-like parameter sets the
|
|
28
|
+
// boundary; any default-like parameter to its LEFT is mis-ordered.
|
|
29
|
+
// "Default-like" includes both initializer-bearing params (`a = 1`)
|
|
30
|
+
// and optional params (`a?: T`): both let callers elide the argument,
|
|
31
|
+
// so placing them before a required param forces callers to write
|
|
32
|
+
// `undefined` explicitly — which defeats the point of both forms.
|
|
30
33
|
sawNonDefaultAfter := false
|
|
31
34
|
for i := len(params) - 1; i >= 0; i-- {
|
|
32
35
|
p := params[i]
|
|
@@ -38,16 +41,9 @@ func (defaultParamLast) Check(ctx *Context, node *shimast.Node) {
|
|
|
38
41
|
continue
|
|
39
42
|
}
|
|
40
43
|
if decl.DotDotDotToken != nil {
|
|
41
|
-
// Rest parameters
|
|
42
|
-
// participate in the default-position check.
|
|
44
|
+
// Rest parameters must be last by grammar; skip them.
|
|
43
45
|
continue
|
|
44
46
|
}
|
|
45
|
-
// typescript-eslint canonical treats both default-initialized and
|
|
46
|
-
// optional (`a?: T`) parameters as default-like for ordering: both
|
|
47
|
-
// permit callers to elide the argument, so a non-optional / non-
|
|
48
|
-
// default parameter after either one forces the caller to spell
|
|
49
|
-
// `undefined`. The round-1 implementation only checked Initializer;
|
|
50
|
-
// round 2 adds the QuestionToken branch.
|
|
51
47
|
isDefaultLike := decl.Initializer != nil || decl.QuestionToken != nil
|
|
52
48
|
if !isDefaultLike {
|
|
53
49
|
sawNonDefaultAfter = true
|
|
@@ -74,7 +74,9 @@ func (noExAssign) Check(ctx *Context, node *shimast.Node) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// walkAssignments invokes `report` on every `<name> = ...` shape inside
|
|
77
|
-
// `root`. Used by no-ex-assign
|
|
77
|
+
// `root`. Used by no-ex-assign to scan a single catch block; the
|
|
78
|
+
// file-wide no-func-assign / no-class-assign scans go through
|
|
79
|
+
// reportAssignmentsToDeclarations instead.
|
|
78
80
|
func walkAssignments(root *shimast.Node, name string, report func(*shimast.Node)) {
|
|
79
81
|
if root == nil {
|
|
80
82
|
return
|
|
@@ -110,6 +112,8 @@ func (noEmptyCharacterClass) Check(ctx *Context, node *shimast.Node) {
|
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
// hasEmptyCharClass reports whether the regex source text src contains an
|
|
116
|
+
// empty character class (`[]` or `[^]`). Respects backslash escapes.
|
|
113
117
|
func hasEmptyCharClass(src string) bool {
|
|
114
118
|
// Walk the regex literal source manually, respecting escapes.
|
|
115
119
|
for i := 0; i < len(src); i++ {
|
|
@@ -143,6 +147,10 @@ func (noMisleadingCharacterClass) Check(ctx *Context, node *shimast.Node) {
|
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
// regexHasSurrogatePair reports whether the regex source text src contains
|
|
151
|
+
// a non-BMP character (code point >= U+10000) inside a character class
|
|
152
|
+
// without the `u` flag. Such characters are stored as surrogate pairs in
|
|
153
|
+
// the source and the class will only match one half of the pair.
|
|
146
154
|
func regexHasSurrogatePair(src string) bool {
|
|
147
155
|
// Strip the trailing flags so we don't misread the `u` flag — it
|
|
148
156
|
// suppresses this rule.
|
|
@@ -188,6 +196,10 @@ func (noLossOfPrecision) Check(ctx *Context, node *shimast.Node) {
|
|
|
188
196
|
}
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
// numericLiteralLosesPrecision reports whether the decimal integer literal
|
|
200
|
+
// text exceeds Number.MAX_SAFE_INTEGER (2^53 - 1). Non-decimal literals
|
|
201
|
+
// (hex, octal, binary) and literals with exponents or decimal points are
|
|
202
|
+
// exempt because their precision loss is caller-visible and intentional.
|
|
191
203
|
func numericLiteralLosesPrecision(text string) bool {
|
|
192
204
|
// Strip underscore separators, exponents, decimal/hex/oct/binary
|
|
193
205
|
// markers — for the simple-base-10 integer case the round-trip
|
|
@@ -204,7 +216,8 @@ func numericLiteralLosesPrecision(text string) bool {
|
|
|
204
216
|
if trimmed == "" {
|
|
205
217
|
return false
|
|
206
218
|
}
|
|
207
|
-
// 2^53 =
|
|
219
|
+
// Number.MAX_SAFE_INTEGER is 2^53-1 = 9007199254740991; 2^53 itself
|
|
220
|
+
// (9007199254740992) is the first integer that float64 cannot round-trip.
|
|
208
221
|
const maxSafe = "9007199254740992"
|
|
209
222
|
if len(trimmed) < len(maxSafe) {
|
|
210
223
|
return false
|
|
@@ -219,38 +232,80 @@ func numericLiteralLosesPrecision(text string) bool {
|
|
|
219
232
|
type noClassAssign struct{}
|
|
220
233
|
|
|
221
234
|
func (noClassAssign) Name() string { return "no-class-assign" }
|
|
222
|
-
func (noClassAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.
|
|
235
|
+
func (noClassAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
|
|
223
236
|
func (noClassAssign) Check(ctx *Context, node *shimast.Node) {
|
|
224
|
-
|
|
225
|
-
if decl == nil || decl.Name() == nil {
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
name := identifierText(decl.Name())
|
|
229
|
-
if name == "" {
|
|
230
|
-
return
|
|
231
|
-
}
|
|
232
|
-
walkAssignments(ctx.File.AsNode(), name, func(target *shimast.Node) {
|
|
233
|
-
ctx.Report(target, "'"+name+"' is a class.")
|
|
234
|
-
})
|
|
237
|
+
reportAssignmentsToDeclarations(ctx, node, shimast.KindClassDeclaration, "is a class.")
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
// no-func-assign: same idea, but for function declarations.
|
|
238
241
|
type noFuncAssign struct{}
|
|
239
242
|
|
|
240
243
|
func (noFuncAssign) Name() string { return "no-func-assign" }
|
|
241
|
-
func (noFuncAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.
|
|
244
|
+
func (noFuncAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
|
|
242
245
|
func (noFuncAssign) Check(ctx *Context, node *shimast.Node) {
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
reportAssignmentsToDeclarations(ctx, node, shimast.KindFunctionDeclaration, "is a function.")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// reportAssignmentsToDeclarations flags every `<name> = …` assignment whose
|
|
250
|
+
// target identifier names a `declKind` declaration found anywhere in the
|
|
251
|
+
// file. It walks the file exactly once — gathering declared names and
|
|
252
|
+
// assignment targets in the same pass — so the cost is linear in file size.
|
|
253
|
+
//
|
|
254
|
+
// The earlier shape registered for `declKind` directly and, on every
|
|
255
|
+
// declaration, re-scanned the whole file for assignments: O(declarations ×
|
|
256
|
+
// file size), which blows up quadratically on a file with many top-level
|
|
257
|
+
// functions. Visiting `KindSourceFile` once and cross-referencing afterward
|
|
258
|
+
// keeps the same findings without the repeated scans.
|
|
259
|
+
func reportAssignmentsToDeclarations(
|
|
260
|
+
ctx *Context,
|
|
261
|
+
file *shimast.Node,
|
|
262
|
+
declKind shimast.Kind,
|
|
263
|
+
noun string,
|
|
264
|
+
) {
|
|
265
|
+
if ctx == nil || file == nil {
|
|
245
266
|
return
|
|
246
267
|
}
|
|
247
|
-
|
|
248
|
-
|
|
268
|
+
declared := map[string]struct{}{}
|
|
269
|
+
var targets []*shimast.Node
|
|
270
|
+
walkDescendants(file, func(n *shimast.Node) {
|
|
271
|
+
switch n.Kind {
|
|
272
|
+
case declKind:
|
|
273
|
+
if name := declarationName(n); name != "" {
|
|
274
|
+
declared[name] = struct{}{}
|
|
275
|
+
}
|
|
276
|
+
case shimast.KindBinaryExpression:
|
|
277
|
+
if expr := n.AsBinaryExpression(); expr != nil &&
|
|
278
|
+
expr.OperatorToken != nil && isAssignmentOperator(expr.OperatorToken.Kind) &&
|
|
279
|
+
expr.Left != nil && expr.Left.Kind == shimast.KindIdentifier {
|
|
280
|
+
targets = append(targets, expr.Left)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
if len(declared) == 0 || len(targets) == 0 {
|
|
249
285
|
return
|
|
250
286
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
287
|
+
for _, target := range targets {
|
|
288
|
+
name := identifierText(target)
|
|
289
|
+
if _, ok := declared[name]; ok {
|
|
290
|
+
ctx.Report(target, "'"+name+"' "+noun)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// declarationName returns the bound name of a class or function declaration
|
|
296
|
+
// node, or "" when the node is neither (or is anonymous).
|
|
297
|
+
func declarationName(n *shimast.Node) string {
|
|
298
|
+
switch n.Kind {
|
|
299
|
+
case shimast.KindFunctionDeclaration:
|
|
300
|
+
if d := n.AsFunctionDeclaration(); d != nil {
|
|
301
|
+
return identifierText(d.Name())
|
|
302
|
+
}
|
|
303
|
+
case shimast.KindClassDeclaration:
|
|
304
|
+
if d := n.AsClassDeclaration(); d != nil {
|
|
305
|
+
return identifierText(d.Name())
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return ""
|
|
254
309
|
}
|
|
255
310
|
|
|
256
311
|
// no-prototype-builtins: `obj.hasOwnProperty(x)` — should be
|
|
@@ -351,6 +406,9 @@ func (noControlRegex) Check(ctx *Context, node *shimast.Node) {
|
|
|
351
406
|
}
|
|
352
407
|
}
|
|
353
408
|
|
|
409
|
+
// regexContainsControl reports whether the regex source text src contains a
|
|
410
|
+
// literal control character (U+0000–U+001F, excluding \t, \n, \r) or a
|
|
411
|
+
// \xNN / \uNNNN escape that resolves to a control character.
|
|
354
412
|
func regexContainsControl(src string) bool {
|
|
355
413
|
for i := 0; i < len(src); i++ {
|
|
356
414
|
c := src[i]
|
|
@@ -382,6 +440,8 @@ func regexContainsControl(src string) bool {
|
|
|
382
440
|
return false
|
|
383
441
|
}
|
|
384
442
|
|
|
443
|
+
// hexDigit converts an ASCII hex byte ('0'-'9', 'a'-'f', 'A'-'F') to its
|
|
444
|
+
// integer value. Returns -1 for non-hex bytes.
|
|
385
445
|
func hexDigit(b byte) int {
|
|
386
446
|
switch {
|
|
387
447
|
case b >= '0' && b <= '9':
|
|
@@ -412,6 +472,10 @@ func (noIrregularWhitespace) Check(ctx *Context, node *shimast.Node) {
|
|
|
412
472
|
}
|
|
413
473
|
}
|
|
414
474
|
|
|
475
|
+
// isIrregularWhitespace reports whether rune r is a non-standard whitespace
|
|
476
|
+
// character that the TypeScript parser accepts but is almost certainly a
|
|
477
|
+
// copy-paste artifact: vertical tab, form feed, non-breaking space, and the
|
|
478
|
+
// various Unicode space and line separator code points.
|
|
415
479
|
func isIrregularWhitespace(r rune) bool {
|
|
416
480
|
switch r {
|
|
417
481
|
case '\v', '\f',
|
|
@@ -458,6 +522,9 @@ func (noFallthrough) Check(ctx *Context, node *shimast.Node) {
|
|
|
458
522
|
}
|
|
459
523
|
}
|
|
460
524
|
|
|
525
|
+
// isTerminating reports whether stmt is a statement that unconditionally
|
|
526
|
+
// transfers control out of the current block: break, continue, return,
|
|
527
|
+
// throw, or a block whose last statement is terminating.
|
|
461
528
|
func isTerminating(stmt *shimast.Node) bool {
|
|
462
529
|
if stmt == nil {
|
|
463
530
|
return false
|
|
@@ -19,6 +19,9 @@ import (
|
|
|
19
19
|
type awaitThenable struct{}
|
|
20
20
|
|
|
21
21
|
func (awaitThenable) Name() string { return "await-thenable" }
|
|
22
|
+
func (awaitThenable) NeedsTypeChecker() bool {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
22
25
|
func (awaitThenable) Visits() []shimast.Kind {
|
|
23
26
|
return []shimast.Kind{shimast.KindAwaitExpression}
|
|
24
27
|
}
|
|
@@ -64,6 +67,15 @@ func (awaitThenable) Check(ctx *Context, node *shimast.Node) {
|
|
|
64
67
|
// reference to globalPromise, and `GetPropertyOfType` filters `then` as
|
|
65
68
|
// a partial member, so without iterating constituents the rule would
|
|
66
69
|
// fire on legitimate code.
|
|
70
|
+
// isAwaitable reports whether t is safe to await. A type is awaitable when:
|
|
71
|
+
// - its flags include Any, Unknown, or Never (these escape static strictness);
|
|
72
|
+
// - it is a Promise (GetPromisedTypeOfPromise returns non-nil); or
|
|
73
|
+
// - it is thenable (has a callable `then` property).
|
|
74
|
+
//
|
|
75
|
+
// For union and intersection types the function recurses into constituents: if
|
|
76
|
+
// ANY constituent is awaitable the whole type is considered awaitable. This is
|
|
77
|
+
// necessary because GetPromisedTypeOfPromise returns nil on composite types
|
|
78
|
+
// like `Promise<X> | number` even though the expression can legally be awaited.
|
|
67
79
|
func isAwaitable(checker *shimchecker.Checker, t *shimchecker.Type) bool {
|
|
68
80
|
if checker == nil || t == nil {
|
|
69
81
|
return false
|
|
@@ -91,6 +103,9 @@ func isAwaitable(checker *shimchecker.Checker, t *shimchecker.Type) bool {
|
|
|
91
103
|
return isThenableType(checker, t)
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
// isThenableType reports whether t has a callable `then` property, which is
|
|
107
|
+
// the runtime-observable contract for "thenable" in the ES spec. The check
|
|
108
|
+
// intentionally mirrors what the JS engine uses at await-time.
|
|
94
109
|
func isThenableType(checker *shimchecker.Checker, t *shimchecker.Type) bool {
|
|
95
110
|
if checker == nil || t == nil {
|
|
96
111
|
return false
|
package/linthost/rules_protos.go
CHANGED
|
@@ -2,8 +2,9 @@ package linthost
|
|
|
2
2
|
|
|
3
3
|
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
4
4
|
|
|
5
|
-
// no-iterator: `obj.__iterator__` is a non-standard SpiderMonkey-era
|
|
6
|
-
//
|
|
5
|
+
// no-iterator: accessing `obj.__iterator__` is a non-standard SpiderMonkey-era
|
|
6
|
+
// extension that predates `Symbol.iterator`. Modern code should use
|
|
7
|
+
// `Symbol.iterator` and the iterable protocol instead.
|
|
7
8
|
// https://eslint.org/docs/latest/rules/no-iterator
|
|
8
9
|
type noIterator struct{}
|
|
9
10
|
|
package/linthost/rules_self.go
CHANGED
|
@@ -31,6 +31,10 @@ func (noSelfAssign) Check(ctx *Context, node *shimast.Node) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// isAssignableLeftHand reports whether node can appear as the left-hand side
|
|
35
|
+
// of a plain assignment expression. Only simple identifier and member-access
|
|
36
|
+
// shapes are checked here; complex destructuring patterns are excluded because
|
|
37
|
+
// a textual equality test would produce too many false negatives.
|
|
34
38
|
func isAssignableLeftHand(node *shimast.Node) bool {
|
|
35
39
|
if node == nil {
|
|
36
40
|
return false
|
|
@@ -67,6 +71,8 @@ func (noSelfCompare) Check(ctx *Context, node *shimast.Node) {
|
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
// isComparisonOperator reports whether kind is one of the eight standard
|
|
75
|
+
// comparison operators: ==, ===, !=, !==, <, >, <=, >=.
|
|
70
76
|
func isComparisonOperator(kind shimast.Kind) bool {
|
|
71
77
|
switch kind {
|
|
72
78
|
case
|
|
@@ -22,6 +22,9 @@ func (noTemplateCurlyInString) Check(ctx *Context, node *shimast.Node) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// hasTemplatePlaceholder reports whether `text` contains a `${...}`
|
|
26
|
+
// sequence — i.e. at least one `${` followed by a closing `}`. A lone
|
|
27
|
+
// `${` with no matching brace is not flagged.
|
|
25
28
|
func hasTemplatePlaceholder(text string) bool {
|
|
26
29
|
if !strings.Contains(text, "${") {
|
|
27
30
|
return false
|
|
@@ -57,6 +60,9 @@ func (noMultiStr) Check(ctx *Context, node *shimast.Node) {
|
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
// hasBackslashLineContinuation reports whether `src` contains a backslash
|
|
64
|
+
// immediately followed by a newline (`\n` or `\r`), which is the raw-source
|
|
65
|
+
// signature of a backslash line-continuation inside a string literal.
|
|
60
66
|
func hasBackslashLineContinuation(src string) bool {
|
|
61
67
|
for i := 0; i < len(src)-1; i++ {
|
|
62
68
|
if src[i] == '\\' {
|
|
@@ -89,6 +95,9 @@ func (noUselessConcat) Check(ctx *Context, node *shimast.Node) {
|
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
// isStringLikeLiteral reports whether `node` is a string literal or a
|
|
99
|
+
// no-substitution template literal — the two kinds that can be trivially
|
|
100
|
+
// concatenated with `+`.
|
|
92
101
|
func isStringLikeLiteral(node *shimast.Node) bool {
|
|
93
102
|
if node == nil {
|
|
94
103
|
return false
|
|
@@ -115,6 +124,7 @@ func (noOctal) Check(ctx *Context, node *shimast.Node) {
|
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
// isAsciiDigit reports whether b is an ASCII decimal digit (0–9).
|
|
118
128
|
func isAsciiDigit(b byte) bool { return b >= '0' && b <= '9' }
|
|
119
129
|
|
|
120
130
|
func init() {
|
|
@@ -7,6 +7,7 @@ import (
|
|
|
7
7
|
"strings"
|
|
8
8
|
|
|
9
9
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
10
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
// no-alert: `alert()` / `confirm()` / `prompt()`. Rarely the right
|
|
@@ -253,10 +254,15 @@ func (noLoneBlocks) Check(ctx *Context, node *shimast.Node) {
|
|
|
253
254
|
switch parent.Kind {
|
|
254
255
|
case shimast.KindBlock, shimast.KindSourceFile, shimast.KindModuleBlock:
|
|
255
256
|
default:
|
|
257
|
+
// Block is the body of a control-flow statement (if/for/while/…) —
|
|
258
|
+
// those braces are not lone; only report blocks nested inside another
|
|
259
|
+
// statement list (another Block, SourceFile, or ModuleBlock).
|
|
256
260
|
return
|
|
257
261
|
}
|
|
258
|
-
//
|
|
259
|
-
// are
|
|
262
|
+
// isFunctionLikeKind returns false for all three parent kinds above
|
|
263
|
+
// (Block/SourceFile/ModuleBlock are never function-like), so this guard
|
|
264
|
+
// is a no-op. It is left in place to document intent: if the switch were
|
|
265
|
+
// ever widened to admit function-body containers, this guard would fire.
|
|
260
266
|
if isFunctionLikeKind(parent) {
|
|
261
267
|
return
|
|
262
268
|
}
|
|
@@ -737,16 +743,72 @@ func (noUnneededTernary) Visits() []shimast.Kind {
|
|
|
737
743
|
}
|
|
738
744
|
func (noUnneededTernary) Check(ctx *Context, node *shimast.Node) {
|
|
739
745
|
cond := node.AsConditionalExpression()
|
|
740
|
-
if cond == nil {
|
|
746
|
+
if cond == nil || cond.Condition == nil {
|
|
741
747
|
return
|
|
742
748
|
}
|
|
743
749
|
t := stripParens(cond.WhenTrue)
|
|
744
750
|
f := stripParens(cond.WhenFalse)
|
|
745
751
|
tBool, tOk := isLiteralBoolean(t)
|
|
746
752
|
fBool, fOk := isLiteralBoolean(f)
|
|
747
|
-
if tOk && fOk && tBool != fBool {
|
|
748
|
-
|
|
753
|
+
if !(tOk && fOk && tBool != fBool) {
|
|
754
|
+
return
|
|
755
|
+
}
|
|
756
|
+
message := "Unnecessary use of conditional expression for boolean."
|
|
757
|
+
src := ctx.File.Text()
|
|
758
|
+
condStart := shimscanner.SkipTrivia(src, cond.Condition.Pos())
|
|
759
|
+
if condStart < 0 || condStart >= cond.Condition.End() {
|
|
760
|
+
ctx.Report(node, message)
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
condText := src[condStart:cond.Condition.End()]
|
|
764
|
+
var replacement string
|
|
765
|
+
if tBool {
|
|
766
|
+
// `cond ? true : false` → `Boolean(cond)`
|
|
767
|
+
replacement = "Boolean(" + condText + ")"
|
|
768
|
+
} else {
|
|
769
|
+
// `cond ? false : true` → `!cond`. Wrap the condition in parentheses
|
|
770
|
+
// when it is not already a primary expression so operator precedence
|
|
771
|
+
// does not flip the meaning (e.g. `a || b` must become `!(a || b)`).
|
|
772
|
+
if needsParensForUnaryNegation(cond.Condition) {
|
|
773
|
+
replacement = "!(" + condText + ")"
|
|
774
|
+
} else {
|
|
775
|
+
replacement = "!" + condText
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
editPos := shimscanner.SkipTrivia(src, node.Pos())
|
|
779
|
+
if editPos < 0 || editPos >= node.End() {
|
|
780
|
+
ctx.Report(node, message)
|
|
781
|
+
return
|
|
782
|
+
}
|
|
783
|
+
ctx.ReportFix(
|
|
784
|
+
node,
|
|
785
|
+
message,
|
|
786
|
+
TextEdit{Pos: editPos, End: node.End(), Text: replacement},
|
|
787
|
+
)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// needsParensForUnaryNegation reports whether `cond` must be wrapped in
|
|
791
|
+
// parentheses before prefixing with `!`. Anything looser than a unary /
|
|
792
|
+
// member / primary expression flips precedence when negated. Mirrors
|
|
793
|
+
// ESLint's `no-unneeded-ternary` autofix safety check.
|
|
794
|
+
func needsParensForUnaryNegation(node *shimast.Node) bool {
|
|
795
|
+
inner := stripParens(node)
|
|
796
|
+
if inner == nil {
|
|
797
|
+
return false
|
|
798
|
+
}
|
|
799
|
+
switch inner.Kind {
|
|
800
|
+
case shimast.KindBinaryExpression,
|
|
801
|
+
shimast.KindConditionalExpression,
|
|
802
|
+
shimast.KindYieldExpression,
|
|
803
|
+
shimast.KindAwaitExpression,
|
|
804
|
+
shimast.KindArrowFunction,
|
|
805
|
+
shimast.KindFunctionExpression,
|
|
806
|
+
shimast.KindAsExpression,
|
|
807
|
+
shimast.KindSatisfiesExpression,
|
|
808
|
+
shimast.KindTypeAssertionExpression:
|
|
809
|
+
return true
|
|
749
810
|
}
|
|
811
|
+
return false
|
|
750
812
|
}
|
|
751
813
|
|
|
752
814
|
// no-unused-expressions: an expression statement whose value isn't used.
|
|
@@ -783,8 +845,13 @@ func isProductiveExpression(node *shimast.Node) bool {
|
|
|
783
845
|
shimast.KindPrefixUnaryExpression,
|
|
784
846
|
shimast.KindPostfixUnaryExpression,
|
|
785
847
|
shimast.KindTaggedTemplateExpression:
|
|
786
|
-
//
|
|
787
|
-
//
|
|
848
|
+
// Most of these kinds are unconditionally productive (call, new, await,
|
|
849
|
+
// yield, delete, tagged template). The inner switch narrows the two kinds
|
|
850
|
+
// that can be non-productive: a BinaryExpression is only productive when
|
|
851
|
+
// it is an assignment, and a PrefixUnaryExpression is only productive when
|
|
852
|
+
// it is ++ or --. KindPostfixUnaryExpression is always productive (++ and
|
|
853
|
+
// -- are the only postfix operators). All un-matched cases reach the
|
|
854
|
+
// outer `return true` below.
|
|
788
855
|
switch expr.Kind {
|
|
789
856
|
case shimast.KindBinaryExpression:
|
|
790
857
|
bin := expr.AsBinaryExpression()
|
|
@@ -1089,9 +1156,27 @@ func (preferTemplate) Check(ctx *Context, node *shimast.Node) {
|
|
|
1089
1156
|
}
|
|
1090
1157
|
}
|
|
1091
1158
|
hasString, hasOther := concatChainShape(node)
|
|
1092
|
-
if hasString && hasOther {
|
|
1093
|
-
|
|
1159
|
+
if !(hasString && hasOther) {
|
|
1160
|
+
return
|
|
1161
|
+
}
|
|
1162
|
+
message := "Unexpected string concatenation."
|
|
1163
|
+
src := ctx.File.Text()
|
|
1164
|
+
operands := flattenConcatOperands(node)
|
|
1165
|
+
template, ok := renderConcatAsTemplate(src, operands)
|
|
1166
|
+
if !ok {
|
|
1167
|
+
ctx.Report(node, message)
|
|
1168
|
+
return
|
|
1094
1169
|
}
|
|
1170
|
+
editPos := shimscanner.SkipTrivia(src, node.Pos())
|
|
1171
|
+
if editPos < 0 || editPos >= node.End() {
|
|
1172
|
+
ctx.Report(node, message)
|
|
1173
|
+
return
|
|
1174
|
+
}
|
|
1175
|
+
ctx.ReportFix(
|
|
1176
|
+
node,
|
|
1177
|
+
message,
|
|
1178
|
+
TextEdit{Pos: editPos, End: node.End(), Text: template},
|
|
1179
|
+
)
|
|
1095
1180
|
}
|
|
1096
1181
|
|
|
1097
1182
|
func concatChainShape(node *shimast.Node) (hasString bool, hasOther bool) {
|
|
@@ -1112,6 +1197,86 @@ func concatChainShape(node *shimast.Node) (hasString bool, hasOther bool) {
|
|
|
1112
1197
|
return false, true
|
|
1113
1198
|
}
|
|
1114
1199
|
|
|
1200
|
+
// flattenConcatOperands walks a `+` chain left-to-right and returns each
|
|
1201
|
+
// leaf operand in source order. Parenthesized sub-expressions are kept as
|
|
1202
|
+
// a single operand so the rendered template literal does not lose their
|
|
1203
|
+
// grouping. Mirrors ESLint's behavior of treating `("a" + b)` as one
|
|
1204
|
+
// expression slot when it appears inside a larger chain.
|
|
1205
|
+
func flattenConcatOperands(node *shimast.Node) []*shimast.Node {
|
|
1206
|
+
if node == nil {
|
|
1207
|
+
return nil
|
|
1208
|
+
}
|
|
1209
|
+
if node.Kind == shimast.KindBinaryExpression {
|
|
1210
|
+
bin := node.AsBinaryExpression()
|
|
1211
|
+
if bin != nil && bin.OperatorToken != nil && bin.OperatorToken.Kind == shimast.KindPlusToken {
|
|
1212
|
+
out := flattenConcatOperands(bin.Left)
|
|
1213
|
+
out = append(out, flattenConcatOperands(bin.Right)...)
|
|
1214
|
+
return out
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return []*shimast.Node{node}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// renderConcatAsTemplate renders the flattened concat operands as a single
|
|
1221
|
+
// backtick template literal. String-like literals contribute their value
|
|
1222
|
+
// directly (with template-specific escaping); any other expression becomes
|
|
1223
|
+
// a `${…}` placeholder copied verbatim from the source text. Returns ok=false
|
|
1224
|
+
// when an operand cannot be rendered (typically because its source range is
|
|
1225
|
+
// unavailable), so the caller falls back to detection-only.
|
|
1226
|
+
func renderConcatAsTemplate(src string, operands []*shimast.Node) (string, bool) {
|
|
1227
|
+
if len(operands) == 0 {
|
|
1228
|
+
return "", false
|
|
1229
|
+
}
|
|
1230
|
+
var sb strings.Builder
|
|
1231
|
+
sb.WriteByte('`')
|
|
1232
|
+
for _, operand := range operands {
|
|
1233
|
+
if operand == nil {
|
|
1234
|
+
return "", false
|
|
1235
|
+
}
|
|
1236
|
+
inner := stripParens(operand)
|
|
1237
|
+
if isStringLikeLiteral(inner) {
|
|
1238
|
+
sb.WriteString(escapeTemplateLiteralBody(stringLiteralText(inner)))
|
|
1239
|
+
continue
|
|
1240
|
+
}
|
|
1241
|
+
pos := shimscanner.SkipTrivia(src, operand.Pos())
|
|
1242
|
+
end := operand.End()
|
|
1243
|
+
if pos < 0 || pos >= end || end > len(src) {
|
|
1244
|
+
return "", false
|
|
1245
|
+
}
|
|
1246
|
+
sb.WriteString("${")
|
|
1247
|
+
sb.WriteString(src[pos:end])
|
|
1248
|
+
sb.WriteByte('}')
|
|
1249
|
+
}
|
|
1250
|
+
sb.WriteByte('`')
|
|
1251
|
+
return sb.String(), true
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// escapeTemplateLiteralBody escapes the characters that would otherwise
|
|
1255
|
+
// terminate or interpolate a template literal body: backslash, backtick,
|
|
1256
|
+
// and the `${` sequence. Matches the canonical ESLint `prefer-template`
|
|
1257
|
+
// fixer escape set.
|
|
1258
|
+
func escapeTemplateLiteralBody(text string) string {
|
|
1259
|
+
var sb strings.Builder
|
|
1260
|
+
sb.Grow(len(text))
|
|
1261
|
+
for i := 0; i < len(text); i++ {
|
|
1262
|
+
ch := text[i]
|
|
1263
|
+
switch ch {
|
|
1264
|
+
case '\\', '`':
|
|
1265
|
+
sb.WriteByte('\\')
|
|
1266
|
+
sb.WriteByte(ch)
|
|
1267
|
+
case '$':
|
|
1268
|
+
if i+1 < len(text) && text[i+1] == '{' {
|
|
1269
|
+
sb.WriteString("\\$")
|
|
1270
|
+
} else {
|
|
1271
|
+
sb.WriteByte('$')
|
|
1272
|
+
}
|
|
1273
|
+
default:
|
|
1274
|
+
sb.WriteByte(ch)
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return sb.String()
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1115
1280
|
// require-yield: `function* gen() { return 1; }` — generators that
|
|
1116
1281
|
// never yield are usually unintended.
|
|
1117
1282
|
type requireYield struct{}
|
package/linthost/rules_throw.go
CHANGED
|
@@ -21,6 +21,8 @@ func (noThrowLiteral) Check(ctx *Context, node *shimast.Node) {
|
|
|
21
21
|
ctx.Report(throw.Expression, "Expected an error object to be thrown.")
|
|
22
22
|
return
|
|
23
23
|
}
|
|
24
|
+
// `undefined` can appear as either KindUndefinedKeyword (handled above) or
|
|
25
|
+
// as a plain KindIdentifier whose text is "undefined" — check both forms.
|
|
24
26
|
if id := identifierText(expr); id == "undefined" {
|
|
25
27
|
ctx.Report(throw.Expression, "Expected an error object to be thrown.")
|
|
26
28
|
}
|
package/linthost/rules_ts.go
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Core TypeScript-specific lint rules: direct ports of the most commonly
|
|
2
|
+
// enabled @typescript-eslint/recommended and @typescript-eslint/stylistic
|
|
3
|
+
// rules that require only AST inspection (no checker or scope analysis).
|
|
4
|
+
// Each rule is registered in the package init function at the bottom.
|
|
1
5
|
package linthost
|
|
2
6
|
|
|
3
7
|
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
@@ -83,6 +87,10 @@ func (noInferrableTypes) Check(ctx *Context, node *shimast.Node) {
|
|
|
83
87
|
ctx.Report(typeNode, "Type annotation here is unnecessary.")
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
// isInferrablePair reports whether typeNode is a type annotation that
|
|
91
|
+
// TypeScript would have inferred automatically from the initializer init.
|
|
92
|
+
// Only covers the scalar literal kinds: string, number, boolean, bigint,
|
|
93
|
+
// null, and undefined.
|
|
86
94
|
func isInferrablePair(typeNode, init *shimast.Node) bool {
|
|
87
95
|
switch typeNode.Kind {
|
|
88
96
|
case shimast.KindStringKeyword:
|
|
@@ -101,6 +109,8 @@ func isInferrablePair(typeNode, init *shimast.Node) bool {
|
|
|
101
109
|
return false
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
// isUnaryNumeric reports whether node is a unary +/- applied to a numeric
|
|
113
|
+
// literal (e.g. `-1`, `+0`). Used by isInferrablePair for the number case.
|
|
104
114
|
func isUnaryNumeric(node *shimast.Node) bool {
|
|
105
115
|
if node == nil || node.Kind != shimast.KindPrefixUnaryExpression {
|
|
106
116
|
return false
|
|
@@ -202,6 +212,9 @@ func (preferAsConst) Check(ctx *Context, node *shimast.Node) {
|
|
|
202
212
|
)
|
|
203
213
|
}
|
|
204
214
|
|
|
215
|
+
// literalsMatchSourceText reports whether lhs and rhs are both literal
|
|
216
|
+
// expressions whose source text is identical. Used by prefer-as-const to
|
|
217
|
+
// detect `x as "foo"` where "foo" matches x's literal value.
|
|
205
218
|
func literalsMatchSourceText(file *shimast.SourceFile, lhs, rhs *shimast.Node) bool {
|
|
206
219
|
if lhs == nil || rhs == nil {
|
|
207
220
|
return false
|