Skip to main content

kiss/
rust_parsing.rs

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
49// Note: Cannot use par_iter() here because syn::File contains proc_macro2 types
50// which are not Send. Parallelism is applied during analysis instead.
51pub 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}