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