openapi_from_source/
parser.rs

1use anyhow::{Context, Result};
2use log::{debug, warn};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6/// AST (Abstract Syntax Tree) parser for Rust source files.
7///
8/// The `AstParser` uses the `syn` crate to parse Rust source code into an abstract syntax tree,
9/// which can then be analyzed to extract route definitions, type information, and other metadata.
10///
11/// # Example
12///
13/// ```no_run
14/// use openapi_from_source::parser::AstParser;
15/// use std::path::Path;
16///
17/// let parsed = AstParser::parse_file(Path::new("src/main.rs")).unwrap();
18/// println!("Parsed {} items", parsed.syntax_tree.items.len());
19/// ```
20pub struct AstParser;
21
22/// A successfully parsed Rust file with its abstract syntax tree.
23///
24/// Contains both the original file path and the parsed syntax tree structure.
25#[derive(Debug)]
26pub struct ParsedFile {
27    /// Path to the source file
28    pub path: PathBuf,
29    /// The parsed abstract syntax tree
30    pub syntax_tree: syn::File,
31}
32
33impl AstParser {
34    /// Parses a single Rust source file into an AST.
35    ///
36    /// This method reads the file content and uses `syn::parse_file` to parse it into
37    /// a syntax tree. If parsing fails (e.g., due to syntax errors), an error is returned.
38    ///
39    /// # Arguments
40    ///
41    /// * `path` - Path to the Rust source file to parse
42    ///
43    /// # Returns
44    ///
45    /// Returns a `ParsedFile` containing the file path and syntax tree on success.
46    ///
47    /// # Errors
48    ///
49    /// Returns an error if:
50    /// - The file cannot be read
51    /// - The file contains invalid Rust syntax
52    pub fn parse_file(path: &Path) -> Result<ParsedFile> {
53        debug!("Parsing file: {}", path.display());
54        
55        // Read file content
56        let content = fs::read_to_string(path)
57            .with_context(|| format!("Failed to read file: {}", path.display()))?;
58        
59        // Parse the file using syn
60        let syntax_tree = syn::parse_file(&content)
61            .with_context(|| format!("Failed to parse Rust syntax in file: {}", path.display()))?;
62        
63        debug!("Successfully parsed file: {}", path.display());
64        
65        Ok(ParsedFile {
66            path: path.to_path_buf(),
67            syntax_tree,
68        })
69    }
70
71    /// Parses multiple Rust source files, continuing even if some fail.
72    ///
73    /// This method attempts to parse all provided files, collecting both successes and failures.
74    /// Files that fail to parse are logged as warnings, but parsing continues for remaining files.
75    /// This allows the tool to generate partial documentation even when some files have syntax errors.
76    ///
77    /// # Arguments
78    ///
79    /// * `paths` - Slice of file paths to parse
80    ///
81    /// # Returns
82    ///
83    /// Returns a vector of `Result<ParsedFile>`, one for each input path. Successful parses
84    /// contain `Ok(ParsedFile)`, while failures contain `Err` with error details.
85    pub fn parse_files(paths: &[PathBuf]) -> Vec<Result<ParsedFile>> {
86        debug!("Parsing {} files", paths.len());
87        
88        let results: Vec<Result<ParsedFile>> = paths
89            .iter()
90            .map(|path| {
91                match Self::parse_file(path) {
92                    Ok(parsed) => Ok(parsed),
93                    Err(e) => {
94                        warn!("Failed to parse {}: {}", path.display(), e);
95                        Err(e)
96                    }
97                }
98            })
99            .collect();
100        
101        let success_count = results.iter().filter(|r| r.is_ok()).count();
102        let failure_count = results.len() - success_count;
103        
104        debug!(
105            "Parsing complete: {} succeeded, {} failed",
106            success_count, failure_count
107        );
108        
109        results
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use std::fs;
117    use std::io::Write;
118    use tempfile::TempDir;
119
120    /// Helper function to create a temporary file with content
121    fn create_temp_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
122        let file_path = dir.path().join(name);
123        let mut file = fs::File::create(&file_path).unwrap();
124        file.write_all(content.as_bytes()).unwrap();
125        file_path
126    }
127
128    #[test]
129    fn test_parse_valid_rust_file() {
130        let temp_dir = TempDir::new().unwrap();
131        let valid_code = r#"
132            use std::collections::HashMap;
133
134            pub struct User {
135                pub id: u32,
136                pub name: String,
137            }
138
139            pub fn get_user(id: u32) -> Option<User> {
140                None
141            }
142        "#;
143
144        let file_path = create_temp_file(&temp_dir, "valid.rs", valid_code);
145        let result = AstParser::parse_file(&file_path);
146
147        assert!(result.is_ok());
148        let parsed = result.unwrap();
149        assert_eq!(parsed.path, file_path);
150        assert!(!parsed.syntax_tree.items.is_empty());
151    }
152
153    #[test]
154    fn test_parse_invalid_rust_file() {
155        let temp_dir = TempDir::new().unwrap();
156        let invalid_code = r#"
157            pub struct User {
158                pub id: u32
159                pub name: String  // Missing comma
160            }
161            
162            fn broken( {  // Invalid syntax
163                let x = ;
164            }
165        "#;
166
167        let file_path = create_temp_file(&temp_dir, "invalid.rs", invalid_code);
168        let result = AstParser::parse_file(&file_path);
169
170        assert!(result.is_err());
171        let err_msg = result.unwrap_err().to_string();
172        assert!(err_msg.contains("Failed to parse Rust syntax"));
173    }
174
175    #[test]
176    fn test_parse_nonexistent_file() {
177        let result = AstParser::parse_file(Path::new("/nonexistent/file.rs"));
178
179        assert!(result.is_err());
180        let err_msg = result.unwrap_err().to_string();
181        assert!(err_msg.contains("Failed to read file"));
182    }
183
184    #[test]
185    fn test_parse_empty_file() {
186        let temp_dir = TempDir::new().unwrap();
187        let file_path = create_temp_file(&temp_dir, "empty.rs", "");
188        let result = AstParser::parse_file(&file_path);
189
190        assert!(result.is_ok());
191        let parsed = result.unwrap();
192        assert!(parsed.syntax_tree.items.is_empty());
193    }
194
195    #[test]
196    fn test_parse_files_batch() {
197        let temp_dir = TempDir::new().unwrap();
198
199        let valid_code1 = "pub fn hello() {}";
200        let valid_code2 = "pub struct World;";
201        let invalid_code = "pub fn broken( {";
202
203        let file1 = create_temp_file(&temp_dir, "file1.rs", valid_code1);
204        let file2 = create_temp_file(&temp_dir, "file2.rs", valid_code2);
205        let file3 = create_temp_file(&temp_dir, "file3.rs", invalid_code);
206
207        let paths = vec![file1.clone(), file2.clone(), file3.clone()];
208        let results = AstParser::parse_files(&paths);
209
210        assert_eq!(results.len(), 3);
211
212        // First two should succeed
213        assert!(results[0].is_ok());
214        assert!(results[1].is_ok());
215
216        // Third should fail
217        assert!(results[2].is_err());
218
219        // Verify the successful parses
220        assert_eq!(results[0].as_ref().unwrap().path, file1);
221        assert_eq!(results[1].as_ref().unwrap().path, file2);
222    }
223
224    #[test]
225    fn test_parse_files_all_valid() {
226        let temp_dir = TempDir::new().unwrap();
227
228        let code1 = "pub fn func1() {}";
229        let code2 = "pub fn func2() {}";
230        let code3 = "pub fn func3() {}";
231
232        let file1 = create_temp_file(&temp_dir, "a.rs", code1);
233        let file2 = create_temp_file(&temp_dir, "b.rs", code2);
234        let file3 = create_temp_file(&temp_dir, "c.rs", code3);
235
236        let paths = vec![file1, file2, file3];
237        let results = AstParser::parse_files(&paths);
238
239        assert_eq!(results.len(), 3);
240        assert!(results.iter().all(|r| r.is_ok()));
241    }
242
243    #[test]
244    fn test_parse_files_all_invalid() {
245        let temp_dir = TempDir::new().unwrap();
246
247        let invalid1 = "pub fn broken( {";
248        let invalid2 = "struct Missing }";
249        let invalid3 = "let x = ;";
250
251        let file1 = create_temp_file(&temp_dir, "bad1.rs", invalid1);
252        let file2 = create_temp_file(&temp_dir, "bad2.rs", invalid2);
253        let file3 = create_temp_file(&temp_dir, "bad3.rs", invalid3);
254
255        let paths = vec![file1, file2, file3];
256        let results = AstParser::parse_files(&paths);
257
258        assert_eq!(results.len(), 3);
259        assert!(results.iter().all(|r| r.is_err()));
260    }
261
262    #[test]
263    fn test_parse_files_empty_list() {
264        let paths: Vec<PathBuf> = vec![];
265        let results = AstParser::parse_files(&paths);
266
267        assert_eq!(results.len(), 0);
268    }
269
270    #[test]
271    fn test_parse_file_with_complex_syntax() {
272        let temp_dir = TempDir::new().unwrap();
273        let complex_code = r#"
274            use serde::{Deserialize, Serialize};
275            use std::collections::HashMap;
276
277            #[derive(Debug, Serialize, Deserialize)]
278            pub struct User {
279                pub id: u32,
280                #[serde(rename = "userName")]
281                pub name: String,
282                pub email: Option<String>,
283            }
284
285            impl User {
286                pub fn new(id: u32, name: String) -> Self {
287                    Self {
288                        id,
289                        name,
290                        email: None,
291                    }
292                }
293            }
294
295            pub async fn get_users() -> Vec<User> {
296                vec![]
297            }
298        "#;
299
300        let file_path = create_temp_file(&temp_dir, "complex.rs", complex_code);
301        let result = AstParser::parse_file(&file_path);
302
303        assert!(result.is_ok());
304        let parsed = result.unwrap();
305        
306        // Should have multiple items (use statements, struct, impl, function)
307        assert!(parsed.syntax_tree.items.len() >= 4);
308    }
309}