diff --git a/internal/config/config.go b/internal/config/config.go index c43fab908..aa946a524 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -39,6 +39,7 @@ import ( "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_implied_eval" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_invalid_void_type" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_meaningless_void_operator" + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_misused_new" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_misused_promises" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_misused_spread" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_mixed_enums" @@ -391,6 +392,7 @@ func registerAllTypeScriptEslintPluginRules() { GlobalRuleRegistry.Register("@typescript-eslint/no-for-in-array", no_for_in_array.NoForInArrayRule) GlobalRuleRegistry.Register("@typescript-eslint/no-implied-eval", no_implied_eval.NoImpliedEvalRule) GlobalRuleRegistry.Register("@typescript-eslint/no-meaningless-void-operator", no_meaningless_void_operator.NoMeaninglessVoidOperatorRule) + GlobalRuleRegistry.Register("@typescript-eslint/no-misused-new", no_misused_new.NoMisusedNewRule) GlobalRuleRegistry.Register("@typescript-eslint/no-misused-promises", no_misused_promises.NoMisusedPromisesRule) GlobalRuleRegistry.Register("@typescript-eslint/no-misused-spread", no_misused_spread.NoMisusedSpreadRule) GlobalRuleRegistry.Register("@typescript-eslint/no-mixed-enums", no_mixed_enums.NoMixedEnumsRule) diff --git a/internal/plugins/typescript/rules/no_misused_new/no_misused_new.go b/internal/plugins/typescript/rules/no_misused_new/no_misused_new.go new file mode 100644 index 000000000..2c45fcb4c --- /dev/null +++ b/internal/plugins/typescript/rules/no_misused_new/no_misused_new.go @@ -0,0 +1,74 @@ +package no_misused_new + +import ( + "github.com/microsoft/typescript-go/shim/ast" + "github.com/web-infra-dev/rslint/internal/rule" +) + +/** + * check whether the name of the return type of the node is the same as the name of the parent node. + */ +func check(node *ast.Node) bool { + parentName := node.Parent.Name() + if parentName == nil { + return false + } + + nodeType := node.Type() + if nodeType != nil && ast.IsTypeReferenceNode(nodeType) { + typeName := nodeType.AsTypeReference().TypeName + if ast.IsIdentifier(typeName) { + return typeName.Text() == parentName.Text() + } + } + return false +} + +var NoMisusedNewRule = rule.CreateRule(rule.Rule{ + Name: "no-misused-new", + Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { + return rule.RuleListeners{ + ast.KindMethodDeclaration: func(node *ast.Node) { + parentKind := node.Parent.Kind + if parentKind != ast.KindClassDeclaration && parentKind != ast.KindClassExpression { + return + } + + if node.Name().Text() != "new" { + return + } + // If the function body exists, it's valid for this rule. + body := node.Body() + if body != nil { + return + } + + if check(node) { + ctx.ReportNode(node, rule.RuleMessage{ + Id: "errorMessageClass", + Description: "Class cannot have method named `new`.", + }) + } + }, + ast.KindConstructSignature: func(node *ast.Node) { + if node.Parent.Kind != ast.KindInterfaceDeclaration { + return + } + if check(node) { + ctx.ReportNode(node, rule.RuleMessage{ + Id: "errorMessageInterface", + Description: "interfaces cannot be constructed, only classes.", + }) + } + }, + ast.KindMethodSignature: func(node *ast.Node) { + if node.Name().Text() == "constructor" { + ctx.ReportNode(node, rule.RuleMessage{ + Id: "errorMessageInterface", + Description: "interfaces cannot be constructed, only classes.", + }) + } + }, + } + }, +}) diff --git a/internal/plugins/typescript/rules/no_misused_new/no_misused_new_test.go b/internal/plugins/typescript/rules/no_misused_new/no_misused_new_test.go new file mode 100644 index 000000000..7efdf8fd9 --- /dev/null +++ b/internal/plugins/typescript/rules/no_misused_new/no_misused_new_test.go @@ -0,0 +1,174 @@ +package no_misused_new + +import ( + "testing" + + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures" + "github.com/web-infra-dev/rslint/internal/rule_tester" +) + +func TestNoMisusedNewRule(t *testing.T) { + rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &NoMisusedNewRule, []rule_tester.ValidTestCase{ + { + Code: ` + declare abstract class C { + foo() {} + get new(); + bar(); + } + `, + }, + { + Code: ` + class C { + constructor(); + } + `, + }, + { + Code: ` + const foo = class { + constructor(); + }; + `, + }, + { + Code: ` + const foo = class { + new(): X; + }; + `, + }, + { + Code: ` + class C { + constructor() {} + } + `, + }, + { + Code: ` + const foo = class { + new() {} + }; + `, + }, + { + Code: ` + const foo = class { + constructor() {} + }; + `, + }, + { + Code: ` + interface I { + new (): {}; + } + `, + }, + { + Code: `type T = { new (): T };`, + }, + { + Code: ` + export default class { + constructor(); + } + `, + }, + { + Code: ` + interface foo { + new (): bar; + } + `, + }, + { + Code: ` + interface foo { + new (): 'x'; + } + `, + }, + }, []rule_tester.InvalidTestCase{ + { + Code: ` + interface I { + new (): I; + constructor(): void; + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + { + MessageId: "errorMessageInterface", + Line: 3, + }, + { + MessageId: "errorMessageInterface", + Line: 4, + }, + }, + }, + { + Code: ` + interface G { + new (): G; + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + { + MessageId: "errorMessageInterface", + }, + }, + }, + { + Code: ` + type T = { + constructor(): void; + }; + `, + Errors: []rule_tester.InvalidTestCaseError{ + { + MessageId: "errorMessageInterface", + }, + }, + }, + { + Code: ` + class C { + new(): C; + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + { + MessageId: "errorMessageClass", + }, + }, + }, + { + Code: ` + declare abstract class C { + new(): C; + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + { + MessageId: "errorMessageClass", + }, + }, + }, + { + Code: ` + interface I { + constructor(): ''; + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + { + MessageId: "errorMessageInterface", + }, + }, + }, + }) +} diff --git a/packages/rslint-test-tools/rstest.config.mts b/packages/rslint-test-tools/rstest.config.mts index 55466041e..d42c3e336 100644 --- a/packages/rslint-test-tools/rstest.config.mts +++ b/packages/rslint-test-tools/rstest.config.mts @@ -93,7 +93,7 @@ export default defineConfig({ // './tests/typescript-eslint/rules/no-loss-of-precision.test.ts', // './tests/typescript-eslint/rules/no-magic-numbers.test.ts', // './tests/typescript-eslint/rules/no-meaningless-void-operator.test.ts', - // './tests/typescript-eslint/rules/no-misused-new.test.ts', + './tests/typescript-eslint/rules/no-misused-new.test.ts', // './tests/typescript-eslint/rules/no-misused-promises.test.ts', // './tests/typescript-eslint/rules/no-misused-spread.test.ts', // './tests/typescript-eslint/rules/no-mixed-enums.test.ts', diff --git a/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/no-misused-new.test.ts.snap b/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/no-misused-new.test.ts.snap new file mode 100644 index 000000000..f15c4beea --- /dev/null +++ b/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/no-misused-new.test.ts.snap @@ -0,0 +1,197 @@ +// Rstest Snapshot v1 + +exports[`no-misused-new > invalid 1`] = ` +{ + "code": " +interface I { + new (): I; + constructor(): void; +} + ", + "diagnostics": [ + { + "message": "interfaces cannot be constructed, only classes.", + "messageId": "errorMessageInterface", + "range": { + "end": { + "column": 13, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/no-misused-new", + }, + { + "message": "interfaces cannot be constructed, only classes.", + "messageId": "errorMessageInterface", + "range": { + "end": { + "column": 23, + "line": 4, + }, + "start": { + "column": 3, + "line": 4, + }, + }, + "ruleName": "@typescript-eslint/no-misused-new", + }, + ], + "errorCount": 2, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`no-misused-new > invalid 2`] = ` +{ + "code": " +interface G { + new (): G; +} + ", + "diagnostics": [ + { + "message": "interfaces cannot be constructed, only classes.", + "messageId": "errorMessageInterface", + "range": { + "end": { + "column": 19, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/no-misused-new", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`no-misused-new > invalid 3`] = ` +{ + "code": " +type T = { + constructor(): void; +}; + ", + "diagnostics": [ + { + "message": "interfaces cannot be constructed, only classes.", + "messageId": "errorMessageInterface", + "range": { + "end": { + "column": 23, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/no-misused-new", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`no-misused-new > invalid 4`] = ` +{ + "code": " +class C { + new(): C; +} + ", + "diagnostics": [ + { + "message": "Class cannot have method named \`new\`.", + "messageId": "errorMessageClass", + "range": { + "end": { + "column": 12, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/no-misused-new", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`no-misused-new > invalid 5`] = ` +{ + "code": " +declare abstract class C { + new(): C; +} + ", + "diagnostics": [ + { + "message": "Class cannot have method named \`new\`.", + "messageId": "errorMessageClass", + "range": { + "end": { + "column": 12, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/no-misused-new", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`no-misused-new > invalid 6`] = ` +{ + "code": " +interface I { + constructor(): ''; +} + ", + "diagnostics": [ + { + "message": "interfaces cannot be constructed, only classes.", + "messageId": "errorMessageInterface", + "range": { + "end": { + "column": 21, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/no-misused-new", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`;