+
Skip to content
This repository was archived by the owner on Aug 31, 2023. It is now read-only.

feat(rome_js_analyze): add noEmptyInterface rule #3801

Merged
merged 3 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions crates/rome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ define_dategories! {
"lint/nursery/noConditionalAssignment": "https://docs.rome.tools/lint/rules/noConditionalAssignment",
"lint/nursery/noConstAssign": "https://docs.rome.tools/lint/rules/noConstAssign",
"lint/nursery/noDupeKeys":"https://docs.rome.tools/lint/rules/noDupeKeys",
"lint/nursery/noEmptyInterface": "https://docs.rome.tools/lint/rules/noEmptyInterface",
"lint/nursery/noExplicitAny": "https://docs.rome.tools/lint/rules/noExplicitAny",
"lint/nursery/noInvalidConstructorSuper": "https://docs.rome.tools/lint/rules/noInvalidConstructorSuper",
"lint/nursery/noPrecisionLoss": "https://docs.rome.tools/lint/rules/noPrecisionLoss",
Expand Down
3 changes: 2 additions & 1 deletion crates/rome_js_analyze/src/analyzers/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

168 changes: 168 additions & 0 deletions crates/rome_js_analyze/src/analyzers/nursery/no_empty_interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use crate::JsRuleAction;
use rome_analyze::context::RuleContext;
use rome_analyze::{declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic};
use rome_console::markup;
use rome_diagnostics::Applicability;
use rome_js_factory::{
make,
syntax::{TsType, T},
};
use rome_js_syntax::{
JsAnyDeclarationClause, TriviaPieceKind, TsInterfaceDeclaration, TsTypeAliasDeclaration,
};
use rome_rowan::{AstNode, AstNodeList, BatchMutationExt};

declare_rule! {
/// Disallow the declaration of empty interfaces.
///
/// > 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.
///
/// Source: https://typescript-eslint.io/rules/no-empty-interface
///
/// ## Examples
///
/// ### Invalid
/// ```ts,expect_diagnostic
/// interface A {}
/// ```
///
/// ```ts,expect_diagnostic
/// // A === B
/// interface A extends B {}
/// ```
///
/// ### Valid
/// ```ts
/// interface A {
/// prop: string;
/// }
///
/// // The interface can be used as an union type.
/// interface A extends B, C {}
/// ```
///
pub(crate) NoEmptyInterface {
version: "11.0.0",
name: "noEmptyInterface",
recommended: false,
}
}

pub enum DiagnosticMessage {
NoEmptyInterface,
NoEmptyInterfaceWithSuper,
}

impl DiagnosticMessage {
/// Convert a [DiagnosticMessage] to a string
fn as_str(&self) -> &'static str {
match self {
Self::NoEmptyInterface => "An empty interface is equivalent to '{}'.",
Self::NoEmptyInterfaceWithSuper => {
"An interface declaring no members is equivalent to its supertype."
}
}
}

/// Retrieves a [TsTypeAliasDeclaration] from a [DiagnosticMessage] that will be used to
/// replace it on the rule action
fn fix_with(&self, node: &TsInterfaceDeclaration) -> Option<TsTypeAliasDeclaration> {
match self {
Self::NoEmptyInterface => make_type_alias_from_interface(
node,
TsType::from(make::ts_object_type(
make::token(T!['{']),
make::ts_type_member_list([]),
make::token(T!['}']),
)),
),
Self::NoEmptyInterfaceWithSuper => {
let super_interface = node.extends_clause()?.types().into_iter().next()?.ok()?;
let type_arguments = super_interface.type_arguments();
let ts_reference_type = make::ts_reference_type(super_interface.name().ok()?);

let ts_reference_type = if type_arguments.is_some() {
ts_reference_type
.with_type_arguments(type_arguments?)
.build()
} else {
ts_reference_type.build()
};

make_type_alias_from_interface(node, TsType::from(ts_reference_type))
}
}
}
}

impl Rule for NoEmptyInterface {
type Query = Ast<TsInterfaceDeclaration>;
type State = DiagnosticMessage;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let has_no_members = node.members().is_empty();
let extends_clause_count = if let Some(extends_clause) = node.extends_clause() {
extends_clause.types().into_iter().count()
} else {
0
};

if extends_clause_count == 0 && has_no_members {
return Some(DiagnosticMessage::NoEmptyInterface);
}

if extends_clause_count == 1 && has_no_members {
return Some(DiagnosticMessage::NoEmptyInterfaceWithSuper);
}

None
}

fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let diagnostic = RuleDiagnostic::new(rule_category!(), ctx.query().range(), state.as_str());

Some(diagnostic)
}

fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
let mut mutation = ctx.root().begin();
let node = ctx.query();

mutation.replace_node(
JsAnyDeclarationClause::from(node.clone()),
JsAnyDeclarationClause::from(state.fix_with(node)?),
);

Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::Always,
message: markup! { "Convert empty interface to type alias." }.to_owned(),
mutation,
})
}
}

