1#![feature(proc_macro)]
16#![recursion_limit = "128"]
17
18#[macro_use] mod acquire;
19mod new_mock;
20mod given;
21mod expect;
22mod generate;
23mod data;
24
25extern crate proc_macro;
26#[macro_use] extern crate lazy_static;
27
28extern crate syn;
29#[macro_use] extern crate synom;
30#[macro_use] extern crate quote;
31
32#[cfg(test)]#[macro_use]
33extern crate galvanic_assert;
34
35use proc_macro::TokenStream;
36
37use new_mock::handle_new_mock;
38use given::handle_given;
39use expect::handle_expect_interactions;
40use generate::handle_generate_mocks;
41use data::*;
42
43use std::env;
44use std::fs::File;
45use std::io::Write;
46use std::path::Path;
47
48
49enum MockedTraitLocation {
50 TraitDef(syn::Path),
51 Referred(syn::Path)
52}
53
54named!(parse_trait_path -> MockedTraitLocation,
55 delimited!(
56 punct!("("),
57 do_parse!(
58 external: option!(alt!(keyword!("intern") | keyword!("extern"))) >> path: call!(syn::parse::path) >>
59 (match external {
60 Some(..) => MockedTraitLocation::Referred(path),
61 None => MockedTraitLocation::TraitDef(path)
62 })
63 ),
64 punct!(")")
65 )
66);
67
68#[proc_macro_attribute]
69pub fn mockable(args: TokenStream, input: TokenStream) -> TokenStream {
70 let s = input.to_string();
71 let trait_item = syn::parse_item(&s).expect("Expecting a trait definition.");
72
73 let args_str = &args.to_string();
74
75 match trait_item.node {
76 syn::ItemKind::Trait(safety, generics, bounds, items) => {
77 let mut mockable_traits = acquire!(MOCKABLE_TRAITS);
78
79 if args_str.is_empty() {
80 mockable_traits.insert(trait_item.ident.clone().into(), TraitInfo::new(safety, generics, bounds, items));
81 return input;
82 }
83
84 let trait_location = parse_trait_path(args_str)
85 .expect(concat!("#[mockable(..)] requires the absolute path of the trait's module.",
86 "It must be preceded with `extern`/`intern` if the trait is defined in another crate/module"));
87 match trait_location {
88 MockedTraitLocation::TraitDef(mut trait_path) => {
89 trait_path.segments.push(trait_item.ident.clone().into());
90 mockable_traits.insert(trait_path, TraitInfo::new(safety, generics, bounds, items));
91 input
92 },
93 MockedTraitLocation::Referred(mut trait_path) => {
94 trait_path.segments.push(trait_item.ident.clone().into());
95 mockable_traits.insert(trait_path, TraitInfo::new(safety, generics, bounds, items));
96 "".parse().unwrap()
97 }
98 }
99 },
100 _ => panic!("Expecting a trait definition.")
101 }
102}
103
104
105#[proc_macro_attribute]
106pub fn use_mocks(_: TokenStream, input: TokenStream) -> TokenStream {
107 use MacroInvocationPos::*;
108
109 let mut reassembled = String::new();
111 let parsed = syn::parse_item(&input.to_string()).unwrap();
112 let mut remainder = quote!(#parsed).to_string();
113
114 let mut absolute_pos = 0;
116 while !remainder.is_empty() {
117
118 match find_next_mock_macro_invocation(&remainder) {
119 None => {
120 reassembled.push_str(&remainder);
121 remainder = String::new();
122 },
123 Some(invocation) => {
124 let (left, new_absolute_pos, right) = match invocation {
125 NewMock(pos) => handle_macro(&remainder, pos, absolute_pos, handle_new_mock),
126 Given(pos) => handle_macro(&remainder, pos, absolute_pos, handle_given),
127 ExpectInteractions(pos) => handle_macro(&remainder, pos, absolute_pos, handle_expect_interactions),
128 };
129
130 absolute_pos = new_absolute_pos;
131 reassembled.push_str(&left);
132 remainder = right;
133 }
134 }
135 }
136
137 let mut mock_using_item = syn::parse_item(&reassembled).expect("Reassembled function whi");
139 mock_using_item.vis = syn::Visibility::Public;
140
141 let item_ident = &mock_using_item.ident;
142 let item_vis = &mock_using_item.vis;
143 let mod_fn = syn::Ident::from(format!("mod_{}", item_ident));
144
145
146 if let syn::ItemKind::Mod(Some(ref mut mod_items)) = mock_using_item.node {
147 insert_use_generated_mocks_into_modules(mod_items);
148 }
149
150 let mocks = handle_generate_mocks();
151
152 let generated_mock = (quote! {
153 #[allow(unused_imports)]
154 #item_vis use self::#mod_fn::#item_ident;
155 mod #mod_fn {
156 #![allow(dead_code)]
157 #![allow(unused_imports)]
158 #![allow(unused_variables)]
159 use super::*;
160
161 #mock_using_item
162
163 pub(in self) mod mock {
164 use std;
165 use super::*;
166
167 #(#mocks)*
168 }
169 }
170 }).to_string();
171
172 debug(&item_ident, &generated_mock);
173 generated_mock.parse().unwrap()
174}
175
176fn insert_use_generated_mocks_into_modules(mod_items: &mut Vec<syn::Item>) {
177 for item in mod_items.iter_mut() {
178 if let syn::ItemKind::Mod(Some(ref mut sub_mod_items)) = item.node {
179 insert_use_generated_mocks_into_modules(sub_mod_items);
180 }
181 }
182 mod_items.push(syn::parse_item(quote!(pub use super::*;).as_str()).unwrap());
183}
184
185fn debug(item_ident: &syn::Ident, generated_mock: &str) {
186 if let Some((_, path)) = env::vars().find(|&(ref key, _)| key == "GA_WRITE_MOCK") {
187 if path.is_empty() {
188 println!("{}", generated_mock);
189 } else {
190 let success = File::create(Path::new(&path).join(&(item_ident.to_string())))
191 .and_then(|mut f| f.write_all(generated_mock.as_bytes()));
192 if let Err(err) = success {
193 eprintln!("Unable to write generated mock to file '{}' because: {}", path, err);
194 }
195 }
196 }
197}
198
199fn has_balanced_quotes(source: &str) -> bool {
200 let mut count = 0;
201 let mut skip = false;
202 for c in source.chars() {
203 if skip {
204 skip = false;
205 continue;
206 }
207
208 if c == '\\' {
209 skip = true;
210 } else if c == '\"' {
211 count += 1;
212 }
213 }
215 count % 2 == 0
216}
217
218enum MacroInvocationPos {
220 NewMock(usize),
221 Given(usize),
222 ExpectInteractions(usize),
223}
224
225fn find_next_mock_macro_invocation(source: &str) -> Option<MacroInvocationPos> {
232 use MacroInvocationPos::*;
233 let macro_names = ["new_mock !", "given !", "expect_interactions !"];
235 macro_names.into_iter()
237 .filter_map(|&mac| {
238 source.find(mac).and_then(|pos| {
239 if has_balanced_quotes(&source[.. pos]) {
240 Some((pos, mac))
241 } else { None }
242 })
243 })
244 .min_by_key(|&(pos, _)| pos)
245 .and_then(|(pos, mac)| Some(match mac {
246 "new_mock !" => NewMock(pos),
247 "given !" => Given(pos),
248 "expect_interactions !" => ExpectInteractions(pos),
249 _ => panic!("Unreachable. No variant for macro name: {}", mac)
250 }))
251}
252
253fn handle_macro<F>(source: &str, mac_pos_relative_to_source: usize, absolute_pos_of_source: usize, handler: F) -> (String, usize, String)
254where F: Fn(&str, usize) -> (String, String) {
255 let absolute_pos_of_mac = absolute_pos_of_source + mac_pos_relative_to_source;
256
257 let (left_of_mac, right_with_mac) = source.split_at(mac_pos_relative_to_source);
258 let (mut generated_source, unhandled_source) = handler(right_with_mac, absolute_pos_of_mac);
259 generated_source.push_str(&unhandled_source);
260
261 (left_of_mac.to_string(), absolute_pos_of_mac, generated_source)
262}
263
264
265#[cfg(test)]
266mod test_has_balanced_quotes {
267 use super::*;
268
269 #[test]
270 fn should_have_balanced_quotes_if_none_exist() {
271 let x = "df df df";
272 assert!(has_balanced_quotes(x));
273 }
274
275 #[test]
276 fn should_have_balanced_quotes_if_single_pair() {
277 let x = "df \"df\" df";
278 assert!(has_balanced_quotes(x));
279 }
280
281 #[test]
282 fn should_have_balanced_quotes_if_single_pair_with_escapes() {
283 let x = "df \"d\\\"f\" df";
284 assert!(has_balanced_quotes(x));
285 }
286
287 #[test]
288 fn should_have_balanced_quotes_if_multiple_pairs() {
289 let x = "df \"df\" \"df\" df";
290 assert!(has_balanced_quotes(x));
291 }
292
293 #[test]
294 fn should_not_have_balanced_quotes_if_single() {
295 let x = "df \"df df";
296 assert!(!has_balanced_quotes(x));
297 }
298
299 #[test]
300 fn should_not_have_balanced_quotes_if_escaped_pair() {
301 let x = "df \"d\\\" df";
302 assert!(!has_balanced_quotes(x));
303 }
304}