Skip to content

Commit 084ce73

Browse files
committed
feat: port rule no-setter-return
1 parent 1e5a9c6 commit 084ce73

File tree

7 files changed

+282
-0
lines changed

7 files changed

+282
-0
lines changed

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ import (
123123
"github.com/web-infra-dev/rslint/internal/rules/no_loss_of_precision"
124124
"github.com/web-infra-dev/rslint/internal/rules/no_sparse_arrays"
125125
"github.com/web-infra-dev/rslint/internal/rules/no_template_curly_in_string"
126+
"github.com/web-infra-dev/rslint/internal/rules/no_setter_return"
126127
)
127128

128129
// RslintConfig represents the top-level configuration array
@@ -422,6 +423,7 @@ func registerAllCoreEslintRules() {
422423
GlobalRuleRegistry.Register("no-loss-of-precision", no_loss_of_precision.NoLossOfPrecisionRule)
423424
GlobalRuleRegistry.Register("no-template-curly-in-string", no_template_curly_in_string.NoTemplateCurlyInString)
424425
GlobalRuleRegistry.Register("no-sparse-arrays", no_sparse_arrays.NoSparseArraysRule)
426+
GlobalRuleRegistry.Register("no-setter-return", no_setter_return.NoSetterReturnRule)
425427
}
426428

