diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index 41f424ee69f..9ca0a5755b9 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -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", diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index beb4f278faa..bebcc783c9b 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -4,6 +4,7 @@ use rome_analyze::declare_group; mod no_banned_types; mod no_conditional_assignment; mod no_dupe_keys; +mod no_empty_interface; mod no_explicit_any; mod no_invalid_constructor_super; mod no_precision_loss; @@ -11,4 +12,4 @@ mod no_unsafe_finally; mod use_flat_map; mod use_numeric_literals; mod use_valid_for_direction; -declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_banned_types :: NoBannedTypes , self :: no_conditional_assignment :: NoConditionalAssignment , self :: no_dupe_keys :: NoDupeKeys , self :: no_explicit_any :: NoExplicitAny , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_precision_loss :: NoPrecisionLoss , self :: no_unsafe_finally :: NoUnsafeFinally , self :: use_flat_map :: UseFlatMap , self :: use_numeric_literals :: UseNumericLiterals , self :: use_valid_for_direction :: UseValidForDirection ,] } } +declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_banned_types :: NoBannedTypes , self :: no_conditional_assignment :: NoConditionalAssignment , self :: no_dupe_keys :: NoDupeKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_explicit_any :: NoExplicitAny , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_precision_loss :: NoPrecisionLoss , self :: no_unsafe_finally :: NoUnsafeFinally , self :: use_flat_map :: UseFlatMap , self :: use_numeric_literals :: UseNumericLiterals , self :: use_valid_for_direction :: UseValidForDirection ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/nursery/no_empty_interface.rs b/crates/rome_js_analyze/src/analyzers/nursery/no_empty_interface.rs new file mode 100644 index 00000000000..7f68f4f83ac --- /dev/null +++ b/crates/rome_js_analyze/src/analyzers/nursery/no_empty_interface.rs @@ -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 { + 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; + type State = DiagnosticMessage; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> 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, state: &Self::State) -> Option { + let diagnostic = RuleDiagnostic::new(rule_category!(), ctx.query().range(), state.as_str()); + + Some(diagnostic) + } + + fn action(ctx: &RuleContext, state: &Self::State) -> Option { + 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 { + 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) +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/invalid.ts b/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/invalid.ts new file mode 100644 index 00000000000..949e1f11a6f --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/invalid.ts @@ -0,0 +1,13 @@ +interface Baz extends Foo {} + +interface Foo {} + +interface Foo extends Array {} + +interface Foo extends Array {} + +interface Foo extends Bar {} + +declare module FooBar { + export interface Bar extends Baz {} +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/invalid.ts.snap b/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/invalid.ts.snap new file mode 100644 index 00000000000..02316b886a2 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/invalid.ts.snap @@ -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 {} + +interface Foo extends Array {} + +interface Foo extends Bar {} + +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 {} + + 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 {} + + +``` + +``` +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 {} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + 7 │ interface Foo extends Array {} + + i Safe fix: Convert empty interface to type alias. + + 3 3 │ interface Foo {} + 4 4 │ + 5 │ - interface·Foo·extends·Array·{} + 5 │ + type·Foo·=·Array + 6 6 │ + 7 7 │ interface Foo extends Array {} + + +``` + +``` +invalid.ts:7:1 lint/nursery/noEmptyInterface FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! An interface declaring no members is equivalent to its supertype. + + 5 │ interface Foo extends Array {} + 6 │ + > 7 │ interface Foo extends Array {} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + 9 │ interface Foo extends Bar {} + + i Safe fix: Convert empty interface to type alias. + + 5 5 │ interface Foo extends Array {} + 6 6 │ + 7 │ - interface·Foo·extends·Array·{} + 7 │ + type·Foo·=·Array + 8 8 │ + 9 9 │ interface Foo extends Bar {} + + +``` + +``` +invalid.ts:9:1 lint/nursery/noEmptyInterface FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! An interface declaring no members is equivalent to its supertype. + + 7 │ interface Foo extends Array {} + 8 │ + > 9 │ interface Foo extends Bar {} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 │ + 11 │ declare module FooBar { + + i Safe fix: Convert empty interface to type alias. + + 7 7 │ interface Foo extends Array {} + 8 8 │ + 9 │ - interface·Foo·extends·Bar·{} + 9 │ + type·Foo·=·Bar + 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 │ + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/valid.ts b/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/valid.ts new file mode 100644 index 00000000000..6042e5c257e --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/valid.ts @@ -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 {} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/valid.ts.snap b/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/valid.ts.snap new file mode 100644 index 00000000000..325481f0f09 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/valid.ts.snap @@ -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 {} + +``` + + diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index 91acb9dac2c..eeb7b88fce3 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -737,6 +737,8 @@ struct NurserySchema { no_const_assign: Option, #[doc = "Prevents object literals having more than one property declaration for the same name. If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored, which is likely a mistake."] no_dupe_keys: Option, + #[doc = "Disallow the declaration of empty interfaces."] + no_empty_interface: Option, #[doc = "Disallow the any type usage"] no_explicit_any: Option, #[doc = "Prevents the incorrect use of super() inside classes. It also checks whether a call super() is missing from classes that extends other constructors."] @@ -760,11 +762,12 @@ struct NurserySchema { } impl Nursery { const CATEGORY_NAME: &'static str = "nursery"; - pub(crate) const CATEGORY_RULES: [&'static str; 14] = [ + pub(crate) const CATEGORY_RULES: [&'static str; 15] = [ "noBannedTypes", "noConditionalAssignment", "noConstAssign", "noDupeKeys", + "noEmptyInterface", "noExplicitAny", "noInvalidConstructorSuper", "noPrecisionLoss", diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index c073defa37a..86d77a9d128 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -797,6 +797,17 @@ } ] }, + "noEmptyInterface": { + "description": "Disallow the declaration of empty interfaces.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, "noExplicitAny": { "description": "Disallow the any type usage", "anyOf": [ diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index 6bbe463df03..4d2ce1360d9 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -363,6 +363,10 @@ export interface Nursery { * Prevents object literals having more than one property declaration for the same name. If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored, which is likely a mistake. */ noDupeKeys?: RuleConfiguration; + /** + * Disallow the declaration of empty interfaces. + */ + noEmptyInterface?: RuleConfiguration; /** * Disallow the any type usage */ @@ -615,6 +619,7 @@ export type Category = | "lint/nursery/noConditionalAssignment" | "lint/nursery/noConstAssign" | "lint/nursery/noDupeKeys" + | "lint/nursery/noEmptyInterface" | "lint/nursery/noExplicitAny" | "lint/nursery/noInvalidConstructorSuper" | "lint/nursery/noPrecisionLoss" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index c073defa37a..86d77a9d128 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -797,6 +797,17 @@ } ] }, + "noEmptyInterface": { + "description": "Disallow the declaration of empty interfaces.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, "noExplicitAny": { "description": "Disallow the any type usage", "anyOf": [ diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index 8f818eddddd..a086610bb9e 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -467,6 +467,12 @@ Prevents object literals having more than one property declaration for the same If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored, which is likely a mistake.
+

+ noEmptyInterface +

+Disallow the declaration of empty interfaces. +
+

noExplicitAny

diff --git a/website/src/pages/lint/rules/noEmptyInterface.md b/website/src/pages/lint/rules/noEmptyInterface.md new file mode 100644 index 00000000000..a704ee89701 --- /dev/null +++ b/website/src/pages/lint/rules/noEmptyInterface.md @@ -0,0 +1,72 @@ +--- +title: Lint Rule noEmptyInterface +parent: lint/rules/index +--- + +# noEmptyInterface (since v11.0.0) + +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 +interface A {} +``` + +
nursery/noEmptyInterface.js:1:1 lint/nursery/noEmptyInterface  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   An empty interface is equivalent to '{}'.
+  
+  > 1 │ interface A {}
+   ^^^^^^^^^^^^^^
+    2 │ 
+  
+   Safe fix: Convert empty interface to type alias.
+  
+    1  - interface·A·{}
+      1+ type·A·=·{}
+    2 2  
+  
+
+ +```ts +// A === B +interface A extends B {} +``` + +
nursery/noEmptyInterface.js:2:1 lint/nursery/noEmptyInterface  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   An interface declaring no members is equivalent to its supertype.
+  
+    1 │ // A === B
+  > 2 │ interface A extends B {}
+   ^^^^^^^^^^^^^^^^^^^^^^^^
+    3 │ 
+  
+   Safe fix: Convert empty interface to type alias.
+  
+    1 1  // A === B
+    2  - interface·A·extends·B·{}
+      2+ type·A·=·B
+    3 3  
+  
+
+ +### Valid + +```ts +interface A { + prop: string; +} + +// The interface can be used as an union type. +interface A extends B, C {} +``` +