herolib_code/parser/
mod.rs

1//! Parser module for analyzing Rust source code.
2//!
3//! This module provides functionality to walk directories, parse Rust source files,
4//! and extract structured information about code elements including:
5//!
6//! - **Enums**: With variants, documentation, and attributes
7//! - **Structs**: With fields, documentation, and attributes
8//! - **Methods**: As implemented on structs, with parameters and documentation
9//!
10//! # Example
11//!
12//! ```no_run
13//! use herolib_code::parser::{CodebaseParser, WalkerConfig};
14//!
15//! // Parse a directory with default settings
16//! let parser = CodebaseParser::new();
17//! let codebase = parser.parse_directory("./src").unwrap();
18//!
19//! // Print all discovered structs
20//! for struct_info in &codebase.structs {
21//!     println!("Struct: {}", struct_info.name);
22//!     if let Some(doc) = &struct_info.doc_comment {
23//!         println!("  Doc: {}", doc);
24//!     }
25//!     for method in &struct_info.methods {
26//!         println!("  Method: {}", method.name);
27//!     }
28//! }
29//! ```
30
31mod error;
32mod rust_parser;
33mod types;
34mod walker;
35
36pub use error::{ParseError, ParseResult};
37pub use rust_parser::RustParser;
38pub use types::{
39    CodeBase, EnumInfo, EnumVariant, FieldInfo, FileInfo, MethodInfo, ParameterInfo, Receiver,
40    StructInfo, Visibility,
41};
42pub use walker::{DirectoryWalker, WalkerConfig};
43
44use std::path::Path;
45
46/// High-level codebase parser that combines directory walking with Rust parsing.
47///
48/// This is the main entry point for parsing a Rust codebase. It walks through
49/// directories, discovers Rust source files, and parses them to extract
50/// structured code information.
51pub struct CodebaseParser {
52    /// Configuration for the directory walker.
53    walker_config: WalkerConfig,
54    /// Whether to include private items.
55    include_private: bool,
56    /// Whether to continue parsing on syntax errors.
57    continue_on_error: bool,
58}
59
60impl Default for CodebaseParser {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl CodebaseParser {
67    /// Creates a new CodebaseParser with default settings.
68    pub fn new() -> Self {
69        Self {
70            walker_config: WalkerConfig::default(),
71            include_private: true,
72            continue_on_error: true,
73        }
74    }
75
76    /// Sets the walker configuration.
77    pub fn walker_config(mut self, config: WalkerConfig) -> Self {
78        self.walker_config = config;
79        self
80    }
81
82    /// Sets whether to include private items.
83    pub fn include_private(mut self, include: bool) -> Self {
84        self.include_private = include;
85        self
86    }
87
88    /// Sets whether to continue parsing when a file has syntax errors.
89    pub fn continue_on_error(mut self, continue_on_error: bool) -> Self {
90        self.continue_on_error = continue_on_error;
91        self
92    }
93
94    /// Parses all Rust files in the given directory.
95    ///
96    /// # Arguments
97    ///
98    /// * `path` - Path to the directory to parse.
99    ///
100    /// # Returns
101    ///
102    /// A CodeBase containing all parsed elements from the directory.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the directory cannot be walked or if parsing fails
107    /// (when `continue_on_error` is false).
108    pub fn parse_directory<P: AsRef<Path>>(&self, path: P) -> ParseResult<CodeBase> {
109        let path = path.as_ref();
110        let walker = DirectoryWalker::new(self.walker_config.clone());
111        let rust_files = walker.discover_rust_files(path)?;
112
113        let rust_parser = RustParser::new().include_private(self.include_private);
114        let mut codebase = CodeBase::new();
115
116        for file_path in rust_files {
117            match rust_parser.parse_file(&file_path) {
118                Ok(file_codebase) => {
119                    codebase.merge(file_codebase);
120                }
121                Err(e) => {
122                    if !self.continue_on_error {
123                        return Err(e);
124                    }
125                    // Log or ignore the error and continue
126                    eprintln!("Warning: Failed to parse {:?}: {}", file_path, e);
127                }
128            }
129        }
130
131        Ok(codebase)
132    }
133
134    /// Parses a single Rust source file.
135    ///
136    /// # Arguments
137    ///
138    /// * `path` - Path to the Rust source file.
139    ///
140    /// # Returns
141    ///
142    /// A CodeBase containing all parsed elements from the file.
143    pub fn parse_file<P: AsRef<Path>>(&self, path: P) -> ParseResult<CodeBase> {
144        let rust_parser = RustParser::new().include_private(self.include_private);
145        rust_parser.parse_file(path)
146    }
147
148    /// Parses Rust source code from a string.
149    ///
150    /// # Arguments
151    ///
152    /// * `source` - The Rust source code as a string.
153    /// * `file_path` - The path to use for error messages.
154    ///
155    /// # Returns
156    ///
157    /// A CodeBase containing all parsed elements.
158    pub fn parse_source(&self, source: &str, file_path: &str) -> ParseResult<CodeBase> {
159        let rust_parser = RustParser::new().include_private(self.include_private);
160        rust_parser.parse_source(source, file_path.to_string())
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use std::fs;
168    use tempfile::tempdir;
169
170    #[test]
171    fn test_codebase_parser_directory() {
172        let dir = tempdir().unwrap();
173        let dir_path = dir.path();
174
175        // Create some Rust files
176        fs::write(
177            dir_path.join("lib.rs"),
178            r#"
179/// A test struct.
180pub struct TestStruct {
181    /// A field.
182    pub value: i32,
183}
184
185impl TestStruct {
186    /// Creates a new instance.
187    pub fn new(value: i32) -> Self {
188        Self { value }
189    }
190}
191"#,
192        )
193        .unwrap();
194
195        fs::write(
196            dir_path.join("enums.rs"),
197            r#"
198/// A test enum.
199#[derive(Debug, Clone)]
200pub enum TestEnum {
201    /// First variant.
202    First,
203    /// Second variant.
204    Second(i32),
205}
206"#,
207        )
208        .unwrap();
209
210        let parser = CodebaseParser::new();
211        let codebase = parser.parse_directory(dir_path).unwrap();
212
213        assert_eq!(codebase.structs.len(), 1);
214        assert_eq!(codebase.enums.len(), 1);
215        assert_eq!(codebase.files.len(), 2);
216
217        let test_struct = &codebase.structs[0];
218        assert_eq!(test_struct.name, "TestStruct");
219        assert_eq!(test_struct.methods.len(), 1);
220
221        let test_enum = &codebase.enums[0];
222        assert_eq!(test_enum.name, "TestEnum");
223        assert_eq!(test_enum.variants.len(), 2);
224    }
225
226    #[test]
227    fn test_codebase_parser_source() {
228        let source = r#"
229pub struct Point {
230    pub x: f64,
231    pub y: f64,
232}
233"#;
234
235        let parser = CodebaseParser::new();
236        let codebase = parser.parse_source(source, "test.rs").unwrap();
237
238        assert_eq!(codebase.structs.len(), 1);
239        assert_eq!(codebase.structs[0].name, "Point");
240    }
241}