feat(linter): typescript-eslint/consistent-type-definitions (#2885)

Co-authored-by: wenzhe <mysteryven@gmail.com>
This commit is contained in:
Todor Andonov 2024-04-10 16:09:05 +03:00 committed by GitHub
parent 255c74ccc5
commit b4b471f4cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 418 additions and 0 deletions

View file

@ -125,6 +125,7 @@ mod typescript {
pub mod ban_ts_comment;
pub mod ban_tslint_comment;
pub mod ban_types;
pub mod consistent_type_definitions;
pub mod no_duplicate_enum_values;
pub mod no_empty_interface;
pub mod no_explicit_any;
@ -458,6 +459,7 @@ oxc_macros::declare_all_lint_rules! {
typescript::ban_ts_comment,
typescript::ban_tslint_comment,
typescript::ban_types,
typescript::consistent_type_definitions,
typescript::no_duplicate_enum_values,
typescript::no_empty_interface,
typescript::no_explicit_any,

View file

@ -0,0 +1,259 @@
use oxc_ast::{
ast::{ExportDefaultDeclarationKind, TSType},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use crate::{context::LintContext, rule::Rule, AstNode};
#[derive(Debug, Error, Diagnostic)]
#[error("typescript-eslint(consistent-type-definitions):")]
#[diagnostic(severity(warning), help("Use an `{0}` instead of a `{1}`"))]
struct ConsistentTypeDefinitionsDiagnostic(
&'static str,
&'static str,
#[label("Use an `{0}` instead of a `{1}`")] pub Span,
);
#[derive(Debug, Default, Clone)]
pub struct ConsistentTypeDefinitions {
config: ConsistentTypeDefinitionsConfig,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
enum ConsistentTypeDefinitionsConfig {
#[default]
Interface,
Type,
}
declare_oxc_lint!(
/// ### What it does
///
/// Enforce type definitions to consistently use either interface or type.
///
/// ### Why is this bad?
///
/// TypeScript provides two common ways to define an object type: interface and type.
/// The two are generally very similar, and can often be used interchangeably.
/// Using the same type declaration style consistently helps with code readability.
///
/// ### Example
/// ```ts
/// // incorrect, when set to "interface"
/// type T = { x: number };
///
/// // incorrect when set to "type"
/// interface T {
/// x: number;
/// }
/// ```
ConsistentTypeDefinitions,
style
);
impl Rule for ConsistentTypeDefinitions {
fn from_configuration(value: serde_json::Value) -> Self {
let config = value.get(0).and_then(serde_json::Value::as_str).map_or_else(
ConsistentTypeDefinitionsConfig::default,
|value| match value {
"type" => ConsistentTypeDefinitionsConfig::Type,
_ => ConsistentTypeDefinitionsConfig::Interface,
},
);
Self { config }
}
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::TSTypeAliasDeclaration(decl) => match &decl.type_annotation {
TSType::TSTypeLiteral(_)
if self.config == ConsistentTypeDefinitionsConfig::Interface =>
{
let start = if decl.modifiers.is_contains_declare() {
decl.span.start + 8
} else {
decl.span.start
};
ctx.diagnostic(ConsistentTypeDefinitionsDiagnostic(
"interface",
"type",
Span::new(start, start + 4),
));
}
_ => {}
},
AstKind::ExportDefaultDeclaration(exp) => match &exp.declaration {
ExportDefaultDeclarationKind::TSInterfaceDeclaration(decl)
if self.config == ConsistentTypeDefinitionsConfig::Type =>
{
ctx.diagnostic(ConsistentTypeDefinitionsDiagnostic(
"type",
"interface",
Span::new(decl.span.start, decl.span.start + 9),
));
}
_ => {}
},
AstKind::TSInterfaceDeclaration(decl)
if self.config == ConsistentTypeDefinitionsConfig::Type =>
{
let start = if decl.modifiers.is_contains_declare() {
decl.span.start + 8
} else {
decl.span.start
};
ctx.diagnostic(ConsistentTypeDefinitionsDiagnostic(
"type",
"interface",
Span::new(start, start + 9),
));
}
_ => {}
}
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
("var foo = {};", Some(serde_json::json!(["interface"]))),
("interface A {}", Some(serde_json::json!(["interface"]))),
(
"
interface A extends B {
x: number;
}
",
Some(serde_json::json!(["interface"])),
),
("type U = string;", Some(serde_json::json!(["interface"]))),
("type V = { x: number } | { y: string };", Some(serde_json::json!(["interface"]))),
(
"
type Record<T, U> = {
[K in T]: U;
};
",
Some(serde_json::json!(["interface"])),
),
("type T = { x: number };", Some(serde_json::json!(["type"]))),
("type A = { x: number } & B & C;", Some(serde_json::json!(["type"]))),
("type A = { x: number } & B<T1> & C<T2>;", Some(serde_json::json!(["type"]))),
(
"
export type W<T> = {
x: T;
};
",
Some(serde_json::json!(["type"])),
),
];
let fail = vec![
("type T = { x: number; };", Some(serde_json::json!(["interface"]))),
("type T={ x: number; };", Some(serde_json::json!(["interface"]))),
("type T= { x: number; };", Some(serde_json::json!(["interface"]))),
(
"
export type W<T> = {
x: T;
};
",
Some(serde_json::json!(["interface"])),
),
("interface T { x: number; }", Some(serde_json::json!(["type"]))),
("interface T{ x: number; }", Some(serde_json::json!(["type"]))),
("interface T { x: number; }", Some(serde_json::json!(["type"]))),
("interface A extends B, C { x: number; };", Some(serde_json::json!(["type"]))),
("interface A extends B<T1>, C<T2> { x: number; };", Some(serde_json::json!(["type"]))),
(
"
export interface W<T> {
x: T;
}
",
Some(serde_json::json!(["type"])),
),
(
"
namespace JSX {
interface Array<T> {
foo(x: (x: number) => T): T[];
}
}
",
Some(serde_json::json!(["type"])),
),
(
"
global {
interface Array<T> {
foo(x: (x: number) => T): T[];
}
}
",
Some(serde_json::json!(["type"])),
),
(
"
declare global {
interface Array<T> {
foo(x: (x: number) => T): T[];
}
}
",
Some(serde_json::json!(["type"])),
),
(
"
declare global {
namespace Foo {
interface Bar {}
}
}
",
Some(serde_json::json!(["type"])),
),
(
"
export default interface Test {
bar(): string;
foo(): number;
}
",
Some(serde_json::json!(["type"])),
),
(
"
export declare type Test = {
foo: string;
bar: string;
};
",
Some(serde_json::json!(["interface"])),
),
(
"
export declare interface Test {
foo: string;
bar: string;
}
",
Some(serde_json::json!(["type"])),
),
];
Tester::new(ConsistentTypeDefinitions::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,157 @@
---
source: crates/oxc_linter/src/tester.rs
expression: consistent_type_definitions
---
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:1:1]
1 │ type T = { x: number; };
· ──┬─
· ╰── Use an `interface` instead of a `type`
╰────
help: Use an `interface` instead of a `type`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:1:1]
1 │ type T={ x: number; };
· ──┬─
· ╰── Use an `interface` instead of a `type`
╰────
help: Use an `interface` instead of a `type`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:1:1]
1 │ type T= { x: number; };
· ──┬─
· ╰── Use an `interface` instead of a `type`
╰────
help: Use an `interface` instead of a `type`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:2:11]
1 │
2 │ export type W<T> = {
· ──┬─
· ╰── Use an `interface` instead of a `type`
3 │ x: T;
╰────
help: Use an `interface` instead of a `type`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:1:1]
1 │ interface T { x: number; }
· ────┬────
· ╰── Use an `type` instead of a `interface`
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:1:1]
1 │ interface T{ x: number; }
· ────┬────
· ╰── Use an `type` instead of a `interface`
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:1:1]
1 │ interface T { x: number; }
· ────┬────
· ╰── Use an `type` instead of a `interface`
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:1:1]
1 │ interface A extends B, C { x: number; };
· ────┬────
· ╰── Use an `type` instead of a `interface`
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:1:1]
1 │ interface A extends B<T1>, C<T2> { x: number; };
· ────┬────
· ╰── Use an `type` instead of a `interface`
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:2:11]
1 │
2 │ export interface W<T> {
· ────┬────
· ╰── Use an `type` instead of a `interface`
3 │ x: T;
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:3:6]
2 │ namespace JSX {
3 │ interface Array<T> {
· ────┬────
· ╰── Use an `type` instead of a `interface`
4 │ foo(x: (x: number) => T): T[];
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:3:6]
2 │ global {
3 │ interface Array<T> {
· ────┬────
· ╰── Use an `type` instead of a `interface`
4 │ foo(x: (x: number) => T): T[];
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:3:6]
2 │ declare global {
3 │ interface Array<T> {
· ────┬────
· ╰── Use an `type` instead of a `interface`
4 │ foo(x: (x: number) => T): T[];
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:4:8]
3 │ namespace Foo {
4 │ interface Bar {}
· ────┬────
· ╰── Use an `type` instead of a `interface`
5 │ }
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:2:19]
1 │
2 │ export default interface Test {
· ────┬────
· ╰── Use an `type` instead of a `interface`
3 │ bar(): string;
╰────
help: Use an `type` instead of a `interface`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:2:19]
1 │
2 │ export declare type Test = {
· ──┬─
· ╰── Use an `interface` instead of a `type`
3 │ foo: string;
╰────
help: Use an `interface` instead of a `type`
⚠ typescript-eslint(consistent-type-definitions):
╭─[consistent_type_definitions.tsx:2:19]
1 │
2 │ export declare interface Test {
· ────┬────
· ╰── Use an `type` instead of a `interface`
3 │ foo: string;
╰────
help: Use an `type` instead of a `interface`