+
Skip to content

Conversation

sakai-ast
Copy link
Contributor

@sakai-ast sakai-ast commented Mar 30, 2025

Summary

Implements: https://eslint.org/docs/latest/rules/no-restricted-imports#patterns
closes #5289

Test Plan

documentation and snapshot tests

@github-actions github-actions bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels Mar 30, 2025
@sakai-ast sakai-ast changed the title feat(noRestrictedImports): add the patterns option WIP WIP: feat(noRestrictedImports): add the patterns option Mar 30, 2025
@sakai-ast sakai-ast changed the title WIP: feat(noRestrictedImports): add the patterns option feat(noRestrictedImports): add the patterns option WIP Mar 30, 2025
@sakai-ast sakai-ast force-pushed the feat/support-patterns-in-no-restricted-imports branch 3 times, most recently from fadd494 to 8379cd9 Compare April 5, 2025 09:29
Copy link

codspeed-hq bot commented Apr 6, 2025

CodSpeed Performance Report

Merging #5506 will degrade performances by 61.17%

Comparing sakai-ast:feat/support-patterns-in-no-restricted-imports (6b178f6) with next (275cc8b)

Summary

⚡ 2 improvements
❌ 2 regressions
✅ 110 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Benchmarks breakdown

Benchmark BASE HEAD Change
js_analyzer[index_3894593175024091846.js] 49.4 ms 52.6 ms -6.05%
json_formatter[eucjp_1600564308684076393.json] 2.6 ms 2.5 ms +6.08%
deserialize_from_json_str[package.json] 1,038.3 µs 930.8 µs +11.55%
deserialize_from_json_str[tsconfig.json] 218 µs 561.5 µs -61.17%

@sakai-ast sakai-ast force-pushed the feat/support-patterns-in-no-restricted-imports branch 3 times, most recently from e33b8d3 to 1ce0555 Compare April 14, 2025 01:59
@sakai-ast sakai-ast force-pushed the feat/support-patterns-in-no-restricted-imports branch 4 times, most recently from 1316c24 to d8516ba Compare April 29, 2025 12:48
@sakai-ast sakai-ast force-pushed the feat/support-patterns-in-no-restricted-imports branch from d8516ba to 21a0265 Compare May 6, 2025 07:06

impl PatternOptions {
// Ensure that mutually exclusive keys are not used together.
fn validate_combination(&self) -> Result<(), &'static str> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to define this validation by outputting JsonSchema from enum and struct, but the output schema is not expected for me (e.g. I'd like to output the enum parameters to oneOf, but the actual output is anyOf and enum parameters are defined to object, which means the option writing style needs to change), so I implemented this validate function.

My challenge is as follows.

use struct and enum for expression of exclusivity 1
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum Patterns {
    /// A simple gitignore-style pattern string.
    Simple(Box<str>),
    /// Additional options to configure the message and allowed/disallowed import names and modules.
    WithOptions(PatternOptions),
}