427429
// getAllTypeScriptEslintPluginRules returns all rules from the global registry.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package no_setter_return
2+
3+
import (
4+
"github.com/microsoft/typescript-go/shim/ast"
5+
"github.com/web-infra-dev/rslint/internal/rule"
6+
)
7+
8+
// buildSetterMessage returns the diagnostic message for a return-with-value in a setter.
9+
func buildSetterMessage() rule.RuleMessage {
10+
return rule.RuleMessage{
11+
Id: "setter",
12+
Description: "Setter cannot return a value.",
13+
}
14+
}
15+
16+
// findEnclosingSetter walks up the parent chain to find if the node is inside a setter.
17+
// Returns the setter node if found, or nil if a different function boundary is hit first.
18+
func findEnclosingSetter(node *ast.Node) *ast.Node {
19+
current := node.Parent
20+
for current != nil {
21+
switch current.Kind {
22+
case ast.KindSetAccessor:
23+
return current
24+
case ast.KindFunctionDeclaration,
25+
ast.KindFunctionExpression,
26+
ast.KindArrowFunction,
27+
ast.KindMethodDeclaration,
28+
ast.KindGetAccessor,
29+
ast.KindConstructor:
30+
// Hit a different function boundary; stop searching.
31+
return nil
32+
}
33+
current = current.Parent
34+
}
35+
return nil
36+
}
37+
38+
// NoSetterReturnRule disallows returning a value from a setter.
39+
// Setters cannot meaningfully return values; any return value is silently ignored.
40+
// A bare `return;` (without a value) is allowed for control flow.
41+
var NoSetterReturnRule = rule.Rule{
42+
Name: "no-setter-return",
43+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
44+
return rule.RuleListeners{
45+
ast.KindReturnStatement: func(node *ast.Node) {
46+
ret := node.AsReturnStatement()
47+
// Allow bare return (no expression)
48+
if ret.Expression == nil {
49+
return
50+
}
51+
// Check if the return statement is inside a setter
52+
if findEnclosingSetter(node) != nil {
53+
ctx.ReportNode(node, buildSetterMessage())
54+
}
55+
},
56+
}
57+
},
58+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# no-setter-return
2+
3+
## Rule Details
4+
5+
Disallows returning a value from a setter. Setters cannot meaningfully return values since any return value is silently ignored by the JavaScript engine. A bare `return;` (without a value) is allowed for control flow purposes.
6+
7+
Examples of **incorrect** code for this rule:
8+
9+
```javascript
10+
var foo = {
11+
set a(val) {
12+
return 1;
13+
},
14+
};
15+
16+
class A {
17+
set a(val) {
18+
return val;
19+
}
20+
}
21+
22+
var bar = {
23+
set a(val) {
24+
return undefined;
25+
},
26+
};
27+
```
28+
29+
Examples of **correct** code for this rule:
30+
31+
```javascript
32+
var foo = {
33+
set a(val) {
34+
val = 1;
35+
},
36+
};
37+
38+
class A {
39+
set a(val) {
40+
if (!val) {
41+
return; // bare return for flow control is fine
42+
}
43+
this._a = val;
44+
}
45+
}
46+
47+
class B {
48+
get a() {
49+
return this._a; // getters can return values
50+
}
51+
}
52+
```
53+
54+
## Original Documentation
55+
56+
- [ESLint no-setter-return](https://eslint.org/docs/latest/rules/no-setter-return)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package no_setter_return
2+
3+
import (
4+
"testing"
5+
6+
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures"
7+
"github.com/web-infra-dev/rslint/internal/rule_tester"
8+
)
9+
10+
func TestNoSetterReturnRule(t *testing.T) {
11+
rule_tester.RunRuleTester(
12+
fixtures.GetRootDir(),
13+
"tsconfig.json",
14+
t,
15+
&NoSetterReturnRule,
16+
// Valid cases
17+
[]rule_tester.ValidTestCase{
18+
// Object literal setter with assignment (no return)
19+
{Code: `var foo = { set a(val) { val = 1; } };`},
20+
// Class setter with assignment (no return)
21+
{Code: `class A { set a(val) { val = 1; } }`},
22+
// Object literal setter with bare return (allowed)
23+
{Code: `var foo = { set a(val) { return; } };`},
24+
// Getter returning a value (not a setter)
25+
{Code: `class A { get a() { return 1; } }`},
26+
// Object literal getter returning a value
27+
{Code: `var foo = { get a() { return 1; } };`},
28+
// Setter with conditional bare return
29+
{Code: `var foo = { set a(val) { if (val) { return; } } };`},
30+
// Class setter with bare return
31+
{Code: `class A { set a(val) { return; } }`},
32+
// Nested function inside setter can return values
33+
{Code: `var foo = { set a(val) { function inner() { return 1; } } };`},
34+
// Arrow function inside setter can return values
35+
{Code: `class A { set a(val) { const fn = () => { return 1; }; } }`},
36+
// Function expression inside setter can return values
37+
{Code: `var foo = { set a(val) { var fn = function() { return 1; }; } };`},
38+
// Regular method returning a value
39+
{Code: `class A { method() { return 1; } }`},
40+
// Regular function returning a value
41+
{Code: `function fn() { return 1; }`},
42+
},
43+
// Invalid cases
44+
[]rule_tester.InvalidTestCase{
45+
// Object literal setter returning a number
46+
{
47+
Code: `var foo = { set a(val) { return 1; } };`,
48+
Errors: []rule_tester.InvalidTestCaseError{
49+
{
50+
MessageId: "setter",
51+
Line: 1,
52+
Column: 26,
53+
},
54+
},
55+
},
56+
// Class setter returning the parameter
57+
{
58+
Code: `class A { set a(val) { return val; } }`,
59+
Errors: []rule_tester.InvalidTestCaseError{
60+
{
61+
MessageId: "setter",
62+
Line: 1,
63+
Column: 24,
64+
},
65+
},
66+
},
67+
// Object literal setter returning undefined explicitly
68+
{
69+
Code: `var foo = { set a(val) { return undefined; } };`,
70+
Errors: []rule_tester.InvalidTestCaseError{
71+
{
72+
MessageId: "setter",
73+
Line: 1,
74+
Column: 26,
75+
},
76+
},
77+
},
78+
// Class setter with conditional return of a value
79+
{
80+
Code: `class A { set a(val) { if (val) { return 1; } } }`,
81+
Errors: []rule_tester.InvalidTestCaseError{
82+
{
83+
MessageId: "setter",
84+
Line: 1,
85+
Column: 35,
86+
},
87+
},
88+
},
89+
},
90+
)
91+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default defineConfig({
2020
'./tests/eslint/rules/no-empty-pattern.test.ts',
2121
'./tests/eslint/rules/getter-return.test.ts',
2222
'./tests/eslint/rules/no-loss-of-precision.test.ts',
23+
'./tests/eslint/rules/no-setter-return.test.ts',
2324

2425
// eslint-plugin-import
2526
'./tests/eslint-plugin-import/rules/no-self-import.test.ts',
@@ -110,6 +111,7 @@ export default defineConfig({
110111
'./tests/typescript-eslint/rules/no-invalid-void-type.test.ts',
111112
// './tests/typescript-eslint/rules/no-loop-func.test.ts',
112113
// './tests/typescript-eslint/rules/no-loss-of-precision.test.ts',
114+
'./tests/eslint/rules/no-setter-return.test.ts',
113115
// './tests/typescript-eslint/rules/no-magic-numbers.test.ts',
114116
// './tests/typescript-eslint/rules/no-meaningless-void-operator.test.ts',
115117
'./tests/typescript-eslint/rules/no-misused-new.test.ts',
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Rstest Snapshot v1
2+
3+
exports[`no-setter-return > invalid 1`] = `
4+
{
5+
"code": "var foo = { set a(val) { return 1; } };",
6+
"diagnostics": [
7+
{
8+
"message": "Setter cannot return a value.",
9+
"messageId": "setter",
10+
"range": {
11+
"end": {
12+
"column": 35,
13+
"line": 1,
14+
},
15+
"start": {
16+
"column": 26,
17+
"line": 1,
18+
},
19+
},
20+
"ruleName": "no-setter-return",
21+
},
22+
],
23+
"errorCount": 1,
24+
"fileCount": 1,
25+
"ruleCount": 1,
26+
}
27+
`;
28+
29+
exports[`no-setter-return > invalid 2`] = `
30+
{
31+
"code": "class A { set a(val) { return val; } }",
32+
"diagnostics": [
33+
{
34+
"message": "Setter cannot return a value.",
35+
"messageId": "setter",
36+
"range": {
37+
"end": {
38+
"column": 35,
39+
"line": 1,
40+
},
41+
"start": {
42+
"column": 24,
43+
"line": 1,
44+
},
45+
},
46+
"ruleName": "no-setter-return",
47+
},
48+
],
49+
"errorCount": 1,
50+
"fileCount": 1,
51+
"ruleCount": 1,
52+
}
53+
`;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { RuleTester } from '../rule-tester';
2+
3+
const ruleTester = new RuleTester();
4+
5+
ruleTester.run('no-setter-return', {
6+
valid: [
7+
'var foo = { set a(val) { val = 1; } };',
8+
'class A { set a(val) { return; } }',
9+
],
10+
invalid: [
11+
{
12+
code: 'var foo = { set a(val) { return 1; } };',
13+
errors: [{ messageId: 'setter' }],
14+
},
15+
{
16+
code: 'class A { set a(val) { return val; } }',
17+
errors: [{ messageId: 'setter' }],
18+
},
19+
],
20+
});

0 commit comments

Comments
 (0)