feat(linter): @typescript-eslint/consistent-indexed-object-style (#3126)

implements
https://typescript-eslint.io/rules/consistent-indexed-object-style/
This commit is contained in:
Todor Andonov 2024-05-25 12:03:00 +03:00 committed by GitHub
parent d971c9cd0b
commit aa26ce9151
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 715 additions and 0 deletions

View file

@ -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,

View file

@ -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<Fix<'a>> {
let params = &tref.type_parameters.as_ref()?;
let end = tref.span.end;
let start = tref.span.start;
let TSType::TSStringKeyword(first) = &params.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<string, any>;",
"type Foo = { [key: string]: any };",
Some(serde_json::json!(["index-signature"])),
),
(
"type Foo<T> = Record<string, T>;",
"type Foo<T> = { [key: string]: T };",
Some(serde_json::json!(["index-signature"])),
),
];
let pass = vec![
("type Foo = Record<string, any>;", 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<T> {
[key: string]: Foo<T>;
}
",
None,
),
(
"
interface Foo<T> {
[key: string]: Foo<T> | 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<string, unknown>;", 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<A> {
[key: string]: A;
}
",
None,
),
(
"
interface Foo<A = any> {
[key: string]: A;
}
",
None,
),
(
"
interface B extends A {
[index: number]: unknown;
}
",
None,
),
(
"
interface Foo<A> {
readonly [key: string]: A;
}
",
None,
),
(
"
interface Foo<A, B> {
[key: A]: B;
}
",
None,
),
(
"
interface Foo<A, B> {
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<string, any>;", Some(serde_json::json!(["index-signature"]))),
("type Foo<T> = Record<string, T>;", 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<T> {
[k: string]: T;
}
",
None,
),
(
"
interface Foo {
[k: string]: A.Foo;
}
",
None,
),
(
"
interface Foo {
[k: string]: { [key: string]: Foo };
}
",
None,
),
("type Foo = Generic<Record<string, any>>;", Some(serde_json::json!(["index-signature"]))),
("function foo(arg: Record<string, any>) {}", Some(serde_json::json!(["index-signature"]))),
("funcction foo(): Record<string, any> {}", Some(serde_json::json!(["index-signature"]))),
];
Tester::new(ConsistentIndexedObjectStyle::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
}

View file

@ -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<A> {
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<A = any> {
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<A> {
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<A, B> {
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<A, B> {
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<string, any>;
· ───────────────────
╰────
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<T> = Record<string, T>;
· ─────────────────
╰────
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<T> {
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<Record<string, any>>;
· ───────────────────
╰────
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<string, any>) {}
· ───────────────────
╰────
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<string, any> {}
· ─
╰────
help: Try insert a semicolon here