Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): add missing test cases to no-empty-interface and add config #2973

Merged
merged 3 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
158 changes: 140 additions & 18 deletions crates/oxc_linter/src/rules/typescript/no_empty_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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
Expand All @@ -44,15 +47,25 @@ 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() {
match &interface.extends {
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));
}
}
_ => {}
}
Expand All @@ -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<number> {}",
"interface Foo extends Array<number | {}> {}",
"interface Bar { bar: string; } interface Foo extends Array<Bar> {}",
"type R = Record<string, unknown>; interface Foo extends R {}",
"interface Foo<T> extends Bar<T> {}",
"declare module FooBar { type Baz = typeof baz; export interface Bar extends Baz {} }",
("interface Foo {}", None),
JoSeBu1 marked this conversation as resolved.
Show resolved Hide resolved
(
"
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<number> {}", None),
("interface Foo extends Array<number | {}> {}", None),
(
"
interface Bar {
bar: string;
}
interface Foo extends Array<Bar> {}
",
None,
),
(
"
type R = Record<string, unknown>;
interface Foo extends R {}
",
None,
),
(
"
interface Foo<T> extends Bar<T> {}
",
None,
),
(
"
declare module FooBar {
type Baz = typeof baz;
export interface Bar extends Baz {}
}
",
None,
),
];

Tester::new(NoEmptyInterface::NAME, pass, fail).test_and_snapshot();
Expand Down
64 changes: 40 additions & 24 deletions crates/oxc_linter/src/snapshots/no_empty_interface.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Bar> {}
· ───────────────────────────────────
╭─[no_empty_interface.tsx:5:4]
4 │ }
5 │ interface Foo extends Array<Bar> {}
· ───────────────────────────────────
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<string, unknown>; interface Foo extends R {}
· ──────────────────────────
╭─[no_empty_interface.tsx:3:4]
2 │ type R = Record<string, unknown>;
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<T> extends Bar<T> {}
· ──────────────────────────────────
╭─[no_empty_interface.tsx:2:4]
1 │
2 │ interface Foo<T> extends Bar<T> {}
· ──────────────────────────────────
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 │ }
╰────
Loading