Skip to content

Commit

Permalink
feat(linter): implement noRestrictedTypes (#3585)
Browse files Browse the repository at this point in the history
  • Loading branch information
minht11 committed Sep 3, 2024
1 parent 73656ec commit 70557f2
Show file tree
Hide file tree
Showing 15 changed files with 682 additions and 92 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b

#### New features

- Add [nursery/noRestrictedTypes](https://biomejs.dev/linter/no-restricted-types/). Contributed by @minht11
- Add support for GraphQL linting. Contributed by @ematipico

- Add [nursery/noDynamicNamespaceImportAccess](https://biomejs.dev/linter/no-dynamic-namespace-import-access/). Contributed by @minht11
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

204 changes: 112 additions & 92 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ define_categories! {
"lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword",
"lint/nursery/noReactSpecificProps": "https://biomejs.dev/linter/rules/no-react-specific-props",
"lint/nursery/noRestrictedImports": "https://biomejs.dev/linter/rules/no-restricted-imports",
"lint/nursery/noRestrictedTypes": "https://biomejs.dev/linter/rules/no-restricted-types",
"lint/nursery/noShorthandPropertyOverrides": "https://biomejs.dev/linter/rules/no-shorthand-property-overrides",
"lint/nursery/noStaticElementInteractions": "https://biomejs.dev/linter/rules/no-static-element-interactions",
"lint/nursery/noSubstr": "https://biomejs.dev/linter/rules/no-substr",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod no_label_without_control;
pub mod no_misplaced_assertion;
pub mod no_react_specific_props;
pub mod no_restricted_imports;
pub mod no_restricted_types;
pub mod no_static_element_interactions;
pub mod no_substr;
pub mod no_undeclared_dependencies;
Expand Down Expand Up @@ -60,6 +61,7 @@ declare_lint_group! {
self :: no_misplaced_assertion :: NoMisplacedAssertion ,
self :: no_react_specific_props :: NoReactSpecificProps ,
self :: no_restricted_imports :: NoRestrictedImports ,
self :: no_restricted_types :: NoRestrictedTypes ,
self :: no_static_element_interactions :: NoStaticElementInteractions ,
self :: no_substr :: NoSubstr ,
self :: no_undeclared_dependencies :: NoUndeclaredDependencies ,
Expand Down
130 changes: 130 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_restricted_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use crate::JsRuleAction;
use ::serde::{Deserialize, Serialize};
use biome_analyze::context::RuleContext;
use biome_analyze::{
declare_lint_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, RuleSource,
};
use biome_console::markup;
use biome_deserialize_macros::Deserializable;
use biome_js_factory::make;
use biome_js_syntax::TsReferenceType;
use biome_rowan::AstNode;
use biome_rowan::BatchMutationExt;
use biome_unicode_table::is_js_ident;
use rustc_hash::FxHashMap;

#[cfg(feature = "schemars")]
use schemars::JsonSchema;

declare_lint_rule! {
/// Disallow user defined types.
///
/// This rule allows you to specify type names that you don’t want to use in your application.
///
/// To prevent use of commonly misleading types, you can refer to [noBannedTypes](https://biomejs.dev/linter/rules/no-banned-types/)
///
/// ## Options
///
/// Use the options to specify additional types that you want to restrict in your
/// source code.
///
/// ```json
/// {
/// "//": "...",
/// "options": {
/// "types": {
/// "Foo": {
/// "message": "Only bar is allowed",
/// "use": "bar"
/// },
/// "OldAPI": {
/// "message": "Use NewAPI instead"
/// }
/// }
/// }
/// }
/// ```
///
/// In the example above, the rule will emit a diagnostics if tried to use `Foo` or `OldAPI` are used.
///
pub NoRestrictedTypes {
version: "next",
name: "noRestrictedTypes",
language: "ts",
sources: &[
RuleSource::EslintTypeScript("no-restricted-types"),
],
recommended: false,
fix_kind: FixKind::Safe,
}
}

impl Rule for NoRestrictedTypes {
type Query = Ast<TsReferenceType>;
type State = CustomRestrictedType;
type Signals = Option<Self::State>;
type Options = NoRestrictedTypesOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let ts_reference_type = ctx.query();
let options = ctx.options();

let ts_any_name = ts_reference_type.name().ok()?;
let identifier = ts_any_name.as_js_reference_identifier()?;
let identifier_token = identifier.value_token().ok()?;
let token_name = identifier_token.text_trimmed();

let restricted_type = options.types.get(token_name)?.clone();

Some(restricted_type)
}

fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(RuleDiagnostic::new(
rule_category!(),
ctx.query().range(),
markup! { {state.message} }.to_owned(),
))
}

fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
let suggested_type = state.use_instead.as_ref()?;
if !is_js_ident(suggested_type) {
return None;
}

let mut mutation = ctx.root().begin();

let ts_reference_type = ctx.query();
let ts_any_name = ts_reference_type.name().ok()?;
let identifier = ts_any_name.as_js_reference_identifier()?;
let prev_token = identifier.value_token().ok()?;

let new_token = make::ident(suggested_type);

mutation.replace_element(prev_token.into(), new_token.into());

Some(JsRuleAction::new(
ActionCategory::QuickFix,
ctx.metadata().applicability(),
markup! { "Use '"{suggested_type}"' instead" }.to_owned(),
mutation,
))
}
}

#[derive(Clone, Debug, Default, Deserializable, Deserialize, Serialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NoRestrictedTypesOptions {
types: FxHashMap<String, CustomRestrictedType>,
}

#[derive(Debug, Clone, Default, Deserializable, Deserialize, Serialize, Eq, PartialEq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CustomRestrictedType {
message: String,
#[serde(rename = "use")]
use_instead: Option<String>,
}
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/options.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
"linter": {
"enabled": true,
"rules": {
"nursery": {
"noRestrictedTypes": {
"level": "error",
"options": {
"types": {
"CustomType": {
"message": "Only CustomType2 is allowed",
"use": "CustomType2"
},
"Bar": {
"message": "Replace Bar with Foo"
},
"InvalidUse": {
"message": "Do not use this type",
"use": "@"
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type CustomType = unknown
function fn2(arg: CustomType) {
return arg;
}


class Foo<T extends CustomType = String> extends Bar<CustomType> implements CustomType<Object> {
constructor(foo: String | Object) {}

exit(): CustomType<String> {
const foo: String = 1 as CustomType;
}
}


const foo: Bar = 1;

const identifier: InvalidUse = 'foo';
Loading

0 comments on commit 70557f2

Please sign in to comment.