/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/fmt_editorconfig.rs
957 строк
28 KB
Fibi
feat(fmt): infer config from .editorconfig (#34071)
23 июн 2026, 14:35
Не верифицирован
23 июн 2026, 14:35
969f8cd
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. //! Minimal [EditorConfig](https://editorconfig.org/) loader used by `deno fmt`. //! //! Walks up the directory tree from a given file, parses encountered //! `.editorconfig` files, and resolves matching properties for that file. //! Results are cached so repeated lookups within a tree do not re-read //! and re-parse the same files. use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; use deno_config::deno_json::FmtOptionsConfig; use deno_config::deno_json::NewLineKind; use regex::Regex; /// Properties resolved from `.editorconfig` files for a particular file. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct EditorConfigProperties { pub indent_style: Option<IndentStyle>, pub indent_size: Option<IndentSize>, pub tab_width: Option<u8>, pub max_line_length: Option<u32>, pub end_of_line: Option<EndOfLine>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IndentStyle { Space, Tab, } /// Resolved value of `indent_size`. Per the editorconfig spec, the /// literal `indent_size = tab` means "use `tab_width`" regardless of /// `indent_style`, so it is tracked distinctly from a numeric width. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IndentSize { Width(u8), UseTabWidth, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EndOfLine { Lf, Crlf, Cr, } impl EditorConfigProperties { /// Apply the resolved properties to `cfg`, filling in only fields /// that are currently `None`. Deno's own config and CLI flags /// therefore always take precedence. pub fn apply_to(&self, cfg: &mut FmtOptionsConfig) { if cfg.use_tabs.is_none() && let Some(style) = self.indent_style { cfg.use_tabs = Some(matches!(style, IndentStyle::Tab)); } if cfg.indent_width.is_none() { // Per the editorconfig spec: // * `indent_size = tab` resolves to `tab_width` regardless of // `indent_style`. // * when `indent_size` is unset and `indent_style = tab`, // `indent_size` defaults to `tab_width`. // For a numeric `indent_size` the value is taken as-is. let indent = match self.indent_size { Some(IndentSize::Width(n)) => Some(n), Some(IndentSize::UseTabWidth) => self.tab_width, None if matches!(self.indent_style, Some(IndentStyle::Tab)) => { self.tab_width } None => None, }; if let Some(n) = indent { cfg.indent_width = Some(n); } } if cfg.line_width.is_none() && let Some(n) = self.max_line_length { cfg.line_width = Some(n); } if cfg.new_line_kind.is_none() { cfg.new_line_kind = match self.end_of_line { Some(EndOfLine::Lf) => Some(NewLineKind::LineFeed), Some(EndOfLine::Crlf) => Some(NewLineKind::CarriageReturnLineFeed), // No mapping for CR-only; leave unset. Some(EndOfLine::Cr) | None => None, }; } } pub fn is_empty(&self) -> bool { self.indent_style.is_none() && self.indent_size.is_none() && self.tab_width.is_none() && self.max_line_length.is_none() && self.end_of_line.is_none() } } #[derive(Debug)] struct ParsedFile { root: bool, sections: Vec<Section>, } #[derive(Debug)] struct Section { /// Anchored regex compiled from the section's glob pattern, /// matched against the slash-separated path relative to the /// `.editorconfig` directory. `None` if the pattern was empty or /// failed to compile (in which case the section is inert). Patterns /// without a `/` match the basename in any subdirectory; this is /// encoded by a `(?:.*/)?` prefix in the compiled regex. regex: Option<Regex>, properties: SectionProperties, } #[derive(Debug, Clone, Default)] struct SectionProperties { indent_style: Option<IndentStyle>, indent_size: Option<IndentSize>, tab_width: Option<u8>, max_line_length: Option<u32>, end_of_line: Option<EndOfLine>, } /// One entry in a resolved `.editorconfig` chain — a parsed file and /// the directory it lives in (needed to compute the path relative to /// the `.editorconfig` for pattern matching). #[derive(Debug)] struct ChainEntry { dir: PathBuf, file: Arc<ParsedFile>, } /// Cache of parsed `.editorconfig` files, keyed by the absolute path /// of the directory the file lives in. The cache also memoizes the /// resolved chain (outermost → innermost) for each starting directory /// so that the directory walk runs once per unique directory rather /// than once per file. #[derive(Debug, Default)] pub struct EditorConfigCache { files: Mutex<HashMap<PathBuf, Option<Arc<ParsedFile>>>>, chains: Mutex<HashMap<PathBuf, Arc<Vec<ChainEntry>>>>, } impl EditorConfigCache { pub fn new() -> Self { Self::default() } /// Resolve `.editorconfig` properties for `file_path`, which must be /// an absolute path. Returns `Default::default()` if no /// `.editorconfig` files apply. /// /// The common case (no `.editorconfig` anywhere up the tree) is kept /// cheap: the directory walk is memoized per directory, so resolving /// every file in a batch costs one cached chain lookup per file and /// performs no per-file filesystem work. Symlinks are not resolved; /// the literal path is walked, matching the editorconfig reference /// implementation (and avoiding a `realpath` syscall per file). pub fn resolve(&self, file_path: &Path) -> EditorConfigProperties { // The relative-path computation (`strip_prefix`) and the parent // walk both rely on an absolute path; a relative one would stop the // walk early and silently miss parent `.editorconfig`s above cwd. // fmt's `FileCollector` always resolves against an absolute base, so // this holds in practice — assert it to keep the contract explicit. debug_assert!( file_path.is_absolute(), "resolve() requires an absolute path, got {}", file_path.display() ); let Some(start) = file_path.parent() else { return EditorConfigProperties::default(); }; let chain = self.resolve_chain(start); if chain.is_empty() { return EditorConfigProperties::default(); } let mut out = EditorConfigProperties::default(); for entry in chain.iter() { // Compute the file path relative to this .editorconfig's dir. let Ok(rel) = file_path.strip_prefix(&entry.dir) else { continue; }; let rel = path_to_forward_slash(rel); for section in &entry.file.sections { if let Some(re) = §ion.regex && re.is_match(&rel) { merge_section(&mut out, §ion.properties); } } } out } /// Resolve the (outermost → innermost) chain of `.editorconfig` /// files that apply to anything in `dir`. Result is cached per dir, /// so files in the same directory share the same walk. fn resolve_chain(&self, dir: &Path) -> Arc<Vec<ChainEntry>> { if let Some(c) = self.chains.lock().unwrap().get(dir) { return c.clone(); } let mut entries: Vec<ChainEntry> = Vec::new(); let mut cur: Option<&Path> = Some(dir); while let Some(d) = cur { let ec_path = d.join(".editorconfig"); if let Some(parsed) = self.read_and_parse(&ec_path) { let is_root = parsed.root; entries.push(ChainEntry { dir: d.to_path_buf(), file: parsed, }); if is_root { break; } } cur = d.parent(); } // Apply outermost first so nearer files override farther ones. entries.reverse(); let arc = Arc::new(entries); self .chains .lock() .unwrap() .insert(dir.to_path_buf(), arc.clone()); arc } fn read_and_parse(&self, path: &Path) -> Option<Arc<ParsedFile>> { { let files = self.files.lock().unwrap(); if let Some(cached) = files.get(path) { return cached.clone(); } } let parsed = std::fs::read_to_string(path) .ok() .map(|s| Arc::new(parse(&s))); if parsed.is_some() { log::debug!("Found .editorconfig at {} and using it", path.display()); } let mut files = self.files.lock().unwrap(); files.insert(path.to_path_buf(), parsed.clone()); parsed } } fn merge_section(dst: &mut EditorConfigProperties, src: &SectionProperties) { if let Some(v) = src.indent_style { dst.indent_style = Some(v); } if let Some(v) = src.indent_size { dst.indent_size = Some(v); } if let Some(v) = src.tab_width { dst.tab_width = Some(v); } if let Some(v) = src.max_line_length { dst.max_line_length = Some(v); } if let Some(v) = src.end_of_line { dst.end_of_line = Some(v); } } fn parse(contents: &str) -> ParsedFile { let mut root = false; let mut sections: Vec<Section> = Vec::new(); let mut current: Option<Section> = None; for raw_line in contents.lines() { let line = strip_comment(raw_line).trim(); if line.is_empty() { continue; } if let Some(rest) = line.strip_prefix('[') && let Some(pattern) = rest.strip_suffix(']') { if let Some(prev) = current.take() { sections.push(prev); } let regex = compile_glob_regex(pattern); current = Some(Section { regex, properties: SectionProperties::default(), }); continue; } let Some((key, value)) = line.split_once('=') else { continue; }; let key = key.trim().to_ascii_lowercase(); let value = value.trim(); if current.is_none() { // Preamble: only "root" is meaningful. if key == "root" && value.eq_ignore_ascii_case("true") { root = true; } continue; } let props = &mut current.as_mut().unwrap().properties; match key.as_str() { "indent_style" => { props.indent_style = match value.to_ascii_lowercase().as_str() { "tab" => Some(IndentStyle::Tab), "space" => Some(IndentStyle::Space), _ => None, }; } "indent_size" => { // "tab" means use tab_width; otherwise parse as integer, // clamping out-of-range values rather than dropping them. if value.eq_ignore_ascii_case("tab") { props.indent_size = Some(IndentSize::UseTabWidth); } else if let Some(n) = parse_saturating_u8(value) { props.indent_size = Some(IndentSize::Width(n)); } } "tab_width" => { if let Some(n) = parse_saturating_u8(value) { props.tab_width = Some(n); } } "max_line_length" => { if !value.eq_ignore_ascii_case("off") && let Some(n) = parse_saturating_u32(value) { props.max_line_length = Some(n); } } "end_of_line" => { props.end_of_line = match value.to_ascii_lowercase().as_str() { "lf" => Some(EndOfLine::Lf), "crlf" => Some(EndOfLine::Crlf), "cr" => Some(EndOfLine::Cr), _ => None, }; } _ => {} } } if let Some(prev) = current.take() { sections.push(prev); } ParsedFile { root, sections } } /// Parse a non-negative integer editorconfig value, saturating to the /// target type's maximum on overflow instead of discarding the value. /// Returns `None` for negative or non-numeric input so the property is /// simply ignored. fn parse_saturating_u8(value: &str) -> Option<u8> { let n = value.trim().parse::<u64>().ok()?; Some(n.min(u8::MAX as u64) as u8) } fn parse_saturating_u32(value: &str) -> Option<u32> { let n = value.trim().parse::<u64>().ok()?; Some(n.min(u32::MAX as u64) as u32) } fn strip_comment(s: &str) -> &str { // EditorConfig allows ';' or '#' as comment markers. They start // a comment if at the beginning of a line or preceded by whitespace. let mut prev_ws = true; for (i, ch) in s.char_indices() { if (ch == ';' || ch == '#') && prev_ws { return &s[..i]; } prev_ws = ch.is_whitespace(); } s } /// Compile a `.editorconfig` section glob pattern to a regex anchored /// against the slash-separated relative path of a file. Returns `None` /// if the pattern is empty or fails to compile (the section then has /// no effect). fn compile_glob_regex(pattern: &str) -> Option<Regex> { if pattern.is_empty() { return None; } // If the pattern doesn't contain a path separator it matches the // basename in any subdirectory; otherwise it's anchored at the // `.editorconfig` directory. let match_any_dir = !pattern.contains('/'); let pattern_re = glob_to_regex(pattern, match_any_dir); Regex::new(&pattern_re).ok() } fn path_to_forward_slash(p: &Path) -> String { let s = p.to_string_lossy().into_owned(); if std::path::MAIN_SEPARATOR == '/' { s } else { s.replace(std::path::MAIN_SEPARATOR, "/") } } /// Convert an editorconfig glob pattern to a regex string anchored /// with `^` and `$`. If `match_any_dir`, the pattern is allowed to /// be preceded by any number of leading directory components. fn glob_to_regex(pattern: &str, match_any_dir: bool) -> String { glob_to_regex_depth(pattern, match_any_dir, 0) } /// Maximum brace-nesting depth expanded before a pattern degrades to a /// literal match. Guards against a stack overflow on a pathological /// pattern such as `{a,{a,{a,...}}}` nested thousands deep. const MAX_GLOB_DEPTH: u32 = 32; fn glob_to_regex_depth( pattern: &str, match_any_dir: bool, depth: u32, ) -> String { let mut out = String::from("^"); if match_any_dir { out.push_str("(?:.*/)?"); } let pattern = pattern.strip_prefix('/').unwrap_or(pattern); let chars: Vec<char> = pattern.chars().collect(); let mut i = 0; while i < chars.len() { let c = chars[i]; match c { '*' => { if i + 1 < chars.len() && chars[i + 1] == '*' { // Treat `**/` as zero or more path components so that // `**/foo.ts` also matches `foo.ts` at the root, matching // gitignore-style user expectations. if i + 2 < chars.len() && chars[i + 2] == '/' { out.push_str("(?:[^/]*/)*"); i += 3; continue; } out.push_str(".*"); i += 2; continue; } else { out.push_str("[^/]*"); } } '?' => out.push_str("[^/]"), '{' => { // Find matching '}'. let mut brace_depth = 1; let mut j = i + 1; while j < chars.len() && brace_depth > 0 { match chars[j] { '{' => brace_depth += 1, '}' => { brace_depth -= 1; if brace_depth == 0 { break; } } _ => {} } j += 1; } if brace_depth == 0 { let group: String = chars[i + 1..j].iter().collect(); // Numeric range {n..m}. Bounds are parsed as integers, so // leading zeros are ignored and numbers are emitted in their // natural decimal form, matching the editorconfig reference. if let Some((lhs, rhs)) = group.split_once("..") && let (Ok(lo), Ok(hi)) = (lhs.trim().parse::<i64>(), rhs.trim().parse::<i64>()) { let (a, b) = if lo <= hi { (lo, hi) } else { (hi, lo) }; // Bound the enumeration so a pathological range such as // `{1..1000000000}` cannot exhaust memory while building the // regex. Real editorconfig ranges are tiny; a larger span // degrades to a literal match (the section simply will not // apply) rather than hanging or crashing. const MAX_RANGE_SPAN: i64 = 4096; let span_ok = b.checked_sub(a).is_some_and(|span| span < MAX_RANGE_SPAN); if span_ok { out.push('('); for n in a..=b { if n != a { out.push('|'); } for ch in n.to_string().chars() { regex_push_escaped(&mut out, ch); } } out.push(')'); i = j + 1; continue; } // Span too large: fall through to literal handling below. } // Comma alternatives let alts = split_top_level_commas(&group); if alts.len() > 1 && depth < MAX_GLOB_DEPTH { out.push_str("(?:"); for (k, alt) in alts.iter().enumerate() { if k > 0 { out.push('|'); } out.push_str(&glob_inner_to_regex(alt, depth + 1)); } out.push(')'); i = j + 1; continue; } // A comma-less `{...}` group (no alternation, no numeric // range) is a literal *including* the braces, per the // editorconfig reference implementation — `{word}.ts` // matches the literal string `{word}.ts`, not `word.ts`. // This branch is also reached when alternation nesting // exceeds MAX_GLOB_DEPTH, where degrading to a literal is // the safe fallback. regex_push_escaped(&mut out, '{'); for ch in group.chars() { regex_push_escaped(&mut out, ch); } regex_push_escaped(&mut out, '}'); i = j + 1; continue; } else { // Unmatched brace - treat literally. regex_push_escaped(&mut out, '{'); } } '[' => { // Character class; pass through, but translate `[!...]` -> `[^...]`. let mut j = i + 1; let mut negate = false; if j < chars.len() && (chars[j] == '!' || chars[j] == '^') { negate = true; j += 1; } let mut class_chars = Vec::new(); while j < chars.len() && chars[j] != ']' { class_chars.push(chars[j]); j += 1; } if j < chars.len() { out.push('['); if negate { out.push('^'); } for ch in class_chars { // Inside a char class, escape `\`, `]`, `^`. match ch { '\\' | ']' => { out.push('\\'); out.push(ch); } _ => out.push(ch), } } out.push(']'); i = j + 1; continue; } else { regex_push_escaped(&mut out, '['); } } _ => regex_push_escaped(&mut out, c), } i += 1; } out.push('$'); out } fn glob_inner_to_regex(s: &str, depth: u32) -> String { // Recursive use for alternatives - reuse the same logic without // adding `^`/`$` anchors or the leading any-dir prefix. let inner = glob_to_regex_depth(s, false, depth); // Strip the surrounding ^...$. inner .strip_prefix('^') .and_then(|t| t.strip_suffix('$')) .unwrap_or(&inner) .to_string() } fn split_top_level_commas(s: &str) -> Vec<String> { let mut out = Vec::new(); let mut current = String::new(); let mut depth = 0i32; for ch in s.chars() { match ch { '{' => { depth += 1; current.push(ch); } '}' => { depth -= 1; current.push(ch); } ',' if depth == 0 => { out.push(std::mem::take(&mut current)); } _ => current.push(ch), } } if !current.is_empty() || !out.is_empty() { out.push(current); } out } fn regex_push_escaped(out: &mut String, ch: char) { match ch { '.' | '+' | '(' | ')' | '|' | '^' | '$' | '\\' | '[' | ']' | '{' | '}' => { out.push('\\'); out.push(ch); } _ => out.push(ch), } } #[cfg(test)] mod tests { use super::*; fn parse_str(s: &str) -> ParsedFile { parse(s) } fn match_regex(pattern: &str, text: &str) -> bool { Regex::new(pattern) .map(|re| re.is_match(text)) .unwrap_or_else(|err| panic!("invalid regex {pattern:?}: {err}")) } #[test] fn parses_root_and_sections() { let f = parse_str( r"root = true [*] indent_style = space indent_size = 2 [*.py] indent_size = 4 ", ); assert!(f.root); assert_eq!(f.sections.len(), 2); let s0_re = f.sections[0].regex.as_ref().unwrap(); assert!(s0_re.is_match("foo.ts")); assert!(s0_re.is_match("a/b/foo.ts")); assert_eq!( f.sections[0].properties.indent_style, Some(IndentStyle::Space) ); assert_eq!( f.sections[0].properties.indent_size, Some(IndentSize::Width(2)) ); let s1_re = f.sections[1].regex.as_ref().unwrap(); assert!(s1_re.is_match("a.py")); assert!(!s1_re.is_match("a.ts")); assert_eq!( f.sections[1].properties.indent_size, Some(IndentSize::Width(4)) ); } #[test] fn parses_tab_and_max_line_length() { let f = parse_str( r"[*] indent_style = tab tab_width = 4 max_line_length = 100 end_of_line = crlf ", ); let p = &f.sections[0].properties; assert_eq!(p.indent_style, Some(IndentStyle::Tab)); assert_eq!(p.tab_width, Some(4)); assert_eq!(p.max_line_length, Some(100)); assert_eq!(p.end_of_line, Some(EndOfLine::Crlf)); } #[test] fn strips_comments() { let f = parse_str( r"; preamble comment root = true # ignored [*] # section indent_size = 2 ; inline ", ); assert!(f.root); assert_eq!( f.sections[0].properties.indent_size, Some(IndentSize::Width(2)) ); } #[test] fn max_line_length_off() { let f = parse_str( r"[*] max_line_length = off ", ); assert_eq!(f.sections[0].properties.max_line_length, None); } #[test] fn glob_basic_extension() { let r = glob_to_regex("*.ts", true); assert!(match_regex(&r, "foo.ts")); assert!(match_regex(&r, "sub/foo.ts")); assert!(!match_regex(&r, "foo.js")); } #[test] fn glob_braces() { let r = glob_to_regex("*.{ts,tsx,js}", true); assert!(match_regex(&r, "foo.ts")); assert!(match_regex(&r, "foo.tsx")); assert!(match_regex(&r, "foo.js")); assert!(!match_regex(&r, "foo.py")); } #[test] fn glob_double_star() { let r = glob_to_regex("**/foo.ts", false); assert!(match_regex(&r, "foo.ts")); assert!(match_regex(&r, "a/foo.ts")); assert!(match_regex(&r, "a/b/foo.ts")); assert!(!match_regex(&r, "a/foo.js")); } #[test] fn glob_single_star_no_slash() { let r = glob_to_regex("foo/*.ts", false); assert!(match_regex(&r, "foo/bar.ts")); assert!(!match_regex(&r, "foo/sub/bar.ts")); } #[test] fn glob_with_slash_anchored() { // Pattern with '/' is anchored at config dir root. let r = glob_to_regex("src/*.ts", false); assert!(match_regex(&r, "src/a.ts")); assert!(!match_regex(&r, "lib/src/a.ts")); } #[test] fn glob_question_mark() { let r = glob_to_regex("?.ts", true); assert!(match_regex(&r, "a.ts")); assert!(!match_regex(&r, "ab.ts")); } #[test] fn glob_char_class() { let r = glob_to_regex("[abc].ts", true); assert!(match_regex(&r, "a.ts")); assert!(match_regex(&r, "b.ts")); assert!(!match_regex(&r, "d.ts")); } #[test] fn glob_negated_char_class() { let r = glob_to_regex("[!abc].ts", true); assert!(!match_regex(&r, "a.ts")); assert!(match_regex(&r, "d.ts")); } #[test] fn glob_numeric_range() { let r = glob_to_regex("file{1..3}.txt", true); assert!(match_regex(&r, "file1.txt")); assert!(match_regex(&r, "file2.txt")); assert!(match_regex(&r, "file3.txt")); assert!(!match_regex(&r, "file4.txt")); } #[test] fn glob_reversed_numeric_range() { let r = glob_to_regex("file{3..1}.txt", true); assert!(match_regex(&r, "file1.txt")); assert!(match_regex(&r, "file2.txt")); assert!(match_regex(&r, "file3.txt")); } #[test] fn glob_leading_zero_range_matches_reference() { // Bounds are parsed as integers, so leading zeros are ignored, // matching the editorconfig reference implementation. let r = glob_to_regex("file{01..03}.txt", true); assert!(match_regex(&r, "file1.txt")); assert!(match_regex(&r, "file3.txt")); } #[test] fn glob_huge_numeric_range_degrades() { // A pathological range must not be expanded into a giant regex; it // degrades to a literal that simply does not match normal files. let r = glob_to_regex("file{1..1000000000}.txt", true); assert!(r.len() < 100, "regex unexpectedly large: {} chars", r.len()); assert!(!match_regex(&r, "file5.txt")); } #[test] fn glob_deeply_nested_braces_do_not_overflow() { // Nest braces well past MAX_GLOB_DEPTH; this must return without // overflowing the stack and still produce a valid regex. let mut p = String::from("x"); for _ in 0..5000 { p = format!("{{a,{p}}}"); } let r = glob_to_regex(&p, false); assert!(r.starts_with('^') && r.ends_with('$')); // Must compile rather than blow up. assert!(Regex::new(&r).is_ok()); } #[test] fn glob_commaless_brace_is_literal() { // A `{...}` group with no comma and no range is a literal that // includes the braces, matching the editorconfig reference (rather // than stripping the braces and matching `word.ts`). let r = glob_to_regex("{word}.ts", true); assert!(match_regex(&r, "{word}.ts")); assert!(!match_regex(&r, "word.ts")); } #[test] fn glob_empty_and_trailing_comma_braces() { // `{}` is a comma-less literal (matches the literal braces); `{a,}` // is a two-way alternation where one branch is empty. These are // degenerate inputs but must still compile to valid regexes. let empty = glob_to_regex("a{}b", false); assert!(match_regex(&empty, "a{}b")); assert!(!match_regex(&empty, "ab")); let trailing = glob_to_regex("a{x,}b", false); assert!(match_regex(&trailing, "axb")); assert!(match_regex(&trailing, "ab")); } #[test] fn glob_unbalanced_brace_compiles() { // An unbalanced brace must still translate to a valid regex. let r = glob_to_regex("foo{bar.ts", true); assert!(match_regex(&r, "foo{bar.ts")); } #[test] fn parse_saturating_clamps() { assert_eq!(parse_saturating_u8("8"), Some(8)); assert_eq!(parse_saturating_u8("256"), Some(255)); assert_eq!(parse_saturating_u8("-1"), None); assert_eq!(parse_saturating_u8("nope"), None); assert_eq!(parse_saturating_u32("99999999999"), Some(u32::MAX)); } #[test] fn indent_size_overflow_clamped() { let f = parse_str("[*]\nindent_size = 1000\n"); assert_eq!( f.sections[0].properties.indent_size, Some(IndentSize::Width(255)) ); } #[test] fn max_line_length_overflow_clamped() { let f = parse_str("[*]\nmax_line_length = 99999999999\n"); assert_eq!(f.sections[0].properties.max_line_length, Some(u32::MAX)); } #[test] fn apply_indent_tab_with_width() { let mut cfg = FmtOptionsConfig::default(); let props = EditorConfigProperties { indent_style: Some(IndentStyle::Tab), tab_width: Some(4), ..Default::default() }; props.apply_to(&mut cfg); assert_eq!(cfg.use_tabs, Some(true)); assert_eq!(cfg.indent_width, Some(4)); } #[test] fn apply_does_not_override_existing() { let mut cfg = FmtOptionsConfig { use_tabs: Some(false), indent_width: Some(2), ..Default::default() }; let props = EditorConfigProperties { indent_style: Some(IndentStyle::Tab), indent_size: Some(IndentSize::Width(8)), ..Default::default() }; props.apply_to(&mut cfg); assert_eq!(cfg.use_tabs, Some(false)); assert_eq!(cfg.indent_width, Some(2)); } #[test] fn apply_indent_size_tab_uses_tab_width_without_indent_style() { // Per the spec, `indent_size = tab` resolves to `tab_width` even when // `indent_style` is not set. let mut cfg = FmtOptionsConfig::default(); let props = EditorConfigProperties { indent_size: Some(IndentSize::UseTabWidth), tab_width: Some(4), ..Default::default() }; props.apply_to(&mut cfg); assert_eq!(cfg.indent_width, Some(4)); } #[test] fn parses_indent_size_tab() { let f = parse_str("[*]\nindent_size = tab\ntab_width = 4\n"); let p = &f.sections[0].properties; assert_eq!(p.indent_size, Some(IndentSize::UseTabWidth)); assert_eq!(p.tab_width, Some(4)); } #[test] fn apply_end_of_line_maps_to_new_line_kind() { let mut cfg = FmtOptionsConfig::default(); let props = EditorConfigProperties { end_of_line: Some(EndOfLine::Crlf), ..Default::default() }; props.apply_to(&mut cfg); assert_eq!(cfg.new_line_kind, Some(NewLineKind::CarriageReturnLineFeed)); } }