Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,48 @@ use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::{
AstNode,
context::{ContextHost, LintContext},
rule::Rule,
};

fn consistent_indexed_object_style_diagnostic(a: &str, b: &str, span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn(format!("A {a} is preferred over an {b}."))
.with_help(format!("A {a} is preferred over an {b}."))
.with_label(span)
fn consistent_indexed_object_style_diagnostic(
preferred: ConsistentIndexedObjectStyleConfig,
span: Span,
) -> OxcDiagnostic {
let (warning_message, help_message) = match preferred {
ConsistentIndexedObjectStyleConfig::Record => (
"A record is preferred over an index signature.",
"Use a record type like `Record<string, unknown>` instead of an index signature.",
),
ConsistentIndexedObjectStyleConfig::IndexSignature => (
"An index signature is preferred over a record.",
"Use an index signature like `{ [key: string]: unknown }` instead of a record type.",
),
};

OxcDiagnostic::warn(warning_message).with_help(help_message).with_label(span)
}

#[derive(Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct ConsistentIndexedObjectStyle {
/// When set to `true`, enforces the use of `Record` type for indexed object types.
/// When set to `false`, enforces the use of indexed signature types.
is_record_mode: bool,
/// When set to `record`, enforces the use of a `Record` for indexed object types, e.g. `Record<string, unknown>`.
/// When set to `index-signature`, enforces the use of indexed signature types, e.g. `{ [key: string]: unknown }`.
preferred_style: ConsistentIndexedObjectStyleConfig,
}

impl Default for ConsistentIndexedObjectStyle {
fn default() -> Self {
Self { is_record_mode: true }
Self { preferred_style: ConsistentIndexedObjectStyleConfig::Record }
}
}

#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
enum ConsistentIndexedObjectStyleConfig {
#[default]
Record,
Expand All @@ -45,47 +59,54 @@ declare_oxc_lint!(
///
/// Choose between requiring either `Record` type or indexed signature types.
///
/// These two types are equivalent, this rule enforces consistency in picking one style over the other:
///
/// ```ts
/// type Foo = Record<string, unknown>;
///
/// type Foo = {
/// [key: string]: unknown;
/// }
/// ```
///
/// ### Why is this bad?
///
/// Inconsistent style for indexed object types can harm readability in a project.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule with the default "record":
/// ```ts
/// /*eslint consistent-indexed-object-style: ["error", "record"]*/
/// Examples of **incorrect** code for this rule with
/// `consistent-indexed-object-style: ["error", "record"]` (default):
///
/// ```ts
/// interface Foo {
/// [key: string]: unknown;
/// [key: string]: unknown;
/// }
/// type Foo = {
/// [key: string]: unknown;
/// [key: string]: unknown;
/// };
/// ```
///
/// Examples of **correct** code for this rule:
/// Examples of **correct** code for this rule with
/// `consistent-indexed-object-style: ["error", "record"]` (default):
/// ```ts
/// /*eslint consistent-indexed-object-style: ["error", "record"]*/
///
/// type Foo = Record<string, unknown>;
/// ```
///
/// Examples of **incorrect** code for this rule with "index-signature":
/// Examples of **incorrect** code for this rule with
/// `consistent-indexed-object-style: ["error", "index-signature"]`:
/// ```ts
/// /*eslint consistent-indexed-object-style: ["error", "index-signature"]*/
///
/// type Foo = Record<string, unknown>;
/// ```
///
/// Examples of **correct** code for this rule:
/// Examples of **correct** code for this rule with
/// `consistent-indexed-object-style: ["error", "index-signature"]`:
/// ```ts
/// /*eslint consistent-indexed-object-style: ["error", "index-signature"]*/
///
/// interface Foo {
/// [key: string]: unknown;
/// [key: string]: unknown;
/// }
/// type Foo = {
/// [key: string]: unknown;
/// [key: string]: unknown;
/// };
/// ```
ConsistentIndexedObjectStyle,
Expand All @@ -104,11 +125,13 @@ impl Rule for ConsistentIndexedObjectStyle {
_ => ConsistentIndexedObjectStyleConfig::IndexSignature,
},
);
Self { is_record_mode: config == ConsistentIndexedObjectStyleConfig::Record }
Self { preferred_style: config }
}

fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if self.is_record_mode {
let preferred_style = self.preferred_style;

if self.preferred_style == ConsistentIndexedObjectStyleConfig::Record {
match node.kind() {
AstKind::TSInterfaceDeclaration(inf) => {
if inf.body.body.len() > 1 {
Expand All @@ -126,16 +149,14 @@ impl Rule for ConsistentIndexedObjectStyle {
TSTypeName::IdentifierReference(ide) => {
if ide.name != inf.id.name {
ctx.diagnostic(consistent_indexed_object_style_diagnostic(
"record",
"index signature",
preferred_style,
sig.span,
));
}
}
TSTypeName::QualifiedName(_) | TSTypeName::ThisExpression(_) => {
ctx.diagnostic(consistent_indexed_object_style_diagnostic(
"record",
"index signature",
preferred_style,
sig.span,
));
}
Expand All @@ -153,8 +174,7 @@ impl Rule for ConsistentIndexedObjectStyle {

if dec.id.name != ide.name {
ctx.diagnostic(consistent_indexed_object_style_diagnostic(
"record",
"index signature",
preferred_style,
sig.span,
));
}
Expand All @@ -163,8 +183,7 @@ impl Rule for ConsistentIndexedObjectStyle {
}
_ => {
ctx.diagnostic(consistent_indexed_object_style_diagnostic(
"record",
"index signature",
preferred_style,
sig.span,
));
}
Expand All @@ -190,16 +209,14 @@ impl Rule for ConsistentIndexedObjectStyle {

if ide.name != dec.id.name {
ctx.diagnostic(consistent_indexed_object_style_diagnostic(
"record",
"index signature",
preferred_style,
sig.span,
));
}
}
TSTypeName::QualifiedName(_) | TSTypeName::ThisExpression(_) => {
ctx.diagnostic(consistent_indexed_object_style_diagnostic(
"record",
"index signature",
preferred_style,
sig.span,
));
}
Expand All @@ -217,8 +234,7 @@ impl Rule for ConsistentIndexedObjectStyle {

if dec.id.name != ide.name {
ctx.diagnostic(consistent_indexed_object_style_diagnostic(
"record",
"index signature",
preferred_style,
sig.span,
));
}
Expand All @@ -227,8 +243,7 @@ impl Rule for ConsistentIndexedObjectStyle {
}
_ => {
ctx.diagnostic(consistent_indexed_object_style_diagnostic(
"record",
"index signature",
preferred_style,
sig.span,
));
}
Expand All @@ -252,11 +267,7 @@ impl Rule for ConsistentIndexedObjectStyle {
&tref.type_arguments.as_ref().and_then(|params| params.params.first())
{
ctx.diagnostic_with_fix(
consistent_indexed_object_style_diagnostic(
"index signature",
"record",
tref.span,
),
consistent_indexed_object_style_diagnostic(preferred_style, tref.span),
|fixer| {
let key = fixer.source_range(first.span);
let params_span = Span::new(first.span.end + 1, tref.span.end - 1);
Expand All @@ -267,8 +278,7 @@ impl Rule for ConsistentIndexedObjectStyle {
);
} else {
ctx.diagnostic(consistent_indexed_object_style_diagnostic(
"index signature",
"record",
preferred_style,
tref.span,
));
}
Expand Down
Loading
Loading