Skip to content
Merged
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
74 changes: 63 additions & 11 deletions crates/oxc_linter/src/rules/typescript/no_empty_object_type.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::borrow::Cow;

use lazy_regex::Regex;
use oxc_ast::{
AstKind,
ast::{TSInterfaceDeclaration, TSTypeLiteral},
Expand Down Expand Up @@ -30,8 +31,8 @@ pub struct NoEmptyObjectTypeConfig {
allow_interfaces: AllowInterfaces,
/** Whether to allow empty object type literals. */
allow_object_types: AllowObjectTypes,
/** allow interfaces and object type aliases with the configured name */
allow_with_name: String,
/** allow interfaces and object type aliases with the configured name pattern (regex) */
allow_with_name: Option<Regex>,
}

impl std::ops::Deref for NoEmptyObjectType {
Expand Down Expand Up @@ -85,6 +86,52 @@ declare_oxc_lint!(
/// }
/// type TypeWith = { property: boolean };
/// ```
///
/// ### Options
///
/// #### `allowInterfaces`
///
/// Whether to allow empty interfaces. Default: `"never"`.
///
/// Allowed values are:
/// - `'always'`: to always allow interfaces with no fields
/// - `'never'` _(default)_: to never allow interfaces with no fields
/// - `'with-single-extends'`: to allow empty interfaces that `extend` from a single base interface
///
/// Examples of **correct** code for this rule with `{ allowInterfaces: 'with-single-extends' }`:
/// ```ts
/// interface Base {
/// value: boolean;
/// }
/// interface Derived extends Base {}
/// ```
///
/// #### `allowObjectTypes`
///
/// Whether to allow empty object type literals. Default: `"never"`.
///
/// Allowed values are:
/// - `'always'`: to always allow object type literals with no fields
/// - `'never'` _(default)_: to never allow object type literals with no fields
///
/// #### `allowWithName`
///
/// A stringified regular expression to allow interfaces and object type aliases with the configured name.
///
/// This can be useful if your existing code style includes a pattern of declaring empty types with `{}` instead of `object`.
///
/// Example of **incorrect** code for this rule with `{ allowWithName: 'Props$' }`:
/// ```ts
/// interface InterfaceValue {}
/// type TypeValue = {};
/// ```
///
/// Example of **correct** code for this rule with `{ allowWithName: 'Props$' }`:
/// ```ts
/// interface InterfaceProps {}
/// type TypeProps = {};
/// ```
///
NoEmptyObjectType,
typescript,
restriction,
Expand All @@ -93,7 +140,7 @@ declare_oxc_lint!(
impl Rule for NoEmptyObjectType {
fn from_configuration(value: serde_json::Value) -> Self {
let (allow_interfaces, allow_object_types, allow_with_name) = value.get(0).map_or(
(AllowInterfaces::Never, AllowObjectTypes::Never, String::default()),
(AllowInterfaces::Never, AllowObjectTypes::Never, None),
|config| {
(
config
Expand All @@ -109,8 +156,7 @@ impl Rule for NoEmptyObjectType {
config
.get("allowWithName")
.and_then(serde_json::Value::as_str)
.map(String::from)
.unwrap_or_default(),
.and_then(|pattern| Regex::new(pattern).ok()),
)
},
);
Expand All @@ -128,7 +174,7 @@ impl Rule for NoEmptyObjectType {
ctx,
interface,
self.allow_interfaces,
&self.allow_with_name,
self.allow_with_name.as_ref(),
);
}
AstKind::TSTypeLiteral(typeliteral) if typeliteral.members.is_empty() => {
Expand All @@ -137,7 +183,7 @@ impl Rule for NoEmptyObjectType {
typeliteral,
node.id(),
self.allow_object_types,
&self.allow_with_name,
self.allow_with_name.as_ref(),
);
}
_ => {}
Expand All @@ -153,12 +199,14 @@ fn check_interface_declaration(
ctx: &LintContext,
interface: &TSInterfaceDeclaration,
allow_interfaces: AllowInterfaces,
allow_with_name: &str,
allow_with_name: Option<&Regex>,
) {
if allow_interfaces == AllowInterfaces::Always {
return;
}
if interface.id.name.as_str() == allow_with_name {
if let Some(pattern) = allow_with_name
&& pattern.is_match(interface.id.name.as_str())
{
return;
}
if interface.extends.is_empty()
Expand All @@ -176,15 +224,17 @@ fn check_type_literal(
type_literal: &TSTypeLiteral,
node_id: NodeId,
allow_object_types: AllowObjectTypes,
allow_with_name: &str,
allow_with_name: Option<&Regex>,
) {
if matches!(allow_object_types, AllowObjectTypes::Always) {
return;
}
match ctx.nodes().parent_kind(node_id) {
AstKind::TSIntersectionType(_) => return,
AstKind::TSTypeAliasDeclaration(alias) => {
if alias.id.name.as_str() == allow_with_name {
if let Some(pattern) = allow_with_name
&& pattern.is_match(alias.id.name.as_str())
{
return;
}
}
Expand Down Expand Up @@ -288,8 +338,10 @@ fn test() {
("type Base = {};", Some(serde_json::json!([{ "allowObjectTypes": "always" }]))),
("type Base = {};", Some(serde_json::json!([{ "allowWithName": "Base" }]))),
("type BaseProps = {};", Some(serde_json::json!([{ "allowWithName": "BaseProps" }]))),
("type BaseProps = {};", Some(serde_json::json!([{ "allowWithName": "Props$" }]))),
("interface Base {}", Some(serde_json::json!([{ "allowWithName": "Base" }]))),
("interface BaseProps {}", Some(serde_json::json!([{ "allowWithName": "BaseProps" }]))),
("interface BaseProps {}", Some(serde_json::json!([{ "allowWithName": "Props$" }]))),
];

let fail = vec![
Expand Down
Loading