code_tour/lib.rs
1#![cfg_attr(nightly, feature(proc_macro_span))]
2
3use proc_macro2::{Span, TokenStream, TokenTree};
4use quote::{quote, quote_spanned, ToTokens};
5#[cfg(feature = "interactive")]
6use syn::Block;
7use syn::{parse, AttrStyle, Ident, ItemFn, Pat, Path, Stmt};
8
9mod colours;
10mod format;
11
12#[proc_macro_attribute]
13pub fn code_tour(
14 _attributes: proc_macro::TokenStream,
15 input: proc_macro::TokenStream,
16) -> proc_macro::TokenStream {
17 if let Ok(mut function) = parse::<ItemFn>(input.clone()) {
18 let block_statements = &function.block.stmts;
19 let mut statements = Vec::with_capacity(block_statements.len());
20
21 for statement in block_statements.into_iter() {
22 // That's a `let` binding.
23 if let Stmt::Local(local) = statement {
24 // Extract the local pattern.
25 if let Pat::Ident(local_pat) = &local.pat {
26 // There is attributes, which are normally `///`
27 // (outer doc) or `/** */` (outer block) comments
28 // before the `let` binding.
29 if !local.attrs.is_empty() {
30 // Write `println!("{}", ident_comment)`.
31 {
32 let formatted_comment = local
33 .attrs
34 .iter()
35 .filter_map(|attribute| match (attribute.style, &attribute.path) {
36 (
37 AttrStyle::Outer,
38 Path {
39 leading_colon: None,
40 segments,
41 },
42 ) if !segments.is_empty()
43 && segments[0].ident
44 == Ident::new("doc", Span::call_site()) =>
45 {
46 if let Some(TokenTree::Literal(literal)) =
47 attribute.tokens.clone().into_iter().nth(1)
48 {
49 let literal_string =
50 literal.to_string().replace("\\\'", "'");
51
52 Some(format!(
53 "▍{:<80}",
54 &literal_string[1..literal_string.len() - 1]
55 ))
56 } else {
57 None
58 }
59 }
60
61 _ => None,
62 })
63 .map(colours::comment)
64 .collect::<Vec<String>>()
65 .join("\n ");
66
67 let empty_line = colours::comment(format!("▍{:<80}", " "));
68 let formatted_comment = format!(
69 "\n {empty}\n {comment}\n {empty}\n",
70 empty = empty_line,
71 comment = formatted_comment
72 );
73
74 statements.push(println(quote!("{}", #formatted_comment)));
75 }
76
77 let mut local_without_attrs = local.clone();
78 local_without_attrs.attrs = vec![];
79
80 // Write `println!("{}", stringify!(<local>))`.
81 {
82 let statement =
83 colours::statement(format::rust_code(&local_without_attrs));
84
85 statements.push(println(quote!(" {}\n", #statement)));
86 }
87
88 // Write the original statement, without the documentation.
89 {
90 statements.push(Stmt::Local(local_without_attrs));
91 }
92
93 // Write `println!("{:?}", <ident>)`.
94 {
95 statements.push(println({
96 let ident = &local_pat.ident;
97
98 quote!(
99 " ◀︎ {}\n\n",
100 format!("{:#?}", #ident).replace("\n", "\n ▐ ")
101 )
102 }));
103 }
104
105 // Insert “Press Enter to continue…”
106 #[cfg(feature = "interactive")]
107 {
108 let stream = quote!({
109 {
110 use std::io::BufRead;
111
112 let mut line = String::new();
113 let stdin = ::std::io::stdin();
114
115 println!(
116 "\n(Press Enter to continue, otherwise Ctrl-C to exit).\n\n"
117 );
118
119 stdin
120 .lock()
121 .read_line(&mut line)
122 .expect("Failed to read a line from the user.");
123 }
124 });
125
126 let block = parse::<Block>(stream.into()).unwrap();
127
128 for statement in block.stmts {
129 statements.push(statement);
130 }
131 }
132
133 continue;
134 }
135 }
136 }
137
138 statements.push(statement.clone());
139 }
140
141 function.block.stmts = statements;
142
143 quote!(#function).to_token_stream().into()
144 } else {
145 let span = TokenStream::from(input).into_iter().nth(0).unwrap().span();
146 quote_spanned!(span => compile_error!("`code_tour` works on functions only")).into()
147 }
148}
149
150fn println(tokens: TokenStream) -> Stmt {
151 let stream = quote!(if ::std::env::var("CODE_TOUR_QUIET").is_err() {
152 println!(#tokens);
153 });
154 let semi = parse::<Stmt>(stream.into()).unwrap();
155
156 semi
157}