browserslist/config/
parser.rs

1use super::PartialConfig;
2use crate::error::Error;
3use ahash::AHashSet;
4
5pub(crate) fn parse<S: AsRef<str>>(
6    source: &str,
7    env: S,
8    throw_on_missing: bool,
9) -> Result<PartialConfig, Error> {
10    let env = env.as_ref();
11    let mut encountered_sections = AHashSet::new();
12    let mut current_section = Some("defaults");
13
14    let config = source
15        .lines()
16        .map(|line| {
17            if let Some(index) = line.find('#') {
18                &line[..index]
19            } else {
20                line
21            }
22        })
23        .map(|line| line.trim())
24        .filter(|line| !line.is_empty())
25        .try_fold(
26            (Vec::new(), Option::<Vec<String>>::None),
27            |(mut defaults_queries, mut env_queries), line| {
28                if line.starts_with('[') && line.ends_with(']') {
29                    let sections = line
30                        .trim()
31                        .trim_start_matches('[')
32                        .trim_end_matches(']')
33                        .split(' ')
34                        .filter(|env| !env.is_empty())
35                        .collect::<Vec<_>>();
36                    current_section = sections.iter().find(|section| **section == env).copied();
37                    for section in sections {
38                        if encountered_sections.contains(section) {
39                            return Err(Error::DuplicatedSection(section.to_string()));
40                        } else {
41                            encountered_sections.insert(section);
42                        }
43                    }
44                    Ok((
45                        defaults_queries,
46                        if env_queries.is_some() {
47                            // we've collected queries of current env, so return it as-is
48                            env_queries
49                        } else if encountered_sections.contains(env) {
50                            // get ready for collecting queries of current env
51                            Some(vec![])
52                        } else {
53                            None
54                        },
55                    ))
56                } else {
57                    if current_section.is_some() {
58                        // if env queries are prepared, we should add queries to them, not the "defaults"
59                        if let Some(env_queries) = env_queries.as_mut() {
60                            env_queries.push(line.to_string());
61                        } else {
62                            defaults_queries.push(line.to_string());
63                        }
64                    }
65                    Ok((defaults_queries, env_queries))
66                }
67            },
68        )
69        .map(|(defaults, env)| PartialConfig { defaults, env });
70
71    if throw_on_missing && env != "defaults" && !encountered_sections.contains(env) {
72        Err(Error::MissingEnv(env.to_string()))
73    } else {
74        config
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn empty() {
84        let source = "  \t  \n  \r\n  # comment ";
85        let config = parse(source, "production", false).unwrap();
86        assert!(config.defaults.is_empty());
87        assert!(config.env.is_none());
88    }
89
90    #[test]
91    fn no_sections() {
92        let source = r"
93last 2 versions
94not dead
95";
96        let config = parse(source, "production", false).unwrap();
97        assert_eq!(&*config.defaults, ["last 2 versions", "not dead"]);
98        assert!(config.env.is_none());
99    }
100
101    #[test]
102    fn single_line() {
103        let source = r"last 2 versions, not dead";
104        let config = parse(source, "production", false).unwrap();
105        assert_eq!(&*config.defaults, ["last 2 versions, not dead"]);
106        assert!(config.env.is_none());
107    }
108
109    #[test]
110    fn empty_lines() {
111        let source = r"
112last 2 versions
113
114
115not dead
116";
117        let config = parse(source, "production", false).unwrap();
118        assert_eq!(&*config.defaults, ["last 2 versions", "not dead"]);
119        assert!(config.env.is_none());
120    }
121
122    #[test]
123    fn comments() {
124        let source = r"
125last 2 versions  #trailing comment
126#line comment
127not dead
128";
129        let config = parse(source, "production", false).unwrap();
130        assert_eq!(&*config.defaults, ["last 2 versions", "not dead"]);
131        assert!(config.env.is_none());
132    }
133
134    #[test]
135    fn spaces() {
136        let source = "    last 2 versions     \n  not dead    ";
137        let config = parse(source, "production", false).unwrap();
138        assert_eq!(&*config.defaults, ["last 2 versions", "not dead"]);
139        assert!(config.env.is_none());
140    }
141
142    #[test]
143    fn one_section() {
144        let source = r"
145[production]
146last 2 versions
147not dead
148";
149        let config = parse(source, "production", false).unwrap();
150        assert!(config.defaults.is_empty());
151        assert_eq!(
152            config.env.as_deref().unwrap(),
153            ["last 2 versions", "not dead"]
154        );
155    }
156
157    #[test]
158    fn defaults_and_env_mixed() {
159        let source = r"
160> 1%
161
162[production]
163last 2 versions
164not dead
165";
166        let config = parse(source, "production", false).unwrap();
167        assert_eq!(&*config.defaults, ["> 1%"]);
168        assert_eq!(
169            config.env.as_deref().unwrap(),
170            ["last 2 versions", "not dead"]
171        );
172    }
173
174    #[test]
175    fn multi_sections() {
176        let source = r"
177[production]
178> 1%
179ie 10
180
181[  modern]
182last 1 chrome version
183last 1 firefox version
184
185[ssr  ]
186node 12
187";
188        let config = parse(source, "production", false).unwrap();
189        assert!(config.defaults.is_empty());
190        assert_eq!(config.env.as_deref().unwrap(), ["> 1%", "ie 10"]);
191
192        let config = parse(source, "modern", false).unwrap();
193        assert!(config.defaults.is_empty());
194        assert_eq!(
195            config.env.as_deref().unwrap(),
196            ["last 1 chrome version", "last 1 firefox version"]
197        );
198
199        let config = parse(source, "ssr", false).unwrap();
200        assert!(config.defaults.is_empty());
201        assert_eq!(config.env.as_deref().unwrap(), ["node 12"]);
202    }
203
204    #[test]
205    fn shared_multi_sections() {
206        let source = r"
207[production   development]
208> 1%
209ie 10
210";
211        let config = parse(source, "development", false).unwrap();
212        assert!(config.defaults.is_empty());
213        assert_eq!(config.env.as_deref().unwrap(), ["> 1%", "ie 10"]);
214    }
215
216    #[test]
217    fn duplicated_sections() {
218        let source = r"
219[production production]
220> 1%
221ie 10
222";
223        assert_eq!(
224            parse(source, "testing", false),
225            Err(Error::DuplicatedSection("production".into()))
226        );
227
228        let source = r"
229[development]
230last 1 chrome version
231
232[production]
233> 1 %
234not dead
235
236[development]
237last 1 firefox version
238";
239        assert_eq!(
240            parse(source, "testing", false),
241            Err(Error::DuplicatedSection("development".into()))
242        );
243    }
244
245    #[test]
246    fn mismatch_section() {
247        let source = r"
248[production]
249> 1%
250ie 10
251";
252        let config = parse(source, "development", false).unwrap();
253        assert!(config.defaults.is_empty());
254        assert!(config.env.is_none());
255    }
256
257    #[test]
258    fn throw_on_missing_env() {
259        let source = "node 16";
260        let err = parse(source, "SSR", true).unwrap_err();
261        assert_eq!(err, Error::MissingEnv("SSR".into()));
262    }
263
264    #[test]
265    fn dont_throw_if_existed() {
266        let source = r"
267[production]
268> 1%
269ie 10
270";
271        let config = parse(source, "production", true).unwrap();
272        assert!(config.defaults.is_empty());
273        assert!(config.env.is_some());
274    }
275
276    #[test]
277    fn dont_throw_for_defaults() {
278        let source = r"
279[production]
280> 1%
281ie 10
282";
283        let config = parse(source, "defaults", true).unwrap();
284        assert!(config.defaults.is_empty());
285        assert!(config.env.is_none());
286    }
287}