Skip to content
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(editorconfig): expand unknown globs into known globs #3218

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",]
);
}
}