@ttsc/lint 0.12.3 → 0.12.4
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 +10 -4
- 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/ITtscLintPluginMeta.d.ts +9 -1
- package/lib/structures/TtscLintRule.d.ts +10 -1
- package/lib/structures/TtscLintRuleMap.d.ts +2 -2
- package/linthost/ast_helpers.go +17 -0
- package/linthost/compile.go +37 -0
- package/linthost/config.go +184 -35
- package/linthost/config_format.go +257 -244
- package/linthost/directives.go +72 -0
- package/linthost/dispatch.go +33 -33
- package/linthost/engine.go +4 -0
- package/linthost/eslint_runtime.go +31 -0
- package/linthost/fix.go +29 -0
- package/linthost/format.go +23 -0
- package/linthost/host.go +11 -0
- package/linthost/print_dispatch.go +7 -5
- package/linthost/print_doc.go +4 -0
- package/linthost/print_nodes_call.go +9 -0
- 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 +15 -0
- 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 +3 -0
- package/linthost/rules_format_quotes.go +3 -0
- package/linthost/rules_format_sort_imports.go +4 -0
- package/linthost/rules_gap.go +20 -0
- package/linthost/rules_imports.go +5 -7
- package/linthost/rules_logic.go +11 -0
- 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 +24 -1
- package/linthost/rules_promise.go +12 -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 +14 -4
- 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 +14 -1
- 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 +16 -4
- package/src/structures/ITtscLintConfig.ts +2 -2
- package/src/structures/ITtscLintFormatConfig.ts +53 -55
- package/src/structures/ITtscLintPluginMeta.ts +9 -1
- package/src/structures/TtscLintRule.ts +10 -1
- package/src/structures/TtscLintRuleMap.ts +17 -18
package/linthost/rules_arrays.go
CHANGED
|
@@ -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
|
|
29
|
-
//
|
|
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
|
|
16
|
-
// but
|
|
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
|
|
package/linthost/rules_dupes.go
CHANGED
|
@@ -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]
|
|
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
|
|
58
|
+
if seen[key] {
|
|
59
59
|
ctx.Report(prop, "Duplicate key '"+key+"'.")
|
|
60
|
-
_ = first
|
|
61
60
|
continue
|
|
62
61
|
}
|
|
63
|
-
seen[key] =
|
|
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 ""
|
package/linthost/rules_empty.go
CHANGED
|
@@ -83,8 +83,9 @@ func (noEmptyPattern) Check(ctx *Context, node *shimast.Node) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// isFunctionLikeKind reports whether
|
|
87
|
-
//
|
|
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
|
package/linthost/rules_escape.go
CHANGED
|
@@ -73,6 +73,12 @@ 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
|
+
// 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.
|
|
76
82
|
func reportStringEscapes(ctx *Context, raw string, base int, whitelist string) {
|
|
77
83
|
if len(raw) < 2 {
|
|
78
84
|
return
|
|
@@ -117,6 +123,9 @@ func reportStringEscapes(ctx *Context, raw string, base int, whitelist string) {
|
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
|
|
126
|
+
// reportRegexEscapes walks the pattern body of a regex literal and reports
|
|
127
|
+
// backslashes that escape non-special characters. `base` is the source offset
|
|
128
|
+
// of `raw[0]`. Character-class context (`[…]`) widens the legal set slightly.
|
|
120
129
|
func reportRegexEscapes(ctx *Context, raw string, base int) {
|
|
121
130
|
if len(raw) < 3 || raw[0] != '/' {
|
|
122
131
|
return
|
|
@@ -166,6 +175,9 @@ func reportRegexEscapes(ctx *Context, raw string, base int) {
|
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
// isUselessStringEscape reports whether a backslash before `ch` is redundant
|
|
179
|
+
// inside a string or template literal. The `whitelist` contains the characters
|
|
180
|
+
// that are valid escape targets for the specific literal kind (string vs template).
|
|
169
181
|
func isUselessStringEscape(ch byte, whitelist string) bool {
|
|
170
182
|
// Whitespace + control chars are escape sequences too.
|
|
171
183
|
if ch < 0x20 {
|
|
@@ -181,6 +193,9 @@ func isUselessStringEscape(ch byte, whitelist string) bool {
|
|
|
181
193
|
return true
|
|
182
194
|
}
|
|
183
195
|
|
|
196
|
+
// isUselessRegexEscape reports whether a backslash before `ch` is redundant
|
|
197
|
+
// in a regex pattern. `inClass` is true when the escape occurs inside a `[…]`
|
|
198
|
+
// character class, which widens the set of meaningful escapes.
|
|
184
199
|
func isUselessRegexEscape(ch byte, inClass bool) bool {
|
|
185
200
|
if ch < 0x20 {
|
|
186
201
|
return false
|
package/linthost/rules_eval.go
CHANGED
|
@@ -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
|
}
|
|
@@ -256,6 +256,9 @@ func hasReflowAncestor(node *shimast.Node) bool {
|
|
|
256
256
|
return false
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
// isReflowKind reports whether `k` is one of the node kinds the
|
|
260
|
+
// format/print-width rule visits. Kept in sync with Visits() so
|
|
261
|
+
// hasReflowAncestor does not need to call Visits() at runtime.
|
|
259
262
|
func isReflowKind(k shimast.Kind) bool {
|
|
260
263
|
switch k {
|
|
261
264
|
case shimast.KindObjectLiteralExpression,
|
|
@@ -25,6 +25,9 @@ import (
|
|
|
25
25
|
// also intentionally out of scope.
|
|
26
26
|
type formatQuotes struct{}
|
|
27
27
|
|
|
28
|
+
// formatQuotesOptions mirrors `TtscLintRuleOptions.Quotes`. Prefer accepts
|
|
29
|
+
// `"double"` (the default, enforces double quotes) or `"single"` (enforces
|
|
30
|
+
// single quotes). Any other value is treated as the default.
|
|
28
31
|
type formatQuotesOptions struct {
|
|
29
32
|
Prefer string `json:"prefer"`
|
|
30
33
|
}
|
|
@@ -173,6 +173,8 @@ func loadSortImportsOptions(ctx *Context) resolvedSortImportsOptions {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
// hasThirdPartyGroup reports whether at least one group in the list is the
|
|
177
|
+
// third-party catchall. Used to decide whether a sentinel needs to be appended.
|
|
176
178
|
func hasThirdPartyGroup(groups []sortImportsGroup) bool {
|
|
177
179
|
for _, g := range groups {
|
|
178
180
|
if g.thirdParty {
|
|
@@ -449,6 +451,8 @@ func reportNamedSpecifierSort(ctx *Context, decl *shimast.Node, caseInsensitive
|
|
|
449
451
|
)
|
|
450
452
|
}
|
|
451
453
|
|
|
454
|
+
// joinSortedSpecifiers returns the specifier texts joined with ", " in their
|
|
455
|
+
// already-sorted order. The caller owns the sort; this is a formatting step only.
|
|
452
456
|
func joinSortedSpecifiers(entries []specifierEntry) string {
|
|
453
457
|
texts := make([]string, len(entries))
|
|
454
458
|
for i, e := range entries {
|
package/linthost/rules_gap.go
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// Miscellaneous AST-only rules that cover the gap between the core ESLint
|
|
2
|
+
// "Possible Problems" list and the @typescript-eslint strict/stylistic
|
|
3
|
+
// presets: empty static blocks, setter returns, unused labels, dynamic
|
|
4
|
+
// delete, nullish-coalescing with non-null assertions, unnecessary type
|
|
5
|
+
// constraints, unsafe built-in types, wrapper object types, useless
|
|
6
|
+
// constructors, non-literal enum members, type-assertion style, type
|
|
7
|
+
// definition style, dot notation, and unsafe declaration merging.
|
|
8
|
+
// Each rule is registered in init() at the bottom of the file.
|
|
1
9
|
package linthost
|
|
2
10
|
|
|
3
11
|
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
@@ -35,6 +43,9 @@ func (noSetterReturn) Check(ctx *Context, node *shimast.Node) {
|
|
|
35
43
|
ctx.Report(node, "Setter should not return a value.")
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
// isInsideDirectSetter reports whether node is lexically nested inside a
|
|
47
|
+
// SetAccessor without an intervening function boundary. "Direct" means the
|
|
48
|
+
// return value would actually be a setter return, not a nested callback's.
|
|
38
49
|
func isInsideDirectSetter(node *shimast.Node) bool {
|
|
39
50
|
for p := node.Parent; p != nil; p = p.Parent {
|
|
40
51
|
if p.Kind == shimast.KindSetAccessor {
|
|
@@ -99,6 +110,9 @@ func (noDynamicDelete) Check(ctx *Context, node *shimast.Node) {
|
|
|
99
110
|
ctx.Report(node, "Do not delete dynamically computed property keys.")
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
// isStaticPropertyKey reports whether node is a literal key that can
|
|
114
|
+
// appear in a delete expression without triggering no-dynamic-delete:
|
|
115
|
+
// string literals, no-substitution template literals, and numeric literals.
|
|
102
116
|
func isStaticPropertyKey(node *shimast.Node) bool {
|
|
103
117
|
node = stripParens(node)
|
|
104
118
|
if node == nil {
|
|
@@ -263,6 +277,9 @@ func fileShadowsWrapperName(file *shimast.SourceFile, name string) bool {
|
|
|
263
277
|
return false
|
|
264
278
|
}
|
|
265
279
|
|
|
280
|
+
// wrapperPrimitive maps a boxed object type name (e.g. "String") to its
|
|
281
|
+
// primitive keyword equivalent (e.g. "string"). Returns ("", false) for
|
|
282
|
+
// names that are not recognized wrapper types.
|
|
266
283
|
func wrapperPrimitive(name string) (string, bool) {
|
|
267
284
|
switch name {
|
|
268
285
|
case "String":
|
|
@@ -365,6 +382,9 @@ func (dotNotation) Check(ctx *Context, node *shimast.Node) {
|
|
|
365
382
|
ctx.Report(node, "Use dot notation instead of a string literal property access.")
|
|
366
383
|
}
|
|
367
384
|
|
|
385
|
+
// isSimpleIdentifierName reports whether value is a valid bare identifier
|
|
386
|
+
// (ASCII letters, digits, underscore, dollar sign; digits not first). Used
|
|
387
|
+
// by dot-notation to decide whether bracket access can become dot access.
|
|
368
388
|
func isSimpleIdentifierName(value string) bool {
|
|
369
389
|
if value == "" {
|
|
370
390
|
return false
|
|
@@ -67,13 +67,11 @@ func (noImportTypeSideEffects) Check(ctx *Context, node *shimast.Node) {
|
|
|
67
67
|
}
|
|
68
68
|
src := ctx.File.Text()
|
|
69
69
|
for _, spec := range named.Elements.Nodes {
|
|
70
|
-
// Locate the `type` keyword token at the head of
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// the source. SkipTrivia honors the lexer's notion of trivia, so
|
|
76
|
-
// the post-skip position is the actual first token byte.
|
|
70
|
+
// Locate the `type` keyword token at the head of each specifier by
|
|
71
|
+
// skipping leading trivia (whitespace + comments). A naive
|
|
72
|
+
// findKeyword scan would risk matching `type` inside a block comment
|
|
73
|
+
// such as `/* type alias */`, corrupting the source. SkipTrivia
|
|
74
|
+
// anchors the scan at the actual first token byte.
|
|
77
75
|
typePos := shimscanner.SkipTrivia(src, spec.Pos())
|
|
78
76
|
if typePos < 0 || typePos+len("type") > len(src) {
|
|
79
77
|
continue
|
package/linthost/rules_logic.go
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Correctness and equality rules — a focused set from ESLint's "Possible
|
|
2
|
+
// Problems" and "Suggestions" categories that catch logic errors rather than
|
|
3
|
+
// style issues: redundant boolean casts, unsafe negations, loose equality,
|
|
4
|
+
// NaN comparisons, constant conditions, and assignment-in-condition.
|
|
5
|
+
// AST-only, no scope analysis.
|
|
1
6
|
package linthost
|
|
2
7
|
|
|
3
8
|
import shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
@@ -71,6 +76,12 @@ func isInBooleanContext(node *shimast.Node) bool {
|
|
|
71
76
|
return false
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
// skipParents walks up through any wrapping ParenthesizedExpression nodes and
|
|
80
|
+
// returns the outermost parenthesized wrapper. This is the inverse of
|
|
81
|
+
// stripParens: where stripParens descends into the canonical inner expression,
|
|
82
|
+
// skipParents climbs up to the outermost paren so that structural parent
|
|
83
|
+
// comparisons (e.g. "is this expression the test of an if?") resolve against
|
|
84
|
+
// the node the parser actually attached as a child, not the inner form.
|
|
74
85
|
func skipParents(node *shimast.Node) *shimast.Node {
|
|
75
86
|
for node != nil && node.Parent != nil && node.Parent.Kind == shimast.KindParenthesizedExpression {
|
|
76
87
|
node = node.Parent
|
package/linthost/rules_loops.go
CHANGED
|
@@ -57,6 +57,10 @@ func (forDirection) Check(ctx *Context, node *shimast.Node) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// updateDirection returns the direction (+1 for incrementing, -1 for
|
|
61
|
+
// decrementing, 0 for unknown) that the incrementor expression moves the
|
|
62
|
+
// named counter variable. Only simple patterns are recognised: i++, i--,
|
|
63
|
+
// ++i, --i, i += N, and i -= N. Compound or computed expressions return 0.
|
|
60
64
|
func updateDirection(node *shimast.Node, counter string) int {
|
|
61
65
|
if node == nil || counter == "" {
|
|
62
66
|
return 0
|
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
|
|
@@ -110,6 +110,8 @@ func (noEmptyCharacterClass) Check(ctx *Context, node *shimast.Node) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// hasEmptyCharClass reports whether the regex source text src contains an
|
|
114
|
+
// empty character class (`[]` or `[^]`). Respects backslash escapes.
|
|
113
115
|
func hasEmptyCharClass(src string) bool {
|
|
114
116
|
// Walk the regex literal source manually, respecting escapes.
|
|
115
117
|
for i := 0; i < len(src); i++ {
|
|
@@ -143,6 +145,10 @@ func (noMisleadingCharacterClass) Check(ctx *Context, node *shimast.Node) {
|
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
// regexHasSurrogatePair reports whether the regex source text src contains
|
|
149
|
+
// a non-BMP character (code point >= U+10000) inside a character class
|
|
150
|
+
// without the `u` flag. Such characters are stored as surrogate pairs in
|
|
151
|
+
// the source and the class will only match one half of the pair.
|
|
146
152
|
func regexHasSurrogatePair(src string) bool {
|
|
147
153
|
// Strip the trailing flags so we don't misread the `u` flag — it
|
|
148
154
|
// suppresses this rule.
|
|
@@ -188,6 +194,10 @@ func (noLossOfPrecision) Check(ctx *Context, node *shimast.Node) {
|
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
196
|
|
|
197
|
+
// numericLiteralLosesPrecision reports whether the decimal integer literal
|
|
198
|
+
// text exceeds Number.MAX_SAFE_INTEGER (2^53 - 1). Non-decimal literals
|
|
199
|
+
// (hex, octal, binary) and literals with exponents or decimal points are
|
|
200
|
+
// exempt because their precision loss is caller-visible and intentional.
|
|
191
201
|
func numericLiteralLosesPrecision(text string) bool {
|
|
192
202
|
// Strip underscore separators, exponents, decimal/hex/oct/binary
|
|
193
203
|
// markers — for the simple-base-10 integer case the round-trip
|
|
@@ -204,7 +214,8 @@ func numericLiteralLosesPrecision(text string) bool {
|
|
|
204
214
|
if trimmed == "" {
|
|
205
215
|
return false
|
|
206
216
|
}
|
|
207
|
-
// 2^53 =
|
|
217
|
+
// Number.MAX_SAFE_INTEGER is 2^53-1 = 9007199254740991; 2^53 itself
|
|
218
|
+
// (9007199254740992) is the first integer that float64 cannot round-trip.
|
|
208
219
|
const maxSafe = "9007199254740992"
|
|
209
220
|
if len(trimmed) < len(maxSafe) {
|
|
210
221
|
return false
|
|
@@ -351,6 +362,9 @@ func (noControlRegex) Check(ctx *Context, node *shimast.Node) {
|
|
|
351
362
|
}
|
|
352
363
|
}
|
|
353
364
|
|
|
365
|
+
// regexContainsControl reports whether the regex source text src contains a
|
|
366
|
+
// literal control character (U+0000–U+001F, excluding \t, \n, \r) or a
|
|
367
|
+
// \xNN / \uNNNN escape that resolves to a control character.
|
|
354
368
|
func regexContainsControl(src string) bool {
|
|
355
369
|
for i := 0; i < len(src); i++ {
|
|
356
370
|
c := src[i]
|
|
@@ -382,6 +396,8 @@ func regexContainsControl(src string) bool {
|
|
|
382
396
|
return false
|
|
383
397
|
}
|
|
384
398
|
|
|
399
|
+
// hexDigit converts an ASCII hex byte ('0'-'9', 'a'-'f', 'A'-'F') to its
|
|
400
|
+
// integer value. Returns -1 for non-hex bytes.
|
|
385
401
|
func hexDigit(b byte) int {
|
|
386
402
|
switch {
|
|
387
403
|
case b >= '0' && b <= '9':
|
|
@@ -412,6 +428,10 @@ func (noIrregularWhitespace) Check(ctx *Context, node *shimast.Node) {
|
|
|
412
428
|
}
|
|
413
429
|
}
|
|
414
430
|
|
|
431
|
+
// isIrregularWhitespace reports whether rune r is a non-standard whitespace
|
|
432
|
+
// character that the TypeScript parser accepts but is almost certainly a
|
|
433
|
+
// copy-paste artifact: vertical tab, form feed, non-breaking space, and the
|
|
434
|
+
// various Unicode space and line separator code points.
|
|
415
435
|
func isIrregularWhitespace(r rune) bool {
|
|
416
436
|
switch r {
|
|
417
437
|
case '\v', '\f',
|
|
@@ -458,6 +478,9 @@ func (noFallthrough) Check(ctx *Context, node *shimast.Node) {
|
|
|
458
478
|
}
|
|
459
479
|
}
|
|
460
480
|
|
|
481
|
+
// isTerminating reports whether stmt is a statement that unconditionally
|
|
482
|
+
// transfers control out of the current block: break, continue, return,
|
|
483
|
+
// throw, or a block whose last statement is terminating.
|
|
461
484
|
func isTerminating(stmt *shimast.Node) bool {
|
|
462
485
|
if stmt == nil {
|
|
463
486
|
return false
|
|
@@ -64,6 +64,15 @@ func (awaitThenable) Check(ctx *Context, node *shimast.Node) {
|
|
|
64
64
|
// reference to globalPromise, and `GetPropertyOfType` filters `then` as
|
|
65
65
|
// a partial member, so without iterating constituents the rule would
|
|
66
66
|
// fire on legitimate code.
|
|
67
|
+
// isAwaitable reports whether t is safe to await. A type is awaitable when:
|
|
68
|
+
// - its flags include Any, Unknown, or Never (these escape static strictness);
|
|
69
|
+
// - it is a Promise (GetPromisedTypeOfPromise returns non-nil); or
|
|
70
|
+
// - it is thenable (has a callable `then` property).
|
|
71
|
+
//
|
|
72
|
+
// For union and intersection types the function recurses into constituents: if
|
|
73
|
+
// ANY constituent is awaitable the whole type is considered awaitable. This is
|
|
74
|
+
// necessary because GetPromisedTypeOfPromise returns nil on composite types
|
|
75
|
+
// like `Promise<X> | number` even though the expression can legally be awaited.
|
|
67
76
|
func isAwaitable(checker *shimchecker.Checker, t *shimchecker.Type) bool {
|
|
68
77
|
if checker == nil || t == nil {
|
|
69
78
|
return false
|
|
@@ -91,6 +100,9 @@ func isAwaitable(checker *shimchecker.Checker, t *shimchecker.Type) bool {
|
|
|
91
100
|
return isThenableType(checker, t)
|
|
92
101
|
}
|
|
93
102
|
|
|
103
|
+
// isThenableType reports whether t has a callable `then` property, which is
|
|
104
|
+
// the runtime-observable contract for "thenable" in the ES spec. The check
|
|
105
|
+
// intentionally mirrors what the JS engine uses at await-time.
|
|
94
106
|
func isThenableType(checker *shimchecker.Checker, t *shimchecker.Type) bool {
|
|
95
107
|
if checker == nil || t == nil {
|
|
96
108
|
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() {
|
|
@@ -253,10 +253,15 @@ func (noLoneBlocks) Check(ctx *Context, node *shimast.Node) {
|
|
|
253
253
|
switch parent.Kind {
|
|
254
254
|
case shimast.KindBlock, shimast.KindSourceFile, shimast.KindModuleBlock:
|
|
255
255
|
default:
|
|
256
|
+
// Block is the body of a control-flow statement (if/for/while/…) —
|
|
257
|
+
// those braces are not lone; only report blocks nested inside another
|
|
258
|
+
// statement list (another Block, SourceFile, or ModuleBlock).
|
|
256
259
|
return
|
|
257
260
|
}
|
|
258
|
-
//
|
|
259
|
-
// are
|
|
261
|
+
// isFunctionLikeKind returns false for all three parent kinds above
|
|
262
|
+
// (Block/SourceFile/ModuleBlock are never function-like), so this guard
|
|
263
|
+
// is a no-op. It is left in place to document intent: if the switch were
|
|
264
|
+
// ever widened to admit function-body containers, this guard would fire.
|
|
260
265
|
if isFunctionLikeKind(parent) {
|
|
261
266
|
return
|
|
262
267
|
}
|
|
@@ -783,8 +788,13 @@ func isProductiveExpression(node *shimast.Node) bool {
|
|
|
783
788
|
shimast.KindPrefixUnaryExpression,
|
|
784
789
|
shimast.KindPostfixUnaryExpression,
|
|
785
790
|
shimast.KindTaggedTemplateExpression:
|
|
786
|
-
//
|
|
787
|
-
//
|
|
791
|
+
// Most of these kinds are unconditionally productive (call, new, await,
|
|
792
|
+
// yield, delete, tagged template). The inner switch narrows the two kinds
|
|
793
|
+
// that can be non-productive: a BinaryExpression is only productive when
|
|
794
|
+
// it is an assignment, and a PrefixUnaryExpression is only productive when
|
|
795
|
+
// it is ++ or --. KindPostfixUnaryExpression is always productive (++ and
|
|
796
|
+
// -- are the only postfix operators). All un-matched cases reach the
|
|
797
|
+
// outer `return true` below.
|
|
788
798
|
switch expr.Kind {
|
|
789
799
|
case shimast.KindBinaryExpression:
|
|
790
800
|
bin := expr.AsBinaryExpression()
|
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
|