ec4rs/
file.rs

1use std::path::{Path, PathBuf};
2
3use crate::{ConfigParser, Error, ParseError, Properties, PropertiesSource, Section};
4
5/// Convenience wrapper for an [`ConfigParser`] that reads files.
6pub struct ConfigFile {
7    // TODO: Arc<Path>. It's more important to have cheap clones than mutability.
8    /// The path to the open file.
9    pub path: PathBuf,
10    /// A [`ConfigParser`] that reads from the file.
11    pub reader: ConfigParser<std::io::BufReader<std::fs::File>>,
12}
13
14impl ConfigFile {
15    /// Opens a file for reading and uses it to construct an [`ConfigParser`].
16    ///
17    /// If the file cannot be opened, wraps the [`std::io::Error`] in a [`ParseError`].
18    pub fn open(path: impl Into<PathBuf>) -> Result<ConfigFile, ParseError> {
19        let path = path.into();
20        let file = std::fs::File::open(&path).map_err(ParseError::Io)?;
21        let reader = ConfigParser::new_buffered_with_path(file, Some(path.as_ref()))?;
22        Ok(ConfigFile { path, reader })
23    }
24
25    /// Wraps a [`ParseError`] in an [`Error::InFile`].
26    ///
27    /// Uses the path and current line number from this instance.
28    pub fn add_error_context(&self, error: ParseError) -> Error {
29        Error::InFile(self.path.clone(), self.reader.line_no(), error)
30    }
31}
32
33impl Iterator for ConfigFile {
34    type Item = Result<Section, ParseError>;
35    fn next(&mut self) -> Option<Self::Item> {
36        self.reader.next()
37    }
38}
39
40impl std::iter::FusedIterator for ConfigFile {}
41
42impl PropertiesSource for &mut ConfigFile {
43    /// Adds properties from the file's sections to the specified [`Properties`] map.
44    ///
45    /// Uses [`ConfigFile::path`] when determining applicability to stop `**` from going too far.
46    /// Returns parse errors wrapped in an [`Error::InFile`].
47    fn apply_to(self, props: &mut Properties, path: impl AsRef<Path>) -> Result<(), crate::Error> {
48        let get_parent = || self.path.parent();
49        let path = if let Some(parent) = get_parent() {
50            let path = path.as_ref();
51            path.strip_prefix(parent).unwrap_or(path)
52        } else {
53            path.as_ref()
54        };
55        match self.reader.apply_to(props, path) {
56            Ok(()) => Ok(()),
57            Err(crate::Error::Parse(e)) => Err(self.add_error_context(e)),
58            Err(e) => panic!("unexpected error variant {:?}", e),
59        }
60    }
61}
62
63/// Directory traverser for finding and opening EditorConfig files.
64///
65/// All the contained files are open for reading and have not had any sections read.
66/// When iterated over, either by using it as an [`Iterator`]
67/// or by calling [`ConfigFiles::iter`],
68/// returns [`ConfigFile`]s in the order that they would apply to a [`Properties`] map.
69pub struct ConfigFiles(Vec<ConfigFile>);
70
71impl ConfigFiles {
72    /// Searches for EditorConfig files that might apply to a file at the specified path.
73    ///
74    /// This function does not canonicalize the path,
75    /// but will join relative paths onto the current working directory.
76    ///
77    /// EditorConfig files are assumed to be named `.editorconfig`
78    /// unless an override is supplied as the second argument.
79    #[allow(clippy::needless_pass_by_value)]
80    pub fn open(
81        path: impl AsRef<Path>,
82        config_path_override: Option<impl AsRef<std::path::Path>>,
83    ) -> Result<ConfigFiles, Error> {
84        use std::borrow::Cow;
85        let filename = config_path_override
86            .as_ref()
87            .map_or_else(|| ".editorconfig".as_ref(), |f| f.as_ref());
88        Ok(ConfigFiles(if filename.is_relative() {
89            let mut abs_path = Cow::from(path.as_ref());
90            if abs_path.is_relative() {
91                abs_path = std::env::current_dir()
92                    .map_err(Error::InvalidCwd)?
93                    .join(&path)
94                    .into()
95            }
96            let mut path = abs_path.as_ref();
97            let mut vec = Vec::new();
98            while let Some(dir) = path.parent() {
99                if let Ok(file) = ConfigFile::open(dir.join(filename)) {
100                    let should_break = file.reader.is_root;
101                    vec.push(file);
102                    if should_break {
103                        break;
104                    }
105                }
106                path = dir;
107            }
108            vec
109        } else {
110            // TODO: Better errors.
111            vec![ConfigFile::open(filename).map_err(Error::Parse)?]
112        }))
113    }
114
115    /// Returns an iterator over the contained [`ConfigFiles`].
116    pub fn iter(&self) -> impl Iterator<Item = &ConfigFile> {
117        self.0.iter().rev()
118    }
119
120    // To maintain the invariant that these files have not had any sections read,
121    // there is no `iter_mut` method.
122}
123
124impl Iterator for ConfigFiles {
125    type Item = ConfigFile;
126    fn next(&mut self) -> Option<ConfigFile> {
127        self.0.pop()
128    }
129}
130
131impl std::iter::FusedIterator for ConfigFiles {}
132
133impl PropertiesSource for ConfigFiles {
134    /// Adds properties from the files' sections to the specified [`Properties`] map.
135    ///
136    /// Ignores the files' paths when determining applicability.
137    fn apply_to(self, props: &mut Properties, path: impl AsRef<Path>) -> Result<(), crate::Error> {
138        let path = path.as_ref();
139        for mut file in self {
140            file.apply_to(props, path)?;
141        }
142        Ok(())
143    }
144}