diff --git a/crates/oxc_linter/src/rules/typescript/no_empty_interface.rs b/crates/oxc_linter/src/rules/typescript/no_empty_interface.rs index 7f6bb7f31..0165b3431 100644 --- a/crates/oxc_linter/src/rules/typescript/no_empty_interface.rs +++ b/crates/oxc_linter/src/rules/typescript/no_empty_interface.rs @@ -5,6 +5,7 @@ use oxc_diagnostics::{ }; use oxc_macros::declare_oxc_lint; use oxc_span::Span; +use serde_json::Value; use crate::{context::LintContext, rule::Rule, AstNode}; @@ -21,7 +22,9 @@ struct NoEmptyInterfaceDiagnostic(#[label] pub Span); struct NoEmptyInterfaceExtendDiagnostic(#[label] pub Span); #[derive(Debug, Default, Clone)] -pub struct NoEmptyInterface; +pub struct NoEmptyInterface { + allow_single_extends: bool, +} declare_oxc_lint!( /// ### What it does @@ -44,6 +47,13 @@ declare_oxc_lint!( ); impl Rule for NoEmptyInterface { + fn from_configuration(value: Value) -> Self { + let allow_single_extends = value.get(0).map_or(true, |config| { + config.get("allow_single_extends").and_then(Value::as_bool).unwrap_or_default() + }); + + Self { allow_single_extends } + } fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { if let AstKind::TSInterfaceDeclaration(interface) = node.kind() { if interface.body.body.is_empty() { @@ -51,8 +61,11 @@ impl Rule for NoEmptyInterface { None => { ctx.diagnostic(NoEmptyInterfaceDiagnostic(interface.span)); } + Some(extends) if extends.len() == 1 => { - ctx.diagnostic(NoEmptyInterfaceExtendDiagnostic(interface.span)); + if !self.allow_single_extends { + ctx.diagnostic(NoEmptyInterfaceExtendDiagnostic(interface.span)); + } } _ => {} } @@ -66,25 +79,134 @@ 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 {}", + ( + " + interface Foo { + name: string; + } + ", + None, + ), + ( + " + 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 {} + ", + None, + ), + ( + " + interface Foo { + name: string; + } + + interface Bar extends Foo {} + ", + Some(serde_json::json!([{ "allow_single_extends": true }])), + ), + ( + " + interface Foo { + props: string; + } + + interface Bar extends Foo {} + + class Bar {} + ", + Some(serde_json::json!([{ "allow_single_extends": true }])), + ), ]; 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 {} }", + ("interface Foo {}", None), + ( + " + interface Foo { + props: string; + } + + interface Bar extends Foo {} + + class Baz {} + ", + Some(serde_json::json!([{ "allow_single_extends": false }])), + ), + ( + " + interface Foo { + props: string; + } + + interface Bar extends Foo {} + + class Bar {} + ", + Some(serde_json::json!([{ "allow_single_extends": false }])), + ), + ( + " + interface Foo { + props: string; + } + + interface Bar extends Foo {} + + const bar = class Bar {}; + ", + Some(serde_json::json!([{ "allow_single_extends": false }])), + ), + ( + " + interface Foo { + name: string; + } + + interface Bar extends Foo {} + ", + Some(serde_json::json!([{ "allow_single_extends": false }])), + ), + ("interface Foo extends Array {}", None), + ("interface Foo extends Array {}", None), + ( + " + interface Bar { + bar: string; + } + interface Foo extends Array {} + ", + None, + ), + ( + " + type R = Record; + interface Foo extends R {} + ", + None, + ), + ( + " + interface Foo extends Bar {} + ", + None, + ), + ( + " + declare module FooBar { + type Baz = typeof baz; + export interface Bar extends Baz {} + } + ", + None, + ), ]; Tester::new(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 index 14690ebc4..a4120f49c 100644 --- a/crates/oxc_linter/src/snapshots/no_empty_interface.snap +++ b/crates/oxc_linter/src/snapshots/no_empty_interface.snap @@ -9,27 +9,35 @@ expression: no_empty_interface ╰──── ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype - ╭─[no_empty_interface.tsx:1:34] - 1 │ interface Foo { props: string; } interface Bar extends Foo {} class Baz {} - · ──────────────────────────── + ╭─[no_empty_interface.tsx:6:4] + 5 │ + 6 │ interface Bar extends Foo {} + · ──────────────────────────── + 7 │ ╰──── ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype - ╭─[no_empty_interface.tsx:1:34] - 1 │ interface Foo { props: string; } interface Bar extends Foo {} class Bar {} - · ──────────────────────────── + ╭─[no_empty_interface.tsx:6:4] + 5 │ + 6 │ interface Bar extends Foo {} + · ──────────────────────────── + 7 │ ╰──── ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype - ╭─[no_empty_interface.tsx:1:34] - 1 │ interface Foo { props: string; } interface Bar extends Foo {} const bar = class Bar {}; - · ──────────────────────────── + ╭─[no_empty_interface.tsx:6:4] + 5 │ + 6 │ interface Bar extends Foo {} + · ──────────────────────────── + 7 │ ╰──── ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype - ╭─[no_empty_interface.tsx:1:33] - 1 │ interface Foo { name: string; } interface Bar extends Foo {} - · ──────────────────────────── + ╭─[no_empty_interface.tsx:6:4] + 5 │ + 6 │ interface Bar extends Foo {} + · ──────────────────────────── + 7 │ ╰──── ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype @@ -45,25 +53,33 @@ expression: no_empty_interface ╰──── ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype - ╭─[no_empty_interface.tsx:1:32] - 1 │ interface Bar { bar: string; } interface Foo extends Array {} - · ─────────────────────────────────── + ╭─[no_empty_interface.tsx:5:4] + 4 │ } + 5 │ interface Foo extends Array {} + · ─────────────────────────────────── + 6 │ ╰──── ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype - ╭─[no_empty_interface.tsx:1:35] - 1 │ type R = Record; interface Foo extends R {} - · ────────────────────────── + ╭─[no_empty_interface.tsx:3:4] + 2 │ type R = Record; + 3 │ interface Foo extends R {} + · ────────────────────────── + 4 │ ╰──── ⚠ 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 {} - · ────────────────────────────────── + ╭─[no_empty_interface.tsx:2:4] + 1 │ + 2 │ interface Foo extends Bar {} + · ────────────────────────────────── + 3 │ ╰──── ⚠ typescript-eslint(no-empty-interface): an interface declaring no members is equivalent to its supertype - ╭─[no_empty_interface.tsx:1:55] - 1 │ declare module FooBar { type Baz = typeof baz; export interface Bar extends Baz {} } - · ──────────────────────────── + ╭─[no_empty_interface.tsx:4:13] + 3 │ type Baz = typeof baz; + 4 │ export interface Bar extends Baz {} + · ──────────────────────────── + 5 │ } ╰────