Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter) Add ban_types typescript eslint rule #953

Merged
merged 3 commits into from
Oct 3, 2023
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
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(
Boshen marked this conversation as resolved.
Show resolved Hide resolved
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
Loading