/// Builds a [TsTypeAliasDeclaration] from an [TsInterfaceDeclaration].
fn make_type_alias_from_interface(
node: &TsInterfaceDeclaration,
ts_type: TsType,
) -> Option<TsTypeAliasDeclaration> {
let type_params = node.type_parameters();
let new_node = make::ts_type_alias_declaration(
make::token(T![type]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
node.id().ok()?,
make::token(T![=]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
ts_type,
);

let new_node = if type_params.is_some() {
new_node.with_type_parameters(type_params?).build()
} else {
new_node.build()
};

Some(new_node)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface Baz extends Foo {}

interface Foo {}

interface Foo extends Array<number> {}

interface Foo extends Array<number | {}> {}

interface Foo<T> extends Bar<T> {}

declare module FooBar {
export interface Bar extends Baz {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
source: crates/rome_js_analyze/tests/spec_tests.rs
assertion_line: 73
expression: invalid.ts
---
# Input
```js
interface Baz extends Foo {}

interface Foo {}

interface Foo extends Array<number> {}

interface Foo extends Array<number | {}> {}

interface Foo<T> extends Bar<T> {}

declare module FooBar {
export interface Bar extends Baz {}
}

```

# Diagnostics
```
invalid.ts:1:1 lint/nursery/noEmptyInterface FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! An interface declaring no members is equivalent to its supertype.

> 1 │ interface Baz extends Foo {}
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 │
3 │ interface Foo {}

i Safe fix: Convert empty interface to type alias.

1 │ - interface·Baz·extends·Foo·{}
1 │ + type·Baz·=·Foo
2 2 │
3 3 │ interface Foo {}


```

```
invalid.ts:3:1 lint/nursery/noEmptyInterface FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! An empty interface is equivalent to '{}'.

1 │ interface Baz extends Foo {}
2 │
> 3 │ interface Foo {}
│ ^^^^^^^^^^^^^^^^
4 │
5 │ interface Foo extends Array<number> {}

i Safe fix: Convert empty interface to type alias.

1 1 │ interface Baz extends Foo {}
2 2 │
3 │ - interface·Foo·{}
3 │ + type·Foo·=·{}
4 4 │
5 5 │ interface Foo extends Array<number> {}


```

```
invalid.ts:5:1 lint/nursery/noEmptyInterface FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! An interface declaring no members is equivalent to its supertype.

3 │ interface Foo {}
4 │
> 5 │ interface Foo extends Array<number> {}
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6 │
7 │ interface Foo extends Array<number | {}> {}

i Safe fix: Convert empty interface to type alias.

3 3 │ interface Foo {}
4 4 │
5 │ - interface·Foo·extends·Array<number>·{}
5 │ + type·Foo·=·Array<number>
6 6 │
7 7 │ interface Foo extends Array<number | {}> {}


```

```
invalid.ts:7:1 lint/nursery/noEmptyInterface FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! An interface declaring no members is equivalent to its supertype.

5 │ interface Foo extends Array<number> {}
6 │
> 7 │ interface Foo extends Array<number | {}> {}
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 │
9 │ interface Foo<T> extends Bar<T> {}

i Safe fix: Convert empty interface to type alias.

5 5 │ interface Foo extends Array<number> {}
6 6 │
7 │ - interface·Foo·extends·Array<number·|·{}>·{}
7 │ + type·Foo·=·Array<number·|·{}>
8 8 │
9 9 │ interface Foo<T> extends Bar<T> {}


```

```
invalid.ts:9:1 lint/nursery/noEmptyInterface FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! An interface declaring no members is equivalent to its supertype.

7 │ interface Foo extends Array<number | {}> {}
8 │
> 9 │ interface Foo<T> extends Bar<T> {}
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 │
11 │ declare module FooBar {

i Safe fix: Convert empty interface to type alias.

7 7 │ interface Foo extends Array<number | {}> {}
8 8 │
9 │ - interface·Foo<T>·extends·Bar<T>·{}
9 │ + type·Foo<T>·=·Bar<T>
10 10 │
11 11 │ declare module FooBar {


```

```
invalid.ts:12:10 lint/nursery/noEmptyInterface FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! An interface declaring no members is equivalent to its supertype.

11 │ declare module FooBar {
> 12 │ export interface Bar extends Baz {}
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
13 │ }
14 │

i Safe fix: Convert empty interface to type alias.

10 10 │
11 11 │ declare module FooBar {
12 │ - ··export·interface·Bar·extends·Baz·{}
12 │ + ··export·type·Bar·=·Baz
13 13 │ }
14 14 │


```


Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface A extends B {
prop: number;
}

// valid because extending multiple interfaces
// can be used instead of a union type
interface Baz extends Foo, Bar {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: crates/rome_js_analyze/tests/spec_tests.rs
assertion_line: 73
expression: valid.ts
---
# Input
```js
interface A extends B {
prop: number;
}

// valid because extending multiple interfaces
// can be used instead of a union type
interface Baz extends Foo, Bar {}

```


Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载