herolib-code 0.3.13

Code analysis and parsing utilities for Rust source files
Documentation
//! Parser module for analyzing Rust source code.
//!
//! This module provides functionality to walk directories, parse Rust source files,
//! and extract structured information about code elements including:
//!
//! - **Enums**: With variants, documentation, and attributes
//! - **Structs**: With fields, documentation, and attributes
//! - **Methods**: As implemented on structs, with parameters and documentation
//!
//! # Example
//!
//! ```no_run
//! use herolib_code::parser::{CodebaseParser, WalkerConfig};
//!
//! // Parse a directory with default settings
//! let parser = CodebaseParser::new();
//! let codebase = parser.parse_directory("./src").unwrap();
//!
//! // Print all discovered structs
//! for struct_info in &codebase.structs {
//!     println!("Struct: {}", struct_info.name);
//!     if let Some(doc) = &struct_info.doc_comment {
//!         println!("  Doc: {}", doc);
//!     }
//!     for method in &struct_info.methods {
//!         println!("  Method: {}", method.name);
//!     }
//! }
//! ```

mod error;
mod rust_parser;
mod types;
mod walker;

pub use error::{ParseError, ParseResult};
pub use rust_parser::RustParser;
pub use types::{
    CodeBase, EnumInfo, EnumVariant, FieldInfo, FileInfo, MethodInfo, ParameterInfo, Receiver,
    StructInfo, Visibility,
};
pub use walker::{DirectoryWalker, WalkerConfig};

use std::path::Path;

/// High-level codebase parser that combines directory walking with Rust parsing.
///
/// This is the main entry point for parsing a Rust codebase. It walks through
/// directories, discovers Rust source files, and parses them to extract
/// structured code information.
pub struct CodebaseParser {
    /// Configuration for the directory walker.
    walker_config: WalkerConfig,
    /// Whether to include private items.
    include_private: bool,
    /// Whether to continue parsing on syntax errors.
    continue_on_error: bool,
}

impl Default for CodebaseParser {
    fn default() -> Self {
        Self::new()
    }
}

impl CodebaseParser {
    /// Creates a new CodebaseParser with default settings.
    pub fn new() -> Self {
        Self {
            walker_config: WalkerConfig::default(),
            include_private: true,
            continue_on_error: true,
        }
    }

    /// Sets the walker configuration.
    pub fn walker_config(mut self, config: WalkerConfig) -> Self {
        self.walker_config = config;
        self
    }

    /// Sets whether to include private items.
    pub fn include_private(mut self, include: bool) -> Self {
        self.include_private = include;
        self
    }

    /// Sets whether to continue parsing when a file has syntax errors.
    pub fn continue_on_error(mut self, continue_on_error: bool) -> Self {
        self.continue_on_error = continue_on_error;
        self
    }

    /// Parses all Rust files in the given directory.
    ///
    /// # Arguments
    ///
    /// * `path` - Path to the directory to parse.
    ///
    /// # Returns
    ///
    /// A CodeBase containing all parsed elements from the directory.
    ///
    /// # Errors
    ///
    /// Returns an error if the directory cannot be walked or if parsing fails
    /// (when `continue_on_error` is false).
    pub fn parse_directory<P: AsRef<Path>>(&self, path: P) -> ParseResult<CodeBase> {
        let path = path.as_ref();
        let walker = DirectoryWalker::new(self.walker_config.clone());
        let rust_files = walker.discover_rust_files(path)?;

        let rust_parser = RustParser::new().include_private(self.include_private);
        let mut codebase = CodeBase::new();

        for file_path in rust_files {
            match rust_parser.parse_file(&file_path) {
                Ok(file_codebase) => {
                    codebase.merge(file_codebase);
                }
                Err(e) => {
                    if !self.continue_on_error {
                        return Err(e);
                    }
                    // Log or ignore the error and continue
                    eprintln!("Warning: Failed to parse {:?}: {}", file_path, e);
                }
            }
        }

        Ok(codebase)
    }

    /// Parses a single Rust source file.
    ///
    /// # Arguments
    ///
    /// * `path` - Path to the Rust source file.
    ///
    /// # Returns
    ///
    /// A CodeBase containing all parsed elements from the file.
    pub fn parse_file<P: AsRef<Path>>(&self, path: P) -> ParseResult<CodeBase> {
        let rust_parser = RustParser::new().include_private(self.include_private);
        rust_parser.parse_file(path)
    }

    /// Parses Rust source code from a string.
    ///
    /// # Arguments
    ///
    /// * `source` - The Rust source code as a string.
    /// * `file_path` - The path to use for error messages.
    ///
    /// # Returns
    ///
    /// A CodeBase containing all parsed elements.
    pub fn parse_source(&self, source: &str, file_path: &str) -> ParseResult<CodeBase> {
        let rust_parser = RustParser::new().include_private(self.include_private);
        rust_parser.parse_source(source, file_path.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_codebase_parser_directory() {
        let dir = tempdir().unwrap();
        let dir_path = dir.path();

        // Create some Rust files
        fs::write(
            dir_path.join("lib.rs"),
            r#"
/// A test struct.
pub struct TestStruct {
    /// A field.
    pub value: i32,
}

impl TestStruct {
    /// Creates a new instance.
    pub fn new(value: i32) -> Self {
        Self { value }
    }
}
"#,
        )
        .unwrap();

        fs::write(
            dir_path.join("enums.rs"),
            r#"
/// A test enum.
#[derive(Debug, Clone)]
pub enum TestEnum {
    /// First variant.
    First,
    /// Second variant.
    Second(i32),
}
"#,
        )
        .unwrap();

        let parser = CodebaseParser::new();
        let codebase = parser.parse_directory(dir_path).unwrap();

        assert_eq!(codebase.structs.len(), 1);
        assert_eq!(codebase.enums.len(), 1);
        assert_eq!(codebase.files.len(), 2);

        let test_struct = &codebase.structs[0];
        assert_eq!(test_struct.name, "TestStruct");
        assert_eq!(test_struct.methods.len(), 1);

        let test_enum = &codebase.enums[0];
        assert_eq!(test_enum.name, "TestEnum");
        assert_eq!(test_enum.variants.len(), 2);
    }

    #[test]
    fn test_codebase_parser_source() {
        let source = r#"
pub struct Point {
    pub x: f64,
    pub y: f64,
}
"#;

        let parser = CodebaseParser::new();
        let codebase = parser.parse_source(source, "test.rs").unwrap();

        assert_eq!(codebase.structs.len(), 1);
        assert_eq!(codebase.structs[0].name, "Point");
    }
}