1use nom::{
2 branch::alt,
3 bytes::complete::{tag, tag_no_case, take_while1, take_while_m_n},
4 character::complete::{anychar, char, i32, one_of, space0, space1, u16, u32},
5 combinator::{all_consuming, consumed, map, opt, recognize, value, verify},
6 multi::{many0, many_till},
7 number::complete::{double, float},
8 sequence::{delimited, pair, preceded, separated_pair, terminated, tuple},
9 IResult,
10};
11
12type PResult<'a, Output> = IResult<&'a str, Output>;
13
14#[derive(Debug, Clone)]
15pub enum QueryAtom<'a> {
16 Last {
17 count: u16,
18 major: bool,
19 name: Option<&'a str>,
20 },
21 Unreleased(Option<&'a str>),
22 Years(f64),
23 Since {
24 year: i32,
25 month: u32,
26 day: u32,
27 },
28 Percentage {
29 comparator: Comparator,
30 popularity: f32,
31 stats: Stats<'a>,
32 },
33 Cover {
34 coverage: f32,
35 stats: Stats<'a>,
36 },
37 Supports(&'a str, Option<SupportKind>),
38 Electron(VersionRange<'a>),
39 Node(VersionRange<'a>),
40 Browser(&'a str, VersionRange<'a>),
41 FirefoxESR,
42 OperaMini,
43 CurrentNode,
44 MaintainedNode,
45 Phantom(bool),
46 BrowserslistConfig,
47 Defaults,
48 Dead,
49 Extends(&'a str),
50 Unknown(&'a str), }
52
53#[derive(Debug, Clone)]
54pub enum Stats<'a> {
55 Global,
56 Region(&'a str),
57}
58
59#[derive(Debug, Clone)]
60pub enum SupportKind {
61 Fully,
62 Partially,
63}
64
65fn parse_version_keyword(input: &str) -> PResult<&str> {
66 terminated(tag_no_case("version"), opt(char('s')))(input)
67}
68
69fn parse_last(input: &str) -> PResult<QueryAtom> {
70 map(
71 tuple((
72 terminated(tag_no_case("last"), space1),
73 terminated(u16, space1),
74 opt(terminated(
75 verify(
76 take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'),
77 |s: &str| {
78 !s.eq_ignore_ascii_case("version")
79 && !s.eq_ignore_ascii_case("versions")
80 && !s.eq_ignore_ascii_case("major")
81 },
82 ),
83 space1,
84 )),
85 opt(terminated(tag_no_case("major"), space1)),
86 parse_version_keyword,
87 )),
88 |(_, count, name, major, _)| {
89 if matches!(name, Some(name) if name.eq_ignore_ascii_case("major")) && major.is_none() {
90 QueryAtom::Last {
91 count,
92 major: true,
93 name: None,
94 }
95 } else {
96 QueryAtom::Last {
97 count,
98 major: major.is_some(),
99 name,
100 }
101 }
102 },
103 )(input)
104}
105
106fn parse_unreleased(input: &str) -> PResult<QueryAtom> {
107 map(
108 delimited(
109 terminated(tag_no_case("unreleased"), space1),
110 opt(terminated(
111 take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'),
112 space1,
113 )),
114 parse_version_keyword,
115 ),
116 QueryAtom::Unreleased,
117 )(input)
118}
119
120fn parse_years(input: &str) -> PResult<QueryAtom> {
121 map(
122 delimited(
123 terminated(tag_no_case("last"), space1),
124 terminated(double, space1),
125 terminated(tag_no_case("year"), opt(char('s'))),
126 ),
127 QueryAtom::Years,
128 )(input)
129}
130
131fn parse_since(input: &str) -> PResult<QueryAtom> {
132 map(
133 tuple((
134 terminated(tag_no_case("since"), one_of(" \t")),
135 i32,
136 opt(preceded(char('-'), u32)),
137 opt(preceded(char('-'), u32)),
138 )),
139 |(_, year, month, day)| QueryAtom::Since {
140 year,
141 month: month.unwrap_or(1),
142 day: day.unwrap_or(1),
143 },
144 )(input)
145}
146
147#[derive(Debug, Clone)]
148pub enum Comparator {
149 Less,
150 LessOrEqual,
151 Greater,
152 GreaterOrEqual,
153}
154
155fn parse_compare_operator(input: &str) -> PResult<Comparator> {
156 map(
157 tuple((alt((char('<'), char('>'))), opt(char('=')))),
158 |(relation, equals)| match relation {
159 '<' if equals.is_some() => Comparator::LessOrEqual,
160 '<' => Comparator::Less,
161 '>' if equals.is_some() => Comparator::GreaterOrEqual,
162 _ => Comparator::Greater,
163 },
164 )(input)
165}
166
167fn parse_region(input: &str) -> PResult<Stats> {
168 map(
169 recognize(preceded(
170 opt(tag_no_case("alt-")),
171 take_while_m_n(2, 2, char::is_alphabetic),
172 )),
173 Stats::Region,
174 )(input)
175}
176
177fn parse_percentage(input: &str) -> PResult<QueryAtom> {
178 map(
179 tuple((
180 terminated(parse_compare_operator, space0),
181 terminated(float, char('%')),
182 opt(preceded(
183 tuple((space1, tag_no_case("in"), space1)),
184 parse_region,
185 )),
186 )),
187 |(comparator, value, stats)| QueryAtom::Percentage {
188 comparator,
189 popularity: value,
190 stats: stats.unwrap_or(Stats::Global),
191 },
192 )(input)
193}
194
195fn parse_cover(input: &str) -> PResult<QueryAtom> {
196 map(
197 tuple((
198 preceded(
199 terminated(tag_no_case("cover"), space1),
200 terminated(float, char('%')),
201 ),
202 opt(preceded(
203 tuple((space1, tag_no_case("in"), space1)),
204 parse_region,
205 )),
206 )),
207 |(value, stats)| QueryAtom::Cover {
208 coverage: value,
209 stats: stats.unwrap_or(Stats::Global),
210 },
211 )(input)
212}
213
214fn parse_supports(input: &str) -> PResult<QueryAtom> {
215 map(
216 separated_pair(
217 opt(terminated(
218 alt((
219 value(SupportKind::Fully, tag_no_case("fully")),
220 value(SupportKind::Partially, tag_no_case("partially")),
221 )),
222 space1,
223 )),
224 terminated(tag_no_case("supports"), space1),
225 take_while1(|c: char| c.is_alphanumeric() || c == '-'),
226 ),
227 |(kind, name)| QueryAtom::Supports(name, kind),
228 )(input)
229}
230
231#[derive(Debug, Clone)]
232pub enum VersionRange<'a> {
233 Bounded(&'a str, &'a str),
234 Unbounded(Comparator, &'a str),
235 Accurate(&'a str),
236}
237
238fn parse_version(input: &str) -> PResult<&str> {
239 take_while1(|c: char| c.is_ascii_digit() || c == '.')(input)
240}
241
242fn parse_version_range(input: &str) -> PResult<VersionRange> {
243 alt((
244 map(
245 preceded(
246 space1,
247 separated_pair(
248 parse_version,
249 delimited(space0, char('-'), space0),
250 parse_version,
251 ),
252 ),
253 |(from, to)| VersionRange::Bounded(from, to),
254 ),
255 map(
256 preceded(
257 space0,
258 separated_pair(parse_compare_operator, space0, parse_version),
259 ),
260 |(comparator, version)| VersionRange::Unbounded(comparator, version),
261 ),
262 map(preceded(space1, parse_version), VersionRange::Accurate),
263 ))(input)
264}
265
266fn parse_electron(input: &str) -> PResult<QueryAtom> {
267 map(
268 preceded(tag_no_case("electron"), parse_version_range),
269 QueryAtom::Electron,
270 )(input)
271}
272
273fn parse_node(input: &str) -> PResult<QueryAtom> {
274 map(
275 preceded(tag_no_case("node"), parse_version_range),
276 QueryAtom::Node,
277 )(input)
278}
279
280fn parse_browser(input: &str) -> PResult<QueryAtom> {
281 map(
282 pair(
283 take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'),
284 alt((
285 parse_version_range,
286 map(preceded(space1, tag_no_case("tp")), VersionRange::Accurate),
287 )),
288 ),
289 |(name, version)| QueryAtom::Browser(name, version),
290 )(input)
291}
292
293fn parse_firefox_esr(input: &str) -> PResult<QueryAtom> {
294 value(
295 QueryAtom::FirefoxESR,
296 tuple((
297 alt((tag_no_case("firefox"), tag_no_case("fx"), tag_no_case("ff"))),
298 space1,
299 tag_no_case("esr"),
300 )),
301 )(input)
302}
303
304fn parse_opera_mini(input: &str) -> PResult<QueryAtom> {
305 value(
306 QueryAtom::OperaMini,
307 tuple((
308 alt((tag_no_case("operamini"), tag_no_case("op_mini"))),
309 space1,
310 tag_no_case("all"),
311 )),
312 )(input)
313}
314
315fn parse_current_node(input: &str) -> PResult<QueryAtom> {
316 value(
317 QueryAtom::CurrentNode,
318 tuple((tag_no_case("current"), space1, tag_no_case("node"))),
319 )(input)
320}
321
322fn parse_maintained_node(input: &str) -> PResult<QueryAtom> {
323 value(
324 QueryAtom::MaintainedNode,
325 tuple((
326 tag_no_case("maintained"),
327 space1,
328 tag_no_case("node"),
329 space1,
330 tag_no_case("versions"),
331 )),
332 )(input)
333}
334
335fn parse_phantom(input: &str) -> PResult<QueryAtom> {
336 map(
337 preceded(
338 terminated(tag_no_case("phantomjs"), space1),
339 alt((tag("1.9"), tag("2.1"))),
340 ),
341 |version| QueryAtom::Phantom(version == "2.1"),
342 )(input)
343}
344
345fn parse_browserslist_config(input: &str) -> PResult<QueryAtom> {
346 value(
347 QueryAtom::BrowserslistConfig,
348 tag_no_case("browserslist config"),
349 )(input)
350}
351
352fn parse_defaults(input: &str) -> PResult<QueryAtom> {
353 value(QueryAtom::Defaults, tag_no_case("defaults"))(input)
354}
355
356fn parse_dead(input: &str) -> PResult<QueryAtom> {
357 value(QueryAtom::Dead, tag_no_case("dead"))(input)
358}
359
360fn parse_extends(input: &str) -> PResult<QueryAtom> {
361 map(
362 preceded(
363 terminated(tag_no_case("extends"), space1),
364 take_while1(|c: char| {
365 c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
366 }),
367 ),
368 QueryAtom::Extends,
369 )(input)
370}
371
372fn parse_unknown(input: &str) -> PResult<QueryAtom> {
373 map(
374 recognize(many_till(anychar, parse_composition_operator)),
375 QueryAtom::Unknown,
376 )(input)
377}
378
379fn parse_query_atom(input: &str) -> PResult<QueryAtom> {
380 alt((
381 parse_last,
382 parse_unreleased,
383 parse_years,
384 parse_since,
385 parse_percentage,
386 parse_cover,
387 parse_supports,
388 parse_electron,
389 parse_node,
390 parse_firefox_esr,
391 parse_opera_mini,
392 parse_current_node,
393 parse_maintained_node,
394 parse_phantom,
395 parse_browser,
396 parse_browserslist_config,
397 parse_defaults,
398 parse_dead,
399 parse_extends,
400 parse_unknown,
401 ))(input)
402}
403
404#[derive(Debug)]
405pub(crate) struct SingleQuery<'a> {
406 pub(crate) raw: &'a str,
407 pub(crate) atom: QueryAtom<'a>,
408 pub(crate) negated: bool,
409 pub(crate) is_and: bool,
410}
411
412fn parse_and(input: &str) -> PResult<bool> {
413 value(true, delimited(space1, tag_no_case("and"), space1))(input)
414}
415
416fn parse_or(input: &str) -> PResult<bool> {
417 alt((
418 value(false, delimited(space0, char(','), space0)),
419 value(false, delimited(space1, tag_no_case("or"), space1)),
420 ))(input)
421}
422
423fn parse_composition_operator(input: &str) -> PResult<bool> {
424 alt((parse_and, parse_or))(input)
425}
426
427fn parse_single_query(input: &str) -> PResult<SingleQuery> {
428 map(
429 tuple((
430 parse_composition_operator,
431 consumed(pair(
432 opt(terminated(tag_no_case("not"), space1)),
433 parse_query_atom,
434 )),
435 )),
436 |(is_and, (raw, (negated, atom)))| SingleQuery {
437 raw,
438 atom,
439 negated: negated.is_some(),
440 is_and,
441 },
442 )(input)
443}
444
445pub(crate) fn parse_browserslist_query(input: &str) -> PResult<Vec<SingleQuery>> {
446 let input = input.trim();
447 if input.is_empty() {
449 return Ok(("", vec![]));
450 }
451
452 map(
453 all_consuming(tuple((
454 consumed(pair(
455 opt(terminated(tag_no_case("not"), space1)),
457 parse_query_atom,
458 )),
459 many0(parse_single_query),
460 ))),
461 |((first_raw, (negated, first)), mut queries)| {
462 queries.insert(
463 0,
464 SingleQuery {
465 raw: first_raw,
466 atom: first,
467 negated: negated.is_some(),
468 is_and: false,
469 },
470 );
471 queries
472 },
473 )(input)
474}
475
476pub(crate) fn parse_electron_version(version: &str) -> Result<f32, crate::error::Error> {
477 all_consuming(terminated(float, opt(pair(char('.'), u16))))(version)
478 .map(|(_, v)| v)
479 .map_err(|_: nom::Err<nom::error::Error<_>>| {
480 crate::error::Error::UnknownElectronVersion(version.to_string())
481 })
482}
483
484#[cfg(test)]
485mod tests {
486 use crate::{opts::Opts, test::run_compare};
487 use test_case::test_case;
488
489 #[test_case(""; "empty")]
490 #[test_case("ie >= 6, ie <= 7"; "comma")]
491 #[test_case("ie >= 6 and ie <= 7"; "and")]
492 #[test_case("ie < 11 and not ie 7"; "and with not")]
493 #[test_case("last 1 Baidu version and not <2%"; "with not and one-version browsers as and query")]
494 #[test_case("ie >= 6 or ie <= 7"; "or")]
495 #[test_case("ie < 11 or not ie 7"; "or with not")]
496 #[test_case("last 2 versions and > 1%"; "swc issue 4871")]
497 fn valid(query: &str) {
498 run_compare(query, &Opts::default(), None);
499 }
500}