impl Deserializable for Patterns {
    fn deserialize(
        ctx: &mut impl DeserializationContext,
        value: &impl DeserializableValue,
        name: &str,
    ) -> Option<Self> {
        if value.visitable_type()? == DeserializableType::Str {
            biome_deserialize::Deserializable::deserialize(ctx, value, name).map(Self::Simple)
        } else {
            biome_deserialize::Deserializable::deserialize(ctx, value, name).map(Self::WithOptions)
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct PatternOptions {
    #[serde(flatten)]
    matcher: Matcher,

    #[serde(default)]
    case_sensitive: bool,

    #[serde(skip_serializing_if = "Option::is_none")]
    message: Option<Box<str>>,

    #[serde(skip_serializing_if = "Option::is_none", flatten)]
    name_rule: Option<ImportNameRule>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged, deny_unknown_fields)]
pub enum Matcher {
    Group { group: Box<[Box<str>]> },
    Regex { regex: Box<str> },
}

impl Default for Matcher {
    fn default() -> Self {
        Matcher::Group {
            group: Vec::new().into_boxed_slice(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged, rename_all = "camelCase", deny_unknown_fields)]
pub enum ImportNameRule {
    /// An array of specific import names to forbid within the matched modules.
    ImportNames { import_names: Box<[Box<str>]> },

    /// An array of specific import names to allow within the matched modules.
    AllowImportNames { allow_import_names: Box<[Box<str>]> },

    /// A regex pattern for import names to forbid within the matched modules.
    ImportNamePattern { import_name_pattern: Box<str> },

    /// A regex pattern for import names to allow within the matched modules.
    AllowImportNamePattern { allow_import_name_pattern: Box<str> },
}

impl Deserializable for PatternOptions {
    fn deserialize(
        ctx: &mut impl DeserializationContext,
        value: &impl DeserializableValue,
        name: &str,
    ) -> Option<Self> {
        value.deserialize(ctx, PatternOptionsVisitor, name)
    }
}

struct PatternOptionsVisitor;

impl DeserializationVisitor for PatternOptionsVisitor {
    type Output = PatternOptions;

    const EXPECTED_TYPE: DeserializableTypes = DeserializableTypes::MAP;

    fn visit_map(
        self,
        ctx: &mut impl DeserializationContext,
        members: impl Iterator<Item = Option<(impl DeserializableValue, impl DeserializableValue)>>,
        _range: TextRange,
        _name: &str,
    ) -> Option<Self::Output> {
        let mut group: Option<Box<[Box<str>]>> = None;
        let mut regex: Option<Box<str>> = None;
        let mut case_sensitive = false;
        let mut message: Option<Box<str>> = None;
        let mut import_name_rule: Option<ImportNameRule> = None;

        for (key_v, value_v) in members.flatten() {
            let key_txt = Text::deserialize(ctx, &key_v, "")?;
            match key_txt.text() {
                "group" => {
                    let vec: Vec<Box<str>> = Deserializable::deserialize(ctx, &value_v, "group")?;
                    group = Some(vec.into_boxed_slice());
                }
                "regex" => {
                    let txt: Text = Deserializable::deserialize(ctx, &value_v, "regex")?;
                    regex = Some(txt.text().into());
                }
                "caseSensitive" => {
                    case_sensitive = Deserializable::deserialize(ctx, &value_v, "caseSensitive")?;
                }
                "message" => {
                    let txt: Text = Deserializable::deserialize(ctx, &value_v, "message")?;
                    message = Some(txt.text().into());
                }
                "importNames" => {
                    let v: Vec<Box<str>> =
                        Deserializable::deserialize(ctx, &value_v, "importNames")?;
                    import_name_rule = Some(ImportNameRule::ImportNames {
                        import_names: v.into_boxed_slice(),
                    });
                }
                "allowImportNames" => {
                    let v: Vec<Box<str>> =
                        Deserializable::deserialize(ctx, &value_v, "allowImportNames")?;
                    import_name_rule = Some(ImportNameRule::AllowImportNames {
                        allow_import_names: v.into_boxed_slice(),
                    });
                }
                "importNamePattern" => {
                    let txt: Text =
                        Deserializable::deserialize(ctx, &value_v, "importNamePattern")?;
                    import_name_rule = Some(ImportNameRule::ImportNamePattern {
                        import_name_pattern: txt.text().into(),
                    });
                }
                "allowImportNamePattern" => {
                    let txt: Text =
                        Deserializable::deserialize(ctx, &value_v, "allowImportNamePattern")?;
                    import_name_rule = Some(ImportNameRule::AllowImportNamePattern {
                        allow_import_name_pattern: txt.text().into(),
                    });
                }

                _other => (),
            }
        }

        if let Some(group) = group {
            Some(PatternOptions {
                matcher: Matcher::Group { group },
                case_sensitive,
                message,
                name_rule: import_name_rule,
            })
        } else if let Some(regex) = regex {
            Some(PatternOptions {
                matcher: Matcher::Regex { regex },
                case_sensitive,
                message,
                name_rule: import_name_rule,
            })
        } else {
            None
        }
    }
}
extract from the output scheme
		"PatternOptions": {
			"anyOf": [
				{ "$ref": "#/definitions/GlobRule" },
				{ "$ref": "#/definitions/RegexRule" }
			]
		},

		"RegexRule": {
			"type": "object",
			"anyOf": [
				{
					"description": "An array of specific import names to forbid within the matched modules.",
					"type": "object",
					"required": ["import_names"],
					"properties": {
						"import_names": { "type": "array", "items": { "type": "string" } }
					},
					"additionalProperties": false
				},
				{
					"description": "An array of specific import names to allow within the matched modules.",
					"type": "object",
					"required": ["allow_import_names"],
					"properties": {
						"allow_import_names": {
							"type": "array",
							"items": { "type": "string" }
						}
					},
					"additionalProperties": false
				},
				{
					"description": "A regex pattern for import names to forbid within the matched modules.",
					"type": "object",
					"required": ["import_name_pattern"],
					"properties": { "import_name_pattern": { "type": "string" } },
					"additionalProperties": false
				},
				{
					"description": "A regex pattern for import names to allow within the matched modules.",
					"type": "object",
					"required": ["allow_import_name_pattern"],
					"properties": { "allow_import_name_pattern": { "type": "string" } },
					"additionalProperties": false
				}
			],
			"properties": {
				"caseSensitive": { "default": false, "type": "boolean" },
				"message": { "type": ["string", "null"] },
				"regex": { "default": "", "type": "string" }
			},
			"additionalProperties": false
		},
use struct and enum for expression of exclusivity 2
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum Patterns {
    /// A simple gitignore-style pattern string.
    Simple(Box<str>),
    /// Additional options to configure the message and allowed/disallowed import names and modules.
    WithOptions(PatternOptions),
}

impl Deserializable for Patterns {
    fn deserialize(
        ctx: &mut impl DeserializationContext,
        value: &impl DeserializableValue,
        name: &str,
    ) -> Option<Self> {
        if value.visitable_type()? == DeserializableType::Str {
            biome_deserialize::Deserializable::deserialize(ctx, value, name).map(Self::Simple)
        } else {
            biome_deserialize::Deserializable::deserialize(ctx, value, name).map(Self::WithOptions)
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum PatternOptions {
    Glob(GlobRule),
    Regex(RegexRule),
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct GlobRule {
    group: Box<[Box<str>]>,

    #[serde(default)]
    case_sensitive: bool,

    #[serde(skip_serializing_if = "Option::is_none")]
    message: Option<Box<str>>,

    #[serde(skip_serializing_if = "Option::is_none", flatten)]
    name_rule: Option<ImportNameRule>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct RegexRule {
    regex: Box<str>,

    #[serde(default)]
    case_sensitive: bool,

    #[serde(skip_serializing_if = "Option::is_none")]
    message: Option<Box<str>>,

    #[serde(skip_serializing_if = "Option::is_none", flatten)]
    name_rule: Option<ImportNameRule>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged, rename_all = "camelCase", deny_unknown_fields)]
pub enum ImportNameRule {
    /// An array of specific import names to forbid within the matched modules.
    ImportNames { import_names: Box<[Box<str>]> },

    /// An array of specific import names to allow within the matched modules.
    AllowImportNames { allow_import_names: Box<[Box<str>]> },

    /// A regex pattern for import names to forbid within the matched modules.
    ImportNamePattern { import_name_pattern: Box<str> },

    /// A regex pattern for import names to allow within the matched modules.
    AllowImportNamePattern { allow_import_name_pattern: Box<str> },
}

impl Deserializable for PatternOptions {
    fn deserialize(
        ctx: &mut impl DeserializationContext,
        value: &impl DeserializableValue,
        name: &str,
    ) -> Option<Self> {
        value.deserialize(ctx, PatternOptionsVisitor, name)
    }
}

struct PatternOptionsVisitor;

impl DeserializationVisitor for PatternOptionsVisitor {
    type Output = PatternOptions;

    const EXPECTED_TYPE: DeserializableTypes = DeserializableTypes::MAP;

    fn visit_map(
        self,
        ctx: &mut impl DeserializationContext,
        members: impl Iterator<Item = Option<(impl DeserializableValue, impl DeserializableValue)>>,
        _range: TextRange,
        _name: &str,
    ) -> Option<Self::Output> {
        let mut group: Option<Box<[Box<str>]>> = None;
        let mut regex: Option<Box<str>> = None;
        let mut case_sensitive = false;
        let mut message: Option<Box<str>> = None;
        let mut import_name_rule: Option<ImportNameRule> = None;

        for (key_v, value_v) in members.flatten() {
            let key_txt = Text::deserialize(ctx, &key_v, "")?;
            match key_txt.text() {
                "group" => {
                    let vec: Vec<Box<str>> = Deserializable::deserialize(ctx, &value_v, "group")?;
                    group = Some(vec.into_boxed_slice());
                }
                "regex" => {
                    let txt: Text = Deserializable::deserialize(ctx, &value_v, "regex")?;
                    regex = Some(txt.text().into());
                }
                "caseSensitive" => {
                    case_sensitive = Deserializable::deserialize(ctx, &value_v, "caseSensitive")?;
                }
                "message" => {
                    let txt: Text = Deserializable::deserialize(ctx, &value_v, "message")?;
                    message = Some(txt.text().into());
                }
                "importNames" => {
                    let v: Vec<Box<str>> =
                        Deserializable::deserialize(ctx, &value_v, "importNames")?;
                    import_name_rule = Some(ImportNameRule::ImportNames {
                        import_names: v.into_boxed_slice(),
                    });
                }
                "allowImportNames" => {
                    let v: Vec<Box<str>> =
                        Deserializable::deserialize(ctx, &value_v, "allowImportNames")?;
                    import_name_rule = Some(ImportNameRule::AllowImportNames {
                        allow_import_names: v.into_boxed_slice(),
                    });
                }
                "importNamePattern" => {
                    let txt: Text =
                        Deserializable::deserialize(ctx, &value_v, "importNamePattern")?;
                    import_name_rule = Some(ImportNameRule::ImportNamePattern {
                        import_name_pattern: txt.text().into(),
                    });
                }
                "allowImportNamePattern" => {
                    let txt: Text =
                        Deserializable::deserialize(ctx, &value_v, "allowImportNamePattern")?;
                    import_name_rule = Some(ImportNameRule::AllowImportNamePattern {
                        allow_import_name_pattern: txt.text().into(),
                    });
                }

                _other => (),
            }
        }

        if let Some(group) = group {
            Some(PatternOptions::Glob(GlobRule {
                group: group,
                case_sensitive,
                message,
                name_rule: import_name_rule,
            }))
        } else if let Some(regex) = regex {
            Some(PatternOptions::Regex(RegexRule {
                regex: regex,
                case_sensitive,
                message,
                name_rule: import_name_rule,
            }))
        } else {
            None
        }
    }
}
the output scheme
		"PatternOptions": {
			"type": "object",
			"anyOf": [
				{
					"type": "object",
					"required": ["group"],
					"properties": {
						"group": { "type": "array", "items": { "type": "string" } }
					},
					"additionalProperties": false
				},
				{
					"type": "object",
					"required": ["regex"],
					"properties": { "regex": { "type": "string" } },
					"additionalProperties": false
				}
			],
			"properties": {
				"caseSensitive": { "default": false, "type": "boolean" },
				"message": { "type": ["string", "null"] }
			},
			"additionalProperties": false
		},

I think my lack of knowledge about crates, etc. is a big part of why it doesn't work, so if you have a good solution, please let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @Conaclos and @siketyan I believe you both implemented custom schema representation, maybe you can help

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can implement a custom schema representation as the last resort, by adding an impl of schemars::JsonSchema. I have added one in noUndeclaredDependencies before:

#[cfg(feature = "schemars")]
impl schemars::JsonSchema for DependencyAvailability {
fn schema_name() -> String {
"DependencyAvailability".to_owned()
}
fn json_schema(_generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::*;
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Boolean.into()),
metadata: Some(Box::new(Metadata {
description: Some("This type of dependency will be always available or unavailable.".to_owned()),
..Default::default()
})),
..Default::default()
}),
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Array.into()),
array: Some(Box::new(ArrayValidation {
items: Some(SingleOrVec::Single(Box::new(Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
..Default::default()
})))),
min_items: Some(1),
..Default::default()
})),
metadata: Some(Box::new(Metadata {
description: Some("This type of dependency will be available only if the linted file matches any of the globs.".to_owned()),
..Default::default()
})),
..Default::default()
})
]),
..Default::default()
})),
..Default::default()
})
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried implementing this here: 7e93bda

