Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import (
"github.com/web-infra-dev/rslint/internal/rules/no_const_assign"
"github.com/web-infra-dev/rslint/internal/rules/no_constant_binary_expression"
"github.com/web-infra-dev/rslint/internal/rules/no_constant_condition"
"github.com/web-infra-dev/rslint/internal/rules/no_constructor_return"
)

// RslintConfig represents the top-level configuration array
Expand Down Expand Up @@ -437,6 +438,7 @@ func registerAllCoreEslintRules() {
GlobalRuleRegistry.Register("no-const-assign", no_const_assign.NoConstAssignRule)
GlobalRuleRegistry.Register("no-constant-binary-expression", no_constant_binary_expression.NoConstantBinaryExpressionRule)
GlobalRuleRegistry.Register("no-constant-condition", no_constant_condition.NoConstantConditionRule)
GlobalRuleRegistry.Register("no-constructor-return", no_constructor_return.NoConstructorReturnRule)
}

// getAllTypeScriptEslintPluginRules returns all registered rules (for backward compatibility when no config is provided)
Expand Down
81 changes: 81 additions & 0 deletions internal/rules/no_constructor_return/no_constructor_return.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package no_constructor_return

import (
"github.com/microsoft/typescript-go/shim/ast"
"github.com/web-infra-dev/rslint/internal/rule"
)

// Message builder
func buildUnexpectedMessage() rule.RuleMessage {
return rule.RuleMessage{
Id: "unexpected",
Description: "Unexpected return statement in constructor.",
}
}

// findEnclosingConstructor walks up the tree to find if we're inside a constructor
func findEnclosingConstructor(node *ast.Node) *ast.Node {
if node == nil {
return nil
}

current := node.Parent
for current != nil {
// Check if this is a constructor method
if current.Kind == ast.KindConstructor {
return current
}

// Stop at function boundaries - don't traverse into nested functions
// Return statements in nested functions don't apply to the outer constructor
switch current.Kind {
case ast.KindFunctionDeclaration,
ast.KindFunctionExpression,
ast.KindArrowFunction,
ast.KindMethodDeclaration:
// We've hit a function boundary, stop searching
return nil
}

current = current.Parent
}

return nil
}

// isReturnStatementInConstructor checks if a return statement with a value is inside a constructor
func isReturnStatementInConstructor(node *ast.Node) bool {
if node == nil || node.Kind != ast.KindReturnStatement {
return false
}

returnStmt := node.AsReturnStatement()
if returnStmt == nil {
return false
}

// Only flag return statements that have an expression (return with a value)
// Bare return statements (return;) are allowed for flow control
if returnStmt.Expression == nil {
return false
}

// Check if this return statement is inside a constructor
constructor := findEnclosingConstructor(node)
return constructor != nil
}

// NoConstructorReturnRule disallows returning values in constructors
var NoConstructorReturnRule = rule.CreateRule(rule.Rule{
Name: "no-constructor-return",
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
return rule.RuleListeners{
// Check return statements
ast.KindReturnStatement: func(node *ast.Node) {
if isReturnStatementInConstructor(node) {
ctx.ReportNode(node, buildUnexpectedMessage())
}
},
}
},
})
197 changes: 197 additions & 0 deletions internal/rules/no_constructor_return/no_constructor_return_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package no_constructor_return

import (
"testing"

"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures"
"github.com/web-infra-dev/rslint/internal/rule_tester"
)

func TestNoConstructorReturnRule(t *testing.T) {
rule_tester.RunRuleTester(
fixtures.GetRootDir(),
"tsconfig.json",
t,
&NoConstructorReturnRule,
// Valid cases - ported from ESLint
[]rule_tester.ValidTestCase{
// Regular functions with return statements are allowed
{Code: `function fn() { return }`},
{Code: `function fn(kumiko) { if (kumiko) { return kumiko } }`},
{Code: `const fn = function () { return }`},
{Code: `const fn = function () { if (kumiko) { return kumiko } }`},
{Code: `const fn = () => { return }`},
{Code: `const fn = () => { if (kumiko) { return kumiko } }`},

// Classes without constructors or with empty constructors
{Code: `class C { }`},
{Code: `class C { constructor() {} }`},
{Code: `class C { constructor() { let v } }`},

// Methods and getters can return values
{Code: `class C { method() { return '' } }`},
{Code: `class C { get value() { return '' } }`},

// Constructors with bare return statements (no value) are allowed
{Code: `class C { constructor(a) { if (!a) { return } else { a() } } }`},
{Code: `class C { constructor() { return } }`},
{Code: `class C { constructor() { { return } } }`},

// Nested functions inside constructors can return values
{Code: `class C { constructor() { function fn() { return true } } }`},
{Code: `class C { constructor() { this.fn = function () { return true } } }`},
{Code: `class C { constructor() { this.fn = () => { return true } } }`},

// TypeScript: classes with multiple constructors
{Code: `class C { constructor(); constructor(a?: string) {} }`},
{Code: `class C { constructor(a: string); constructor(a?: string) { if (!a) { return } } }`},

// TypeScript: constructors with type annotations
{Code: `class C { constructor(private x: number) {} }`},
{Code: `class C { constructor(public readonly y: string) { return } }`},
},
// Invalid cases - ported from ESLint
[]rule_tester.InvalidTestCase{
// Constructor returning a value
{
Code: `class C { constructor() { return '' } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 27,
},
},
},
// Constructor with conditional return of a value
{
Code: `class C { constructor(a) { if (!a) { return '' } else { a() } } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 38,
},
},
},

// Additional test cases
// Constructor returning a number
{
Code: `class C { constructor() { return 1 } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 27,
},
},
},
// Constructor returning an object
{
Code: `class C { constructor() { return {} } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 27,
},
},
},
// Constructor returning null
{
Code: `class C { constructor() { return null } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 27,
},
},
},
// Constructor returning undefined explicitly
{
Code: `class C { constructor() { return undefined } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 27,
},
},
},
// Constructor returning a boolean
{
Code: `class C { constructor() { return true } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 27,
},
},
},
// Constructor with multiple returns
{
Code: `class C { constructor(x) { if (x) return 'yes'; return 'no'; } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 35,
},
{
MessageId: "unexpected",
Line: 1,
Column: 49,
},
},
},
// Constructor returning array
{
Code: `class C { constructor() { return [] } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 27,
},
},
},
// Constructor returning function call result
{
Code: `class C { constructor() { return foo() } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 27,
},
},
},
// Constructor returning 'this' (which is the normal behavior but explicit return is flagged)
{
Code: `class C { constructor() { return this } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 27,
},
},
},

// TypeScript: constructor with type annotations still can't return values
{
Code: `class C { constructor(private x: number) { return 1 } }`,
Errors: []rule_tester.InvalidTestCaseError{
{
MessageId: "unexpected",
Line: 1,
Column: 44,
},
},
},
},
)
}
1 change: 1 addition & 0 deletions scripts/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,6 @@ scaffolding
autofix
autofixes
kebab
kumiko
PascalCase
tmpl
Loading