Skip to content

Commit ad7881a

Browse files
authored
feat(editorconfig): expand unknown globs into known globs (#3218)
1 parent c0a708f commit ad7881a

File tree

2 files changed

+212
-28
lines changed

2 files changed

+212
-28
lines changed

crates/biome_configuration/src/diagnostics.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ pub enum EditorConfigDiagnostic {
230230
Incompatible(InconpatibleDiagnostic),
231231
/// A glob pattern that biome doesn't support.
232232
UnknownGlobPattern(UnknownGlobPatternDiagnostic),
233+
/// A glob pattern that contains invalid syntax.
234+
InvalidGlobPattern(InvalidGlobPatternDiagnostic),
233235
}
234236

235237
impl EditorConfigDiagnostic {
@@ -250,6 +252,15 @@ impl EditorConfigDiagnostic {
250252
),
251253
})
252254
}
255+
256+
pub fn invalid_glob_pattern(pattern: impl Into<String>, reason: impl Into<String>) -> Self {
257+
Self::InvalidGlobPattern(InvalidGlobPatternDiagnostic {
258+
message: MessageAndDescription::from(
259+
markup! { "This glob pattern is invalid: "{pattern.into()}" Reason: "{reason.into()}}
260+
.to_owned(),
261+
),
262+
})
263+
}
253264
}
254265

255266
#[derive(Debug, Serialize, Deserialize, Diagnostic)]
@@ -286,6 +297,17 @@ pub struct UnknownGlobPatternDiagnostic {
286297
pub message: MessageAndDescription,
287298
}
288299

