@ttsc/lint 0.5.0-dev.20260429

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.
@@ -0,0 +1,179 @@
1
+ package lint
2
+
3
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
+
5
+ // no-duplicate-case: duplicated `case` labels in a single switch.
6
+ // https://eslint.org/docs/latest/rules/no-duplicate-case
7
+ type noDuplicateCase struct{}
8
+
9
+ func (noDuplicateCase) Name() string { return "no-duplicate-case" }
10
+ func (noDuplicateCase) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSwitchStatement} }
11
+ func (noDuplicateCase) Check(ctx *Context, node *shimast.Node) {
12
+ sw := node.AsSwitchStatement()
13
+ if sw == nil || sw.CaseBlock == nil {
14
+ return
15
+ }
16
+ block := sw.CaseBlock.AsCaseBlock()
17
+ if block == nil || block.Clauses == nil {
18
+ return
19
+ }
20
+ seen := make(map[string]bool, len(block.Clauses.Nodes))
21
+ for _, clause := range block.Clauses.Nodes {
22
+ if clause == nil || clause.Kind != shimast.KindCaseClause {
23
+ continue
24
+ }
25
+ caseClause := clause.AsCaseOrDefaultClause()
26
+ if caseClause == nil || caseClause.Expression == nil {
27
+ continue
28
+ }
29
+ key := nodeText(ctx.File, caseClause.Expression)
30
+ if key == "" {
31
+ continue
32
+ }
33
+ if seen[key] {
34
+ ctx.Report(clause, "Duplicate case label.")
35
+ continue
36
+ }
37
+ seen[key] = true
38
+ }
39
+ }
40
+
41
+ // no-dupe-keys: duplicated property names in a single object literal.
42
+ // https://eslint.org/docs/latest/rules/no-dupe-keys
43
+ type noDupeKeys struct{}
44
+
45
+ func (noDupeKeys) Name() string { return "no-dupe-keys" }
46
+ func (noDupeKeys) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindObjectLiteralExpression} }
47
+ func (noDupeKeys) Check(ctx *Context, node *shimast.Node) {
48
+ obj := node.AsObjectLiteralExpression()
49
+ if obj == nil || obj.Properties == nil {
50
+ return
51
+ }
52
+ seen := make(map[string]*shimast.Node, len(obj.Properties.Nodes))
53
+ for _, prop := range obj.Properties.Nodes {
54
+ key := propertyKey(ctx.File, prop)
55
+ if key == "" {
56
+ continue
57
+ }
58
+ if first, ok := seen[key]; ok {
59
+ ctx.Report(prop, "Duplicate key '"+key+"'.")
60
+ _ = first
61
+ continue
62
+ }
63
+ seen[key] = prop
64
+ }
65
+ }
66
+
67
+ // no-dupe-args: duplicated parameter names in a single function head.
68
+ // https://eslint.org/docs/latest/rules/no-dupe-args
69
+ type noDupeArgs struct{}
70
+
71
+ func (noDupeArgs) Name() string { return "no-dupe-args" }
72
+ func (noDupeArgs) Visits() []shimast.Kind {
73
+ return []shimast.Kind{
74
+ shimast.KindFunctionDeclaration,
75
+ shimast.KindFunctionExpression,
76
+ shimast.KindArrowFunction,
77
+ shimast.KindMethodDeclaration,
78
+ shimast.KindConstructor,
79
+ shimast.KindGetAccessor,
80
+ shimast.KindSetAccessor,
81
+ }
82
+ }
83
+ func (noDupeArgs) Check(ctx *Context, node *shimast.Node) {
84
+ params := node.Parameters()
85
+ seen := make(map[string]bool, len(params))
86
+ for _, param := range params {
87
+ paramDecl := param.AsParameterDeclaration()
88
+ if paramDecl == nil {
89
+ continue
90
+ }
91
+ name := identifierText(paramDecl.Name())
92
+ if name == "" {
93
+ continue
94
+ }
95
+ if seen[name] {
96
+ ctx.Report(param, "Duplicate parameter name '"+name+"'.")
97
+ continue
98
+ }
99
+ seen[name] = true
100
+ }
101
+ }
102
+
103
+ // propertyKey returns a stable key for a property in an object literal.
104
+ // Computed names without a literal payload return "" so the dedupe pass
105
+ // can skip them safely (a typed field is fine — duplicate identical
106
+ // expressions still won't cause false positives because the same expr
107
+ // produces the same source text).
108
+ func propertyKey(file *shimast.SourceFile, prop *shimast.Node) string {
109
+ if prop == nil {
110
+ return ""
111
+ }
112
+ switch prop.Kind {
113
+ case shimast.KindPropertyAssignment:
114
+ assignment := prop.AsPropertyAssignment()
115
+ if assignment == nil {
116
+ return ""
117
+ }
118
+ return staticPropertyKey(file, assignment.Name())
119
+ case shimast.KindShorthandPropertyAssignment:
120
+ short := prop.AsShorthandPropertyAssignment()
121
+ if short == nil {
122
+ return ""
123
+ }
124
+ return staticPropertyKey(file, short.Name())
125
+ case shimast.KindMethodDeclaration:
126
+ method := prop.AsMethodDeclaration()
127
+ if method == nil {
128
+ return ""
129
+ }
130
+ return staticPropertyKey(file, method.Name())
131
+ case shimast.KindGetAccessor, shimast.KindSetAccessor:
132
+ // Getter and setter pairs share a name but are not duplicates.
133
+ // Add a kind suffix so the dedupe key separates them.
134
+ switch prop.Kind {
135
+ case shimast.KindGetAccessor:
136
+ get := prop.AsGetAccessorDeclaration()
137
+ if get == nil {
138
+ return ""
139
+ }
140
+ if k := staticPropertyKey(file, get.Name()); k != "" {
141
+ return "get:" + k
142
+ }
143
+ case shimast.KindSetAccessor:
144
+ set := prop.AsSetAccessorDeclaration()
145
+ if set == nil {
146
+ return ""
147
+ }
148
+ if k := staticPropertyKey(file, set.Name()); k != "" {
149
+ return "set:" + k
150
+ }
151
+ }
152
+ }
153
+ return ""
154
+ }
155
+
156
+ func staticPropertyKey(file *shimast.SourceFile, name *shimast.Node) string {
157
+ if name == nil {
158
+ return ""
159
+ }
160
+ switch name.Kind {
161
+ case shimast.KindIdentifier:
162
+ return identifierText(name)
163
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
164
+ return stringLiteralText(name)
165
+ case shimast.KindNumericLiteral, shimast.KindBigIntLiteral:
166
+ return numericLiteralText(name)
167
+ case shimast.KindComputedPropertyName:
168
+ // fall back to source text so a computed `[`foo`]` still compares
169
+ // against the literal `foo` form when both appear together.
170
+ return nodeText(file, name)
171
+ }
172
+ return ""
173
+ }
174
+
175
+ func init() {
176
+ Register(noDuplicateCase{})
177
+ Register(noDupeKeys{})
178
+ Register(noDupeArgs{})
179
+ }
@@ -0,0 +1,110 @@
1
+ package lint
2
+
3
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
+
5
+ // no-empty: empty block statements (`{}`), but allow empty catch
6
+ // clauses since they're idiomatic for "ignore the error".
7
+ // https://eslint.org/docs/latest/rules/no-empty
8
+ type noEmpty struct{}
9
+
10
+ func (noEmpty) Name() string { return "no-empty" }
11
+ func (noEmpty) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBlock} }
12
+ func (noEmpty) Check(ctx *Context, node *shimast.Node) {
13
+ block := node.AsBlock()
14
+ if block == nil || block.Statements == nil {
15
+ return
16
+ }
17
+ if len(block.Statements.Nodes) > 0 {
18
+ return
19
+ }
20
+ parent := node.Parent
21
+ if parent != nil && parent.Kind == shimast.KindCatchClause {
22
+ return // tolerated — see ESLint default options
23
+ }
24
+ if isFunctionLikeKind(parent) {
25
+ return // empty function body is `no-empty-function`'s job
26
+ }
27
+ ctx.Report(node, "Empty block statement.")
28
+ }
29
+
30
+ // no-empty-function: empty function / method / arrow / accessor bodies.
31
+ // https://eslint.org/docs/latest/rules/no-empty-function
32
+ type noEmptyFunction struct{}
33
+
34
+ func (noEmptyFunction) Name() string { return "no-empty-function" }
35
+ func (noEmptyFunction) Visits() []shimast.Kind {
36
+ return []shimast.Kind{
37
+ shimast.KindFunctionDeclaration,
38
+ shimast.KindFunctionExpression,
39
+ shimast.KindArrowFunction,
40
+ shimast.KindMethodDeclaration,
41
+ shimast.KindGetAccessor,
42
+ shimast.KindSetAccessor,
43
+ shimast.KindConstructor,
44
+ }
45
+ }
46
+ func (noEmptyFunction) Check(ctx *Context, node *shimast.Node) {
47
+ body := node.Body()
48
+ if body == nil {
49
+ return
50
+ }
51
+ if body.Kind != shimast.KindBlock {
52
+ return
53
+ }
54
+ block := body.AsBlock()
55
+ if block == nil || block.Statements == nil {
56
+ return
57
+ }
58
+ if len(block.Statements.Nodes) == 0 {
59
+ ctx.Report(node, "Unexpected empty function.")
60
+ }
61
+ }
62
+
63
+ // no-empty-pattern: `({}) => x` or `function ({}) {}` — destructuring
64
+ // patterns with no bindings are usually a bug.
65
+ // https://eslint.org/docs/latest/rules/no-empty-pattern
66
+ type noEmptyPattern struct{}
67
+
68
+ func (noEmptyPattern) Name() string { return "no-empty-pattern" }
69
+ func (noEmptyPattern) Visits() []shimast.Kind {
70
+ return []shimast.Kind{shimast.KindObjectBindingPattern, shimast.KindArrayBindingPattern}
71
+ }
72
+ func (noEmptyPattern) Check(ctx *Context, node *shimast.Node) {
73
+ pattern := node.AsBindingPattern()
74
+ if pattern == nil || pattern.Elements == nil {
75
+ return
76
+ }
77
+ if len(pattern.Elements.Nodes) == 0 {
78
+ shape := "object"
79
+ if node.Kind == shimast.KindArrayBindingPattern {
80
+ shape = "array"
81
+ }
82
+ ctx.Report(node, "Unexpected empty "+shape+" pattern.")
83
+ }
84
+ }
85
+
86
+ // isFunctionLikeKind reports whether the node represents a function-like
87
+ // host whose body is the relevant scope for the empty check.
88
+ func isFunctionLikeKind(n *shimast.Node) bool {
89
+ if n == nil {
90
+ return false
91
+ }
92
+ switch n.Kind {
93
+ case
94
+ shimast.KindFunctionDeclaration,
95
+ shimast.KindFunctionExpression,
96
+ shimast.KindArrowFunction,
97
+ shimast.KindMethodDeclaration,
98
+ shimast.KindGetAccessor,
99
+ shimast.KindSetAccessor,
100
+ shimast.KindConstructor:
101
+ return true
102
+ }
103
+ return false
104
+ }
105
+
106
+ func init() {
107
+ Register(noEmpty{})
108
+ Register(noEmptyFunction{})
109
+ Register(noEmptyPattern{})
110
+ }
@@ -0,0 +1,57 @@
1
+ package lint
2
+
3
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
+
5
+ // no-eval: forbid `eval(...)` calls. Members of nested namespaces (e.g.
6
+ // `globalThis.eval(...)`) are not flagged here — that's
7
+ // `no-implied-eval`, which we don't ship in v0.
8
+ // https://eslint.org/docs/latest/rules/no-eval
9
+ type noEval struct{}
10
+
11
+ func (noEval) Name() string { return "no-eval" }
12
+ func (noEval) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindCallExpression} }
13
+ func (noEval) Check(ctx *Context, node *shimast.Node) {
14
+ call := node.AsCallExpression()
15
+ if call == nil {
16
+ return
17
+ }
18
+ if callCalleeName(call) == "eval" {
19
+ ctx.Report(node, "eval can be harmful.")
20
+ }
21
+ }
22
+
23
+ // no-script-url: forbid `"javascript:..."` literals. Often used to inject
24
+ // inline JS via DOM `href`/`src`; legacy and dangerous.
25
+ // https://eslint.org/docs/latest/rules/no-script-url
26
+ type noScriptURL struct{}
27
+
28
+ func (noScriptURL) Name() string { return "no-script-url" }
29
+ func (noScriptURL) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral} }
30
+ func (noScriptURL) Check(ctx *Context, node *shimast.Node) {
31
+ text := stringLiteralText(node)
32
+ if isJavaScriptURL(text) {
33
+ ctx.Report(node, "Script URL is a form of eval.")
34
+ }
35
+ }
36
+
37
+ func isJavaScriptURL(text string) bool {
38
+ const prefix = "javascript:"
39
+ if len(text) < len(prefix) {
40
+ return false
41
+ }
42
+ for i := 0; i < len(prefix); i++ {
43
+ c := text[i]
44
+ if c >= 'A' && c <= 'Z' {
45
+ c += 'a' - 'A'
46
+ }
47
+ if c != prefix[i] {
48
+ return false
49
+ }
50
+ }
51
+ return true
52
+ }
53
+
54
+ func init() {
55
+ Register(noEval{})
56
+ Register(noScriptURL{})
57
+ }
@@ -0,0 +1,123 @@
1
+ package lint
2
+
3
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
+
5
+ // no-unsafe-finally: `return` / `break` / `continue` / `throw` inside a
6
+ // `finally` clause silently overrides the in-flight exception or value.
7
+ // https://eslint.org/docs/latest/rules/no-unsafe-finally
8
+ type noUnsafeFinally struct{}
9
+
10
+ func (noUnsafeFinally) Name() string { return "no-unsafe-finally" }
11
+ func (noUnsafeFinally) Visits() []shimast.Kind {
12
+ return []shimast.Kind{
13
+ shimast.KindReturnStatement,
14
+ shimast.KindBreakStatement,
15
+ shimast.KindContinueStatement,
16
+ shimast.KindThrowStatement,
17
+ }
18
+ }
19
+ func (noUnsafeFinally) Check(ctx *Context, node *shimast.Node) {
20
+ finallyAncestor := walkToFinally(node)
21
+ if finallyAncestor == nil {
22
+ return
23
+ }
24
+ keyword := keywordOfControl(node)
25
+ ctx.Report(node, "Unsafe usage of "+keyword+".")
26
+ }
27
+
28
+ func walkToFinally(node *shimast.Node) *shimast.Node {
29
+ cur := node.Parent
30
+ for cur != nil {
31
+ if isFunctionLikeKind(cur) || cur.Kind == shimast.KindSourceFile {
32
+ return nil
33
+ }
34
+ if cur.Kind == shimast.KindBlock {
35
+ grand := cur.Parent
36
+ if grand != nil && grand.Kind == shimast.KindTryStatement {
37
+ try := grand.AsTryStatement()
38
+ if try != nil && try.FinallyBlock == cur {
39
+ return cur
40
+ }
41
+ }
42
+ }
43
+ // `break` / `continue` inside an inner loop within finally
44
+ // targets that loop and is therefore safe.
45
+ switch cur.Kind {
46
+ case shimast.KindForStatement,
47
+ shimast.KindForInStatement,
48
+ shimast.KindForOfStatement,
49
+ shimast.KindWhileStatement,
50
+ shimast.KindDoStatement,
51
+ shimast.KindSwitchStatement:
52
+ if node.Kind == shimast.KindBreakStatement || node.Kind == shimast.KindContinueStatement {
53
+ return nil
54
+ }
55
+ }
56
+ cur = cur.Parent
57
+ }
58
+ return nil
59
+ }
60
+
61
+ func keywordOfControl(node *shimast.Node) string {
62
+ switch node.Kind {
63
+ case shimast.KindReturnStatement:
64
+ return "return"
65
+ case shimast.KindBreakStatement:
66
+ return "break"
67
+ case shimast.KindContinueStatement:
68
+ return "continue"
69
+ case shimast.KindThrowStatement:
70
+ return "throw"
71
+ }
72
+ return "control flow"
73
+ }
74
+
75
+ // no-useless-catch: `catch (err) { throw err; }` adds no behavior.
76
+ // https://eslint.org/docs/latest/rules/no-useless-catch
77
+ type noUselessCatch struct{}
78
+
79
+ func (noUselessCatch) Name() string { return "no-useless-catch" }
80
+ func (noUselessCatch) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindCatchClause} }
81
+ func (noUselessCatch) Check(ctx *Context, node *shimast.Node) {
82
+ clause := node.AsCatchClause()
83
+ if clause == nil || clause.VariableDeclaration == nil || clause.Block == nil {
84
+ return
85
+ }
86
+ binding := clause.VariableDeclaration.AsVariableDeclaration()
87
+ if binding == nil {
88
+ return
89
+ }
90
+ bindingName := identifierText(binding.Name())
91
+ if bindingName == "" {
92
+ return
93
+ }
94
+ block := clause.Block.AsBlock()
95
+ if block == nil || block.Statements == nil || len(block.Statements.Nodes) != 1 {
96
+ return
97
+ }
98
+ stmt := block.Statements.Nodes[0]
99
+ if stmt == nil || stmt.Kind != shimast.KindThrowStatement {
100
+ return
101
+ }
102
+ throw := stmt.AsThrowStatement()
103
+ if throw == nil {
104
+ return
105
+ }
106
+ if identifierText(throw.Expression) != bindingName {
107
+ return
108
+ }
109
+ // Ignore when the surrounding try-catch has a `finally` block — the
110
+ // catch may exist solely to keep the finally semantics intact.
111
+ if try := node.Parent; try != nil && try.Kind == shimast.KindTryStatement {
112
+ tryStmt := try.AsTryStatement()
113
+ if tryStmt != nil && tryStmt.FinallyBlock != nil {
114
+ return
115
+ }
116
+ }
117
+ ctx.Report(node, "Unnecessary try/catch wrapper.")
118
+ }
119
+
120
+ func init() {
121
+ Register(noUnsafeFinally{})
122
+ Register(noUselessCatch{})
123
+ }