From 1aaeb794a45c7828e089d6f7be46f1abc7a287a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=89=AF=E4=BB=94?= <32487868+cijiugechu@users.noreply.github.com> Date: Fri, 7 Jul 2023 16:00:06 +0800 Subject: [PATCH] feat(linter): implement `no-misused-new` (#525) --- crates/oxc_linter/src/rules.rs | 3 +- .../src/rules/typescript/no_misused_new.rs | 142 ++++++++++++++++++ .../src/snapshots/no_misused_new.snap | 54 +++++++ 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 crates/oxc_linter/src/rules/typescript/no_misused_new.rs create mode 100644 crates/oxc_linter/src/snapshots/no_misused_new.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index fe89d4999..358d66a70 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -58,7 +58,8 @@ oxc_macros::declare_all_lint_rules! { typescript::no_empty_interface, typescript::no_extra_non_null_assertion, typescript::no_non_null_asserted_optional_chain, - typescript::no_unnecessary_type_constraint + typescript::no_unnecessary_type_constraint, + typescript::no_misused_new } #[cfg(test)] diff --git a/crates/oxc_linter/src/rules/typescript/no_misused_new.rs b/crates/oxc_linter/src/rules/typescript/no_misused_new.rs new file mode 100644 index 000000000..825003044 --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/no_misused_new.rs @@ -0,0 +1,142 @@ +use oxc_ast::{ast::{TSSignature, TSType, TSTypeName, PropertyKey, ClassElement}, AstKind}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::{Span, GetSpan}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("typescript-eslint(no-misused-new): Interfaces cannot be constructed, only classes.")] +#[diagnostic( + severity(warning), + help("Consider removing this method from your interface.") +)] +struct NoMisusedNewInterfaceDiagnostic(#[label] pub Span); + +#[derive(Debug, Error, Diagnostic)] +#[error("typescript-eslint(no-misused-new): Class cannot have method named `new`.")] +#[diagnostic( + severity(warning), + help("This method name is confusing, consider renaming the method to `constructor`") +)] +struct NoMisusedNewClassDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoMisusedNew; + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforce valid definition of `new` and `constructor` + /// + /// ### Why is this bad? + /// + /// JavaScript classes may define a constructor method that runs + /// when a class instance is newly created. + /// TypeScript allows interfaces that describe a static class object to define + /// a new() method (though this is rarely used in real world code). + /// Developers new to JavaScript classes and/or TypeScript interfaces may + /// sometimes confuse when to use constructor or new. + /// + /// ### Example + /// ```typescript + // declare class C { + // new(): C; + // } + + // interface I { + // new (): I; + // constructor(): void; + // } + /// ``` + NoMisusedNew, + correctness +); + + +impl Rule for NoMisusedNew { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::TSInterfaceDeclaration(interface_decl) => { + let decl_name = &interface_decl.id.name; + + for signature in &interface_decl.body.body { + if let TSSignature::TSConstructSignatureDeclaration(sig) = signature && + let Some(return_type) = &sig.return_type && + let TSType::TSTypeReference(type_ref) = &return_type.type_annotation && + let TSTypeName::IdentifierName(id) = &type_ref.type_name && + id.name == decl_name { + + ctx.diagnostic(NoMisusedNewInterfaceDiagnostic( + Span::new(sig.span.start, sig.span.start + 3) + )); + } + + } + } + AstKind::TSMethodSignature(method_sig) => { + if let PropertyKey::Identifier(id) = &method_sig.key { + if id.name == "constructor" { + ctx.diagnostic(NoMisusedNewInterfaceDiagnostic(method_sig.key.span())); + } + } + } + AstKind::Class(cls) => { + if let Some(cls_id) = &cls.id { + let cls_name = &cls_id.name; + + for element in &cls.body.body { + if let ClassElement::MethodDefinition(method) = element && + let PropertyKey::Identifier(id) = &method.key && + id.name == "new" && + method.value.body.is_none() && + let Some(return_type) = &method.value.return_type && + let TSType::TSTypeReference(type_ref) = &return_type.type_annotation && + let TSTypeName::IdentifierName(current_id) = &type_ref.type_name && + current_id.name == cls_name { + + ctx.diagnostic(NoMisusedNewClassDiagnostic(method.key.span())); + + } + } + } + } + _ => {} + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "declare abstract class C { foo() {} get new();bar();}", + "class C { constructor();}", + "const foo = class { constructor();};", + "const foo = class { new(): X;};", + "class C { new() {} }", + "class C { constructor() {} }", + "const foo = class { new() {} };", + "const foo = class { constructor() {} };", + "interface I { new (): {}; }", + "type T = { new (): T };", + "export default class { constructor(); }", + "interface foo { new (): bar; }", + "interface foo { new (): 'x'; }" + ]; + + let fail = vec![ + "interface I { new (): I; constructor(): void;}", + "interface G { new (): G;}", + "type T = { constructor(): void;};", + "class C { new(): C;}", + "declare abstract class C { new(): C;}", + "interface I { constructor(): '';}" + ]; + + Tester::new_without_config(NoMisusedNew::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_misused_new.snap b/crates/oxc_linter/src/snapshots/no_misused_new.snap new file mode 100644 index 000000000..27922228a --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_misused_new.snap @@ -0,0 +1,54 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_misused_new +--- + ⚠ typescript-eslint(no-misused-new): Interfaces cannot be constructed, only classes. + ╭─[no_misused_new.tsx:1:1] + 1 │ interface I { new (): I; constructor(): void;} + · ─── + ╰──── + help: Consider removing this method from your interface. + + ⚠ typescript-eslint(no-misused-new): Interfaces cannot be constructed, only classes. + ╭─[no_misused_new.tsx:1:1] + 1 │ interface I { new (): I; constructor(): void;} + · ─────────── + ╰──── + help: Consider removing this method from your interface. + + ⚠ typescript-eslint(no-misused-new): Interfaces cannot be constructed, only classes. + ╭─[no_misused_new.tsx:1:1] + 1 │ interface G { new (): G;} + · ─── + ╰──── + help: Consider removing this method from your interface. + + ⚠ typescript-eslint(no-misused-new): Interfaces cannot be constructed, only classes. + ╭─[no_misused_new.tsx:1:1] + 1 │ type T = { constructor(): void;}; + · ─────────── + ╰──── + help: Consider removing this method from your interface. + + ⚠ typescript-eslint(no-misused-new): Class cannot have method named `new`. + ╭─[no_misused_new.tsx:1:1] + 1 │ class C { new(): C;} + · ─── + ╰──── + help: This method name is confusing, consider renaming the method to `constructor` + + ⚠ typescript-eslint(no-misused-new): Class cannot have method named `new`. + ╭─[no_misused_new.tsx:1:1] + 1 │ declare abstract class C { new(): C;} + · ─── + ╰──── + help: This method name is confusing, consider renaming the method to `constructor` + + ⚠ typescript-eslint(no-misused-new): Interfaces cannot be constructed, only classes. + ╭─[no_misused_new.tsx:1:1] + 1 │ interface I { constructor(): '';} + · ─────────── + ╰──── + help: Consider removing this method from your interface. + +