derive_deftly_macros/
check.rs

1//! Implementation of the `expect` option
2
3use crate::prelude::*;
4
5/// Value for an `expect`
6#[derive(Debug, Clone, Copy, Eq, PartialEq, EnumString, Display)]
7#[allow(non_camel_case_types)]
8pub enum Target {
9    items,
10    expr,
11}
12
13/// Local context for a syntax check operation
14struct Checking<'t> {
15    ctx: &'t framework::Context<'t>,
16    output: &'t mut TokenStream,
17    target: DdOptVal<Target>,
18}
19
20/// Main entrypoint
21///
22/// Checks that `output` can be parsed as `target`.
23///
24/// If not, replaces `output` with something which will generate
25/// compiler error(s) which the user will find helpful:
26///  * A `compile_error!` invocation with the original error span
27///  * include_file!` for a generated temporary file
28///    containing the text of the output,
29///    so that the compiler will point to the actual error.
30pub fn check_expected_target_syntax(
31    ctx: &framework::Context,
32    output: &mut TokenStream,
33    target: DdOptVal<Target>,
34) {
35    check::Checking {
36        ctx,
37        output,
38        target,
39    }
40    .check();
41}
42
43pub fn check_expect_opcontext(
44    op: &DdOptVal<Target>,
45    context: OpContext,
46) -> syn::Result<()> {
47    use OpContext as OC;
48    match (context, op.value) {
49        (OC::TemplateDefinition, Target::items) => Ok(()),
50        (OC::TemplateDefinition, _) => {
51            Err(op.span.error(
52                "predefined templates must always expand to items", //
53            ))
54        }
55        _ => Ok(()),
56    }
57}
58
59impl Target {
60    /// Checks if `ts` can parse as `self`, returning the error if not
61    fn perform_check(self, ts: TokenStream) -> Option<syn::Error> {
62        fn chk<T: Parse>(ts: TokenStream) -> Option<syn::Error> {
63            syn::parse2::<Discard<T>>(ts).err()
64        }
65
66        use Target::*;
67        match self {
68            items => chk::<Concatenated<Discard<syn::Item>>>(ts),
69            expr => chk::<syn::Expr>(ts),
70        }
71    }
72
73    /// Tokens for `include!...` to include syntax element(s) like `self`
74    fn include_syntax(self, file: &str) -> TokenStream {
75        use Target::*;
76        match self {
77            items => quote! { include!{ #file } },
78            expr => quote! { include!( #file ) },
79        }
80    }
81
82    /// Make a single output, syntactically a `self.target`, out of pieces
83    ///
84    /// `err` is a `compile_error!` call,
85    /// and `expansion` is typically the template expansion output.
86    fn combine_outputs(
87        self,
88        mut err: TokenStream,
89        expansion: TokenStream,
90    ) -> TokenStream {
91        use Target::*;
92        match self {
93            items => {
94                err.extend(expansion);
95                err
96            }
97            expr => quote!( ( #err, #expansion ) ),
98        }
99    }
100}
101
102impl Checking<'_> {
103    /// Checks that `tokens` can be parsed as `T`
104    ///
105    /// Does the actual work of [`check_expected_target_syntax`]
106    fn check(self) {
107        let err = self.target.value.perform_check(self.output.clone());
108
109        let err = match err {
110            Some(err) => err,
111            None => return,
112        };
113
114        let broken = mem::take(self.output);
115        let err = err.into_compile_error();
116
117        let expansion = expand_via_file(self.ctx, self.target.value, broken)
118            .map_err(|e| {
119                Span::call_site()
120                    .error(format!(
121 "derive-deftly was unable to write out the expansion to a file for fuller syntax error reporting: {}",
122                    e
123                ))
124                    .into_compile_error()
125            })
126            .unwrap_or_else(|e| e);
127
128        *self.output = self.target.value.combine_outputs(err, expansion);
129    }
130}
131
132/// Turn every `$crate` `Ident` into `crate`
133fn massage_dollar_crate(input: TokenStream) -> TokenStream {
134    input
135        .into_iter()
136        .update(|tt| match tt {
137            TT::Group(g) => {
138                *g = group_clone_set_stream(
139                    &g,
140                    massage_dollar_crate(g.stream()),
141                )
142            }
143            TT::Ident(i) if i == "$crate" => {
144                *i = Ident::new("crate", i.span())
145            }
146            _other => {}
147        })
148        .collect()
149}
150
151/// Constructs an `include!` which includes the text for `broken`
152///
153/// Appends the `include` to `checking.output`.
154///
155/// If this can't be done, reports why not.
156fn expand_via_file(
157    ctx: &framework::Context,
158    target: Target,
159    broken: TokenStream,
160) -> Result<TokenStream, String> {
161    use sha3::{Digest as _, Sha3_256};
162    use std::{fs, io, io::Write as _, path::PathBuf};
163
164    // The expansion TS can contain the weird pseudo-identifier `$crate`.
165    // This can be constructed via `macro_rules!`.  It is a `TT::Ident`
166    // which `Display`s as `$crate`.  But that doesn't parse as a string,
167    // because it's not real Rust surface syntax.
168    //
169    // So when we create the demo file, containing the expansion, we
170    // turn those into `crate`.  This is wrong for name resolution,
171    // so the user might see name resolution errors as well as the
172    // intended syntax error.  There is very little we can do about that.
173    let broken = massage_dollar_crate(broken);
174
175    let text = format!(
176        "// {}, should have been {}:\n{}\n",
177        ctx.expansion_description(),
178        target,
179        broken,
180    );
181
182    let hash: String = {
183        let mut hasher = Sha3_256::new();
184        hasher.update(&text);
185        let hash = hasher.finalize();
186        const HASH_LEN_BYTES: usize = 12;
187        hash[0..HASH_LEN_BYTES].iter().fold(
188            String::with_capacity(HASH_LEN_BYTES * 2),
189            |mut s, b| {
190                write!(s, "{:02x}", b).expect("write to String failed");
191                s
192            },
193        )
194    };
195
196    let dir: PathBuf = [env!("OUT_DIR"), "derive-deftly~expansions~"]
197        .iter()
198        .collect();
199
200    match fs::create_dir(&dir) {
201        Ok(()) => {}
202        Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
203        Err(e) => return Err(format!("create dir {:?}: {}", &dir, e)),
204    };
205
206    let leaf = format!("dd-{}.rs", hash);
207    let some_file = |leaf: &str| {
208        let mut file = dir.clone();
209        file.push(leaf);
210        file
211    };
212    let file = some_file(&leaf);
213    let file = file
214        .to_str()
215        .ok_or_else(|| format!("non UTF-8 path? from env var! {:?}", file))?;
216
217    // We *overwrite* the file in place.
218    //
219    // This is because it's theoretically possible that multiple calls
220    // to this function, at the same time, might be generating files
221    // with identical contents, and therefore the same name.
222    //
223    // So we open it with O_CREATE|O_WRITE but *not* O_TRUNC,
224    // and write our data, and then declare our job done.
225    // This is idempotent and concurrency-safe.
226    //
227    // There is no need to truncate the file, since all writers
228    // are writing the same text.  (If we change the hashing scheme,
229    // we must change the filename too.)
230
231    let mut fh = fs::OpenOptions::new()
232        .write(true)
233        .create(true)
234        .truncate(false)
235        .open(file)
236        .map_err(|e| format!("create/open {:?}: {}", &file, e))?;
237    fh.write_all(text.as_ref())
238        .map_err(|e| format!("write {:?}: {}", &file, e))?;
239
240    Ok(target.include_syntax(file))
241}