Skip to main content

derive_defs/
codegen.rs

1//! Proc-macro code generation.
2//!
3//! This module generates the actual proc-macro code from resolved definitions.
4//!
5//! # Generated Code Structure
6//!
7//! The generated code creates attribute macros that:
8//! 1. Parse the input item (struct/enum)
9//! 2. Apply configured derives and attributes
10//! 3. Handle runtime modifiers (`add`, `omit`, `omit_attrs`)
11//!
12//! # Example Output
13//!
14//! For a definition like:
15//! ```toml
16//! [defs.serialization]
17//! traits = ["Clone", "Serialize", "Deserialize"]
18//! attrs = ['#[serde(rename_all = "camelCase")]']
19//! ```
20//!
21//! The generated code will be:
22//! ```rust,ignore
23//! #[proc_macro_attribute]
24//! pub fn serialization(
25//!     args: TokenStream,
26//!     input: TokenStream,
27//! ) -> TokenStream {
28//!     // Parse modifiers from args
29//!     // Apply derives and attrs to input
30//! }
31//! ```
32
33use std::path::Path;
34
35use crate::resolver::ResolvedConfig;
36use crate::{Error, Result};
37
38/// Header template for generated code.
39const HEADER_TEMPLATE: &str = include_str!("../templates/header.rs");
40
41/// Macro function template.
42const MACRO_TEMPLATE: &str = include_str!("../templates/macro.rs");
43
44/// Generate proc-macro code to the default `OUT_DIR`.
45///
46/// This function reads the `OUT_DIR` environment variable set by Cargo
47/// during the build process and writes the generated code to
48/// `$OUT_DIR/derive_defs.rs`.
49///
50/// # Errors
51///
52/// Returns an error if:
53/// - The `OUT_DIR` environment variable is not set
54/// - The output file cannot be written
55///
56/// # Panics
57///
58/// Panics if `OUT_DIR` is not set. This should only happen outside of a build script.
59///
60/// # Example
61///
62/// ```no_run
63/// // In your build.rs
64/// derive_defs::generate("derive_defs.toml").unwrap();
65/// ```
66pub fn generate(config: ResolvedConfig) -> Result<()> {
67    let out_dir =
68        std::env::var("OUT_DIR").map_err(|_| Error::Validation("OUT_DIR not set".to_string()))?;
69    let output_path = Path::new(&out_dir).join("derive_defs.rs");
70
71    generate_to(config, &output_path)
72}
73
74/// Generate proc-macro code to a specific output path.
75///
76/// Similar to [`generate`], but allows specifying a custom output path instead
77/// of using `OUT_DIR`.
78///
79/// # Errors
80///
81/// Returns an error if the output file cannot be written.
82///
83/// # Example
84///
85/// ```no_run
86/// use derive_defs::resolver::{ResolvedConfig, ResolvedDef};
87/// use std::collections::HashMap;
88///
89/// let config = ResolvedConfig {
90///     defs: HashMap::new(),
91/// };
92/// derive_defs::codegen::generate_to(config, "/tmp/output.rs").unwrap();
93/// ```
94pub fn generate_to<P: AsRef<Path>>(config: ResolvedConfig, output: P) -> Result<()> {
95    let code = generate_code(config);
96
97    std::fs::write(output.as_ref(), code).map_err(Error::CodegenWrite)?;
98
99    Ok(())
100}
101
102/// Generate Rust code string from resolved configuration.
103fn generate_code(config: ResolvedConfig) -> String {
104    let mut code = String::new();
105
106    // Add header with imports and argument parser
107    code.push_str(HEADER_TEMPLATE);
108    code.push('\n');
109
110    // Generate each macro
111    for (name, def) in config.defs {
112        let macro_name = name.replace('.', "_");
113
114        // Build the derive list
115        let derive_list = if def.traits.is_empty() {
116            String::new()
117        } else {
118            format!("#[derive({})]", def.traits.join(", "))
119        };
120
121        // Build the attribute list
122        let attr_list = def.attrs.join("\n");
123
124        // Generate the macro function
125        code.push_str(&generate_macro(&macro_name, &derive_list, &attr_list));
126        code.push('\n');
127    }
128
129    code
130}
131
132/// Generate a single macro function.
133#[allow(clippy::literal_string_with_formatting_args)]
134fn generate_macro(name: &str, derive_list: &str, attr_list: &str) -> String {
135    MACRO_TEMPLATE
136        .replace("{name}", name)
137        .replace("{derive_list:?}", &format!("{derive_list:?}"))
138        .replace("{attr_list:?}", &format!("{attr_list:?}"))
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::resolver::ResolvedDef;
145    use std::collections::HashMap;
146
147    #[test]
148    fn test_generate_code() {
149        let mut defs = HashMap::new();
150        defs.insert(
151            "serialization".to_string(),
152            ResolvedDef {
153                name: "serialization".to_string(),
154                traits: vec![
155                    "Clone".to_string(),
156                    "Serialize".to_string(),
157                    "Deserialize".to_string(),
158                ],
159                attrs: vec!["#[serde(rename_all = \"camelCase\")]".to_string()],
160            },
161        );
162
163        let config = ResolvedConfig { defs };
164        let code = generate_code(config);
165
166        assert!(code.contains("proc_macro_attribute"));
167        assert!(code.contains("fn serialization"));
168        assert!(code.contains("derive(Clone, Serialize, Deserialize)"));
169        assert!(code.contains("serde(rename_all"));
170        assert!(code.contains("struct DefArgs"));
171        assert!(code.contains("impl Parse for DefArgs"));
172    }
173
174    /// Test that generated code converts trait strings to `proc_macro2::Ident`.
175    ///
176    /// This test specifically guards against the bug where trait names were
177    /// passed directly to quote! as strings, resulting in string literals
178    /// instead of identifiers in the generated #[derive(...)] attribute.
179    #[test]
180    fn test_generated_code_converts_traits_to_idents() {
181        let mut defs = HashMap::new();
182        defs.insert(
183            "model".to_string(),
184            ResolvedDef {
185                name: "model".to_string(),
186                traits: vec!["Debug".to_string(), "Clone".to_string()],
187                attrs: vec![],
188            },
189        );
190
191        let config = ResolvedConfig { defs };
192        let code = generate_code(config);
193
194        // The generated code must convert String to proc_macro2::Ident
195        // before using in quote! macro to avoid "expected path, found literal" error
196        assert!(
197            code.contains("proc_macro2::Ident::new"),
198            "Generated code should convert trait names to proc_macro2::Ident"
199        );
200
201        // Check that the code uses call_site() span for the Idents
202        assert!(
203            code.contains("proc_macro2::Span::call_site()"),
204            "Generated code should use call_site() span for Idents"
205        );
206
207        // Check that derives are declared as Vec<proc_macro2::Ident>
208        assert!(
209            code.contains("Vec<proc_macro2::Ident>"),
210            "Generated code should declare derives as Vec<proc_macro2::Ident>"
211        );
212    }
213
214    /// Test that generated code properly handles the derive attribute construction.
215    ///
216    /// This ensures the generated code will produce valid #[derive(Trait1, Trait2)]
217    /// syntax when compiled and executed.
218    #[test]
219    fn test_generated_code_derive_construction() {
220        let mut defs = HashMap::new();
221        defs.insert(
222            "test".to_string(),
223            ResolvedDef {
224                name: "test".to_string(),
225                traits: vec!["Debug".to_string()],
226                attrs: vec![],
227            },
228        );
229
230        let config = ResolvedConfig { defs };
231        let code = generate_code(config);
232
233        // Check that the generated code has the proper structure for building derives
234        assert!(
235            code.contains("let derives: Vec<proc_macro2::Ident>"),
236            "Generated code should create a typed Vec of Idents for derives"
237        );
238
239        // The code should iterate over final_derives and convert each to Ident
240        // Note: In the generated code, this spans multiple lines:
241        //   let derives: Vec<proc_macro2::Ident> = final_derives
242        //       .iter()
243        //       .map(...)
244        assert!(
245            code.contains("final_derives") && code.contains(".iter()"),
246            "Generated code should iterate over final_derives"
247        );
248
249        // The quote! should use the derives variable (which is now Vec<Ident>)
250        assert!(
251            code.contains("#(#derives),*"),
252            "Generated code should use derives in quote! macro"
253        );
254    }
255
256    #[test]
257    fn test_generate_code_empty_traits() {
258        let mut defs = HashMap::new();
259        defs.insert(
260            "empty".to_string(),
261            ResolvedDef {
262                name: "empty".to_string(),
263                traits: vec![],
264                attrs: vec!["#[repr(C)]".to_string()],
265            },
266        );
267
268        let config = ResolvedConfig { defs };
269        let code = generate_code(config);
270
271        assert!(code.contains("fn empty"));
272        assert!(!code.contains("derive()")); // Empty derive list should not be generated
273        assert!(code.contains("repr(C)"));
274
275        // When traits are empty, the code should use quote! {}
276        assert!(
277            code.contains("let derive_attr = if final_derives.is_empty()"),
278            "Generated code should check for empty derives"
279        );
280    }
281
282    #[test]
283    fn test_generate_code_namespaced() {
284        let mut defs = HashMap::new();
285        defs.insert(
286            "common.serialization".to_string(),
287            ResolvedDef {
288                name: "common.serialization".to_string(),
289                traits: vec!["Clone".to_string()],
290                attrs: vec![],
291            },
292        );
293
294        let config = ResolvedConfig { defs };
295        let code = generate_code(config);
296
297        // Namespaced names should have dots replaced with underscores
298        assert!(code.contains("fn common_serialization"));
299    }
300
301    /// Test that generated code includes all necessary helper functions.
302    #[test]
303    fn test_generated_code_includes_helpers() {
304        let config = ResolvedConfig {
305            defs: HashMap::new(),
306        };
307        let code = generate_code(config);
308
309        assert!(
310            code.contains("fn parse_trait_list"),
311            "Generated code should include parse_trait_list function"
312        );
313        assert!(
314            code.contains("fn filter_attrs"),
315            "Generated code should include filter_attrs function"
316        );
317    }
318
319    /// Test that generated code handles runtime modifications (omit/add).
320    #[test]
321    fn test_generated_code_handles_runtime_mods() {
322        let mut defs = HashMap::new();
323        defs.insert(
324            "configurable".to_string(),
325            ResolvedDef {
326                name: "configurable".to_string(),
327                traits: vec!["Debug".to_string(), "Clone".to_string()],
328                attrs: vec![],
329            },
330        );
331
332        let config = ResolvedConfig { defs };
333        let code = generate_code(config);
334
335        // Check for omit handling
336        assert!(
337            code.contains("omit_traits"),
338            "Generated code should handle omit_traits"
339        );
340
341        // Check for add handling
342        assert!(
343            code.contains("add_traits"),
344            "Generated code should handle add_traits"
345        );
346
347        // Check for filtering logic
348        assert!(
349            code.contains(".filter(|t| !omit_traits.contains(t))"),
350            "Generated code should filter omitted traits"
351        );
352    }
353}