venus_core/compile/
source_processor.rs

1//! Source processing for Venus notebooks.
2//!
3//! Provides utilities for transforming notebook source code for different
4//! compilation targets (production builds, etc.).
5
6use proc_macro2::TokenStream;
7use quote::ToTokens;
8use syn::{parse_file, Attribute, File, Item};
9
10/// Process notebook source code for production builds.
11///
12/// This processor transforms notebook source by:
13/// - Removing module-level doc comments (`//!`)
14/// - Stripping `#[venus::cell]` attributes from functions
15/// - Removing the `main` function (to be replaced with generated one)
16///
17/// Uses proper syntax parsing via `syn` to handle edge cases like:
18/// - Braces inside comments and strings
19/// - Nested functions
20/// - Complex attribute syntax
21pub struct NotebookSourceProcessor;
22
23impl NotebookSourceProcessor {
24    /// Process notebook source for production compilation.
25    ///
26    /// Returns the processed source code with Venus-specific metadata stripped.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if the source cannot be parsed as valid Rust.
31    pub fn process_for_production(source: &str) -> Result<String, syn::Error> {
32        let file = parse_file(source)?;
33        let processed = Self::process_file(file);
34        Ok(Self::tokens_to_string(processed))
35    }
36
37    /// Process a parsed file, filtering and transforming items.
38    fn process_file(file: File) -> File {
39        let items = file
40            .items
41            .into_iter()
42            .filter_map(Self::process_item)
43            .collect();
44
45        File {
46            shebang: file.shebang,
47            attrs: Self::filter_module_docs(file.attrs),
48            items,
49        }
50    }
51
52    /// Filter out module-level doc comments (//!).
53    fn filter_module_docs(attrs: Vec<Attribute>) -> Vec<Attribute> {
54        attrs
55            .into_iter()
56            .filter(|attr| {
57                // Keep non-doc attributes
58                !attr.path().is_ident("doc")
59                    // Or if it's a doc attribute, only keep regular /// comments (outer)
60                    || matches!(attr.style, syn::AttrStyle::Outer)
61            })
62            .collect()
63    }
64
65    /// Process a single item, returning None to remove it.
66    fn process_item(item: Item) -> Option<Item> {
67        match item {
68            Item::Fn(mut func) => {
69                // Remove main function
70                if func.sig.ident == "main" {
71                    return None;
72                }
73
74                // Strip #[venus::cell] attribute
75                func.attrs.retain(|attr| !Self::is_venus_cell_attr(attr));
76
77                Some(Item::Fn(func))
78            }
79            // Keep all other items unchanged
80            other => Some(other),
81        }
82    }
83
84    /// Check if an attribute is #[venus::cell].
85    fn is_venus_cell_attr(attr: &Attribute) -> bool {
86        let path = attr.path();
87
88        // Check for #[venus::cell]
89        if path.segments.len() == 2 {
90            let first = &path.segments[0];
91            let second = &path.segments[1];
92            return first.ident == "venus" && second.ident == "cell";
93        }
94
95        false
96    }
97
98    /// Convert tokens back to formatted source code.
99    fn tokens_to_string(file: File) -> String {
100        let tokens: TokenStream = file.into_token_stream();
101        tokens.to_string()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_strips_module_docs() {
111        let source = r#"
112//! Module documentation
113//! More docs
114
115use something;
116
117fn foo() -> i32 { 42 }
118"#;
119        let result = NotebookSourceProcessor::process_for_production(source).unwrap();
120
121        assert!(!result.contains("//!"));
122        assert!(!result.contains("Module documentation"));
123        assert!(result.contains("use something"));
124        assert!(result.contains("fn foo"));
125    }
126
127    #[test]
128    fn test_strips_venus_cell_attribute() {
129        let source = r#"
130#[venus::cell]
131pub fn my_cell() -> i32 { 42 }
132
133#[derive(Debug)]
134struct MyStruct { x: i32 }
135"#;
136        let result = NotebookSourceProcessor::process_for_production(source).unwrap();
137
138        // Venus cell attribute should be stripped
139        assert!(!result.contains("venus :: cell") && !result.contains("venus::cell"));
140        // Function should be preserved
141        assert!(result.contains("pub fn my_cell"));
142        // Other attributes should be preserved (syn may tokenize with spaces)
143        assert!(result.contains("derive") && result.contains("Debug"));
144        assert!(result.contains("struct MyStruct"));
145    }
146
147    #[test]
148    fn test_removes_main_function() {
149        let source = r#"
150fn helper() -> i32 { 1 }
151
152fn main() {
153    println!("Hello");
154    let x = {
155        let y = 2;
156        y + 1
157    };
158}
159
160fn another() -> i32 { 2 }
161"#;
162        let result = NotebookSourceProcessor::process_for_production(source).unwrap();
163
164        assert!(result.contains("fn helper"));
165        assert!(result.contains("fn another"));
166        assert!(!result.contains("fn main"));
167        assert!(!result.contains("println"));
168    }
169
170    #[test]
171    fn test_handles_braces_in_strings() {
172        let source = r#"
173fn foo() -> String {
174    "this has { braces } inside".to_string()
175}
176
177fn main() {
178    println!("main function");
179}
180"#;
181        let result = NotebookSourceProcessor::process_for_production(source).unwrap();
182
183        // Should preserve foo with its string containing braces
184        assert!(result.contains("fn foo"));
185        assert!(result.contains("braces"));
186        // Should remove main
187        assert!(!result.contains("fn main"));
188    }
189
190    #[test]
191    fn test_handles_braces_in_comments() {
192        let source = r#"
193// This comment has { braces }
194fn foo() -> i32 {
195    /* Multi-line comment with { braces } */
196    42
197}
198
199fn main() { }
200"#;
201        let result = NotebookSourceProcessor::process_for_production(source).unwrap();
202
203        assert!(result.contains("fn foo"));
204        assert!(!result.contains("fn main"));
205    }
206
207    #[test]
208    fn test_preserves_regular_doc_comments() {
209        let source = r#"
210//! Module doc (should be removed)
211
212/// Function doc (should be kept)
213fn foo() -> i32 { 42 }
214"#;
215        let result = NotebookSourceProcessor::process_for_production(source).unwrap();
216
217        assert!(!result.contains("Module doc"));
218        // Note: syn may normalize doc comments to #[doc = "..."]
219        // The important thing is the content is preserved
220        assert!(result.contains("foo"));
221    }
222
223    #[test]
224    fn test_handles_nested_functions() {
225        let source = r#"
226fn outer() -> i32 {
227    fn inner() -> i32 { 1 }
228    inner() + 1
229}
230
231fn main() {
232    outer();
233}
234"#;
235        let result = NotebookSourceProcessor::process_for_production(source).unwrap();
236
237        assert!(result.contains("fn outer"));
238        assert!(result.contains("fn inner"));
239        assert!(!result.contains("fn main"));
240    }
241}