browserslist/config/
parser.rs1use 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 env_queries
49 } else if encountered_sections.contains(env) {
50 Some(vec![])
52 } else {
53 None
54 },
55 ))
56 } else {
57 if current_section.is_some() {
58 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}