Skip to content

Commit

Permalink
feat(linter) Add ban_types typescript eslint rule (#953)
Browse files Browse the repository at this point in the history
Part of #503 -> `@typescript-eslint/ban-types`
  • Loading branch information
camc314 committed Oct 3, 2023
1 parent b4b39b8 commit 69f2364
Show file tree
Hide file tree
Showing 3 changed files with 416 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ mod eslint {
mod typescript {
pub mod adjacent_overload_signatures;
pub mod ban_ts_comment;
pub mod ban_types;
pub mod consistent_type_exports;
pub mod no_duplicate_enum_values;
pub mod no_empty_interface;
Expand Down Expand Up @@ -191,6 +192,7 @@ oxc_macros::declare_all_lint_rules! {
eslint::valid_typeof,
typescript::adjacent_overload_signatures,
typescript::ban_ts_comment,
typescript::ban_types,
typescript::consistent_type_exports,
typescript::no_duplicate_enum_values,
typescript::no_empty_interface,
Expand Down
164 changes: 164 additions & 0 deletions crates/oxc_linter/src/rules/typescript/ban_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use oxc_ast::AstKind;
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::{self, Error},
};
use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, Span};

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

#[derive(Debug, Error, Diagnostic)]
pub enum BanTypesDiagnostic {
#[error(
"eslint@typescript-eslint/ban-types: Do not use {0:?} as a type. Use \"{1}\" instead."
)]
Type(Atom, String, #[label] Span),
#[error("eslint@typescript-eslint/ban-types: Prefer explicitly define the object shape. This type means \"any non-nullish value\", which is slightly better than 'unknown', but it's still a broad type.")]
TypeLiteral(#[label] Span),
#[error("eslint@typescript-eslint/ban-types: Don't use `Function` as a type. The `Function` type accepts any function-like value.
It provides no type safety when calling the function, which can be a common source of bugs.
It also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.
If you are expecting the function to accept certain arguments, you should explicitly define the function shape.")]
Function(#[label] Span),
#[error(r#"eslint@typescript-eslint/ban-types: 'The `Object` type actually means "any non-nullish value", so it is marginally better than `unknown`.',
- If you want a type meaning "any object", you probably want `object` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.
- If you really want a type meaning "any non-nullish value", you probably want `NonNullable<unknown>` instead."#)]
Object(#[label] Span),
}

#[derive(Debug, Default, Clone)]
pub struct BanTypes;

declare_oxc_lint!(
/// ### What it does
///
/// This rule bans specific types and can suggest alternatives. Note that it does not ban the corresponding runtime objects from being used.
///
/// ### Why is this bad?
///
/// Some built-in types have aliases, while some types are considered dangerous or harmful. It's often a good idea to ban certain types to help with consistency and safety.
///
/// ### Example
/// ```typescript
/// let foo: String = 'foo';
///
/// let bar: Boolean = true;
/// ```
BanTypes,
correctness
);

impl Rule for BanTypes {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::TSTypeReference(typ) => {
let name = match &typ.type_name {
oxc_ast::ast::TSTypeName::IdentifierReference(v) => &v.name,
oxc_ast::ast::TSTypeName::QualifiedName(_) => return,
};

match name.as_str() {
"String" | "Boolean" | "Number" | "Symbol" | "BigInt" => {
ctx.diagnostic(BanTypesDiagnostic::Type(
name.clone(),
name.to_lowercase(),
typ.span,
));
}
"Object" => {
ctx.diagnostic(BanTypesDiagnostic::Object(typ.span));
}
"Function" => {
ctx.diagnostic(BanTypesDiagnostic::Function(typ.span));
}
_ => {}
}
}
AstKind::TSTypeLiteral(typ) => {
if typ.members.is_empty() {
ctx.diagnostic(BanTypesDiagnostic::TypeLiteral(typ.span));
}
}
_ => {}
}
}
}

#[test]
#[allow(clippy::too_many_lines)]
fn test() {
use crate::tester::Tester;

let pass = vec![
("let a = Object();", None),
("let foo: { x: number; y: number } = { x: 1, y: 1 };", None),
("let g = Object.create(null);", None),
("let h = String(false);", None),
("let b: undefined;", None),
("let c: null;", None),
("let a: [];", None),
("let tuple: [boolean, string] = [true, \"hello\"];", None),
(
"
type Props = {
onClick: () => void;
}",
None,
),
];

let fail = vec![
("let a: String;", None),
("let b: Boolean;", None),
("let c: Number;", None),
("let d: Symbol;", None),
("let e: BigInt;", None),
("let f: Object;", None),
("let g: Function;", None),
("let h: {}; ", None),
("let i: { b: String };", None),
("let j: { c: String };", None),
("function foo(arg0: String) {}", None),
("'foo' as String;", None),
("'baz' as Function;", None),
("let d: Symbol = Symbol('foo');", None),
("let baz: [boolean, Boolean] = [true, false];", None),
("let z = true as Boolean;", None),
("type Props = {};", None),
("let fn: Function = () => true", None),
("const str: String = 'foo';", None),
("const bool: Boolean = true;", None),
("const num: Number = 1;", None),
("const symb: Symbol = Symbol('foo');", None),
("const bigInt: BigInt = 1n;", None),
(
"const emptyObj: {
} = {foo: \"bar\"};",
None,
),
("const emptyEmptyObj: {} = { };", None),
(
"
class Test<T = Boolean> extends Foo<String> implements Bar<Object> {
constructor(foo: String | Object | Function) {}
arg(): Array<String> {
const foo: String = 1 as String;
}
}",
None,
),
(
"
type Props = {
onClick: Function
}",
None,
),
];

Tester::new(BanTypes::NAME, pass, fail).test_and_snapshot();
}
Loading

0 comments on commit 69f2364

Please sign in to comment.