blob: 60abc6b60b9159c074e87d0518fb786058bcf0c9 [file] [log] [blame]
use crate::error::{ParseError, Reason};
/// A single token in a cfg expression
/// <https://doc.rust-lang.org/reference/conditional-compilation.html>
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Token<'a> {
/// A single contiguous term
Key(&'a str),
/// A single contiguous value, without its surrounding quotes
Value(&'a str),
/// A '=', joining a key and a value
Equals,
/// Beginning of an `all()` predicate list
All,
/// Beginning of an `any()` predicate list
Any,
/// Beginning of a `not()` predicate
Not,
/// A `(` for starting a predicate list
OpenParen,
/// A `)` for ending a predicate list
CloseParen,
/// A `,` for separating predicates in a predicate list
Comma,
}
impl std::fmt::Display for Token<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
impl Token<'_> {
#[inline]
fn len(&self) -> usize {
match self {
Token::Key(s) => s.len(),
Token::Value(s) => s.len() + 2,
Token::Equals | Token::OpenParen | Token::CloseParen | Token::Comma => 1,
Token::All | Token::Any | Token::Not => 3,
}
}
}
/// Allows iteration through a cfg expression, yielding
/// a token or a `ParseError`.
///
/// Prefer to use `Expression::parse` rather than directly
/// using the lexer
pub struct Lexer<'a> {
pub(super) inner: &'a str,
original: &'a str,
offset: usize,
}
impl<'a> Lexer<'a> {
/// Creates a Lexer over a cfg expression, it can either be
/// a raw expression eg `key` or in attribute form, eg `cfg(key)`
pub fn new(text: &'a str) -> Self {
let text = if text.starts_with("cfg(") && text.ends_with(')') {
&text[4..text.len() - 1]
} else {
text
};
Self {
inner: text,
original: text,
offset: 0,
}
}
}
/// A wrapper around a particular token that includes the span of the characters
/// in the original string, for diagnostic purposes
#[derive(Debug)]
pub struct LexerToken<'a> {
/// The token that was lexed
pub token: Token<'a>,
/// The range of the token characters in the original license expression
pub span: std::ops::Range<usize>,
}
impl<'a> Iterator for Lexer<'a> {
type Item = Result<LexerToken<'a>, ParseError>;
fn next(&mut self) -> Option<Self::Item> {
// Jump over any whitespace, updating `self.inner` and `self.offset` appropriately
let non_whitespace_index = match self.inner.find(|c: char| !c.is_whitespace()) {
Some(idx) => idx,
None => self.inner.len(),
};
self.inner = &self.inner[non_whitespace_index..];
self.offset += non_whitespace_index;
#[inline]
fn is_ident_start(ch: char) -> bool {
ch == '_' || ch.is_ascii_lowercase() || ch.is_ascii_uppercase()
}
#[inline]
fn is_ident_rest(ch: char) -> bool {
is_ident_start(ch) || ch.is_ascii_digit()
}
match self.inner.chars().next() {
None => None,
Some('=') => Some(Ok(Token::Equals)),
Some('(') => Some(Ok(Token::OpenParen)),
Some(')') => Some(Ok(Token::CloseParen)),
Some(',') => Some(Ok(Token::Comma)),
Some(c) => {
if c == '"' {
match self.inner[1..].find('"') {
Some(ind) => Some(Ok(Token::Value(&self.inner[1..=ind]))),
None => Some(Err(ParseError {
original: self.original.to_owned(),
span: self.offset..self.original.len(),
reason: Reason::UnclosedQuotes,
})),
}
} else if is_ident_start(c) {
let substr = match self.inner[1..].find(|c: char| !is_ident_rest(c)) {
Some(ind) => &self.inner[..=ind],
None => self.inner,
};
match substr {
"all" => Some(Ok(Token::All)),
"any" => Some(Ok(Token::Any)),
"not" => Some(Ok(Token::Not)),
other => Some(Ok(Token::Key(other))),
}
} else {
// clippy tries to help here, but we need
// a Range here, not a RangeInclusive<>
#[allow(clippy::range_plus_one)]
Some(Err(ParseError {
original: self.original.to_owned(),
span: self.offset..self.offset + 1,
reason: Reason::Unexpected(&["<key>", "all", "any", "not"]),
}))
}
}
}
.map(|tok| {
tok.map(|tok| {
let len = tok.len();
let start = self.offset;
self.inner = &self.inner[len..];
self.offset += len;
LexerToken {
token: tok,
span: start..self.offset,
}
})
})
}
}