Skip to content

Commit

Permalink
feat:: add restricted glob
Browse files Browse the repository at this point in the history
  • Loading branch information
Conaclos committed Oct 21, 2024
1 parent f8946c0 commit c79e2ff
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ crossbeam = "0.8.4"
dashmap = "6.1.0"
enumflags2 = "0.7.10"
getrandom = "0.2.15"
globset = "0.4.15"
ignore = "0.4.23"
indexmap = { version = "2.6.0", features = ["serde"] }
insta = "1.40.0"
Expand Down
1 change: 1 addition & 0 deletions crates/biome_js_analyze/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ biome_suppression = { workspace = true }
biome_unicode_table = { workspace = true }
bitvec = "1.0.1"
enumflags2 = { workspace = true }
globset = { workspace = true }
natord = { workspace = true }
regex = { workspace = true }
roaring = "0.10.6"
Expand Down
52 changes: 49 additions & 3 deletions crates/biome_js_analyze/src/assists/source/organize_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ use biome_analyze::{
context::RuleContext, declare_source_rule, ActionCategory, Ast, FixKind, Rule, SourceActionKind,
};
use biome_console::markup;
use biome_deserialize::Deserializable;
use biome_deserialize_macros::Deserializable;
use biome_js_syntax::JsModule;
use biome_rowan::BatchMutationExt;

use crate::JsRuleAction;
use crate::{utils::restricted_glob::RestrictedGlob, JsRuleAction};

pub mod util;
pub mod legacy;
pub mod util;

declare_source_rule! {
/// Provides a whole-source code action to sort the imports in the file
Expand Down Expand Up @@ -47,7 +49,7 @@ impl Rule for OrganizeImports {
type Query = Ast<JsModule>;
type State = State;
type Signals = Option<Self::State>;
type Options = ();
type Options = Options;

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let root = ctx.query();
Expand Down Expand Up @@ -76,3 +78,47 @@ pub enum State {
Legacy(legacy::ImportGroups),
Modern,
}

#[derive(Clone, Debug, Default, serde::Deserialize, Deserializable, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct Options {
legacy: bool,
import_groups: Box<[ImportGroup]>,
}

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum ImportGroup {
Predefined(PredefinedImportGroup),
Custom(RestrictedGlob),
}
impl Deserializable for ImportGroup {
fn deserialize(
value: &impl biome_deserialize::DeserializableValue,
name: &str,
diagnostics: &mut Vec<biome_deserialize::DeserializationDiagnostic>,
) -> Option<Self> {
Some(
if let Some(predefined) = Deserializable::deserialize(value, name, diagnostics) {
ImportGroup::Predefined(predefined)
} else {
ImportGroup::Custom(Deserializable::deserialize(value, name, diagnostics)?)
},
)
}
}

#[derive(Clone, Debug, serde::Deserialize, Deserializable, Eq, PartialEq, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum PredefinedImportGroup {
#[serde(rename = ":blank-line:")]
BlankLine,
#[serde(rename = ":bun:")]
Bun,
#[serde(rename = ":node:")]
Node,
#[serde(rename = ":types:")]
Types,
}
1 change: 1 addition & 0 deletions crates/biome_js_analyze/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::iter;

pub mod batch;
pub mod rename;
pub mod restricted_glob;
pub mod restricted_regex;
#[cfg(test)]
pub mod tests;
Expand Down
154 changes: 154 additions & 0 deletions crates/biome_js_analyze/src/utils/restricted_glob.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use biome_deserialize_macros::Deserializable;

/// A restricted glov pattern only supports the following syntaxes:
///
/// - star `*` that matches zero or more character inside a path segment
/// - globstar `**` that matches zero or more path segments
/// - Use `\*` to escape `*`
/// - `?`, `[`, `]`, `{`, and `}` must be escaped using `\`.
/// These characters are reserved for future use.
/// - `!` must be escaped if it is the first characrter of the pattern
///
/// A path segment is delimited by path separator `/` or the start/end of the path.
#[derive(Clone, Debug, Deserializable, serde::Deserialize, serde::Serialize)]
#[serde(try_from = "String", into = "String")]
pub struct RestrictedGlob(globset::GlobMatcher);

impl std::ops::Deref for RestrictedGlob {
type Target = globset::GlobMatcher;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl std::fmt::Display for RestrictedGlob {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let repr = self.0.glob().to_string();
f.write_str(&repr)
}
}

