From 2e6cb6d3d2d9fa7f054bab94ee8cede28b7be90a Mon Sep 17 00:00:00 2001 From: Boshen Date: Sat, 1 Jul 2023 21:04:57 +0800 Subject: [PATCH] feat(linter): implement @typescript-eslint/no-empty-interface --- crates/oxc_linter/src/rules.rs | 1 + .../rules/typescript/no_empty_interface.rs | 91 +++++++++++++++++++ .../src/snapshots/no_empty_interface.snap | 71 +++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 crates/oxc_linter/src/rules/typescript/no_empty_interface.rs create mode 100644 crates/oxc_linter/src/snapshots/no_empty_interface.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 87d9ddc8d..beabf8b0f 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -54,6 +54,7 @@ oxc_macros::declare_all_lint_rules! { eslint::use_isnan, eslint::valid_typeof, typescript::isolated_declaration, + typescript::no_empty_interface, typescript::no_extra_non_null_assertion, typescript::no_non_null_asserted_optional_chain } diff --git a/crates/oxc_linter/src/rules/typescript/no_empty_interface.rs b/crates/oxc_linter/src/rules/typescript/no_empty_interface.rs new file mode 100644 index 000000000..054100f1e --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/no_empty_interface.rs @@ -0,0 +1,91 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("typescript-eslint(no-empty-interface): an empty interface is equivalent to `{{}}`")] +#[diagnostic(severity(warning))] +struct NoEmptyInterfaceDiagnostic(#[label] pub Span); + +#[derive(Debug, Error, Diagnostic)] +#[error( + "typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype" +)] +#[diagnostic(severity(warning))] +struct NoEmptyInterfaceExtendDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoEmptyInterface; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow the declaration of empty interfaces. + /// + /// ### Why is this bad? + /// + /// An empty interface in TypeScript does very little: any non-nullable value is assignable to {}. + /// Using an empty interface is often a sign of programmer error, such as misunderstanding the concept of {} or forgetting to fill in fields. + /// This rule aims to ensure that only meaningful interfaces are declared in the code. + /// + /// ### Example + /// ```javascript + /// interface Foo {} + /// interface Bar extends Foo {} + /// ``` + NoEmptyInterface, + correctness +); + +impl Rule for NoEmptyInterface { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::TSInterfaceDeclaration(interface) = node.kind() { + if interface.body.body.is_empty() { + match &interface.extends { + None => { + ctx.diagnostic(NoEmptyInterfaceDiagnostic(interface.span)); + } + Some(extends) if extends.len() == 1 => { + ctx.diagnostic(NoEmptyInterfaceExtendDiagnostic(interface.span)); + } + _ => {} + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "interface Foo { name: string; }", + "interface Foo { name: string; } + interface Bar { age: number; } + // valid because extending multiple interfaces can be used instead of a union type + interface Baz extends Foo, Bar {}", + ]; + + let fail = vec![ + "interface Foo {}", + "interface Foo { props: string; } interface Bar extends Foo {} class Baz {}", + "interface Foo { props: string; } interface Bar extends Foo {} class Bar {}", + "interface Foo { props: string; } interface Bar extends Foo {} const bar = class Bar {};", + "interface Foo { name: string; } interface Bar extends Foo {}", + "interface Foo extends Array {}", + "interface Foo extends Array {}", + "interface Bar { bar: string; } interface Foo extends Array {}", + "type R = Record; interface Foo extends R {}", + "interface Foo extends Bar {}", + "declare module FooBar { type Baz = typeof baz; export interface Bar extends Baz {} }", + ]; + + Tester::new_without_config(NoEmptyInterface::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_empty_interface.snap b/crates/oxc_linter/src/snapshots/no_empty_interface.snap new file mode 100644 index 000000000..52af7772e --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_empty_interface.snap @@ -0,0 +1,71 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_empty_interface +--- + ⚠ typescript-eslint(no-empty-interface): an empty interface is equivalent to `{}` + ╭─[no_empty_interface.tsx:1:1] + 1 │ interface Foo {} + · ──────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ interface Foo { props: string; } interface Bar extends Foo {} class Baz {} + · ──────────────────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ interface Foo { props: string; } interface Bar extends Foo {} class Bar {} + · ──────────────────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ interface Foo { props: string; } interface Bar extends Foo {} const bar = class Bar {}; + · ──────────────────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ interface Foo { name: string; } interface Bar extends Foo {} + · ──────────────────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ interface Foo extends Array {} + · ────────────────────────────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ interface Foo extends Array {} + · ─────────────────────────────────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ interface Bar { bar: string; } interface Foo extends Array {} + · ─────────────────────────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ type R = Record; interface Foo extends R {} + · ────────────────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ interface Foo extends Bar {} + · ────────────────────────────────── + ╰──── + + ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype + ╭─[no_empty_interface.tsx:1:1] + 1 │ declare module FooBar { type Baz = typeof baz; export interface Bar extends Baz {} } + · ──────────────────────────── + ╰──── + +