@sakai-ast sakai-ast force-pushed the feat/support-patterns-in-no-restricted-imports branch 2 times, most recently from 68cb3fb to 7e53305 Compare May 6, 2025 08:18

impl PatternOptions {
// Ensure that mutually exclusive keys are not used together.
fn validate_combination(&self) -> Result<(), &'static str> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @Conaclos and @siketyan I believe you both implemented custom schema representation, maybe you can help

@sakai-ast sakai-ast force-pushed the feat/support-patterns-in-no-restricted-imports branch 2 times, most recently from 2ec23e1 to 7e93bda Compare May 17, 2025 04:15
@sakai-ast
Copy link
Contributor Author

Sorry for the delay. I've completed the initial implementation and it's now ready for review.

@sakai-ast sakai-ast marked this pull request as ready for review May 18, 2025 03:53
@sakai-ast sakai-ast requested review from ematipico and siketyan May 18, 2025 03:54
@sakai-ast sakai-ast changed the title feat(noRestrictedImports): add the patterns option WIP feat(noRestrictedImports): add the patterns option May 18, 2025
@sakai-ast sakai-ast force-pushed the feat/support-patterns-in-no-restricted-imports branch from 20e0042 to f12dc17 Compare May 18, 2025 12:39
Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @sakai-ast for the amazing work! Users will really appreciate that. I left a couple of suggestions we should address. Also, there a couple of important things we should address first:

