Skip to content

Commit c854ced

Browse files
authored
feat: implement '@typescript/no-non-null-assertion' (#217)
Co-authored-by: heyongqi10 <[email protected]>
1 parent 51b1826 commit c854ced

File tree

9 files changed

+336
-1
lines changed

9 files changed

+336
-1
lines changed

cmd/rslint/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/web-infra-dev/rslint/internal/rules/no_misused_spread"
3535
"github.com/web-infra-dev/rslint/internal/rules/no_mixed_enums"
3636
"github.com/web-infra-dev/rslint/internal/rules/no_namespace"
37+
"github.com/web-infra-dev/rslint/internal/rules/no_non_null_assertion"
3738
"github.com/web-infra-dev/rslint/internal/rules/no_redundant_type_constituents"
3839
"github.com/web-infra-dev/rslint/internal/rules/no_require_imports"
3940
"github.com/web-infra-dev/rslint/internal/rules/no_unnecessary_boolean_literal_compare"
@@ -126,6 +127,7 @@ func (h *IPCHandler) HandleLint(req api.LintRequest) (*api.LintResponse, error)
126127
no_misused_spread.NoMisusedSpreadRule,
127128
no_mixed_enums.NoMixedEnumsRule,
128129
no_namespace.NoNamespaceRule,
130+
no_non_null_assertion.NoNonNullAssertionRule,
129131
no_redundant_type_constituents.NoRedundantTypeConstituentsRule,
130132
no_require_imports.NoRequireImportsRule,
131133
no_unnecessary_boolean_literal_compare.NoUnnecessaryBooleanLiteralCompareRule,

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/web-infra-dev/rslint/internal/rules/no_misused_spread"
2626
"github.com/web-infra-dev/rslint/internal/rules/no_mixed_enums"
2727
"github.com/web-infra-dev/rslint/internal/rules/no_namespace"
28+
"github.com/web-infra-dev/rslint/internal/rules/no_non_null_assertion"
2829
"github.com/web-infra-dev/rslint/internal/rules/no_redundant_type_constituents"
2930
"github.com/web-infra-dev/rslint/internal/rules/no_require_imports"
3031
"github.com/web-infra-dev/rslint/internal/rules/no_unnecessary_boolean_literal_compare"
@@ -286,6 +287,7 @@ func RegisterAllTypeScriptEslintPluginRules() {
286287
GlobalRuleRegistry.Register("@typescript-eslint/no-misused-spread", no_misused_spread.NoMisusedSpreadRule)
287288
GlobalRuleRegistry.Register("@typescript-eslint/no-mixed-enums", no_mixed_enums.NoMixedEnumsRule)
288289
GlobalRuleRegistry.Register("@typescript-eslint/no-namespace", no_namespace.NoNamespaceRule)
290+
GlobalRuleRegistry.Register("@typescript-eslint/no-non-null-assertion", no_non_null_assertion.NoNonNullAssertionRule)
289291
GlobalRuleRegistry.Register("@typescript-eslint/no-redundant-type-constituents", no_redundant_type_constituents.NoRedundantTypeConstituentsRule)
290292
GlobalRuleRegistry.Register("@typescript-eslint/no-require-imports", no_require_imports.NoRequireImportsRule)
291293
GlobalRuleRegistry.Register("@typescript-eslint/no-unnecessary-boolean-literal-compare", no_unnecessary_boolean_literal_compare.NoUnnecessaryBooleanLiteralCompareRule)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package no_non_null_assertion
2+
3+
import (
4+
"github.com/microsoft/typescript-go/shim/ast"
5+
"github.com/web-infra-dev/rslint/internal/rule"
6+
)
7+
8+
// buildNoNonNullAssertionMessage creates a standardized rule message for the no-non-null-assertion rule.
9+
// This function returns a RuleMessage with a unique identifier and descriptive text about the rule violation.
10+
func buildNoNonNullAssertionMessage() rule.RuleMessage {
11+
return rule.RuleMessage{
12+
Id: "noNonNull",
13+
Description: "Non-null assertion operator (!) is not allowed.",
14+
}
15+
}
16+
17+
// NoNonNullAssertionRule is a linting rule that detects and reports the usage of non-null assertion operators (!).
18+
// The rule allows non-null assertions only in specific contexts where they are necessary, such as
19+
// the left side of assignment expressions where TypeScript requires non-null types.
20+
//
21+
// Rule Configuration:
22+
// - Name: "no-non-null-assertion"
23+
// - Purpose: Prevents unsafe usage of non-null assertions that can lead to runtime errors
24+
// - Exceptions: Assignment expressions where non-null assertion is required by TypeScript
25+
//
26+
// Example violations:
27+
//
28+
// const value = obj!.property; // ❌ Not allowed
29+
// const value = obj?.property; // ✅ Use optional chaining instead
30+
//
31+
// Example allowed usage:
32+
//
33+
// obj!.property = value; // ✅ Allowed in assignment left side
34+
var NoNonNullAssertionRule = rule.CreateRule(rule.Rule{
35+
Name: "no-non-null-assertion",
36+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
37+
return rule.RuleListeners{
38+
// Listen for non-null assertion expressions (!)
39+
ast.KindNonNullExpression: func(node *ast.Node) {
40+
// Check if the non-null assertion is used in an assignment expression
41+
parent := node.Parent
42+
if parent != nil {
43+
// Allow non-null assertions in assignment expressions (left side)
44+
if ast.IsAssignmentExpression(parent, true) {
45+
binaryExpr := parent.AsBinaryExpression()
46+
if binaryExpr != nil && binaryExpr.Left == node {
47+
return
48+
}
49+
}
50+
51+
// Allow non-null assertions in destructuring assignments
52+
if ast.IsArrayLiteralExpression(parent) {
53+
// Check if this array literal is part of a destructuring assignment
54+
grandParent := parent.Parent
55+
if grandParent != nil && ast.IsBinaryExpression(grandParent) {
56+
binaryExpr := grandParent.AsBinaryExpression()
57+
if binaryExpr != nil && binaryExpr.OperatorToken.Kind == ast.KindEqualsToken && binaryExpr.Left == parent {
58+
return
59+
}
60+
}
61+
}
62+
63+
// Allow non-null assertions in parenthesized expressions that are part of assignments
64+
if ast.IsParenthesizedExpression(parent) {
65+
grandParent := parent.Parent
66+
if grandParent != nil && ast.IsBinaryExpression(grandParent) {
67+
binaryExpr := grandParent.AsBinaryExpression()
68+
if binaryExpr != nil && binaryExpr.OperatorToken.Kind == ast.KindEqualsToken && binaryExpr.Left == parent {
69+
return
70+
}
71+
}
72+
}
73+
74+
// Allow non-null assertions in type assertions that are part of assignments
75+
if ast.IsAssertionExpression(parent) {
76+
grandParent := parent.Parent
77+
if grandParent != nil {
78+
if ast.IsBinaryExpression(grandParent) {
79+
binaryExpr := grandParent.AsBinaryExpression()
80+
if binaryExpr != nil && binaryExpr.OperatorToken.Kind == ast.KindEqualsToken && binaryExpr.Left == parent {
81+
return
82+
}
83+
} else if ast.IsParenthesizedExpression(grandParent) {
84+
greatGrandParent := grandParent.Parent
85+
if greatGrandParent != nil && ast.IsBinaryExpression(greatGrandParent) {
86+
binaryExpr := greatGrandParent.AsBinaryExpression()
87+
if binaryExpr != nil && binaryExpr.OperatorToken.Kind == ast.KindEqualsToken && binaryExpr.Left == grandParent {
88+
return
89+
}
90+
}
91+
}
92+
}
93+
}
94+
}
95+
96+
// Report the non-null assertion usage as a violation
97+
// This helps developers identify potentially unsafe code patterns
98+
ctx.ReportNode(node, buildNoNonNullAssertionMessage())
99+
},
100+
}
101+
},
102+
})
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package no_non_null_assertion
2+
3+
import (
4+
"testing"
5+
6+
"github.com/web-infra-dev/rslint/internal/rule_tester"
7+
"github.com/web-infra-dev/rslint/internal/rules/fixtures"
8+
)
9+
10+
func TestNoNonNullAssertionRule(t *testing.T) {
11+
validTestCases := []rule_tester.ValidTestCase{
12+
// Basic valid cases - no non-null assertions
13+
{Code: `const foo = "hello"; console.log(foo);`},
14+
{Code: `function foo(bar: string) { console.log(bar); }`},
15+
{Code: `const foo: string | null = "hello"; if (foo) { console.log(foo); }`},
16+
{Code: `const foo: string | undefined = "hello"; if (foo !== undefined) { console.log(foo); }`},
17+
{Code: `const foo: string | null = "hello"; const bar = foo || "default";`},
18+
{Code: `const foo: string | null = "hello"; const bar = foo ?? "default";`},
19+
{Code: `const foo: string | null = "hello"; const bar = foo?.length || 0;`},
20+
{Code: `const foo: string | null = "hello"; if (foo !== null) { console.log(foo); }`},
21+
}
22+
23+
invalidTestCases := []rule_tester.InvalidTestCase{
24+
// Basic non-null assertion - should report error
25+
{
26+
Code: `const foo: string | null = "hello"; const bar = foo!;`,
27+
Errors: []rule_tester.InvalidTestCaseError{
28+
{
29+
MessageId: "noNonNull",
30+
},
31+
},
32+
},
33+
// Non-null assertion in property access
34+
{
35+
Code: `const foo: string | null = "hello"; const bar = foo!.length;`,
36+
Errors: []rule_tester.InvalidTestCaseError{
37+
{
38+
MessageId: "noNonNull",
39+
},
40+
},
41+
},
42+
// Non-null assertion in function call
43+
{
44+
Code: `const foo: string | null = "hello"; const bar = foo!.toUpperCase();`,
45+
Errors: []rule_tester.InvalidTestCaseError{
46+
{
47+
MessageId: "noNonNull",
48+
},
49+
},
50+
},
51+
// Non-null assertion in array access
52+
{
53+
Code: `const foo: string[] | null = ["hello"]; const bar = foo![0];`,
54+
Errors: []rule_tester.InvalidTestCaseError{
55+
{
56+
MessageId: "noNonNull",
57+
},
58+
},
59+
},
60+
// Chained non-null assertions - should report 2 errors
61+
{
62+
Code: `const foo: string | null = "hello"; const bar = foo!!;`,
63+
Errors: []rule_tester.InvalidTestCaseError{
64+
{
65+
MessageId: "noNonNull",
66+
},
67+
{
68+
MessageId: "noNonNull",
69+
},
70+
},
71+
},
72+
// Non-null assertion in conditional expression
73+
{
74+
Code: `const foo: string | null = "hello"; const bar = foo! ? "yes" : "no";`,
75+
Errors: []rule_tester.InvalidTestCaseError{
76+
{
77+
MessageId: "noNonNull",
78+
},
79+
},
80+
},
81+
// Non-null assertion in logical expression
82+
{
83+
Code: `const foo: string | null = "hello"; const bar = foo! && "yes";`,
84+
Errors: []rule_tester.InvalidTestCaseError{
85+
{
86+
MessageId: "noNonNull",
87+
},
88+
},
89+
},
90+
// Non-null assertion in return statement
91+
{
92+
Code: `function test(): string { const foo: string | null = "hello"; return foo!; }`,
93+
Errors: []rule_tester.InvalidTestCaseError{
94+
{
95+
MessageId: "noNonNull",
96+
},
97+
},
98+
},
99+
// Non-null assertion in variable declaration
100+
{
101+
Code: `let foo: string | null = "hello"; foo = foo!;`,
102+
Errors: []rule_tester.InvalidTestCaseError{
103+
{
104+
MessageId: "noNonNull",
105+
},
106+
},
107+
},
108+
// Non-null assertion in parameters
109+
{
110+
Code: `function test(foo: string | null) { const bar = foo!; }`,
111+
Errors: []rule_tester.InvalidTestCaseError{
112+
{
113+
MessageId: "noNonNull",
114+
},
115+
},
116+
},
117+
// Non-null assertion in object properties
118+
{
119+
Code: `const obj = { foo: "hello" as string | null }; const bar = obj.foo!;`,
120+
Errors: []rule_tester.InvalidTestCaseError{
121+
{
122+
MessageId: "noNonNull",
123+
},
124+
},
125+
},
126+
// Non-null assertion in template strings
127+
{
128+
Code: "const foo: string | null = \"hello\"; const bar = `Value: ${foo!}`;",
129+
Errors: []rule_tester.InvalidTestCaseError{
130+
{
131+
MessageId: "noNonNull",
132+
},
133+
},
134+
},
135+
// Non-null assertion in type assertion
136+
{
137+
Code: `const foo: string | null = "hello"; const bar = (foo! as string).length;`,
138+
Errors: []rule_tester.InvalidTestCaseError{
139+
{
140+
MessageId: "noNonNull",
141+
},
142+
},
143+
},
144+
// Non-null assertion in generics
145+
{
146+
Code: `function test<T extends string | null>(foo: T): T { return foo!; }`,
147+
Errors: []rule_tester.InvalidTestCaseError{
148+
{
149+
MessageId: "noNonNull",
150+
},
151+
},
152+
},
153+
// Non-null assertion in union types
154+
{
155+
Code: `const foo: (string | null)[] = ["hello"]; const bar = foo[0]!;`,
156+
Errors: []rule_tester.InvalidTestCaseError{
157+
{
158+
MessageId: "noNonNull",
159+
},
160+
},
161+
},
162+
// Non-null assertion in nested expressions
163+
{
164+
Code: `const foo: string | null = "hello"; const bar = (foo! + "world").length;`,
165+
Errors: []rule_tester.InvalidTestCaseError{
166+
{
167+
MessageId: "noNonNull",
168+
},
169+
},
170+
},
171+
// Non-null assertion in ternary expressions
172+
{
173+
Code: `const foo: string | null = "hello"; const bar = foo! ? foo!.length : 0;`,
174+
Errors: []rule_tester.InvalidTestCaseError{
175+
{
176+
MessageId: "noNonNull",
177+
},
178+
{
179+
MessageId: "noNonNull",
180+
},
181+
},
182+
},
183+
}
184+
185+
rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &NoNonNullAssertionRule, validTestCases, invalidTestCases)
186+
}
187+
188+
func TestNoNonNullAssertionOptionsParsing(t *testing.T) {
189+
// Test basic rule information
190+
rule := NoNonNullAssertionRule
191+
if rule.Name != "@typescript-eslint/no-non-null-assertion" {
192+
t.Errorf("Expected rule name to be '@typescript-eslint/no-non-null-assertion', got %s", rule.Name)
193+
}
194+
}
195+
196+
func TestNoNonNullAssertionMessage(t *testing.T) {
197+
msg := buildNoNonNullAssertionMessage()
198+
if msg.Id != "noNonNull" {
199+
t.Errorf("Expected message ID to be 'noNonNull', got %s", msg.Id)
200+
}
201+
if msg.Description != "Non-null assertion operator (!) is not allowed." {
202+
t.Errorf("Expected description to be 'Non-null assertion operator (!) is not allowed.', got %s", msg.Description)
203+
}
204+
}
205+
206+
// Test edge cases
207+
func TestNoNonNullAssertionEdgeCases(t *testing.T) {
208+
validTestCases := []rule_tester.ValidTestCase{
209+
// Nested assignment expressions
210+
{Code: `let obj: { prop?: string } = {}; obj.prop! = "value";`},
211+
212+
// Non-null assertion in destructuring assignment
213+
{Code: `let arr: (string | null)[] = ["hello"]; [arr[0]!] = ["world"];`},
214+
215+
// Complex assignment expressions
216+
{Code: `let foo: string | null = "hello"; (foo! as any) = "world";`},
217+
}
218+
219+
invalidTestCases := []rule_tester.InvalidTestCase{
220+
// These test cases are already included in the main test function
221+
}
222+
223+
rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &NoNonNullAssertionRule, validTestCases, invalidTestCases)
224+
}

