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}