impl From<RestrictedGlob> for String {
fn from(value: RestrictedGlob) -> Self {
value.to_string()
}
}

impl std::str::FromStr for RestrictedGlob {
type Err = globset::ErrorKind;

fn from_str(value: &str) -> Result<Self, Self::Err> {
is_restricted_glob(value)?;
let mut glob_builder = globset::GlobBuilder::new(value);
// Allow escaping with `\` on all platforms.
glob_builder.backslash_escape(true);
// Only `**` can match `/`
glob_builder.literal_separator(true);
match glob_builder.build() {
Ok(glob) => Ok(RestrictedGlob(glob.compile_matcher())),
Err(error) => Err(error.kind().clone()),
}
}
}

impl TryFrom<String> for RestrictedGlob {
type Error = globset::ErrorKind;

fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}

#[cfg(feature = "schemars")]
impl schemars::JsonSchema for RestrictedGlob {
fn schema_name() -> String {
"Regex".to_string()
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
String::json_schema(gen)
}
}

/// Returns an error if `pattern` doesn't follow the restricted glob syntax.
fn is_restricted_glob(pattern: &str) -> Result<(), globset::ErrorKind> {
let mut it = pattern.bytes().enumerate();
while let Some((i, c)) = it.next() {
match c {
b'!' if i == 0 => {
return Err(globset::ErrorKind::Regex(
r"Negated globs `!` are not supported. Use `\!` to escape the character."
.to_string(),
));
}
b'\\' => {
// Accept a restrictive set of escape sequence
if let Some((_, c)) = it.next() {
if !matches!(c, b'!' | b'*' | b'?' | b'{' | b'}' | b'[' | b']' | b'\\') {
// SAFETY: safe because of the match
let c = unsafe { char::from_u32_unchecked(c as u32) };
// Escape sequences https://docs.rs/regex/latest/regex/#escape-sequences
// and Perl char classes https://docs.rs/regex/latest/regex/#perl-character-classes-unicode-friendly
return Err(globset::ErrorKind::Regex(format!(
"Escape sequence \\{c} is not supported."
)));
}
} else {
return Err(globset::ErrorKind::DanglingEscape);
}
}
b'?' => {
return Err(globset::ErrorKind::Regex(
r"`?` matcher is not supported. Use `\?` to escape the character.".to_string(),
));
}
b'[' | b']' => {
return Err(globset::ErrorKind::Regex(
r"Character class `[]` are not supported. Use `\[` and `\]` to escape the characters."
.to_string(),
));
}
b'{' | b'}' => {
return Err(globset::ErrorKind::Regex(
r"Alternates `{}` are not supported. Use `\{` and `\}` to escape the characters.".to_string(),
));
}
_ => {}
}
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_is_restricted_glob() {
assert!(is_restricted_glob("!*.js").is_err());
assert!(is_restricted_glob("*.[jt]s").is_err());
assert!(is_restricted_glob("*.{js,ts}").is_err());
assert!(is_restricted_glob("?*.js").is_err());
assert!(is_restricted_glob(r"\").is_err());
assert!(is_restricted_glob("!").is_err());

assert!(is_restricted_glob("*.js").is_ok());
assert!(is_restricted_glob("**/*.js").is_ok());
assert!(is_restricted_glob(r"\*").is_ok());
assert!(is_restricted_glob(r"\!").is_ok());
}

#[test]
fn test_restricted_regex() {
assert!(!"*.js"
.parse::<RestrictedGlob>()
.unwrap()
.is_match("file/path.js"));

assert!("**/*.js"
.parse::<RestrictedGlob>()
.unwrap()
.is_match("file/path.js"));
}
}
4 changes: 2 additions & 2 deletions crates/biome_js_analyze/src/utils/restricted_regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ impl PartialEq for RestrictedRegex {
}
}

/// Rteurns an error if `pattern` doesn't follow the restricted regular expression syntax.
/// Returns an error if `pattern` doesn't follow the restricted regular expression syntax.
fn is_restricted_regex(pattern: &str) -> Result<(), regex::Error> {
let mut it = pattern.bytes();
let mut is_in_char_class = false;
Expand Down Expand Up @@ -218,7 +218,7 @@ mod tests {
use super::*;

#[test]
fn test() {
fn test_is_restricted_regex() {
assert!(is_restricted_regex("^a").is_err());
assert!(is_restricted_regex("a$").is_err());
assert!(is_restricted_regex(r"\").is_err());
Expand Down

0 comments on commit c79e2ff

Please sign in to comment.