300+
#[derive(Debug, Serialize, Deserialize, Diagnostic)]
301+
#[diagnostic(
302+
category = "configuration",
303+
severity = Error,
304+
)]
305+
pub struct InvalidGlobPatternDiagnostic {
306+
#[message]
307+
#[description]
308+
pub message: MessageAndDescription,
309+
}
310+
289311
#[cfg(test)]
290312
mod test {
291313
use crate::{BiomeDiagnostic, PartialConfiguration};

crates/biome_configuration/src/editorconfig.rs

Lines changed: 190 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@
1111
1212
use std::{collections::HashMap, str::FromStr};
1313

14-
use biome_deserialize::StringSet;
1514
use biome_diagnostics::{adapters::IniError, Error};
1615
use biome_formatter::{IndentWidth, LineEnding, LineWidth};
17-
use indexmap::IndexSet;
1816
use serde::{Deserialize, Deserializer};
1917

2018
use crate::{
@@ -50,13 +48,23 @@ impl EditorConfig {
5048
formatter: self.options.remove("*").map(|o| o.to_biome()),
5149
..Default::default()
5250
};
51+
let mut errors = vec![];
5352
let overrides: Vec<_> = self
5453
.options
5554
.into_iter()
56-
.map(|(k, v)| OverridePattern {
57-
include: Some(StringSet::new(IndexSet::from([k]))),
58-
formatter: Some(v.to_biome_override()),
59-
..Default::default()
55+
.map(|(k, v)| {
56+
let patterns = match expand_unknown_glob_patterns(&k) {
57+
Ok(patterns) => patterns,
58+
Err(err) => {
59+
errors.push(err);
60+
vec![k]
61+
}
62+
};
63+
OverridePattern {
64+
include: Some(patterns.into_iter().collect()),
65+
formatter: Some(v.to_biome_override()),
66+
..Default::default()
67+
}
6068
})
6169
.collect();
6270
config.overrides = Some(Overrides(overrides));
@@ -65,15 +73,7 @@ impl EditorConfig {
6573
}
6674

6775
fn validate(&self) -> Vec<EditorConfigDiagnostic> {
68-
let mut errors: Vec<_> = self.options.values().flat_map(|o| o.validate()).collect();
69-
70-
// biome doesn't currently support all the glob patterns that .editorconfig does
71-
errors.extend(
72-
self.options
73-
.keys()
74-
.filter(|k| k.contains('{') || k.contains('}'))
75-
.map(|pattern| EditorConfigDiagnostic::unknown_glob_pattern(pattern.clone())),
76-
);
76+
let errors: Vec<_> = self.options.values().flat_map(|o| o.validate()).collect();
7777

7878
errors
7979
}
@@ -170,6 +170,142 @@ where
170170
.map(Some)
171171
}
172172

173+
/// Turn an unknown glob pattern into a list of known glob patterns. This is part of a hack to support all editorconfig patterns.
174+
///
175+
/// TODO: remove in biome 2.0
176+
fn expand_unknown_glob_patterns(pattern: &str) -> Result<Vec<String>, EditorConfigDiagnostic> {
177+
struct Variants {
178+
/// index of the { character
179+
start: usize,
180+
/// index of the } character
181+
end: usize,
182+
variants: Option<VariantType>,
183+
}
184+
185+
impl Variants {
186+
fn new(start: usize) -> Self {
187+
Self {
188+
start,
189+
end: start,
190+
variants: None,
191+
}
192+
}
193+
194+
fn parse_to_variants(&mut self, s: &str) -> Result<(), EditorConfigDiagnostic> {
195+
let s = s.trim_start_matches('{').trim_end_matches('}');
196+
if s.contains("..") {
197+
let mut parts = s.split("..");
198+
let start = parts.next().ok_or_else(|| {
199+
EditorConfigDiagnostic::invalid_glob_pattern(
200+
s,
201+
"Range pattern must have exactly two parts",
202+
)
203+
})?;
204+
let end = parts.next().ok_or_else(|| {
205+
EditorConfigDiagnostic::invalid_glob_pattern(
206+
s,
207+
"Range pattern must have exactly two parts",
208+
)
209+
})?;
210+
if parts.next().is_some() {
211+
return Err(EditorConfigDiagnostic::invalid_glob_pattern(
212+
s,
213+
"Range pattern must have exactly two parts",
214+
));
215+
}
216+
217+
let start = start.parse().map_err(|err| {
218+
EditorConfigDiagnostic::invalid_glob_pattern(
219+
s,
220+
format!("Error parsing the start of the range: {}", err),
221+
)
222+
})?;
223+
let end = end.parse().map_err(|err| {
224+
EditorConfigDiagnostic::invalid_glob_pattern(
225+
s,
226+
format!("Error parsing the end of the range: {}", err),
227+
)
228+
})?;
229+
self.variants = Some(VariantType::Range((start, end)));
230+
} else {
231+
self.variants = Some(VariantType::List(
232+
s.split(',').map(|s| s.to_string()).collect(),
233+
));
234+
}
235+
236+
Ok(())
237+
}
238+
239+
fn variants(&self) -> Vec<String> {
240+
match &self.variants {
241+
Some(VariantType::List(ref list)) => list.clone(),
242+
Some(VariantType::Range((start, end))) => {
243+
let mut variants = vec![];
244+
for i in *start..=*end {
245+
variants.push(i.to_string());
246+
}
247+
variants
248+
}
249+
None => vec![],
250+
}
251+
}
252+
}
253+
254+
enum VariantType {
255+
List(Vec<String>),
256+
Range((i64, i64)),
257+
}
258+
259+
let mut all_variants = vec![];
260+
let mut current_variants = None;
261+
for (i, c) in pattern.chars().enumerate() {
262+
match c {
263+
'{' => {
264+
if current_variants.is_none() {
265+
current_variants = Some(Variants::new(i));
266+
} else {
267+
// TODO: error, recursive brace expansion is not supported
268+
}
269+
}
270+
'}' => {
271+
if let Some(mut v) = current_variants.take() {
272+
v.end = i;
273+
v.parse_to_variants(&pattern[v.start..=v.end])?;
274+
all_variants.push(v);
275+
}
276+
}
277+
_ => {}
278+
}
279+
}
280+
281+
if all_variants.is_empty() {
282+
return Ok(vec![pattern.to_string()]);
283+
}
284+
285+
let mut expanded_patterns = vec![];
286+
for variants in all_variants.iter().rev() {
287+
if expanded_patterns.is_empty() {
288+
for variant in &variants.variants() {
289+
let mut pattern = pattern.to_string();
290+
pattern.replace_range(variants.start..=variants.end, variant);
291+
expanded_patterns.push(pattern);
292+
}
293+
} else {
294+
let mut new_patterns = vec![];
295+
for existing in &expanded_patterns {
296+
for variant in &variants.variants() {
297+
let mut pattern = existing.clone();
298+
pattern.replace_range(variants.start..=variants.end, variant);
299+
new_patterns.push(pattern);
300+
}
301+
}
302+
expanded_patterns = new_patterns;
303+
}
304+
}
305+
306+
Ok(expanded_patterns)
307+
}
308+
173309
#[cfg(test)]
174310
mod tests {
175311
use super::*;
@@ -256,20 +392,46 @@ insert_final_newline = false
256392
}
257393

258394
#[test]
259-
fn should_emit_diagnostic_glob_pattern() {
260-
let input = r#"
261-
root = true
395+
fn should_expand_glob_pattern_list() {
396+
let pattern = "package.json";
397+
let mut expanded =
398+
expand_unknown_glob_patterns(pattern).expect("Failed to expand glob pattern");
399+
expanded.sort();
400+
assert_eq!(expanded, vec!["package.json"]);
401+
402+
let pattern = "{package.json,.travis.yml}";
403+
let mut expanded =
404+
expand_unknown_glob_patterns(pattern).expect("Failed to expand glob pattern");
405+
expanded.sort();
406+
assert_eq!(expanded, vec![".travis.yml", "package.json"]);
407+
}
262408

263-
[{package.json,.travis.yml}]
264-
indent_style = space
265-
"#;
409+
#[test]
410+
fn should_expand_glob_pattern_list_2() {
411+
let pattern = "**/{foo,bar}.{test,spec}.js";
412+
let mut expanded =
413+
expand_unknown_glob_patterns(pattern).expect("Failed to expand glob pattern");
414+
expanded.sort();
415+
assert_eq!(
416+
expanded,
417+
vec![
418+
"**/bar.spec.js",
419+
"**/bar.test.js",
420+
"**/foo.spec.js",
421+
"**/foo.test.js",
422+
]
423+
);
424+
}
266425

267-
let conf = parse_str(input).expect("Failed to parse editorconfig");
268-
let (_, errors) = conf.to_biome();
269-
assert_eq!(errors.len(), 1);
270-
assert!(matches!(
271-
errors[0],
272-
EditorConfigDiagnostic::UnknownGlobPattern(_)
273-
));
426+
#[test]
427+
fn should_expand_glob_pattern_range() {
428+
let pattern = "**/bar.{1..4}.js";
429+
let mut expanded =
430+
expand_unknown_glob_patterns(pattern).expect("Failed to expand glob pattern");
431+
expanded.sort();
432+
assert_eq!(
433+
expanded,
434+
vec!["**/bar.1.js", "**/bar.2.js", "**/bar.3.js", "**/bar.4.js",]
435+
);
274436
}
275437
}

0 commit comments

Comments
 (0)