Skip to content

Commit

Permalink
feat(editorconfig): expand unknown globs into known globs (#3218)
Browse files Browse the repository at this point in the history
  • Loading branch information
dyc3 authored Jun 20, 2024
1 parent c0a708f commit ad7881a
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 28 deletions.
22 changes: 22 additions & 0 deletions crates/biome_configuration/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ pub enum EditorConfigDiagnostic {
Incompatible(InconpatibleDiagnostic),
/// A glob pattern that biome doesn't support.
UnknownGlobPattern(UnknownGlobPatternDiagnostic),
/// A glob pattern that contains invalid syntax.
InvalidGlobPattern(InvalidGlobPatternDiagnostic),
}

impl EditorConfigDiagnostic {
Expand All @@ -250,6 +252,15 @@ impl EditorConfigDiagnostic {
),
})
}

pub fn invalid_glob_pattern(pattern: impl Into<String>, reason: impl Into<String>) -> Self {
Self::InvalidGlobPattern(InvalidGlobPatternDiagnostic {
message: MessageAndDescription::from(
markup! { "This glob pattern is invalid: "{pattern.into()}" Reason: "{reason.into()}}
.to_owned(),
),
})
}
}

#[derive(Debug, Serialize, Deserialize, Diagnostic)]
Expand Down Expand Up @@ -286,6 +297,17 @@ pub struct UnknownGlobPatternDiagnostic {
pub message: MessageAndDescription,
}

#[derive(Debug, Serialize, Deserialize, Diagnostic)]
#[diagnostic(
category = "configuration",
severity = Error,
)]
pub struct InvalidGlobPatternDiagnostic {
#[message]
#[description]
pub message: MessageAndDescription,
}

