rulex_macro/
lib.rs

1#![cfg_attr(feature = "diagnostics", feature(proc_macro_span))]
2
3extern crate proc_macro;
4
5use std::iter::Peekable;
6
7use proc_macro::{Delimiter, Group, Literal, Span, TokenStream, TokenTree};
8
9use rulex::{
10    error::Diagnostic,
11    options::{CompileOptions, RegexFlavor},
12    Rulex,
13};
14
15mod diagnostic;
16
17#[proc_macro]
18pub fn rulex(items: TokenStream) -> TokenStream {
19    let group = Group::new(Delimiter::None, items);
20    let global_span = group.span();
21
22    match rulex_impl(group.stream().into_iter()) {
23        Ok(lit) => TokenTree::Literal(lit).into(),
24        Err(Error { msg, span }) => {
25            let span = span.unwrap_or(global_span);
26            let msg = format!("error: {msg}");
27            diagnostic::error(&msg, span, span)
28        }
29    }
30}
31
32struct Error {
33    msg: String,
34    span: Option<Span>,
35}
36
37impl Error {
38    fn new(msg: String, span: Span) -> Self {
39        Error { msg, span: Some(span) }
40    }
41
42    fn from_msg(msg: String) -> Self {
43        Error { msg, span: None }
44    }
45}
46
47macro_rules! bail {
48    ($l:literal) => {
49        return Err(Error::from_msg(format!($l)))
50    };
51    ($l:literal, $e:expr) => {
52        return Err(Error::new(format!($l), $e))
53    };
54    ($e1:expr) => {
55        return Err(Error::from_msg($e1))
56    };
57    ($e1:expr, $e2:expr) => {
58        return Err(Error::new($e1, $e2))
59    };
60}
61
62fn expect(
63    iter: &mut Peekable<impl Iterator<Item = TokenTree>>,
64    pred: fn(&TokenTree) -> bool,
65    error_msg: impl Into<String>,
66) -> Result<(), Error> {
67    match iter.peek() {
68        Some(tt) if pred(tt) => {
69            iter.next();
70            Ok(())
71        }
72        Some(tt) => bail!(error_msg.into(), tt.span()),
73        None => bail!(error_msg.into()),
74    }
75}
76
77fn rulex_impl(items: impl Iterator<Item = TokenTree>) -> Result<Literal, Error> {
78    let mut iter = items.peekable();
79
80    let found_hashtag =
81        expect(&mut iter, |t| matches!(t, TokenTree::Punct(p) if p.as_char() == '#'), "");
82
83    let flavor = if found_hashtag.is_ok() {
84        expect(
85            &mut iter,
86            |t| matches!(t, TokenTree::Ident(id) if &id.to_string() == "flavor"),
87            "expected `flavor`",
88        )?;
89        expect(
90            &mut iter,
91            |t| matches!(t, TokenTree::Punct(p) if p.as_char() == '='),
92            "expected `=`",
93        )?;
94
95        get_flavor(iter.next())?
96    } else {
97        RegexFlavor::Rust
98    };
99
100    let group = Group::new(Delimiter::None, iter.collect());
101
102    #[cfg(not(feature = "diagnostics"))]
103    let (span, input) = (group.span(), group.to_string());
104
105    #[cfg(feature = "diagnostics")]
106    let (span, input) = {
107        if let (Some(first), Some(last)) =
108            (group.stream().into_iter().next(), group.stream().into_iter().last())
109        {
110            let span = first.span().join(last.span()).unwrap();
111            (span, span.source_text().unwrap())
112        } else {
113            (group.span_close(), String::new())
114        }
115    };
116
117    let input = input.trim_start_matches("/*«*/").trim_end_matches("/*»*/");
118
119    match Rulex::parse_and_compile(input, Default::default(), CompileOptions { flavor }) {
120        Ok(compiled) => Ok(Literal::string(&compiled)),
121
122        Err(e) => bail!(diagnostic::fmt(Diagnostic::from_compile_error(e, input), group), span),
123    }
124}
125
126fn get_flavor(item: Option<TokenTree>) -> Result<RegexFlavor, Error> {
127    Ok(match item {
128        Some(TokenTree::Ident(id)) => match id.to_string().as_str() {
129            "DotNet" => RegexFlavor::DotNet,
130            "Java" => RegexFlavor::Java,
131            "JavaScript" => RegexFlavor::JavaScript,
132            "Pcre" => RegexFlavor::Pcre,
133            "Python" => RegexFlavor::Python,
134            "Ruby" => RegexFlavor::Ruby,
135            "Rust" => RegexFlavor::Rust,
136            s => bail!(
137                "Expected one of: DotNet, Java, JavaScript, Pcre, Python, Ruby, Rust\nGot: {s}",
138                id.span()
139            ),
140        },
141        Some(tt) => bail!("Unexpected token `{tt}`", tt.span()),
142        None => bail!("Expected identifier"),
143    })
144}