feat(rome_js_analyze): add noEmptyInterface rule
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
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",

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
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 ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/no_empty_interface.rs
+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)
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/invalid.ts
+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 {}
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/valid.ts
+interface A extends B {
+ prop: number;
+}
+
+// valid because extending multiple interfaces
+// can be used instead of a union type
+interface Baz extends Foo, Bar {}

ADD
crates/rome_js_analyze/tests/specs/nursery/noEmptyInterface/valid.ts.snap
+---
+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 {}
+
+```
+
+

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -737,6 +737,8 @@ struct NurserySchema {
no_const_assign: Option<RuleConfiguration>,
#[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<RuleConfiguration>,
+ #[doc = "Disallow the declaration of empty interfaces."]
+ no_empty_interface: Option<RuleConfiguration>,
#[doc = "Disallow the any type usage"]
no_explicit_any: Option<RuleConfiguration>,
#[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",

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
}
]
},
+ "noEmptyInterface": {
+ "description": "Disallow the declaration of empty interfaces.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noExplicitAny": {
"description": "Disallow the any type usage",
"anyOf": [

MODIFY
npm/backend-jsonrpc/src/workspace.ts
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"

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
}
]
},
+ "noEmptyInterface": {
+ "description": "Disallow the declaration of empty interfaces.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noExplicitAny": {
"description": "Disallow the any type usage",
"anyOf": [

MODIFY
website/src/pages/lint/rules/index.mdx
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.
</section>
<section class="rule">
+<h3 data-toc-exclude id="noEmptyInterface">
+ <a href="/lint/rules/noEmptyInterface">noEmptyInterface</a>
+</h3>
+Disallow the declaration of empty interfaces.
+</section>
+<section class="rule">
<h3 data-toc-exclude id="noExplicitAny">
<a href="/lint/rules/noExplicitAny">noExplicitAny</a>
</h3>

feat(rome_js_analyze): add noEmptyInterface rule (#3801)