editorconfig_parser/
lib.rs

1use std::path::Path;
2
3use globset::{Glob, GlobMatcher};
4
5#[derive(Debug, Default, Clone)]
6pub struct EditorConfig {
7    /// Set to true to tell the core not to check any higher directory for EditorConfig settings for on the current filename.
8    root: bool,
9
10    sections: Vec<EditorConfigSection>,
11}
12
13impl EditorConfig {
14    pub fn root(&self) -> bool {
15        self.root
16    }
17
18    pub fn sections(&self) -> &[EditorConfigSection] {
19        &self.sections
20    }
21}
22
23/// <https://spec.editorconfig.org/index.html>
24#[derive(Debug, Default, Clone)]
25pub struct EditorConfigSection {
26    /// Section Name: the string between the beginning `[` and the ending `]`.
27    pub name: String,
28
29    pub matcher: Option<GlobMatcher>,
30
31    pub properties: EditorConfigProperties,
32}
33
34#[derive(Debug, Default, Clone, Eq, PartialEq)]
35pub enum EditorConfigProperty<T> {
36    #[default]
37    None,
38    Unset,
39    Value(T),
40}
41
42#[derive(Debug, Default, Clone, Eq, PartialEq)]
43pub struct EditorConfigProperties {
44    /// Set to tab or space to use tabs or spaces for indentation, respectively.
45    /// Option tab implies that an indentation is to be filled by as many hard tabs as possible, with the rest of the indentation filled by spaces.
46    /// A non-normative explanation can be found in the indentation_ section.
47    /// The values are case-insensitive.
48    pub indent_style: EditorConfigProperty<IndentStyle>,
49
50    /// Set to a whole number defining the number of columns used for each indentation level and the width of soft tabs (when supported).
51    /// If this equals tab, the indent_size shall be set to the tab size, which should be tab_width (if specified); else, the tab size set by the editor.
52    /// The values are case-insensitive.
53    pub indent_size: EditorConfigProperty<usize>,
54
55    /// Set to a whole number defining the number of columns used to represent a tab character.
56    /// This defaults to the value of indent_size and should not usually need to be specified.
57    pub tab_width: EditorConfigProperty<usize>,
58
59    /// Set to lf, cr, or crlf to control how line breaks are represented.
60    /// The values are case-insensitive.
61    pub end_of_line: EditorConfigProperty<EndOfLine>,
62
63    /// Set to latin1, utf-8, utf-8-bom, utf-16be or utf-16le to control the character set.
64    /// Use of utf-8-bom is discouraged.
65    /// The values are case-insensitive.
66    pub charset: EditorConfigProperty<Charset>,
67
68    /// Set to true to remove all whitespace characters preceding newline characters in the file and false to ensure it doesn’t.
69    pub trim_trailing_whitespace: EditorConfigProperty<bool>,
70
71    /// Set to true ensure file ends with a newline when saving and false to ensure it doesn’t.
72    /// Editors must not insert newlines in empty files when saving those files, even if insert_final_newline = true.
73    pub insert_final_newline: EditorConfigProperty<bool>,
74
75    /// Prettier print width.
76    /// Not part of spec <https://github.com/editorconfig/editorconfig-vscode/issues/53#issuecomment-462432616>
77    /// But documented in <https://prettier.io/docs/next/configuration#editorconfig>
78    pub max_line_length: EditorConfigProperty<MaxLineLength>,
79}
80
81#[derive(Debug, Clone, Copy, Eq, PartialEq)]
82pub enum MaxLineLength {
83    /// A numeric line length limit
84    Number(usize),
85    /// Line length limit is disabled
86    Off,
87}
88
89#[derive(Debug, Clone, Copy, Eq, PartialEq)]
90pub enum IndentStyle {
91    Tab,
92    Space,
93}
94
95#[derive(Debug, Clone, Copy, Eq, PartialEq)]
96pub enum EndOfLine {
97    Lf,
98    Cr,
99    Crlf,
100}
101
102#[derive(Debug, Clone, Copy, Eq, PartialEq)]
103pub enum Charset {
104    Latin1,
105    Utf8,
106    Utf8bom,
107    Utf16be,
108    Utf16le,
109}
110
111impl EditorConfig {
112    /// <https://spec.editorconfig.org/index.html#id6>
113    pub fn parse(source_text: &str) -> Self {
114        // EditorConfig files are in an INI-like file format.
115        // To read an EditorConfig file, take one line at a time, from beginning to end.
116        // For each line:
117        // 1. Remove all leading and trailing whitespace.
118        // 2. Process the remaining text as specified for its type below.
119        let mut root = false;
120        let mut sections = vec![];
121        let mut preamble = true;
122        for line in source_text.lines() {
123            let line = line.trim();
124            // Blank: Contains nothing. Blank lines are ignored.
125            if line.is_empty() {
126                continue;
127            }
128            // Comment: starts with a ; or a #. Comment lines are ignored.
129            if line.starts_with([';', '#']) {
130                continue;
131            }
132            // Parse `root`. Must be specified in the preamble. The value is case-insensitive.
133            if preamble
134                && !line.starts_with('[')
135                && let Some((key, value)) = line.split_once('=')
136                && key.trim_end() == "root"
137                && value.trim_start().eq_ignore_ascii_case("true")
138            {
139                root = true;
140            }
141            // Section Header: starts with a [ and ends with a ]. These lines define globs;
142            if let Some(line) = line.strip_prefix('[') {
143                preamble = false;
144                if let Some(line) = line.strip_suffix(']') {
145                    let name = line.to_string();
146                    let matcher = Glob::new(&name).ok().map(|glob| glob.compile_matcher());
147                    sections.push(EditorConfigSection {
148                        name,
149                        matcher,
150                        ..EditorConfigSection::default()
151                    });
152                }
153            }
154            // Key-Value Pair (or Pair): contains a key and a value, separated by an `=`.
155            if let Some(section) = sections.last_mut()
156                && let Some((key, value)) = line.split_once('=')
157            {
158                let value = value.trim_start();
159                let properties = &mut section.properties;
160                match key.trim_end() {
161                    "indent_style" => {
162                        properties.indent_style = IndentStyle::parse(value);
163                    }
164                    "indent_size" => {
165                        properties.indent_size = EditorConfigProperty::<usize>::parse(value);
166                    }
167                    "tab_width" => {
168                        properties.tab_width = EditorConfigProperty::<usize>::parse(value);
169                    }
170                    "end_of_line" => {
171                        properties.end_of_line = EditorConfigProperty::<EndOfLine>::parse(value);
172                    }
173                    "charset" => {
174                        properties.charset = EditorConfigProperty::<Charset>::parse(value);
175                    }
176                    "trim_trailing_whitespace" => {
177                        properties.trim_trailing_whitespace =
178                            EditorConfigProperty::<bool>::parse(value);
179                    }
180                    "insert_final_newline" => {
181                        properties.insert_final_newline =
182                            EditorConfigProperty::<bool>::parse(value);
183                    }
184                    "max_line_length" => {
185                        properties.max_line_length =
186                            EditorConfigProperty::<MaxLineLength>::parse(value);
187                    }
188                    _ => {}
189                }
190            }
191        }
192
193        Self { root, sections }
194    }
195
196    /// Resolve a given path and return the resolved properties.
197    pub fn resolve(&self, path: &Path) -> EditorConfigProperties {
198        let mut properties = EditorConfigProperties::default();
199        for section in &self.sections {
200            if section.matcher.as_ref().is_some_and(|matcher| matcher.is_match(path)) {
201                properties.override_with(&section.properties);
202            }
203        }
204        properties
205    }
206}
207
208impl<T: Copy> EditorConfigProperty<T> {
209    fn override_with(&mut self, other: &Self) {
210        match other {
211            Self::Value(value) => {
212                *self = Self::Value(*value);
213            }
214            Self::Unset => {
215                *self = Self::None;
216            }
217            Self::None => {}
218        }
219    }
220}
221
222impl EditorConfigProperties {
223    fn override_with(&mut self, other: &Self) {
224        self.indent_style.override_with(&other.indent_style);
225        self.indent_size.override_with(&other.indent_size);
226        self.tab_width.override_with(&other.tab_width);
227        self.end_of_line.override_with(&other.end_of_line);
228        self.charset.override_with(&other.charset);
229        self.trim_trailing_whitespace.override_with(&other.trim_trailing_whitespace);
230        self.insert_final_newline.override_with(&other.insert_final_newline);
231        self.max_line_length.override_with(&other.max_line_length);
232    }
233}
234
235impl EditorConfigProperty<usize> {
236    fn parse(s: &str) -> Self {
237        if s.eq_ignore_ascii_case("unset") {
238            Self::Unset
239        } else {
240            s.parse::<usize>().map_or(Self::None, EditorConfigProperty::Value)
241        }
242    }
243}
244
245impl EditorConfigProperty<bool> {
246    fn parse(s: &str) -> Self {
247        if s.eq_ignore_ascii_case("true") {
248            EditorConfigProperty::Value(true)
249        } else if s.eq_ignore_ascii_case("false") {
250            EditorConfigProperty::Value(false)
251        } else if s.eq_ignore_ascii_case("unset") {
252            EditorConfigProperty::Unset
253        } else {
254            EditorConfigProperty::None
255        }
256    }
257}
258
259impl IndentStyle {
260    fn parse(s: &str) -> EditorConfigProperty<Self> {
261        if s.eq_ignore_ascii_case("tab") {
262            EditorConfigProperty::Value(Self::Tab)
263        } else if s.eq_ignore_ascii_case("space") {
264            EditorConfigProperty::Value(Self::Space)
265        } else if s.eq_ignore_ascii_case("unset") {
266            EditorConfigProperty::Unset
267        } else {
268            EditorConfigProperty::None
269        }
270    }
271}
272
273impl EditorConfigProperty<EndOfLine> {
274    fn parse(s: &str) -> Self {
275        if s.eq_ignore_ascii_case("lf") {
276            Self::Value(EndOfLine::Lf)
277        } else if s.eq_ignore_ascii_case("cr") {
278            Self::Value(EndOfLine::Cr)
279        } else if s.eq_ignore_ascii_case("crlf") {
280            Self::Value(EndOfLine::Crlf)
281        } else if s.eq_ignore_ascii_case("unset") {
282            Self::Unset
283        } else {
284            Self::None
285        }
286    }
287}
288
289impl EditorConfigProperty<Charset> {
290    fn parse(s: &str) -> Self {
291        if s.eq_ignore_ascii_case("utf-8") {
292            Self::Value(Charset::Utf8)
293        } else if s.eq_ignore_ascii_case("latin1") {
294            Self::Value(Charset::Latin1)
295        } else if s.eq_ignore_ascii_case("utf-16be") {
296            Self::Value(Charset::Utf16be)
297        } else if s.eq_ignore_ascii_case("utf-16le") {
298            Self::Value(Charset::Utf16le)
299        } else if s.eq_ignore_ascii_case("utf-8-bom") {
300            Self::Value(Charset::Utf8bom)
301        } else if s.eq_ignore_ascii_case("unset") {
302            Self::Unset
303        } else {
304            Self::None
305        }
306    }
307}
308
309impl EditorConfigProperty<MaxLineLength> {
310    fn parse(s: &str) -> Self {
311        if s.eq_ignore_ascii_case("off") {
312            Self::Value(MaxLineLength::Off)
313        } else if s.eq_ignore_ascii_case("unset") {
314            Self::Unset
315        } else if let Ok(n) = s.parse::<usize>() {
316            Self::Value(MaxLineLength::Number(n))
317        } else {
318            Self::None
319        }
320    }
321}