-
-
Notifications
You must be signed in to change notification settings - Fork 714
feat(noRestrictedImports): add the patterns option #5506
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(noRestrictedImports): add the patterns option #5506
Conversation
fadd494
to
8379cd9
Compare
CodSpeed Performance ReportMerging #5506 will degrade performances by 61.17%Comparing Summary
Benchmarks breakdown
|
e33b8d3
to
1ce0555
Compare
1316c24
to
d8516ba
Compare
d8516ba
to
21a0265
Compare
|
||
impl PatternOptions { | ||
// Ensure that mutually exclusive keys are not used together. | ||
fn validate_combination(&self) -> Result<(), &'static str> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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:
biome/crates/biome_js_analyze/src/lint/correctness/no_undeclared_dependencies.rs
Lines 122 to 164 in 6cf3d00
#[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() | |
}) | |
} | |
} |
There was a problem hiding this comment.
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
68cb3fb
to
7e53305
Compare
crates/biome_js_analyze/tests/specs/nursery/noRestrictedImports/invalid.js.snap
Outdated
Show resolved
Hide resolved
crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs
Outdated
Show resolved
Hide resolved
|
||
impl PatternOptions { | ||
// Ensure that mutually exclusive keys are not used together. | ||
fn validate_combination(&self) -> Result<(), &'static str> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
crates/biome_js_analyze/src/lint/nursery/no_restricted_imports.rs
Outdated
Show resolved
Hide resolved
2ec23e1
to
7e93bda
Compare
Sorry for the delay. I've completed the initial implementation and it's now ready for review. |
20e0042
to
f12dc17
Compare
There was a problem hiding this 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.
crates/biome_js_analyze/src/lint/style/no_restricted_imports.rs
Outdated
Show resolved
Hide resolved
} | ||
} | ||
AnyJsImportLike::JsImportCallExpression(import_call) => { | ||
// TODO: We have to parse the context of the import() call to determine |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
crates/biome_js_analyze/src/lint/style/no_restricted_imports.rs
Outdated
Show resolved
Hide resolved
crates/biome_js_analyze/src/lint/style/no_restricted_imports.rs
Outdated
Show resolved
Hide resolved
…-in-no-restricted-imports
@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 And Here 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 |
@ematipico I don't think this is a recursion bug - the methods eventually call the I’ve now:
With these changes, both 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. |
Linting is passing, but it seems the tests aren't due to snapshot changes |
@ematipico |
There was a problem hiding this 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.
There was a problem hiding this 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 :)
.changeset/better-adults-roll.md
Outdated
--- | ||
"@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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--- | |
"@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
There was a problem hiding this comment.
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
crates/biome_js_analyze/src/lint/style/no_restricted_imports.rs
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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 :)
] | ||
} | ||
} | ||
}, |
There was a problem hiding this comment.
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.
] | ||
} | ||
} | ||
}, |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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>
Summary
Implements: https://eslint.org/docs/latest/rules/no-restricted-imports#patterns
closes #5289
Test Plan
documentation and snapshot tests