1 use crate::error::{ParseError, Reason};
2 
3 /// A single token in a cfg expression
4 /// <https://doc.rust-lang.org/reference/conditional-compilation.html>
5 #[derive(Clone, Debug, PartialEq, Eq)]
6 pub enum Token<'a> {
7     /// A single contiguous term
8     Key(&'a str),
9     /// A single continguous value, without its surrounding quotes
10     Value(&'a str),
11     /// A '=', joining a key and a value
12     Equals,
13     /// Beginning of an all() predicate list
14     All,
15     /// Beginning of an any() predicate list
16     Any,
17     /// Beginning of a not() predicate
18     Not,
19     /// A `(` for starting a predicate list
20     OpenParen,
21     /// A `)` for ending a predicate list
22     CloseParen,
23     /// A `,` for separating predicates in a predicate list
24     Comma,
25 }
26 
27 impl<'a> std::fmt::Display for Token<'a> {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result28     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29         std::fmt::Debug::fmt(self, f)
30     }
31 }
32 
33 impl<'a> Token<'a> {
len(&self) -> usize34     fn len(&self) -> usize {
35         match self {
36             Token::Key(s) => s.len(),
37             Token::Value(s) => s.len() + 2,
38             Token::Equals | Token::OpenParen | Token::CloseParen | Token::Comma => 1,
39             Token::All | Token::Any | Token::Not => 3,
40         }
41     }
42 }
43 
44 /// Allows iteration through a cfg expression, yielding
45 /// a token or a `ParseError`.
46 ///
47 /// Prefer to use `Expression::parse` rather than directly
48 /// using the lexer
49 pub struct Lexer<'a> {
50     pub(super) inner: &'a str,
51     original: &'a str,
52     offset: usize,
53 }
54 
55 impl<'a> Lexer<'a> {
56     /// Creates a Lexer over a cfg expression, it can either be
57     /// a raw expression eg `key` or in attribute form, eg `cfg(key)`
new(text: &'a str) -> Self58     pub fn new(text: &'a str) -> Self {
59         let text = if text.starts_with("cfg(") && text.ends_with(')') {
60             &text[4..text.len() - 1]
61         } else {
62             text
63         };
64 
65         Self {
66             inner: text,
67             original: text,
68             offset: 0,
69         }
70     }
71 }
72 
73 /// A wrapper around a particular token that includes the span of the characters
74 /// in the original string, for diagnostic purposes
75 #[derive(Debug)]
76 pub struct LexerToken<'a> {
77     /// The token that was lexed
78     pub token: Token<'a>,
79     /// The range of the token characters in the original license expression
80     pub span: std::ops::Range<usize>,
81 }
82 
83 impl<'a> Iterator for Lexer<'a> {
84     type Item = Result<LexerToken<'a>, ParseError>;
85 
next(&mut self) -> Option<Self::Item>86     fn next(&mut self) -> Option<Self::Item> {
87         // Jump over any whitespace, updating `self.inner` and `self.offset` appropriately
88         let non_whitespace_index = match self.inner.find(|c: char| !c.is_whitespace()) {
89             Some(idx) => idx,
90             None => self.inner.len(),
91         };
92 
93         self.inner = &self.inner[non_whitespace_index..];
94         self.offset += non_whitespace_index;
95 
96         #[inline]
97         fn is_ident_start(ch: char) -> bool {
98             ch == '_' || (('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch))
99         }
100 
101         #[inline]
102         fn is_ident_rest(ch: char) -> bool {
103             is_ident_start(ch) || ('0'..='9').contains(&ch)
104         }
105 
106         match self.inner.chars().next() {
107             None => None,
108             Some('=') => Some(Ok(Token::Equals)),
109             Some('(') => Some(Ok(Token::OpenParen)),
110             Some(')') => Some(Ok(Token::CloseParen)),
111             Some(',') => Some(Ok(Token::Comma)),
112             Some(c) => {
113                 if c == '"' {
114                     match self.inner[1..].find('"') {
115                         Some(ind) => Some(Ok(Token::Value(&self.inner[1..=ind]))),
116                         None => Some(Err(ParseError {
117                             original: self.original.to_owned(),
118                             span: self.offset..self.original.len(),
119                             reason: Reason::UnclosedQuotes,
120                         })),
121                     }
122                 } else if is_ident_start(c) {
123                     let substr = match self.inner[1..].find(|c: char| !is_ident_rest(c)) {
124                         Some(ind) => &self.inner[..=ind],
125                         None => self.inner,
126                     };
127 
128                     match substr {
129                         "all" => Some(Ok(Token::All)),
130                         "any" => Some(Ok(Token::Any)),
131                         "not" => Some(Ok(Token::Not)),
132                         other => Some(Ok(Token::Key(other))),
133                     }
134                 } else {
135                     // clippy tries to help here, but we need
136                     // a Range here, not a RangeInclusive<>
137                     #[allow(clippy::range_plus_one)]
138                     Some(Err(ParseError {
139                         original: self.original.to_owned(),
140                         span: self.offset..self.offset + 1,
141                         reason: Reason::Unexpected(&["<key>", "all", "any", "not"]),
142                     }))
143                 }
144             }
145         }
146         .map(|tok| {
147             tok.map(|tok| {
148                 let len = tok.len();
149 
150                 let start = self.offset;
151                 self.inner = &self.inner[len..];
152                 self.offset += len;
153 
154                 LexerToken {
155                     token: tok,
156                     span: start..self.offset,
157                 }
158             })
159         })
160     }
161 }
162