1use std::path::{Path, PathBuf};
2
3#[derive(Debug)]
4pub enum RustParseError {
5 IoError(std::io::Error),
6 SynError(syn::Error),
7}
8
9impl From<std::io::Error> for RustParseError {
10 fn from(err: std::io::Error) -> Self {
11 Self::IoError(err)
12 }
13}
14
15impl From<syn::Error> for RustParseError {
16 fn from(err: syn::Error) -> Self {
17 Self::SynError(err)
18 }
19}
20
21impl std::fmt::Display for RustParseError {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 Self::IoError(e) => write!(f, "IO error: {e}"),
25 Self::SynError(e) => write!(f, "Syn parse error: {e}"),
26 }
27 }
28}
29
30impl std::error::Error for RustParseError {}
31
32pub struct ParsedRustFile {
33 pub path: PathBuf,
34 pub source: String,
35 pub ast: syn::File,
36}
37
38pub fn parse_rust_file(path: &Path) -> Result<ParsedRustFile, RustParseError> {
39 let source = std::fs::read_to_string(path)?;
40 let ast = syn::parse_file(&source)?;
41
42 Ok(ParsedRustFile {
43 path: path.to_path_buf(),
44 source,
45 ast,
46 })
47}
48
49pub fn parse_rust_files(paths: &[PathBuf]) -> Vec<Result<ParsedRustFile, RustParseError>> {
52 paths.iter().map(|path| parse_rust_file(path)).collect()
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use std::io::Write;
59 use tempfile::NamedTempFile;
60
61 #[test]
62 fn parses_simple_rust_file() {
63 let mut file = NamedTempFile::with_suffix(".rs").unwrap();
64 writeln!(file, "fn main() {{ println!(\"hello\"); }}").unwrap();
65
66 let parsed = parse_rust_file(file.path()).expect("should parse");
67
68 assert_eq!(parsed.path, file.path());
69 assert!(!parsed.source.is_empty());
70 assert!(!parsed.ast.items.is_empty());
71 }
72
73 #[test]
74 fn parses_rust_file_with_struct_and_impl() {
75 let mut file = NamedTempFile::with_suffix(".rs").unwrap();
76 writeln!(
77 file,
78 r"
79struct Counter {{ value: i32 }}
80
81impl Counter {{
82 fn new() -> Self {{ Counter {{ value: 0 }} }}
83 fn increment(&mut self) {{ self.value += 1; }}
84}}
85"
86 )
87 .unwrap();
88
89 let parsed = parse_rust_file(file.path()).expect("should parse");
90
91 assert!(parsed.ast.items.len() >= 2);
92 }
93
94 #[test]
95 fn returns_error_for_invalid_rust() {
96 let mut file = NamedTempFile::with_suffix(".rs").unwrap();
97 writeln!(file, "fn broken {{ }}").unwrap();
98
99 let result = parse_rust_file(file.path());
100 assert!(result.is_err());
101 }
102
103 #[test]
104 fn returns_error_for_nonexistent_file() {
105 let result = parse_rust_file(Path::new("nonexistent_file.rs"));
106 assert!(result.is_err());
107 }
108
109 #[test]
110 fn test_rust_parse_error_enum() {
111 let io_err =
112 RustParseError::IoError(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
113 assert!(matches!(io_err, RustParseError::IoError(_)));
114 }
115
116 #[test]
117 fn test_rust_parse_error_display_fmt() {
118 use std::fmt::Write;
119 let err =
120 RustParseError::IoError(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
121 let mut s = String::new();
122 write!(&mut s, "{err}").unwrap();
123 assert!(s.contains("IO error"));
124 }
125
126 #[test]
127 fn test_parsed_rust_file_struct() {
128 let mut file = NamedTempFile::with_suffix(".rs").unwrap();
129 writeln!(file, "fn foo() {{}}").unwrap();
130 let parsed = parse_rust_file(file.path()).unwrap();
131 assert!(!parsed.source.is_empty());
132 assert!(!parsed.ast.items.is_empty());
133 }
134
135 #[test]
136 fn test_parse_rust_files() {
137 let mut f1 = NamedTempFile::with_suffix(".rs").unwrap();
138 let mut f2 = NamedTempFile::with_suffix(".rs").unwrap();
139 writeln!(f1, "fn a() {{}}").unwrap();
140 writeln!(f2, "fn b() {{}}").unwrap();
141 let paths = vec![f1.path().to_path_buf(), f2.path().to_path_buf()];
142 let results = parse_rust_files(&paths);
143 assert_eq!(results.len(), 2);
144 assert!(results.iter().all(std::result::Result::is_ok));
145 }
146}