diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 0f454274f..1a30911e2 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -119,6 +119,7 @@ mod typescript { pub mod ban_ts_comment; pub mod ban_tslint_comment; pub mod ban_types; + pub mod consistent_indexed_object_style; pub mod consistent_type_definitions; pub mod no_duplicate_enum_values; pub mod no_empty_interface; @@ -487,6 +488,7 @@ oxc_macros::declare_all_lint_rules! { typescript::prefer_enum_initializers, typescript::ban_types, typescript::consistent_type_definitions, + typescript::consistent_indexed_object_style, typescript::no_duplicate_enum_values, typescript::no_empty_interface, typescript::no_explicit_any, diff --git a/crates/oxc_linter/src/rules/typescript/consistent_indexed_object_style.rs b/crates/oxc_linter/src/rules/typescript/consistent_indexed_object_style.rs new file mode 100644 index 000000000..a2a22126e --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/consistent_indexed_object_style.rs @@ -0,0 +1,485 @@ +use oxc_ast::{ + ast::{TSSignature, TSType, TSTypeName, TSTypeReference}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, fixer::Fix, rule::Rule, AstNode}; + +fn consistent_indexed_object_style_diagnostic(a: &str, b: &str, span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!( + "typescript-eslint(consistent-indexed-object-style):A {a} is preferred over an {b}." + )) + .with_help(format!("A {a} is preferred over an {b}.")) + .with_labels([span.into()]) +} + +#[derive(Debug, Default, Clone)] +pub struct ConsistentIndexedObjectStyle { + is_record_mode: bool, +} + +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +enum ConsistentIndexedObjectStyleConfig { + #[default] + Record, + IndexSignature, +} + +declare_oxc_lint!( + /// ### What it does + /// Require or disallow the `Record` type. + /// + /// ### Why is this bad? + /// + /// + /// ### Example + /// ```javascript + /// ``` + ConsistentIndexedObjectStyle, + style +); + +impl Rule for ConsistentIndexedObjectStyle { + fn from_configuration(value: serde_json::Value) -> Self { + let config = value.get(0).and_then(serde_json::Value::as_str).map_or_else( + ConsistentIndexedObjectStyleConfig::default, + |value| match value { + "record" => ConsistentIndexedObjectStyleConfig::Record, + _ => ConsistentIndexedObjectStyleConfig::IndexSignature, + }, + ); + Self { is_record_mode: config == ConsistentIndexedObjectStyleConfig::Record } + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if self.is_record_mode { + match node.kind() { + AstKind::TSInterfaceDeclaration(inf) => { + if inf.body.body.len() > 1 { + return; + } + let member = inf.body.body.first(); + let Some(member) = member else { + return; + }; + + let TSSignature::TSIndexSignature(sig) = member else { return }; + + match &sig.type_annotation.type_annotation { + TSType::TSTypeReference(r) => match &r.type_name { + TSTypeName::IdentifierReference(ide) => { + if ide.name != inf.id.name { + ctx.diagnostic(consistent_indexed_object_style_diagnostic( + "record", + "index signature", + sig.span, + )); + } + } + TSTypeName::QualifiedName(_) => { + ctx.diagnostic(consistent_indexed_object_style_diagnostic( + "record", + "index signature", + sig.span, + )); + } + }, + TSType::TSUnionType(uni) => { + for t in &uni.types { + if let TSType::TSTypeReference(tref) = t { + if let TSTypeName::IdentifierReference(ide) = &tref.type_name { + let Some(AstKind::TSTypeAliasDeclaration(dec)) = + ctx.nodes().parent_kind(node.id()) + else { + return; + }; + + if dec.id.name != ide.name { + ctx.diagnostic( + consistent_indexed_object_style_diagnostic( + "record", + "index signature", + sig.span, + ), + ); + } + } + } + } + } + _ => { + ctx.diagnostic(consistent_indexed_object_style_diagnostic( + "record", + "index signature", + sig.span, + )); + } + } + } + AstKind::TSTypeLiteral(lit) => { + if lit.members.len() > 1 { + return; + } + + let Some(TSSignature::TSIndexSignature(sig)) = lit.members.first() else { + return; + }; + + match &sig.type_annotation.type_annotation { + TSType::TSTypeReference(r) => match &r.type_name { + TSTypeName::IdentifierReference(ide) => { + let Some(parent) = ctx.nodes().parent_kind(node.id()) else { + return; + }; + + let parent_name = + if let AstKind::TSTypeAliasDeclaration(dec) = parent { + &dec.id.name + } else { + return; + }; + + if ide.name != parent_name { + ctx.diagnostic(consistent_indexed_object_style_diagnostic( + "record", + "index signature", + sig.span, + )); + } + } + TSTypeName::QualifiedName(_) => { + ctx.diagnostic(consistent_indexed_object_style_diagnostic( + "record", + "index signature", + sig.span, + )); + } + }, + TSType::TSUnionType(uni) => { + for t in &uni.types { + if let TSType::TSTypeReference(tref) = t { + if let TSTypeName::IdentifierReference(ide) = &tref.type_name { + let Some(AstKind::TSTypeAliasDeclaration(dec)) = + ctx.nodes().parent_kind(node.id()) + else { + return; + }; + + if dec.id.name != ide.name { + ctx.diagnostic( + consistent_indexed_object_style_diagnostic( + "record", + "index signature", + sig.span, + ), + ); + } + } + } + } + } + _ => { + ctx.diagnostic(consistent_indexed_object_style_diagnostic( + "record", + "index signature", + sig.span, + )); + } + } + } + _ => {} + } + } else if let AstKind::TSTypeReference(tref) = node.kind() { + if let TSTypeName::IdentifierReference(ide) = &tref.type_name { + if ide.name != "Record" { + return; + } + + let Some(params) = &tref.type_parameters else { return }; + if params.params.len() != 2 { + return; + } + + let fixer = fix_for_index_signature(ctx, tref); + match fixer { + Some(fix) => { + ctx.diagnostic_with_fix( + consistent_indexed_object_style_diagnostic( + "index signature", + "record", + tref.span, + ), + || fix, + ); + } + None => { + ctx.diagnostic(consistent_indexed_object_style_diagnostic( + "index signature", + "record", + tref.span, + )); + } + } + } + } + } +} + +fn fix_for_index_signature<'a>( + ctx: &LintContext<'a>, + tref: &TSTypeReference<'a>, +) -> Option> { + let params = &tref.type_parameters.as_ref()?; + + let end = tref.span.end; + let start = tref.span.start; + + let TSType::TSStringKeyword(first) = ¶ms.params[0] else { + return None; + }; + + let key = &ctx.source_text()[first.span.start as usize..first.span.end as usize]; + let params = &ctx.source_text()[(first.span.end + 2) as usize..(end - 1) as usize]; + Some(Fix::new(format!("{{ [key: {key}]: {params} }}"), Span::new(start, end))) +} + +#[test] +fn test() { + use crate::tester::Tester; + + let fix = vec![ + ( + "type Foo = Record;", + "type Foo = { [key: string]: any };", + Some(serde_json::json!(["index-signature"])), + ), + ( + "type Foo = Record;", + "type Foo = { [key: string]: T };", + Some(serde_json::json!(["index-signature"])), + ), + ]; + + let pass = vec![ + ("type Foo = Record;", None), + ("interface Foo {}", None), + ( + " + interface Foo { + bar: string; + } + ", + None, + ), + ( + " + interface Foo { + bar: string; + [key: string]: any; + } + ", + None, + ), + ( + " + interface Foo { + [key: string]: any; + bar: string; + } + ", + None, + ), + ("type Foo = { [key: string]: string | Foo };", None), + ("type Foo = { [key: string]: Foo };", None), + ("type Foo = { [key: string]: Foo } | Foo;", None), + ( + " + interface Foo { + [key: string]: Foo; + } + ", + None, + ), + ( + " + interface Foo { + [key: string]: Foo; + } + ", + None, + ), + ( + " + interface Foo { + [key: string]: Foo | string; + } + ", + None, + ), + ("type Foo = {};", None), + ( + " + type Foo = { + bar: string; + [key: string]: any; + }; + ", + None, + ), + ( + " + type Foo = { + bar: string; + }; + ", + None, + ), + ( + " + type Foo = { + [key: string]: any; + bar: string; + }; + ", + None, + ), + ( + " + type Foo = Generic<{ + [key: string]: any; + bar: string; + }>; + ", + None, + ), + ("function foo(arg: { [key: string]: any; bar: string }) {}", None), + ("function foo(): { [key: string]: any; bar: string } {}", None), + ("type Foo = Misc;", Some(serde_json::json!(["index-signature"]))), + ("type Foo = Record;", Some(serde_json::json!(["index-signature"]))), + ("type Foo = { [key: string]: any };", Some(serde_json::json!(["index-signature"]))), + ( + "type Foo = Generic<{ [key: string]: any }>;", + Some(serde_json::json!(["index-signature"])), + ), + ( + "function foo(arg: { [key: string]: any }) {}", + Some(serde_json::json!(["index-signature"])), + ), + ("function foo(): { [key: string]: any } {}", Some(serde_json::json!(["index-signature"]))), + ("type T = A.B;", Some(serde_json::json!(["index-signature"]))), + ]; + + let fail = vec![ + ( + " + interface Foo { + [key: string]: any; + } + ", + None, + ), + ( + " + interface Foo { + readonly [key: string]: any; + } + ", + None, + ), + ( + " + interface Foo { + [key: string]: A; + } + ", + None, + ), + ( + " + interface Foo { + [key: string]: A; + } + ", + None, + ), + ( + " + interface B extends A { + [index: number]: unknown; + } + ", + None, + ), + ( + " + interface Foo { + readonly [key: string]: A; + } + ", + None, + ), + ( + " + interface Foo { + [key: A]: B; + } + ", + None, + ), + ( + " + interface Foo { + readonly [key: A]: B; + } + ", + None, + ), + ("type Foo = { [key: string]: string | Bar };", None), + ("type Foo = { [key: boolean]: any };", None), + ("type Foo = { readonly [key: string]: any };", None), + ("type Foo = Generic<{ [key: boolean]: any }>;", None), + ("type Foo = Generic<{ readonly [key: string]: any }>;", None), + ("function foo(arg: { [key: string]: any }) {}", None), + ("function foo(): { [key: string]: any } {}", None), + ("function foo(arg: { readonly [key: string]: any }) {}", None), + ("function foo(): { readonly [key: string]: any } {}", None), + ("type Foo = Record;", Some(serde_json::json!(["index-signature"]))), + ("type Foo = Record;", Some(serde_json::json!(["index-signature"]))), + ("type Foo = { [k: string]: A.Foo };", None), + ("type Foo = { [key: string]: AnotherFoo };", None), + ("type Foo = { [key: string]: { [key: string]: Foo } };", None), + ("type Foo = { [key: string]: string } | Foo;", None), + ( + " + interface Foo { + [k: string]: T; + } + ", + None, + ), + ( + " + interface Foo { + [k: string]: A.Foo; + } + ", + None, + ), + ( + " + interface Foo { + [k: string]: { [key: string]: Foo }; + } + ", + None, + ), + ("type Foo = Generic>;", Some(serde_json::json!(["index-signature"]))), + ("function foo(arg: Record) {}", Some(serde_json::json!(["index-signature"]))), + ("funcction foo(): Record {}", Some(serde_json::json!(["index-signature"]))), + ]; + + Tester::new(ConsistentIndexedObjectStyle::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/consistent_indexed_object_style.snap b/crates/oxc_linter/src/snapshots/consistent_indexed_object_style.snap new file mode 100644 index 000000000..121e007af --- /dev/null +++ b/crates/oxc_linter/src/snapshots/consistent_indexed_object_style.snap @@ -0,0 +1,228 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: consistent_indexed_object_style +--- + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ [key: string]: any; + · ─────────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ readonly [key: string]: any; + · ──────────────────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ [key: string]: A; + · ───────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ [key: string]: A; + · ───────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface B extends A { + 3 │ [index: number]: unknown; + · ───────────────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ readonly [key: string]: A; + · ────────────────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ [key: A]: B; + · ──────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ readonly [key: A]: B; + · ───────────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:14] + 1 │ type Foo = { [key: string]: string | Bar }; + · ─────────────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:14] + 1 │ type Foo = { [key: boolean]: any }; + · ─────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:14] + 1 │ type Foo = { readonly [key: string]: any }; + · ─────────────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:22] + 1 │ type Foo = Generic<{ [key: boolean]: any }>; + · ─────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:22] + 1 │ type Foo = Generic<{ readonly [key: string]: any }>; + · ─────────────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:21] + 1 │ function foo(arg: { [key: string]: any }) {} + · ────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:19] + 1 │ function foo(): { [key: string]: any } {} + · ────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:21] + 1 │ function foo(arg: { readonly [key: string]: any }) {} + · ─────────────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:19] + 1 │ function foo(): { readonly [key: string]: any } {} + · ─────────────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A index signature is preferred over an record. + ╭─[consistent_indexed_object_style.tsx:1:12] + 1 │ type Foo = Record; + · ─────────────────── + ╰──── + help: A index signature is preferred over an record. + + ⚠ typescript-eslint(consistent-indexed-object-style):A index signature is preferred over an record. + ╭─[consistent_indexed_object_style.tsx:1:15] + 1 │ type Foo = Record; + · ───────────────── + ╰──── + help: A index signature is preferred over an record. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:14] + 1 │ type Foo = { [k: string]: A.Foo }; + · ────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:14] + 1 │ type Foo = { [key: string]: AnotherFoo }; + · ───────────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:14] + 1 │ type Foo = { [key: string]: { [key: string]: Foo } }; + · ───────────────────────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:1:14] + 1 │ type Foo = { [key: string]: string } | Foo; + · ───────────────────── + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ [k: string]: T; + · ─────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ [k: string]: A.Foo; + · ─────────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A record is preferred over an index signature. + ╭─[consistent_indexed_object_style.tsx:3:12] + 2 │ interface Foo { + 3 │ [k: string]: { [key: string]: Foo }; + · ──────────────────────────────────── + 4 │ } + ╰──── + help: A record is preferred over an index signature. + + ⚠ typescript-eslint(consistent-indexed-object-style):A index signature is preferred over an record. + ╭─[consistent_indexed_object_style.tsx:1:20] + 1 │ type Foo = Generic>; + · ─────────────────── + ╰──── + help: A index signature is preferred over an record. + + ⚠ typescript-eslint(consistent-indexed-object-style):A index signature is preferred over an record. + ╭─[consistent_indexed_object_style.tsx:1:19] + 1 │ function foo(arg: Record) {} + · ─────────────────── + ╰──── + help: A index signature is preferred over an record. + + × Expected a semicolon or an implicit semicolon after a statement, but found none + ╭─[consistent_indexed_object_style.tsx:1:10] + 1 │ funcction foo(): Record {} + · ─ + ╰──── + help: Try insert a semicolon here