#[cfg(test)]
mod test {
use crate::{BiomeDiagnostic, PartialConfiguration};
Expand Down
218 changes: 190 additions & 28 deletions crates/biome_configuration/src/editorconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@

use std::{collections::HashMap, str::FromStr};

use biome_deserialize::StringSet;
use biome_diagnostics::{adapters::IniError, Error};
use biome_formatter::{IndentWidth, LineEnding, LineWidth};
use indexmap::IndexSet;
use serde::{Deserialize, Deserializer};

use crate::{
Expand Down Expand Up @@ -50,13 +48,23 @@ impl EditorConfig {
formatter: self.options.remove("*").map(|o| o.to_biome()),
..Default::default()
};
let mut errors = vec![];
let overrides: Vec<_> = self
.options
.into_iter()
.map(|(k, v)| OverridePattern {
include: Some(StringSet::new(IndexSet::from([k]))),
formatter: Some(v.to_biome_override()),
..Default::default()
.map(|(k, v)| {
let patterns = match expand_unknown_glob_patterns(&k) {
Ok(patterns) => patterns,
Err(err) => {
errors.push(err);
vec![k]
}
};
OverridePattern {
include: Some(patterns.into_iter().collect()),
formatter: Some(v.to_biome_override()),
..Default::default()
}
})
.collect();
config.overrides = Some(Overrides(overrides));
Expand All @@ -65,15 +73,7 @@ impl EditorConfig {
}

fn validate(&self) -> Vec<EditorConfigDiagnostic> {
let mut errors: Vec<_> = self.options.values().flat_map(|o| o.validate()).collect();

// biome doesn't currently support all the glob patterns that .editorconfig does
errors.extend(
self.options
.keys()
.filter(|k| k.contains('{') || k.contains('}'))
.map(|pattern| EditorConfigDiagnostic::unknown_glob_pattern(pattern.clone())),
);
let errors: Vec<_> = self.options.values().flat_map(|o| o.validate()).collect();

errors
}
Expand Down Expand Up @@ -170,6 +170,142 @@ where
.map(Some)
}

/// Turn an unknown glob pattern into a list of known glob patterns. This is part of a hack to support all editorconfig patterns.
///
/// TODO: remove in biome 2.0
fn expand_unknown_glob_patterns(pattern: &str) -> Result<Vec<String>, EditorConfigDiagnostic> {
struct Variants {
/// index of the { character
start: usize,
/// index of the } character
end: usize,
variants: Option<VariantType>,
}

impl Variants {
fn new(start: usize) -> Self {
Self {
start,
end: start,
variants: None,
}
}

fn parse_to_variants(&mut self, s: &str) -> Result<(), EditorConfigDiagnostic> {
let s = s.trim_start_matches('{').trim_end_matches('}');
if s.contains("..") {
let mut parts = s.split("..");
let start = parts.next().ok_or_else(|| {
EditorConfigDiagnostic::invalid_glob_pattern(
s,
"Range pattern must have exactly two parts",
)
})?;
let end = parts.next().ok_or_else(|| {
EditorConfigDiagnostic::invalid_glob_pattern(
s,
"Range pattern must have exactly two parts",
)
})?;
if parts.next().is_some() {
return Err(EditorConfigDiagnostic::invalid_glob_pattern(
s,
"Range pattern must have exactly two parts",
));
}

let start = start.parse().map_err(|err| {
EditorConfigDiagnostic::invalid_glob_pattern(
s,
format!("Error parsing the start of the range: {}", err),
)
})?;
let end = end.parse().map_err(|err| {
EditorConfigDiagnostic::invalid_glob_pattern(
s,
format!("Error parsing the end of the range: {}", err),
)
})?;
self.variants = Some(VariantType::Range((start, end)));
} else {
self.variants = Some(VariantType::List(
s.split(',').map(|s| s.to_string()).collect(),
));
}

Ok(())
}

fn variants(&self) -> Vec<String> {
match &self.variants {
Some(VariantType::List(ref list)) => list.clone(),
Some(VariantType::Range((start, end))) => {
let mut variants = vec![];
for i in *start..=*end {
variants.push(i.to_string());
}
variants
}
None => vec![],
}
}
}

enum VariantType {
List(Vec<String>),
Range((i64, i64)),
}

let mut all_variants = vec![];
let mut current_variants = None;
for (i, c) in pattern.chars().enumerate() {
match c {
'{' => {
if current_variants.is_none() {
current_variants = Some(Variants::new(i));
} else {
// TODO: error, recursive brace expansion is not supported
}
}
'}' => {
if let Some(mut v) = current_variants.take() {
v.end = i;
v.parse_to_variants(&pattern[v.start..=v.end])?;
all_variants.push(v);
}
}
_ => {}
}
}

if all_variants.is_empty() {
return Ok(vec![pattern.to_string()]);
}

let mut expanded_patterns = vec![];
for variants in all_variants.iter().rev() {
if expanded_patterns.is_empty() {
for variant in &variants.variants() {
let mut pattern = pattern.to_string();
pattern.replace_range(variants.start..=variants.end, variant);
expanded_patterns.push(pattern);
}
} else {
let mut new_patterns = vec![];
for existing in &expanded_patterns {
for variant in &variants.variants() {
let mut pattern = existing.clone();
pattern.replace_range(variants.start..=variants.end, variant);
new_patterns.push(pattern);
}
}
expanded_patterns = new_patterns;
}
}

Ok(expanded_patterns)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -256,20 +392,46 @@ insert_final_newline = false
}

#[test]
fn should_emit_diagnostic_glob_pattern() {
let input = r#"
root = true
fn should_expand_glob_pattern_list() {
let pattern = "package.json";
let mut expanded =
expand_unknown_glob_patterns(pattern).expect("Failed to expand glob pattern");
expanded.sort();
assert_eq!(expanded, vec!["package.json"]);

let pattern = "{package.json,.travis.yml}";
let mut expanded =
expand_unknown_glob_patterns(pattern).expect("Failed to expand glob pattern");
expanded.sort();
assert_eq!(expanded, vec![".travis.yml", "package.json"]);
}

[{package.json,.travis.yml}]
indent_style = space
"#;
#[test]
fn should_expand_glob_pattern_list_2() {
let pattern = "**/{foo,bar}.{test,spec}.js";
let mut expanded =
expand_unknown_glob_patterns(pattern).expect("Failed to expand glob pattern");
expanded.sort();
assert_eq!(
expanded,
vec![
"**/bar.spec.js",
"**/bar.test.js",
"**/foo.spec.js",
"**/foo.test.js",
]
);
}

let conf = parse_str(input).expect("Failed to parse editorconfig");
let (_, errors) = conf.to_biome();
assert_eq!(errors.len(), 1);
assert!(matches!(
errors[0],
EditorConfigDiagnostic::UnknownGlobPattern(_)
));
#[test]
fn should_expand_glob_pattern_range() {
let pattern = "**/bar.{1..4}.js";
let mut expanded =
expand_unknown_glob_patterns(pattern).expect("Failed to expand glob pattern");
expanded.sort();
assert_eq!(
expanded,
vec!["**/bar.1.js", "**/bar.2.js", "**/bar.3.js", "**/bar.4.js",]
);
}
}

0 comments on commit ad7881a

Please sign in to comment.