From 0dd22107d3b703013e7bca75dd66d1a6f1b2d71d Mon Sep 17 00:00:00 2001 From: nekocode Date: Tue, 15 Oct 2024 23:26:21 +0800 Subject: [PATCH] feat(linter): noRestrictedImports add `patterns` option --- CHANGELOG.md | 1 + Cargo.lock | 1 + crates/biome_js_analyze/Cargo.toml | 1 + .../src/lint/nursery/no_restricted_imports.rs | 43 ++++++++++++++++--- .../nursery/noRestrictedImports/invalid.js | 1 + .../noRestrictedImports/invalid.js.snap | 18 +++++++- .../noRestrictedImports/invalid.options.json | 6 ++- .../nursery/noRestrictedImports/valid.js | 1 + .../nursery/noRestrictedImports/valid.js.snap | 3 +- .../noRestrictedImports/valid.options.json | 6 ++- .../nursery/noRestrictedImports/valid.ts | 4 +- .../nursery/noRestrictedImports/valid.ts.snap | 2 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 8 ++++ .../@biomejs/biome/configuration_schema.json | 13 ++++++ 14 files changed, 96 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a3f8660933..72ffd310c212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b - Add [noImgElement](https://biomejs.dev/linter/rules/no-img-element/). Contributed by @kaioduarte - Add [guardForIn](https://biomejs.dev/linter/rules/guard-for-in/). Contributed by @fireairforce - Add [noUselessStringRaw](https://github.com/biomejs/biome/pull/4263). Contributed by @fireairforce +- Add an option `patterns` to [noRestrictedImports](https://biomejs.dev/linter/rules/no-restricted-imports/). Contributed by @nekocode #### Bug Fixes diff --git a/Cargo.lock b/Cargo.lock index b6883f546322..31af84b8a92e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,6 +750,7 @@ dependencies = [ "biome_unicode_table", "bitvec", "enumflags2", + "ignore", "insta", "natord", "regex", diff --git a/crates/biome_js_analyze/Cargo.toml b/crates/biome_js_analyze/Cargo.toml index 064caedee302..beffb7a3fd27 100644 --- a/crates/biome_js_analyze/Cargo.toml +++ b/crates/biome_js_analyze/Cargo.toml @@ -28,6 +28,7 @@ biome_suppression = { workspace = true } biome_unicode_table = { workspace = true } bitvec = "1.0.1" enumflags2 = { workspace = true } +ignore = { workspace = true } natord = { workspace = true } regex = { workspace = true } roaring = "0.10.6" diff --git a/crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs b/crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs index ead4e83692a9..ebb4dcd5c0b1 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs @@ -4,6 +4,7 @@ use biome_console::markup; use biome_deserialize_macros::Deserializable; use biome_js_syntax::{inner_string_text, AnyJsImportLike}; use biome_rowan::TextRange; +use ignore::gitignore::GitignoreBuilder; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; @@ -19,7 +20,11 @@ declare_lint_rule! { /// "paths": { /// "lodash": "Using lodash is not encouraged", /// "underscore": "Using underscore is not encouraged" - /// } + /// }, + /// "patterns": [{ + /// "group": ["lodash/*", "!lodash/get"], + /// "message": "Using specific lodash modules is not encouraged" + /// }] /// } /// } /// } @@ -44,6 +49,20 @@ pub struct RestrictedImportsOptions { /// A list of names that should trigger the rule #[serde(skip_serializing_if = "FxHashMap::is_empty")] paths: FxHashMap, Box>, + + /// A list of gitignore-style patterns that should trigger the rule + #[serde(skip_serializing_if = "<[_]>::is_empty")] + patterns: Box<[RestrictedImportsPattern]>, +} + +#[derive(Clone, Debug, Default, Deserialize, Deserializable, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct RestrictedImportsPattern { + #[serde(skip_serializing_if = "<[_]>::is_empty")] + group: Box<[Box]>, + + message: String, } impl Rule for NoRestrictedImports { @@ -60,10 +79,24 @@ impl Rule for NoRestrictedImports { let module_name = node.module_name_token()?; let inner_text = inner_string_text(&module_name); - ctx.options() - .paths - .get(inner_text.text()) - .map(|message| (module_name.text_trimmed_range(), message.to_string())) + // Check against exact paths + if let Some(message) = ctx.options().paths.get(inner_text.text()) { + return Some((module_name.text_trimmed_range(), message.to_string())); + } + + // Check against gitignore-style patterns + for pattern in ctx.options().patterns.iter() { + let mut builder = GitignoreBuilder::new(""); + for path in pattern.group.iter() { + builder.add_line(None, path).unwrap(); + } + let gitignore = builder.build().unwrap(); + if gitignore.matched(inner_text.text(), false).is_ignore() { + return Some((module_name.text_trimmed_range(), pattern.message.clone())); + } + } + + None } fn diagnostic(_ctx: &RuleContext, (span, text): &Self::State) -> Option { diff --git a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.js index e22cad3c6d99..2cacef76f2d7 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.js @@ -1,2 +1,3 @@ import eslint from 'eslint'; const l = require('lodash'); +const s = require('lodash/sortBy'); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.js.snap index 8835361d9dba..1292023639b3 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.js.snap @@ -6,6 +6,7 @@ expression: invalid.js ```jsx import eslint from 'eslint'; const l = require('lodash'); +const s = require('lodash/sortBy'); ``` @@ -18,7 +19,7 @@ invalid.js:1:20 lint/nursery/noRestrictedImports ━━━━━━━━━━ > 1 │ import eslint from 'eslint'; │ ^^^^^^^^ 2 │ const l = require('lodash'); - 3 │ + 3 │ const s = require('lodash/sortBy'); ``` @@ -31,9 +32,22 @@ invalid.js:2:19 lint/nursery/noRestrictedImports ━━━━━━━━━━ 1 │ import eslint from 'eslint'; > 2 │ const l = require('lodash'); │ ^^^^^^^^ - 3 │ + 3 │ const s = require('lodash/sortBy'); + 4 │ ``` +``` +invalid.js:3:19 lint/nursery/noRestrictedImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ! It's not recommended to use specific lodash functions + + 1 │ import eslint from 'eslint'; + 2 │ const l = require('lodash'); + > 3 │ const s = require('lodash/sortBy'); + │ ^^^^^^^^^^^^^^^ + 4 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.options.json b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.options.json index be28e75b490a..181beb212a60 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.options.json +++ b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.options.json @@ -10,7 +10,11 @@ "paths": { "eslint": "Importing Eslint is forbidden", "lodash": "It's not recommended to use lodash" - } + }, + "patterns": [{ + "group": ["lodash/*", "!lodash/get"], + "message": "It's not recommended to use specific lodash functions" + }] } } } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.js index b01e0318dda5..930338b90bcd 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.js @@ -1 +1,2 @@ const path = require('lodash'); +const path = require('lodash/get'); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.js.snap index 0729d1d3be89..10159498122d 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.js.snap @@ -5,7 +5,6 @@ expression: valid.js # Input ```jsx const path = require('lodash'); +const path = require('lodash/get'); ``` - - diff --git a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.options.json b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.options.json index 0b42f3625484..88ccb5151d67 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.options.json +++ b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.options.json @@ -8,7 +8,11 @@ "options": { "paths": { "node:fs": "Importing from node:fs is forbidden" - } + }, + "patterns": [{ + "group": ["lodash/*", "!lodash/get"], + "message": "It's not recommended to use lodash" + }] } } } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.ts b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.ts index 2cf05c93d405..984a7b825c3d 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.ts @@ -1 +1,3 @@ -declare module "node:fs" {} \ No newline at end of file +declare module "node:fs" {} +declare module "lodash/get" {} +declare module "lodash/sortBy" {} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.ts.snap index 40259c4ee532..f823ddb3bab0 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/valid.ts.snap @@ -5,4 +5,6 @@ expression: valid.ts # Input ```ts declare module "node:fs" {} +declare module "lodash/get" {} +declare module "lodash/sortBy" {} ``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index a38557656a37..166823fd5c94 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -2406,6 +2406,10 @@ export interface RestrictedImportsOptions { * A list of names that should trigger the rule */ paths: {}; + /** + * A list of gitignore-style patterns that should trigger the rule + */ + patterns: RestrictedImportsPattern[]; } export interface NoRestrictedTypesOptions { types?: {}; @@ -2538,6 +2542,10 @@ For example, for React's `useRef()` hook the value would be `true`, while for `u */ stableResult?: StableHookResult; } +export interface RestrictedImportsPattern { + group: string[]; + message?: string; +} export type Accessibility = "noPublic" | "explicit" | "none"; export type ConsistentArrayType = "shorthand" | "generic"; export type FilenameCases = FilenameCase[]; diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 82f908c7d177..8d7af26eff9b 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2650,10 +2650,23 @@ "description": "A list of names that should trigger the rule", "type": "object", "additionalProperties": { "type": "string" } + }, + "patterns": { + "description": "A list of gitignore-style patterns that should trigger the rule", + "type": "array", + "items": { "$ref": "#/definitions/RestrictedImportsPattern" } } }, "additionalProperties": false }, + "RestrictedImportsPattern": { + "type": "object", + "properties": { + "group": { "type": "array", "items": { "type": "string" } }, + "message": { "default": "", "type": "string" } + }, + "additionalProperties": false + }, "RestrictedModifier": { "type": "string", "enum": ["abstract", "private", "protected", "readonly", "static"]