packages/rslint-test-tools/rstest.config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ export default defineConfig({
1515
'./tests/typescript-eslint/rules/no-require-imports.test.ts',
1616
'./tests/typescript-eslint/rules/no-duplicate-type-constituents.test.ts',
1717
'./tests/typescript-eslint/rules/no_namespace.test.ts',
18+
'./tests/typescript-eslint/rules/no_non_null_assertion.test.ts',
1819
],
1920
});

packages/rslint/fixtures/rslint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"@typescript-eslint/no-empty-function": "error",
2020
"@typescript-eslint/no-empty-interface": "error",
2121
"@typescript-eslint/no-require-imports": "error",
22-
"@typescript-eslint/no-namespace": "error"
22+
"@typescript-eslint/no-namespace": "error",
23+
"@typescript-eslint/no-non-null-assertion": "error"
2324
},
2425
"plugins": ["@typescript-eslint"]
2526
}

packages/vscode-extension/src/Extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export class Extension implements Disposable {
5858
): Rslint {
5959
if (this.rslintInstances.has(id)) {
6060
this.logger.warn(`Rslint instance with id '${id}' already exists`);
61+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6162
return this.rslintInstances.get(id)!;
6263
}
6364

packages/vscode-extension/src/Rslint.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ export class Rslint implements Disposable {
293293
}
294294

295295
this.logger.debug('Final Rslint binary path:', finalBinPath);
296+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296297
return finalBinPath!;
297298
}
298299
}

0 commit comments

Comments
 (0)