  • Lack of new tests: we added many new options, but those are hardly tested. I know we have docs, but those are mostly for showing how they work to the users. We should really have a bigger test suite for us to cover all the new options.
  • We should create a new changeset . It should be a minor, and it should be brief enough to at least explain a basic new option.

}
}
AnyJsImportLike::JsImportCallExpression(import_call) => {
// TODO: We have to parse the context of the import() call to determine
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When do you plan to address this TODO? In this PR or in another PR? If you plan to do it in another PR, you should create an issue (task) for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ematipico This TODO wasn't created by me, and I don't plan to address it in this PR (Of course I would like to resolve this in this PR, but it is taking too long, so I give up for now...). However, should I create an issue for it?
https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/lint/style/no_restricted_imports.rs#L1006-L1009

Another TODO also exists in the no_restricted_imports.rs. Should I also create an issue for this as well?
https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/lint/style/no_restricted_imports.rs#L894-L897

@ematipico ematipico requested review from Conaclos, dyc3 and ematipico July 1, 2025 06:05
@ematipico
Copy link
Member

ematipico commented Jul 1, 2025

@sakai-ast we recently changed how rule options are implemented, and I moved all the new options inside the new place. I resolved the conflicts, so pull the latest changes into your branch. Everything that you implemented should be there. Now, while I was doing that, I noticed a recursion bug that is clippy flags:

Here

https://github.com/sakai-ast/biome/blob/e43aa5202bc6dbcc82601f2bf91b4efb068d667f/crates/biome_rule_options/src/no_restricted_imports.rs#L387-L400

And Here

https://github.com/sakai-ast/biome/blob/e43aa5202bc6dbcc82601f2bf91b4efb068d667f/crates/biome_rule_options/src/no_restricted_imports.rs#L407-L420

As you can see, the implementation of the methods calls itself, causing a recursion. I'm not sure what the correct implementation of those methods is, so I ask you to address them. As a general rule, use cargo clippy or just l to check for possible violations.

@sakai-ast
Copy link
Contributor Author

@ematipico
Thank you for resolving the conflict!

I don't think this is a recursion bug - the methods eventually call the PathOptions and PatternOptions implementations, not themselves.
In my environment, the clippy flags only appear in impl ImportRestrictions for PatternOptions .
This is because the implementation is missing.

I’ve now:

  • added an implementation for PatternOptions (This was missing during the migration)

  • moved some functions into biome_rule_options side(for the above)

  • removed some unnecessary code(This was added during the migration)

  • fixed an issue where only the paths option was honored when both paths and patterns were set(I noticed this while doing this work)

With these changes, both cargo clippy and just l run without errors.

The test output has changed compared with the previous run, but before updating the snapshots I’d like to confirm that these fixes look correct to you.

@ematipico
Copy link
Member

Linting is passing, but it seems the tests aren't due to snapshot changes

@sakai-ast
Copy link
Contributor Author

@ematipico
I had intended to wait for your confirmation for my fix before updating the snapshots, but I have now applied the snapshot fix.

Copy link
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Just a couple docs changes.

@ematipico ematipico changed the base branch from main to next July 9, 2025 06:47
Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we're there. I think we can merge it into the next branch, and @sakai-ast will send a second PR to update the changeset :)

Comment on lines 1 to 6
---
"@biomejs/backend-jsonrpc": minor
"@biomejs/biome": minor
---

An option called `patterns` has been added to the `noRestrictedImports` rule. This option allows you to specify multiple modules to restrict using gitignore-style patterns.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
---
"@biomejs/backend-jsonrpc": minor
"@biomejs/biome": minor
---
An option called `patterns` has been added to the `noRestrictedImports` rule. This option allows you to specify multiple modules to restrict using gitignore-style patterns.
---
"@biomejs/biome": minor
---
An option called `patterns` has been added to the `noRestrictedImports` rule. This option allows you to specify multiple modules to restrict using gitignore-style patterns.

The changesest could receive some love. The changeset is used as a summary to explain new features to our end-users. I believe the first sentence is fine, however, I think we should add one or two examples that show how users can "specify multiple modules to restrict".

However, this PR has been ongoing for a long time, so perhaps we can proceed as follows: we will merge this PR into the next branch, and you, @sakai-ast, will send a new PR to update this changeset (always from the next branch). So you can relax and do without a rush.

Refer to this new section of the contribution section regarding where to send a PR: https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md#creating-pull-requests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your consideration.
I was able to set aside some time and I improved the changeset.
c94f7a2

Copy link
Member

@siketyan siketyan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I left some suggestions for refactoring, not a blocker at all :)

]
}
}
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: looks tab and space indents are mixed here.

]
}
}
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

pub allow_import_names: Box<[Box<str>]>,
}

impl PathOptions {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid to put js-specific stuff in the biome_rule_options crate if possible, as it will be shared among languages.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plus the rule implementation for a language should be colocated.

@ematipico ematipico added this to the Biome v2.2 milestone Jul 20, 2025
@ematipico ematipico merged commit 1f8755b into biomejs:next Jul 22, 2025
26 of 27 checks passed
ematipico added a commit that referenced this pull request Aug 12, 2025
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

Co-authored-by: siketyan <12772118+siketyan@users.noreply.github.com>
Co-authored-by: dyc3 <1808807+dyc3@users.noreply.github.com>
Co-authored-by: ematipico <602478+ematipico@users.noreply.github.com>
Co-authored-by: Conaclos <2358560+Conaclos@users.noreply.github.com>
@github-actions github-actions bot mentioned this pull request Aug 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📎 Support patterns in noRestrictedImports

5 participants

点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载