1#![cfg_attr(feature = "diagnostics", feature(proc_macro_span))]
6
7extern crate proc_macro;
8
9use std::iter::Peekable;
10
11use proc_macro::{Delimiter, Group, Literal, Span, TokenStream, TokenTree};
12
13use pomsky::{
14 Expr,
15 options::{CompileOptions, RegexFlavor},
16};
17
18mod diagnostic;
19
20#[proc_macro]
81pub fn pomsky(items: TokenStream) -> TokenStream {
82 let group = Group::new(Delimiter::None, items);
83 let global_span = group.span();
84
85 match pomsky_impl(group.stream().into_iter()) {
86 Ok(lit) => TokenTree::Literal(lit).into(),
87 Err(Error { msg, span }) => {
88 let span = span.unwrap_or(global_span);
89 diagnostic::error(&msg, span, span)
90 }
91 }
92}
93
94struct Error {
95 msg: String,
96 span: Option<Span>,
97}
98
99impl Error {
100 fn new(msg: String, span: Span) -> Self {
101 Error { msg, span: Some(span) }
102 }
103
104 fn from_msg(msg: String) -> Self {
105 Error { msg, span: None }
106 }
107}
108
109macro_rules! bail {
110 ($l:literal) => {
111 return Err(Error::from_msg(format!($l)))
112 };
113 ($l:literal, $e:expr) => {
114 return Err(Error::new(format!($l), $e))
115 };
116 ($e1:expr) => {
117 return Err(Error::from_msg($e1))
118 };
119 ($e1:expr, $e2:expr) => {
120 return Err(Error::new($e1, $e2))
121 };
122}
123
124fn expect(
125 iter: &mut Peekable<impl Iterator<Item = TokenTree>>,
126 pred: fn(&TokenTree) -> bool,
127 error_msg: impl Into<String>,
128) -> Result<(), Error> {
129 match iter.peek() {
130 Some(tt) if pred(tt) => {
131 iter.next();
132 Ok(())
133 }
134 Some(tt) => bail!(error_msg.into(), tt.span()),
135 None => bail!(error_msg.into()),
136 }
137}
138
139fn pomsky_impl(items: impl Iterator<Item = TokenTree>) -> Result<Literal, Error> {
140 let mut iter = items.peekable();
141
142 let found_hashtag =
143 expect(&mut iter, |t| matches!(t, TokenTree::Punct(p) if p.as_char() == '#'), "");
144
145 let flavor = if found_hashtag.is_ok() {
146 expect(
147 &mut iter,
148 |t| matches!(t, TokenTree::Ident(id) if &id.to_string() == "flavor"),
149 "expected `flavor`",
150 )?;
151 expect(
152 &mut iter,
153 |t| matches!(t, TokenTree::Punct(p) if p.as_char() == '='),
154 "expected `=`",
155 )?;
156
157 get_flavor(iter.next())?
158 } else {
159 RegexFlavor::Rust
160 };
161
162 let group = Group::new(Delimiter::None, iter.collect());
163
164 #[cfg(not(feature = "diagnostics"))]
165 let (span, input) = (group.span(), group.to_string());
166
167 #[cfg(feature = "diagnostics")]
168 let (span, input) = {
169 if let (Some(first), Some(last)) =
170 (group.stream().into_iter().next(), group.stream().into_iter().last())
171 {
172 let span = first.span().join(last.span()).unwrap();
173 (span, span.source_text().unwrap())
174 } else {
175 (group.span_close(), String::new())
176 }
177 };
178
179 let input = input.trim_start_matches("/*«*/").trim_end_matches("/*»*/");
180
181 match Expr::parse_and_compile(input, CompileOptions { flavor, ..Default::default() }) {
182 (Some(compiled), _warnings, _tests) => Ok(Literal::string(&compiled)),
183
184 (None, errors, _) => {
185 let errors =
186 errors.into_iter().map(|d| diagnostic::fmt(d, &group, input)).collect::<Vec<_>>();
187 bail!(errors.join("\n\n"), span)
188 }
189 }
190}
191
192fn get_flavor(item: Option<TokenTree>) -> Result<RegexFlavor, Error> {
193 Ok(match item {
194 Some(TokenTree::Ident(id)) => match id.to_string().as_str() {
195 "DotNet" => RegexFlavor::DotNet,
196 "Java" => RegexFlavor::Java,
197 "JavaScript" => RegexFlavor::JavaScript,
198 "Pcre" => RegexFlavor::Pcre,
199 "Python" => RegexFlavor::Python,
200 "Ruby" => RegexFlavor::Ruby,
201 "Rust" => RegexFlavor::Rust,
202 s => bail!(
203 "Expected one of: DotNet, Java, JavaScript, Pcre, Python, Ruby, Rust\nGot: {s}",
204 id.span()
205 ),
206 },
207 Some(tt) => bail!("Unexpected token `{tt}`", tt.span()),
208 None => bail!("Expected identifier"),
209 })
210}