Skip to main content

r_description/
relations.rs

1//! Parsing of R DESCRIPTION relations strings.
2use crate::version::Version;
3use std::borrow::Cow;
4use std::iter::Peekable;
5use std::str::Chars;
6
7/// Constraint on a package version.
8///
9/// The operators are those documented in Writing R Extensions: `<`, `<=`,
10/// `==`, `!=`, `>=`, `>`.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub enum VersionConstraint {
13    /// <
14    LessThan,
15    /// <=
16    LessThanEqual,
17    /// ==
18    Equal,
19    /// !=
20    NotEqual,
21    /// >
22    GreaterThan,
23    /// >=
24    GreaterThanEqual,
25}
26
27impl std::str::FromStr for VersionConstraint {
28    type Err = String;
29
30    fn from_str(s: &str) -> Result<Self, Self::Err> {
31        match s {
32            ">=" => Ok(VersionConstraint::GreaterThanEqual),
33            "<=" => Ok(VersionConstraint::LessThanEqual),
34            "==" => Ok(VersionConstraint::Equal),
35            "!=" => Ok(VersionConstraint::NotEqual),
36            ">" => Ok(VersionConstraint::GreaterThan),
37            "<" => Ok(VersionConstraint::LessThan),
38            _ => Err(format!("Invalid version constraint: {s}")),
39        }
40    }
41}
42
43impl std::fmt::Display for VersionConstraint {
44    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
45        match self {
46            VersionConstraint::GreaterThanEqual => f.write_str(">="),
47            VersionConstraint::LessThanEqual => f.write_str("<="),
48            VersionConstraint::Equal => f.write_str("=="),
49            VersionConstraint::NotEqual => f.write_str("!="),
50            VersionConstraint::GreaterThan => f.write_str(">"),
51            VersionConstraint::LessThan => f.write_str("<"),
52        }
53    }
54}
55
56/// Let's start with defining all kinds of tokens and
57/// composite nodes.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
59#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
60#[repr(u16)]
61#[allow(missing_docs)]
62pub(crate) enum SyntaxKind {
63    IDENT = 0,  // package name
64    COMMA,      // ,
65    L_PARENS,   // (
66    R_PARENS,   // )
67    L_ANGLE,    // <
68    R_ANGLE,    // >
69    EQUAL,      // =
70    NOT,        // !
71    WHITESPACE, // whitespace
72    NEWLINE,    // newline
73    ERROR,      // as well as errors
74
75    // composite nodes
76    ROOT,       // The entire file
77    RELATION,   // An alternative in a dependency
78    VERSION,    // A version constraint
79    CONSTRAINT, // (">=", "<=", "==", "!=", ">", "<")
80}
81
82/// Convert our `SyntaxKind` into the rowan `SyntaxKind`.
83impl From<SyntaxKind> for rowan::SyntaxKind {
84    fn from(kind: SyntaxKind) -> Self {
85        Self(kind as u16)
86    }
87}
88
89/// A lexer for relations strings.
90pub(crate) struct Lexer<'a> {
91    input: Peekable<Chars<'a>>,
92}
93
94impl<'a> Lexer<'a> {
95    /// Create a new lexer for the given input.
96    pub fn new(input: &'a str) -> Self {
97        Lexer {
98            input: input.chars().peekable(),
99        }
100    }
101
102    fn is_whitespace(c: char) -> bool {
103        c == ' ' || c == '\t' || c == '\r'
104    }
105
106    fn is_valid_ident_char(c: char) -> bool {
107        c.is_ascii_alphanumeric() || c == '-' || c == '.'
108    }
109
110    fn read_while<F>(&mut self, predicate: F) -> String
111    where
112        F: Fn(char) -> bool,
113    {
114        let mut result = String::new();
115        while let Some(&c) = self.input.peek() {
116            if predicate(c) {
117                result.push(c);
118                self.input.next();
119            } else {
120                break;
121            }
122        }
123        result
124    }
125
126    fn next_token(&mut self) -> Option<(SyntaxKind, String)> {
127        if let Some(&c) = self.input.peek() {
128            match c {
129                ',' => {
130                    self.input.next();
131                    Some((SyntaxKind::COMMA, ",".to_owned()))
132                }
133                '(' => {
134                    self.input.next();
135                    Some((SyntaxKind::L_PARENS, "(".to_owned()))
136                }
137                ')' => {
138                    self.input.next();
139                    Some((SyntaxKind::R_PARENS, ")".to_owned()))
140                }
141                '<' => {
142                    self.input.next();
143                    Some((SyntaxKind::L_ANGLE, "<".to_owned()))
144                }
145                '>' => {
146                    self.input.next();
147                    Some((SyntaxKind::R_ANGLE, ">".to_owned()))
148                }
149                '=' => {
150                    self.input.next();
151                    Some((SyntaxKind::EQUAL, "=".to_owned()))
152                }
153                '!' => {
154                    self.input.next();
155                    Some((SyntaxKind::NOT, "!".to_owned()))
156                }
157                '\n' => {
158                    self.input.next();
159                    Some((SyntaxKind::NEWLINE, "\n".to_owned()))
160                }
161                _ if Self::is_whitespace(c) => {
162                    let whitespace = self.read_while(Self::is_whitespace);
163                    Some((SyntaxKind::WHITESPACE, whitespace))
164                }
165                // TODO: separate handling for package names and versions?
166                _ if Self::is_valid_ident_char(c) => {
167                    let key = self.read_while(Self::is_valid_ident_char);
168                    Some((SyntaxKind::IDENT, key))
169                }
170                _ => {
171                    self.input.next();
172                    Some((SyntaxKind::ERROR, c.to_string()))
173                }
174            }
175        } else {
176            None
177        }
178    }
179}
180
181impl Iterator for Lexer<'_> {
182    type Item = (SyntaxKind, String);
183
184    fn next(&mut self) -> Option<Self::Item> {
185        self.next_token()
186    }
187}
188
189pub(crate) fn lex(input: &str) -> Vec<(SyntaxKind, String)> {
190    let mut lexer = Lexer::new(input);
191    lexer.by_ref().collect::<Vec<_>>()
192}
193
194/// A trait for looking up versions of packages.
195pub trait VersionLookup {
196    /// Look up the version of a package.
197    fn lookup_version<'a>(&'a self, package: &'_ str) -> Option<std::borrow::Cow<'a, Version>>;
198}
199
200impl VersionLookup for std::collections::HashMap<String, Version> {
201    fn lookup_version<'a>(&'a self, package: &str) -> Option<Cow<'a, Version>> {
202        self.get(package).map(Cow::Borrowed)
203    }
204}
205
206impl<F> VersionLookup for F
207where
208    F: Fn(&str) -> Option<Version>,
209{
210    fn lookup_version<'a>(&'a self, name: &str) -> Option<Cow<'a, Version>> {
211        self(name).map(Cow::Owned)
212    }
213}
214
215impl VersionLookup for (String, Version) {
216    fn lookup_version<'a>(&'a self, name: &str) -> Option<Cow<'a, Version>> {
217        if name == self.0 {
218            Some(Cow::Borrowed(&self.1))
219        } else {
220            None
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::VersionConstraint;
228
229    #[test]
230    fn parse_r_operators() {
231        assert_eq!("<".parse(), Ok(VersionConstraint::LessThan));
232        assert_eq!("<=".parse(), Ok(VersionConstraint::LessThanEqual));
233        assert_eq!("==".parse(), Ok(VersionConstraint::Equal));
234        assert_eq!("!=".parse(), Ok(VersionConstraint::NotEqual));
235        assert_eq!(">".parse(), Ok(VersionConstraint::GreaterThan));
236        assert_eq!(">=".parse(), Ok(VersionConstraint::GreaterThanEqual));
237    }
238
239    #[test]
240    fn debian_operators_are_rejected() {
241        assert!("<<".parse::<VersionConstraint>().is_err());
242        assert!(">>".parse::<VersionConstraint>().is_err());
243        assert!("=".parse::<VersionConstraint>().is_err());
244    }
245
246    #[test]
247    fn display_uses_r_operators() {
248        assert_eq!(VersionConstraint::LessThan.to_string(), "<");
249        assert_eq!(VersionConstraint::LessThanEqual.to_string(), "<=");
250        assert_eq!(VersionConstraint::Equal.to_string(), "==");
251        assert_eq!(VersionConstraint::NotEqual.to_string(), "!=");
252        assert_eq!(VersionConstraint::GreaterThan.to_string(), ">");
253        assert_eq!(VersionConstraint::GreaterThanEqual.to_string(), ">=");
254    }
255
256    #[test]
257    fn invalid_operator() {
258        assert!("~=".parse::<VersionConstraint>().is_err());
259    }
260}