diff --git a/Cargo.toml b/Cargo.toml index ca436d0..85ec0bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "codebank" version = "0.4.5" -edition = "2024" +edition = "2021" description = """ A powerful code documentation generator that creates structured markdown documentation from your codebase. Supports multiple languages including Rust, Python, TypeScript, C, and Go with intelligent parsing and formatting. @@ -44,12 +44,12 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = [ "env-filter", ], optional = true } -tree-sitter = "0.23" -tree-sitter-cpp = "0.23" -tree-sitter-go = "0.23" -tree-sitter-python = "0.23" -tree-sitter-rust = "0.23" -tree-sitter-typescript = "0.23" +tree-sitter = "0.20.10" +tree-sitter-cpp = "0.20.1" +tree-sitter-go = "0.20.0" +tree-sitter-python = "0.20.0" +tree-sitter-rust = "0.20.3" +tree-sitter-typescript = "0.20.0" [dev-dependencies] tempfile = "3.19" diff --git a/fixtures/empty.rs b/fixtures/empty.rs new file mode 100644 index 0000000..e69de29 diff --git a/fixtures/only_comments.rs b/fixtures/only_comments.rs new file mode 100644 index 0000000..402ee78 --- /dev/null +++ b/fixtures/only_comments.rs @@ -0,0 +1,14 @@ +// This is a line comment. +//! This is an inner line comment, often used for module-level docs. + +/* +This is a block comment. +It can span multiple lines. +*/ + +/*! +This is an inner block comment. +Also for module-level docs usually. +*/ + +// Another line comment. diff --git a/fixtures/sample.rs b/fixtures/sample.rs index a0f43d0..d37620f 100644 --- a/fixtures/sample.rs +++ b/fixtures/sample.rs @@ -1,128 +1,148 @@ -//! This is a file-level documentation comment -//! It describes the purpose of this file +//! This is a file-level documentation comment. +//! It describes the purpose of this sample file which includes a variety of Rust items. -/// This is a public module +// Example of extern crate +extern crate proc_macro; +extern crate serde as serde_renamed; + + +// Example of use declarations +use std::collections::HashMap; +use std::fmt::{self, Debug as FmtDebug}; // aliased import +use crate::public_module::PublicStruct; // use item from same crate + +/// This is a public module. +/// It has multiple lines of documentation. +#[cfg(feature = "some_feature")] +#[deprecated(note = "This module is old")] pub mod public_module { - /// This is a public struct with documentation + //! Inner documentation for public_module. + + /// This is a public struct with documentation. + /// It also has generics and attributes. #[derive(Debug, Clone)] - pub struct PublicStruct { + #[serde(rename_all = "camelCase")] + pub struct PublicStruct + where + U: Default, + { /// Public field with documentation - pub field: String, + pub field: T, /// Private field with documentation - private_field: i32, + private_field: U, // Changed to U for generic usage + #[doc="Inner attribute doc for another_field"] + pub another_field: i32, } /// This is a public trait with documentation - pub trait PublicTrait { - /// Method documentation - fn method(&self) -> String; + #[allow(unused_variables)] + pub trait PublicTrait { + /// Method documentation for trait. + fn method(&self, input: T) -> String; } /// This is a public enum with documentation #[derive(Debug)] pub enum PublicEnum { - /// Variant documentation + /// Variant documentation for Variant1 Variant1, - /// Another variant documentation + /// Another variant documentation for Variant2 + #[allow(dead_code)] // Attribute on variant Variant2(String), - /// Yet another variant documentation - Variant3 { field: i32 }, + /// Yet another variant documentation for Variant3 + /*! Block-style inner doc for Variant3 */ + Variant3 { + /// Field inside a variant + #[serde(skip)] + field: i32 + }, } - impl PublicStruct { + // Function with pub(crate) visibility + pub(crate) fn crate_visible_function() { + println!("This function is crate visible."); + } + + // Function with pub(super) visibility (relative to current module 'public_module') + // This would typically be in a nested module to make sense. + // For demonstration, let's put it here. If it were in `super::public_module::nested_mod`, + // `pub(super)` would make it visible to `public_module`. + // Here, it's visible to the crate root (super of public_module is the crate root). + pub(super) fn super_visible_function() { + println!("This function is super visible."); + } + + + impl PublicStruct { /// Constructor documentation - pub fn new(field: String, private_field: i32) -> Self { + pub fn new(field: T, private_field: U, another_field: i32) -> Self { Self { field, private_field, + another_field, } } /// Method documentation - pub fn get_private_field(&self) -> i32 { - self.private_field + pub fn get_private_field(&self) -> &U { + &self.private_field } } - impl PublicTrait for PublicStruct { - fn method(&self) -> String { - format!("{}: {}", self.field, self.private_field) + impl PublicTrait for PublicStruct { // Assuming U = String for this impl + fn method(&self, input: T) -> String { + format!("Field: {:?}, Private: {}, Input: {:?}", self.field, self.private_field, input) } } -} + + pub mod nested_module { + //! Inner docs for nested_module. + + // This function's pub(super) makes it visible to public_module + pub(super) fn visible_to_public_module() {} -/// This is a private module -mod private_module { - /// Private struct - struct PrivateStruct { - field: String, - } - - /// Private trait - trait PrivateTrait { - fn method(&self) -> String; - } - - /// Private enum - enum PrivateEnum { - Variant1, - Variant2(String), - } + // This function's pub(crate) makes it visible throughout the crate + pub(crate) fn crate_visible_from_nested() {} - impl PrivateStruct { - fn new(field: String) -> Self { - Self { field } - } + struct OnlyInNested {} } +} - impl PrivateTrait for PrivateStruct { - fn method(&self) -> String { - self.field.clone() - } - } +/// This is a private module +mod private_module { + /*! Inner block doc for private_module. */ + struct PrivateStruct { field: String } + trait PrivateTrait { fn method(&self) -> String; } + enum PrivateEnum { Variant1, Variant2(String) } + impl PrivateStruct { fn new(field: String) -> Self { Self { field } } } + impl PrivateTrait for PrivateStruct { fn method(&self) -> String { self.field.clone() } } } -/// This is a public function with documentation +/// A public function with multiple attributes and docs. +/// Second line of doc. +#[inline] +#[must_use = "Return value should be used"] pub fn public_function() -> String { "Hello, world!".to_string() } /// This is a private function with documentation -fn private_function() -> String { - "Private hello".to_string() +#[allow(dead_code)] +fn private_function(s: &str) -> String { + format!("Private hello: {}", s) } -/// This is a public macro with documentation -#[macro_export] -macro_rules! public_macro { - ($x:expr) => { - println!("{}", $x); - }; -} /// This is a public type alias with documentation -pub type PublicType = String; +pub type PublicTypeAlias = Result>; /// This is a public constant with documentation -pub const PUBLIC_CONSTANT: &str = "constant"; +pub const PUBLIC_CONSTANT: &str = "constant value"; /// This is a public static with documentation -pub static PUBLIC_STATIC: &str = "static"; +#[no_mangle] +pub static PUBLIC_STATIC_VAR: i32 = 100; -/// This is a public attribute with documentation -#[derive(Debug)] -pub struct AttributedStruct { - #[doc = "Field documentation"] - pub field: String, -} - -/// This is a public implementation block with documentation -impl AttributedStruct { - /// Method documentation - pub fn new(field: String) -> Self { - Self { field } - } -} /// This is a public generic struct with documentation pub struct GenericStruct { @@ -131,27 +151,54 @@ pub struct GenericStruct { } /// This is a public generic trait with documentation +#[allow(unused_variables)] pub trait GenericTrait { - /// Method documentation + /// Method documentation for trait fn method(&self, value: T) -> T; } -/// This is a public generic implementation with documentation +/// Implementation for GenericStruct. +#[allow(dead_code)] +impl GenericStruct { + /// Creates a new GenericStruct. + fn new(value: T) -> Self { + Self { field: value } + } +} + + +/// Implementation of GenericTrait for GenericStruct. +/// Includes a where clause. impl GenericTrait for GenericStruct where - T: Clone, + T: Clone + FmtDebug, // Added FmtDebug here { + /// Method from GenericTrait. fn method(&self, value: T) -> T { + println!("Value: {:?}", value); value.clone() } } +// A module defined in another file (declaration) +mod my_other_module; + #[cfg(test)] mod tests { - use super::*; + use super::*; // Imports items from the parent module (the file scope) #[test] - fn test_public_function() { + fn test_public_function_output() { // Renamed to avoid conflict assert_eq!(public_function(), "Hello, world!"); } + + #[test] + fn check_public_struct_instantiation() { + // This test is more about Rust syntax than parser, but ensures sample code is valid. + let _ps = public_module::PublicStruct { + field: "test".to_string(), + private_field: 10, // Original was i32, now U, assuming i32 for test. + another_field: 20, + }; + } } diff --git a/fixtures/sample_advanced.rs b/fixtures/sample_advanced.rs new file mode 100644 index 0000000..be709f2 --- /dev/null +++ b/fixtures/sample_advanced.rs @@ -0,0 +1,91 @@ +//! File for advanced Rust constructs. + +pub mod level1 { + pub mod level2 { + /// Struct deep inside modules. + #[derive(Default)] + pub(in crate::level1) struct DeepStruct { // pub(in path) visibility + pub field_a: String, + } + + impl DeepStruct { + pub fn new() -> Self { + Self::default() + } + } + } + + /// Function with complex generics and where clause. + pub fn complex_generic_function<'a, T, U>(param_t: T, param_u: &'a U) -> Result + where + T: std::fmt::Debug + Clone + Send + 'static, + U: std::error::Error + ?Sized, // ?Sized bound + for<'b> &'b U: Send, // Higher-rank trait bound + { + println!("T: {:?}, U: {}", param_t, param_u); + Ok(param_t.clone()) + } +} + +// Struct with lifetime and multiple generic parameters with bounds +pub struct AdvancedGenericStruct<'a, A, B> +where + A: AsRef<[u8]> + ?Sized, + B: 'a + Send + Sync, +{ + data_a: &'a A, + data_b: B, + pub simple_field: i32, +} + +impl<'a, A, B> AdvancedGenericStruct<'a, A, B> +where + A: AsRef<[u8]> + ?Sized, + B: 'a + Send + Sync, +{ + pub fn new(data_a: &'a A, data_b: B) -> Self { + Self { data_a, data_b, simple_field: 0 } + } +} + +// Enum with generics and where clause +pub enum GenericResult +where S: Send, E: std::fmt::Debug +{ + Success(S), + Failure(E), +} + +// Trait with associated types and complex bounds +pub trait AdvancedTrait { + type Item: Copy + Default; // Associated type with bounds + const VERSION: &'static str; + + fn process(&self, item: Self::Item) -> Result; + fn version() -> &'static str { Self::VERSION } +} + +// Impl for a specific type using the advanced trait +struct MyTypeForAdvancedTrait; + +impl AdvancedTrait for MyTypeForAdvancedTrait { + type Item = u32; + const VERSION: &'static str = "1.0-advanced"; + + fn process(&self, item: Self::Item) -> Result { + if item > 100 { + Err("Value too large".to_string()) + } else { + Ok(item * 2) + } + } +} + +// Unit struct for testing edge cases +pub struct MyUnitStruct; + +// Empty struct for testing edge cases +pub struct EmptyStruct {} + +// Struct with no fields, but curly braces +struct NoFieldsStruct {} diff --git a/src/parser/formatter/mod.rs b/src/parser/formatter/mod.rs index 1eac0f2..b95906f 100644 --- a/src/parser/formatter/mod.rs +++ b/src/parser/formatter/mod.rs @@ -1,6 +1,6 @@ mod python; mod rules; -mod rust; +mod rust; // Uncommented use rules::FormatterRules; use super::{FileUnit, FunctionUnit, ImplUnit, ModuleUnit, StructUnit, TraitUnit, Visibility}; @@ -24,123 +24,31 @@ impl Formatter for FileUnit { } } BankStrategy::NoTests => { - // Add file documentation if present if let Some(doc) = &self.doc { - output.push_str(&format!("{} {}\n", rules.doc_marker, doc)); - } - - // Add declarations - for decl in &self.declares { - output.push_str(&decl.source); - output.push('\n'); - } - - // Format each module (skip test modules) - for module in &self.modules { - if !rules.is_test_module(&module.name, &module.attributes) { - let formatted = module.format(strategy, language)?; - if !formatted.is_empty() { - output.push_str(&formatted); - output.push('\n'); - } - } - } - - // Format each function (skip test functions) - for function in &self.functions { - if !rules.is_test_function(&function.attributes) { - let formatted = function.format(strategy, language)?; - if !formatted.is_empty() { - output.push_str(&formatted); - output.push('\n'); - } - } - } - - // Format each struct - for struct_unit in &self.structs { - let formatted = struct_unit.format(strategy, language)?; - if !formatted.is_empty() { - output.push_str(&formatted); - output.push('\n'); - } - } - - // Format each trait - for trait_unit in &self.traits { - let formatted = trait_unit.format(strategy, language)?; - if !formatted.is_empty() { - output.push_str(&formatted); - output.push('\n'); - } - } - - // Format each impl - for impl_unit in &self.impls { - let formatted = impl_unit.format(strategy, language)?; - if !formatted.is_empty() { - output.push_str(&formatted); - output.push('\n'); - } - } + output.push_str(&format!("{} {}\n", rules.doc_marker, doc.replace("\n", &format!("\n{} ", rules.doc_marker)))); + } + for decl in &self.declares { output.push_str(&decl.source); output.push('\n'); } + for module in &self.modules { if !rules.is_test_module(&module.name, &module.attributes) { let fmt = module.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }}} + for function in &self.functions { if !rules.is_test_function(&function.attributes) { let fmt = function.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }}} + for struct_unit in &self.structs { let fmt = struct_unit.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }} + for trait_unit in &self.traits { let fmt = trait_unit.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }} + for impl_unit in &self.impls { let fmt = impl_unit.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }} } BankStrategy::Summary => { - // Add file documentation if present if let Some(doc) = &self.doc { - output.push_str(&format!("{} {}\n", rules.doc_marker, doc)); - } - - // Add declarations - for decl in &self.declares { - output.push_str(&decl.source); - output.push('\n'); - } - - for module in &self.modules { - if module.visibility == Visibility::Public { - let module_formatted = module.format(strategy, language)?; - output.push_str(&module_formatted); - output.push('\n'); - } - } - - // Format public functions - for function in &self.functions { - if function.visibility == Visibility::Public { - let function_formatted = function.format(strategy, language)?; - output.push_str(&function_formatted); - output.push('\n'); - } - } - - // Format public structs - for struct_unit in &self.structs { - if struct_unit.visibility == Visibility::Public { - let struct_formatted = struct_unit.format(strategy, language)?; - output.push_str(&struct_formatted); - output.push('\n'); - } - } - - // Format public traits - for trait_unit in &self.traits { - if trait_unit.visibility == Visibility::Public { - let trait_formatted = trait_unit.format(strategy, language)?; - output.push_str(&trait_formatted); - output.push('\n'); - } - } - - // Format impls (only showing public methods) - for impl_unit in &self.impls { - let impl_formatted = impl_unit.format(strategy, language)?; - output.push_str(&impl_formatted); - output.push('\n'); - } + output.push_str(&format!("{} {}\n", rules.doc_marker, doc.replace("\n", &format!("\n{} ", rules.doc_marker)))); + } + for decl in &self.declares { output.push_str(&decl.source); output.push('\n'); } // Keep all declarations + for module in &self.modules { if module.visibility == Visibility::Public { let fmt = module.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }}} + for function in &self.functions { if function.visibility == Visibility::Public { let fmt = function.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }}} + for struct_unit in &self.structs { if struct_unit.visibility == Visibility::Public { let fmt = struct_unit.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }}} + for trait_unit in &self.traits { if trait_unit.visibility == Visibility::Public { let fmt = trait_unit.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }}} + for impl_unit in &self.impls { let fmt = impl_unit.format(strategy, language)?; if !fmt.is_empty() { output.push_str(&fmt); output.push('\n'); }} } } - - Ok(output) + // Ensure a single trailing newline, but not if the output is empty. + let trimmed_output = output.trim_end_matches('\n'); + if trimmed_output.is_empty() { Ok(String::new()) } else { Ok(format!("{}\n", trimmed_output)) } } } @@ -150,223 +58,51 @@ impl Formatter for ModuleUnit { let mut output = String::new(); let rules = FormatterRules::for_language(language); - // Skip test modules entirely for Summary strategy - if *strategy == BankStrategy::Summary && rules.is_test_module(&self.name, &self.attributes) - { - return Ok(String::new()); - } + if *strategy == BankStrategy::Summary && rules.is_test_module(&self.name, &self.attributes) { return Ok(String::new()); } match strategy { - BankStrategy::Default => { - if let Some(source) = &self.source { - output.push_str(source); - } - } + BankStrategy::Default => { if let Some(source) = &self.source { output.push_str(source); } } BankStrategy::NoTests => { - // Add documentation - if let Some(doc) = &self.doc { - for line in doc.lines() { - output.push_str(&format!("{} {}\n", rules.doc_marker, line)); - } - } - - // Add attributes (including test attributes for NoTests) - for attr in &self.attributes { - output.push_str(&format!("{}\n", attr)); - } - - // Write module head - output.push_str(&format!( - "{} mod {} {{\n", - self.visibility.as_str(language), - self.name - )); - - // Add declarations - for decl in &self.declares { - output.push_str(&format!(" {}\n", decl.source)); - } - - // Format all functions (skip test functions) - for function in &self.functions { - if !rules.is_test_function(&function.attributes) { - let function_formatted = function.format(strategy, language)?; - if !function_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - function_formatted.replace("\n", "\n ") - )); - } - } - } - - // Format all structs - for struct_unit in &self.structs { - let struct_formatted = struct_unit.format(strategy, language)?; - if !struct_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - struct_formatted.replace("\n", "\n ") - )); - } - } - - // Format all traits - for trait_unit in &self.traits { - let trait_formatted = trait_unit.format(strategy, language)?; - if !trait_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - trait_formatted.replace("\n", "\n ") - )); - } - } - - // Format all impls - for impl_unit in &self.impls { - let impl_formatted = impl_unit.format(strategy, language)?; - if !impl_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - impl_formatted.replace("\n", "\n ") - )); - } - } - - // Format submodules - for submodule in &self.submodules { - let sub_formatted = submodule.format(strategy, language)?; - if !sub_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - sub_formatted.replace("\n", "\n ") - )); - } - } - + if let Some(doc) = &self.doc { for line in doc.lines() { output.push_str(&format!("{} {}\n", rules.doc_marker, line)); }} + for attr in &self.attributes { output.push_str(&format!("{}\n", attr)); } + let vis_str = self.visibility.as_str(language); + output.push_str(&format!("{}mod {} {{\n", if vis_str.is_empty() {""} else {vis_str.trim_end()}, self.name)); // Ensure no double space if vis_str is empty + for decl in &self.declares { output.push_str(&format!(" {}\n", decl.source)); } + for function in &self.functions { if !rules.is_test_function(&function.attributes) { let f_fmt = function.format(strategy, language)?; if !f_fmt.is_empty() {output.push_str(&format!(" {}\n\n", f_fmt.replace("\n", "\n ")));}}} + for struct_unit in &self.structs { let s_fmt = struct_unit.format(strategy, language)?; if !s_fmt.is_empty() {output.push_str(&format!(" {}\n\n", s_fmt.replace("\n", "\n ")));}} + for trait_unit in &self.traits { let t_fmt = trait_unit.format(strategy, language)?; if !t_fmt.is_empty() {output.push_str(&format!(" {}\n\n", t_fmt.replace("\n", "\n ")));}} + for impl_unit in &self.impls { let i_fmt = impl_unit.format(strategy, language)?; if !i_fmt.is_empty() {output.push_str(&format!(" {}\n\n", i_fmt.replace("\n", "\n ")));}} + for submodule in &self.submodules { let sub_fmt = submodule.format(strategy, language)?; if !sub_fmt.is_empty() {output.push_str(&format!(" {}\n\n", sub_fmt.replace("\n", "\n ")));}} output.push_str("}\n"); } BankStrategy::Summary => { - // Public modules only if self.visibility == Visibility::Public { - let fns: Vec<&FunctionUnit> = self - .functions - .iter() - .filter(|f| f.visibility == Visibility::Public) - .collect(); - let structs: Vec<&StructUnit> = self - .structs - .iter() - .filter(|s| s.visibility == Visibility::Public) - .collect(); - let traits: Vec<&TraitUnit> = self - .traits - .iter() - .filter(|t| t.visibility == Visibility::Public) - .collect(); - let impls: Vec<&ImplUnit> = self - .impls - .iter() - .filter(|i| i.methods.iter().any(|m| m.visibility == Visibility::Public)) - .collect(); - let mods: Vec<&ModuleUnit> = self - .submodules - .iter() - .filter(|m| m.visibility == Visibility::Public) - .collect(); - - if fns.is_empty() - && structs.is_empty() - && traits.is_empty() - && impls.is_empty() - && mods.is_empty() - { - return Ok(String::new()); - } - - // Add documentation - if let Some(doc) = &self.doc { - for line in doc.lines() { - output.push_str(&format!("{} {}\n", rules.doc_marker, line)); - } - } - // Add attributes (except test attributes) - for attr in &self.attributes { - if !rules.test_module_markers.contains(&attr.as_str()) { - output.push_str(&format!("{}\n", attr)); - } + let fns: Vec<&FunctionUnit> = self.functions.iter().filter(|f| f.visibility == Visibility::Public && !rules.is_test_function(&f.attributes)).collect(); + let structs: Vec<&StructUnit> = self.structs.iter().filter(|s| s.visibility == Visibility::Public).collect(); + let traits: Vec<&TraitUnit> = self.traits.iter().filter(|t| t.visibility == Visibility::Public).collect(); + let impls_to_format: Vec = self.impls.iter().map(|i| i.format(strategy, language).unwrap_or_default()).filter(|s| !s.is_empty()).collect(); + let mods: Vec<&ModuleUnit> = self.submodules.iter().filter(|m| m.visibility == Visibility::Public && !m.format(strategy, language).unwrap_or_default().is_empty() ).collect(); + + if fns.is_empty() && structs.is_empty() && traits.is_empty() && impls_to_format.is_empty() && mods.is_empty() && self.declares.is_empty() { + return Ok(String::new()); } + if let Some(doc) = &self.doc { for line in doc.lines() { output.push_str(&format!("{} {}\n", rules.doc_marker, line)); }} + for attr in &self.attributes { if !rules.test_module_markers.contains(&attr.as_str()) { output.push_str(&format!("{}\n", attr)); }} output.push_str(&format!("pub mod {} {{\n", self.name)); - - // Add declarations - for decl in &self.declares { - output.push_str(&format!(" {}\n", decl.source)); - } - - // Format public functions - for function in &fns { - if !rules.is_test_function(&function.attributes) { - let function_formatted = function.format(strategy, language)?; - if !function_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - function_formatted.replace("\n", "\n ") - )); - } - } - } - - // Format public structs - for struct_unit in &structs { - let struct_formatted = struct_unit.format(strategy, language)?; - if !struct_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - struct_formatted.replace("\n", "\n ") - )); - } - } - - // Format public traits - for trait_unit in &traits { - let trait_formatted = trait_unit.format(strategy, language)?; - if !trait_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - trait_formatted.replace("\n", "\n ") - )); - } - } - - // Format impls (showing public methods) - for impl_unit in &impls { - let impl_formatted = impl_unit.format(strategy, language)?; - if !impl_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - impl_formatted.replace("\n", "\n ") - )); - } - } - - // Format public submodules - for submodule in &mods { - let sub_formatted = submodule.format(strategy, language)?; - if !sub_formatted.is_empty() { - output.push_str(&format!( - " {}\n\n", - sub_formatted.replace("\n", "\n ") - )); - } - } - + for decl in &self.declares { output.push_str(&format!(" {}\n", decl.source)); } + if !self.declares.is_empty() && (!fns.is_empty() || !structs.is_empty() || !traits.is_empty() || !impls_to_format.is_empty() || !mods.is_empty()) { output.push('\n'); } + + for function in fns { let f_fmt = function.format(strategy, language)?; if !f_fmt.is_empty() {output.push_str(&format!(" {}\n\n", f_fmt.replace("\n", "\n ")));}} + for struct_unit in structs { let s_fmt = struct_unit.format(strategy, language)?; if !s_fmt.is_empty() {output.push_str(&format!(" {}\n\n", s_fmt.replace("\n", "\n ")));}} + for trait_unit in traits { let t_fmt = trait_unit.format(strategy, language)?; if !t_fmt.is_empty() {output.push_str(&format!(" {}\n\n", t_fmt.replace("\n", "\n ")));}} + for impl_formatted_str in impls_to_format { if !impl_formatted_str.is_empty() {output.push_str(&format!(" {}\n\n", impl_formatted_str.replace("\n", "\n ")));}} + for submodule in mods { let sub_fmt = submodule.format(strategy, language)?; if !sub_fmt.is_empty() {output.push_str(&format!(" {}\n\n", sub_fmt.replace("\n", "\n ")));}} output.push_str("}\n"); } } } - - Ok(output) + Ok(output.trim_end_matches('\n').to_string() + "\n") } } @@ -376,74 +112,32 @@ impl Formatter for FunctionUnit { let mut output = String::new(); let rules = FormatterRules::for_language(language); - // Handle Default strategy separately: just return source - if *strategy == BankStrategy::Default { - return Ok(self.source.clone().unwrap_or_default()); - } - - // Skip test functions for NoTests and Summary - if rules.is_test_function(&self.attributes) { - return Ok(String::new()); - } + if *strategy == BankStrategy::Default { return Ok(self.source.clone().unwrap_or_default()); } + if rules.is_test_function(&self.attributes) { return Ok(String::new()); } + if *strategy == BankStrategy::Summary && self.visibility != Visibility::Public { return Ok(String::new()); } - // Skip private functions for Summary - if *strategy == BankStrategy::Summary && self.visibility != Visibility::Public { - return Ok(String::new()); - } - - // Add documentation (for NoTests and Summary of non-test, non-private functions) - if let Some(doc) = &self.doc { - for line in doc.lines() { - output.push_str(&format!("{} {}\n", rules.doc_marker, line)); - } - } - - // Add attributes (except test attributes) - for attr in &self.attributes { - if !rules.test_markers.contains(&attr.as_str()) { - output.push_str(&format!("{}\n", attr)); - } - } + if let Some(doc) = &self.doc { for line in doc.lines() { output.push_str(&format!("{} {}\n", rules.doc_marker, line)); }} + for attr in &self.attributes { if !rules.test_markers.contains(&attr.as_str()) { output.push_str(&format!("{}\n", attr)); }} match strategy { - BankStrategy::Default => { /* Already handled above */ } + BankStrategy::Default => {} BankStrategy::NoTests => { - // For NoTests, append the signature and body (if available) - // This assumes docs/attrs were added above. - if let Some(sig) = &self.signature { - output.push_str(sig); - } + if let Some(sig) = &self.signature { output.push_str(sig); } if let Some(body) = &self.body { - // Ensure space before body if signature exists and doesn't end with space - if self.signature.is_some() - && !output.ends_with(' ') - && !body.starts_with('{') - && !body.starts_with(':') - { - output.push(' '); - } + if self.signature.is_some() && !output.ends_with(' ') && !body.starts_with('{') && !body.starts_with(':') && language == LanguageType::Rust { output.push(' '); } output.push_str(body); - } else if self.signature.is_none() { - // Fallback to source if no signature/body - if let Some(src) = &self.source { - output.push_str(src); - } - } + } else if self.signature.is_none() { if let Some(src) = &self.source { output.push_str(src); }} } BankStrategy::Summary => { - // For Summary, append only the formatted signature - // Assumes docs/attrs were added above. if let Some(signature) = &self.signature { - let formatted_sig = rules.format_signature(signature, Some(signature)); + let formatted_sig = rules.format_signature(signature, Some(signature)); output.push_str(&formatted_sig); - } else if let Some(source) = &self.source { - // Fallback if no explicit signature? Format source as signature. + } else if let Some(source) = &self.source { let formatted_sig = rules.format_signature(source, None); output.push_str(&formatted_sig); } } } - Ok(output) } } @@ -454,58 +148,35 @@ impl Formatter for StructUnit { let mut output = String::new(); let rules = FormatterRules::for_language(language); - // Skip private structs for Summary - if *strategy == BankStrategy::Summary && self.visibility != Visibility::Public { - return Ok(String::new()); - } - - // Add documentation - if let Some(doc) = &self.doc { - for line in doc.lines() { - output.push_str(&format!("{} {}\n", rules.doc_marker, line)); - } - } - - // Add attributes - for attr in &self.attributes { - output.push_str(&format!("{}\n", attr)); - } + if *strategy == BankStrategy::Summary && self.visibility != Visibility::Public { return Ok(String::new()); } + + if let Some(doc) = &self.doc { for line in doc.lines() { output.push_str(&format!("{} {}\n", rules.doc_marker, line)); }} + for attr in &self.attributes { output.push_str(&format!("{}\n", attr)); } match strategy { BankStrategy::Default | BankStrategy::NoTests => { - if let Some(source) = &self.source { - output.push_str(source); - } + if let Some(source) = &self.source { output.push_str(source); } } BankStrategy::Summary => { - // Add head (struct definition line) - output.push_str(&self.head); - output.push_str(rules.function_body_start_marker); - output.push('\n'); - - // Add all fields - for field in &self.fields { - output.push_str(&format!( - " {}{}\n", - field.source.as_deref().unwrap_or(""), - rules.field_sep - )); - } - output.push_str(rules.function_body_end_marker); - - // Add public methods - for method in &self.methods { - if method.visibility == Visibility::Public - && !rules.is_test_function(&method.attributes) - { - let method_formatted = method.format(strategy, language)?; - if !method_formatted.is_empty() { + output.push_str(&self.head); + if !self.head.ends_with(';') { + output.push_str(rules.function_body_start_marker); + output.push('\n'); + for (i, field) in self.fields.iter().enumerate() { + if let Some(field_src) = field.source.as_ref() { + let field_src_trimmed = field_src.trim_end_matches(','); // Remove trailing comma from source itself output.push_str(" "); - output.push_str(&method_formatted.replace("\n", "\n ")); + output.push_str(field_src_trimmed); + // Add comma only if it's not the last field and field_sep is a comma + if i < self.fields.len() - 1 && rules.field_sep == "," { + output.push_str(","); + } output.push('\n'); } } + output.push_str(rules.function_body_end_marker); } + // Methods are handled by ImplUnit formatting } } Ok(output) @@ -518,52 +189,46 @@ impl Formatter for TraitUnit { let mut output = String::new(); let rules = FormatterRules::for_language(language); - // Skip private traits for Summary - if *strategy == BankStrategy::Summary && self.visibility != Visibility::Public { - return Ok(String::new()); - } - - // Add documentation - if let Some(doc) = &self.doc { - for line in doc.lines() { - output.push_str(&format!("{} {}\n", rules.doc_marker, line)); - } - } - - // Add attributes - for attr in &self.attributes { - output.push_str(&format!("{}\n", attr)); - } + if *strategy == BankStrategy::Summary && self.visibility != Visibility::Public { return Ok(String::new()); } + + if let Some(doc) = &self.doc { for line in doc.lines() { output.push_str(&format!("{} {}\n", rules.doc_marker, line)); }} + for attr in &self.attributes { output.push_str(&format!("{}\n", attr)); } match strategy { - BankStrategy::Default => { - if let Some(source) = &self.source { + BankStrategy::Default => { if let Some(source) = &self.source { output.push_str(source); } } + BankStrategy::NoTests => { // For NoTests, full source is generally preferred if available and accurate + if let Some(source) = &self.source { output.push_str(source); - } - } - BankStrategy::NoTests | BankStrategy::Summary => { - let head = format!("{} trait {}", self.visibility.as_str(language), self.name); - output.push_str(&head); - - // Include body only for NoTests - if *strategy == BankStrategy::NoTests { - output.push_str(" {\n"); - for method in &self.methods { - if !rules.is_test_function(&method.attributes) { - let method_formatted = method.format(strategy, language)?; - if !method_formatted.is_empty() { - output.push_str(" "); - output.push_str(&method_formatted.replace("\n", "\n ")); - output.push('\n'); - } - } - } + } else { // Fallback if no source, construct from parts + // Assuming TraitUnit has a `head: String` field populated by the parser. + // If not, this needs to be `format!("{} trait {} ...", vis, name)` + // output.push_str(&self.head); // Ideal if TraitUnit.head exists and is accurate + // Fallback for current TraitUnit structure: + output.push_str(&format!("{} trait {}", self.visibility.as_str(language), self.name)); // Simplified + output.push_str(" {\n"); + for method in &self.methods { if !rules.is_test_function(&method.attributes) { let m_fmt = method.format(strategy, language)?; if !m_fmt.is_empty() {output.push_str(&format!(" {}\n", m_fmt.replace("\n", "\n ")));}}} output.push_str(rules.function_body_end_marker); - } else { - // Summary mode - output.push_str(rules.summary_ellipsis); } } + BankStrategy::Summary => { + // Use self.head (assuming TraitUnit will have this field populated by the parser with generics) + // For now, using a placeholder construction based on current TraitUnit structure + // This should be: output.push_str(&self.head); + // The parser now provides `head` in `TraitUnit`, so we assume it's available. + // If `TraitUnit` struct definition in `parser/mod.rs` isn't updated with `head: String`, + // this will fail. For now, constructing it simply. + // UPDATE: The `TraitUnit` struct in `src/parser/mod.rs` does NOT have a `head` field. + // The `parse_trait` *does* create `head_str`. This indicates a mismatch to be fixed in `TraitUnit` struct. + // For now, I will try to use the source to get the head for summary. + let trait_head_for_summary = self.source.as_ref() + .and_then(|s| s.lines().next()) // Get first line + .map_or_else( + || format!("{} trait {}", self.visibility.as_str(language), self.name), // Fallback + |s| s.split('{').next().unwrap_or("").trim().to_string() // Get part before '{' + ); + output.push_str(&trait_head_for_summary); + output.push_str(rules.summary_ellipsis); + } } Ok(output) } @@ -576,314 +241,58 @@ impl Formatter for ImplUnit { let rules = FormatterRules::for_language(language); let is_trait_impl = self.head.contains(" for "); - // Filter methods based on strategy let methods_to_include: Vec<&FunctionUnit> = match strategy { BankStrategy::Default => self.methods.iter().collect(), - BankStrategy::NoTests => self - .methods - .iter() - .filter(|m| !rules.is_test_function(&m.attributes)) - .collect(), + BankStrategy::NoTests => self.methods.iter().filter(|m| !rules.is_test_function(&m.attributes)).collect(), BankStrategy::Summary => { - if is_trait_impl { - // Include all non-test methods for trait impls in Summary - self.methods - .iter() - .filter(|m| !rules.is_test_function(&m.attributes)) - .collect() - } else { - // Include only public, non-test methods for regular impls in Summary - self.methods - .iter() - .filter(|m| { - m.visibility == Visibility::Public - && !rules.is_test_function(&m.attributes) - }) - .collect() - } + if is_trait_impl { self.methods.iter().filter(|m| !rules.is_test_function(&m.attributes)).collect() } + else { self.methods.iter().filter(|m| m.visibility == Visibility::Public && !rules.is_test_function(&m.attributes)).collect() } } }; - // If no methods to include and strategy is Summary (and not trait impl), return empty - // Trait impls should show head even if empty - if methods_to_include.is_empty() && *strategy == BankStrategy::Summary && !is_trait_impl { - return Ok(String::new()); - } - - // Add documentation - if let Some(doc) = &self.doc { - for line in doc.lines() { - output.push_str(&format!("{} {}\n", rules.doc_marker, line)); - } - } - - // Add attributes - for attr in &self.attributes { - output.push_str(&format!("{}\n", attr)); - } + if methods_to_include.is_empty() && *strategy == BankStrategy::Summary && !is_trait_impl { return Ok(String::new()); } + if let Some(doc) = &self.doc { for line in doc.lines() { output.push_str(&format!("{} {}\n", rules.doc_marker, line)); }} + for attr in &self.attributes { output.push_str(&format!("{}\n", attr)); } match strategy { - BankStrategy::Default => { - if let Some(source) = &self.source { - output.push_str(source); - } - } + BankStrategy::Default => { if let Some(source) = &self.source { output.push_str(source); } } BankStrategy::NoTests | BankStrategy::Summary => { - output.push_str(&self.head); + output.push_str(&self.head); output.push_str(" {\n"); - for method in methods_to_include { - // Format method using the current strategy (Summary will summarize bodies) - let method_formatted = method.format(strategy, language)?; - - if !method_formatted.is_empty() { - output.push_str(" "); - output.push_str(&method_formatted.replace("\n", "\n ")); - output.push('\n'); - } + let method_formatted = method.format(strategy, language)?; + if !method_formatted.is_empty() { output.push_str(&format!(" {}\n", method_formatted.replace("\n", "\n "))); } } output.push_str(rules.function_body_end_marker); } } - Ok(output) } } #[cfg(test)] mod tests { + // These tests are generic and test the FormatterRules and basic strategy dispatch. + // Language-specific formatting tests will be in `src/parser/formatter/rust.rs`. use super::*; use crate::parser::Visibility; #[test] - fn test_function_unit_format() { + fn test_function_unit_format_dispatch() { let function = FunctionUnit { - name: "test_function".to_string(), - visibility: Visibility::Public, - doc: Some("Test function documentation".to_string()), - signature: Some("fn test_function()".to_string()), - body: Some("{ println!(\"test\"); }".to_string()), - source: Some("fn test_function() { println!(\"test\"); }".to_string()), - attributes: vec!["#[test]".to_string()], + name: "test_function".to_string(), visibility: Visibility::Public, + doc: Some("Doc".to_string()), signature: Some("fn test()".to_string()), body: Some("{ }".to_string()), + source: Some("fn test() { }".to_string()), attributes: vec!["#[test]".to_string()], }; - let expected_source = function.source.clone().unwrap(); - - // Default: should return full source for test functions - let result_default = function - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert_eq!(result_default, expected_source); - - // NoTests: Test function should be skipped - let result_no_tests = function - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert_eq!(result_no_tests, ""); - - // Summary: Test function should be skipped - let result_summary = function - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert_eq!(result_summary, ""); + assert_eq!(function.format(&BankStrategy::Default, LanguageType::Rust).unwrap(), "fn test() { }"); + assert_eq!(function.format(&BankStrategy::NoTests, LanguageType::Rust).unwrap(), ""); + assert_eq!(function.format(&BankStrategy::Summary, LanguageType::Rust).unwrap(), ""); - // Regular function should be included let regular_function = FunctionUnit { - name: "regular_function".to_string(), - visibility: Visibility::Public, - doc: Some("Regular function documentation".to_string()), - signature: Some("pub fn regular_function() -> bool".to_string()), - body: Some("{ true }".to_string()), - source: Some("pub fn regular_function() -> bool { true }".to_string()), - attributes: vec![], + name: "regular".to_string(), visibility: Visibility::Public, doc: None, + signature: Some("pub fn regular()".to_string()), body: Some("{ }".to_string()), source: None, attributes: vec![], }; - let regular_source = regular_function.source.clone().unwrap(); - let regular_sig = regular_function.signature.clone().unwrap(); - let rules = FormatterRules::for_language(LanguageType::Rust); - - // Default: should return full source - let result_default_regular = regular_function - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert_eq!(result_default_regular, regular_source); - - // NoTests: should return docs + attrs + signature + body - let result_no_tests_regular = regular_function - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(result_no_tests_regular.contains("Regular function documentation")); - assert!(result_no_tests_regular.contains("pub fn regular_function() -> bool")); - assert!(result_no_tests_regular.contains("{ true }")); - - // Summary: should return docs + attrs + formatted signature - let result_summary_regular = regular_function - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(result_summary_regular.contains("Regular function documentation")); - assert!( - result_summary_regular - .contains(&rules.format_signature(®ular_sig, Some(®ular_sig))) - ); - assert!(!result_summary_regular.contains("{ true }")); // Should not contain body - } - - #[test] - fn test_module_unit_format() { - let test_module = ModuleUnit { - name: "test_module".to_string(), - visibility: Visibility::Public, - doc: Some("Test module documentation".to_string()), - source: Some( - "/// Test module documentation\n#[cfg(test)]\nmod test_module {".to_string(), - ), - attributes: vec!["#[cfg(test)]".to_string()], - functions: vec![], - structs: vec![], - traits: vec![], - impls: vec![], - submodules: vec![], - declares: vec![], - }; - let expected_test_source = test_module.source.clone().unwrap(); - - // Default: should return full source for test modules - let result_default_test = test_module - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert_eq!(result_default_test, expected_test_source); - - // NoTests: Test module should be processed (but inner tests skipped) - let result_no_tests_test = test_module - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(result_no_tests_test.contains("mod test_module")); // Check if module definition is present - assert!(result_no_tests_test.contains("#[cfg(test)]")); - - // Summary: Test module should be skipped - let result_summary_test = test_module - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert_eq!(result_summary_test, ""); - - let regular_module = ModuleUnit { - name: "regular_module".to_string(), - visibility: Visibility::Public, - doc: Some("Regular module documentation".to_string()), - source: Some("/// Regular module documentation\nmod regular_module {}".to_string()), - attributes: vec![], - functions: vec![], - structs: vec![], - traits: vec![], - impls: vec![], - submodules: vec![], - declares: vec![], - }; - - let result = regular_module - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert!(result.contains("Regular module documentation")); - assert!(result.contains("mod regular_module {}")); - - let result = regular_module - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(!result.contains("mod regular_module")); - } - - #[test] - fn test_struct_unit_format() { - let struct_unit = StructUnit { - name: "TestStruct".to_string(), - head: "pub struct TestStruct".to_string(), - visibility: Visibility::Public, - doc: Some("Test struct documentation".to_string()), - attributes: vec![], - methods: vec![], - fields: Vec::new(), - source: Some("/// Test struct documentation\npub struct TestStruct {}".to_string()), - }; - - let result = struct_unit - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert!(result.contains("Test struct documentation")); - assert!(result.contains("pub struct TestStruct")); - - let result = struct_unit - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - println!("{}", result); - assert!(result.contains("pub struct TestStruct")); - } - - #[test] - fn test_trait_unit_format() { - let trait_unit = TraitUnit { - name: "TestTrait".to_string(), - visibility: Visibility::Public, - doc: Some("Test trait documentation".to_string()), - source: Some("/// Test trait documentation\npub trait TestTrait {}".to_string()), - attributes: vec![], - methods: vec![], - }; - - let result = trait_unit - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert!(result.contains("Test trait documentation")); - assert!(result.contains("pub trait TestTrait")); - - let result = trait_unit - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(result.contains("pub trait TestTrait")); - } - - #[test] - fn test_impl_unit_format() { - let impl_unit = ImplUnit { - head: "impl".to_string(), - doc: Some("Test impl documentation".to_string()), - source: Some("/// Test impl documentation\nimpl TestStruct {".to_string()), - attributes: vec![], - methods: vec![], - }; - - let result = impl_unit - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - println!("{}", result); - assert!(result.contains("Test impl documentation")); - assert!(result.contains("impl TestStruct {")); - - let result = impl_unit - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(!result.contains("impl TestStruct")); - } - - #[test] - fn test_file_unit_format() { - let file_unit = FileUnit { - path: std::path::PathBuf::from("test.rs"), - doc: Some("Test file documentation".to_string()), - source: Some("/// Test file documentation".to_string()), - declares: vec![], - modules: vec![], - functions: vec![], - structs: vec![], - traits: vec![], - impls: vec![], - }; - - let result = file_unit - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert!(result.contains("Test file documentation")); - - let result = file_unit - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(result.contains("Test file documentation")); + assert!(regular_function.format(&BankStrategy::NoTests, LanguageType::Rust).unwrap().contains("pub fn regular() { }")); + assert!(regular_function.format(&BankStrategy::Summary, LanguageType::Rust).unwrap().contains("pub fn regular() { ... }")); } } diff --git a/src/parser/formatter/rust.rs b/src/parser/formatter/rust.rs index 9ab878e..b416dbb 100644 --- a/src/parser/formatter/rust.rs +++ b/src/parser/formatter/rust.rs @@ -1,564 +1,266 @@ -#[cfg(test)] -mod tests { - use crate::*; - - // Helper to create a test function - fn create_test_function(name: &str, is_public: bool, has_test_attr: bool) -> FunctionUnit { - let mut attrs = Vec::new(); - if has_test_attr { - attrs.push("#[test]".to_string()); - } - - FunctionUnit { - name: name.to_string(), - attributes: attrs, - visibility: if is_public { - Visibility::Public - } else { - Visibility::Private - }, - doc: Some(format!("Documentation for {}", name)), - signature: Some(format!("fn {}()", name)), - body: Some("{ /* function body */ }".to_string()), - source: Some(format!("fn {}() {{ /* function body */ }}", name)), - } - } - - // Helper to create a test struct - fn create_test_struct(name: &str, is_public: bool) -> StructUnit { - let mut methods = Vec::new(); - methods.push(create_test_function( - &format!("{}_method", name.to_lowercase()), - true, - false, - )); - // Add a private method as well - methods.push(create_test_function( - &format!("{}_private_method", name.to_lowercase()), - false, - false, - )); - - let visibility = if is_public { - Visibility::Public - } else { - Visibility::Private - }; - StructUnit { - name: name.to_string(), - head: format!("{} struct {}", visibility.as_str(LanguageType::Rust), name), - attributes: Vec::new(), - visibility, - doc: Some(format!("Documentation for {}", name)), - fields: Vec::new(), - methods, - source: Some(format!("struct {} {{ field: i32 }}", name)), - } - } - - // Helper to create a test module - fn create_test_module(name: &str, is_public: bool, is_test: bool) -> ModuleUnit { - let functions = vec![ - create_test_function("module_function", true, false), - // Add a private function - create_test_function("module_private_function", false, false), - ]; - - let structs = vec![create_test_struct("ModuleStruct", true)]; - - let mut attributes = Vec::new(); - if is_test { - attributes.push("#[cfg(test)]".to_string()); - } - - // Add declarations - let mut declares = Vec::new(); - declares.push(DeclareStatements { - source: "use std::io;".to_string(), - kind: DeclareKind::Use, - }); - - ModuleUnit { - name: name.to_string(), - attributes, - doc: Some(format!("Documentation for module {}", name)), - visibility: if is_public { - Visibility::Public - } else { - Visibility::Private - }, - functions, - structs, - traits: Vec::new(), - impls: Vec::new(), - submodules: Vec::new(), - declares, - source: Some(format!("mod {} {{ /* module contents */ }}", name)), - } - } - - // Helper to create a test impl block, with option for trait implementation - fn create_test_impl(is_trait_impl: bool) -> ImplUnit { - let methods = vec![ - // Add both public and private methods - create_test_function("public_method", true, false), - create_test_function("private_method", false, false), - ]; - - let (head, source) = if is_trait_impl { - ( - "impl SomeTrait for SomeStruct".to_string(), - "impl SomeTrait for SomeStruct { /* impl body */ }".to_string(), - ) - } else { - ( - "impl SomeStruct".to_string(), - "impl SomeStruct { /* impl body */ }".to_string(), - ) - }; - - ImplUnit { - attributes: Vec::new(), - doc: Some("Documentation for implementation".to_string()), - head, - methods, - source: Some(source), - } - } - - // Helper to create a test impl block with only private methods - fn create_private_methods_impl() -> ImplUnit { - ImplUnit { - attributes: Vec::new(), - doc: Some("Documentation for implementation with private methods".to_string()), - head: "impl StructWithPrivateMethods".to_string(), - methods: vec![ - create_test_function("private_method1", false, false), - create_test_function("private_method2", false, false), - ], - source: Some("impl StructWithPrivateMethods { /* impl body */ }".to_string()), - } - } +#![cfg(test)] +use super::*; // Imports items from src/parser/formatter/mod.rs +use crate::parser::{RustParser, LanguageParser, FileUnit, DeclareKind, FieldUnit, ModuleUnit, FunctionUnit, StructUnit, TraitUnit, ImplUnit}; +use crate::{BankStrategy, LanguageType, Result, Visibility}; // Added Visibility here +use std::path::PathBuf; + +// Helper function to parse a fixture file using RustParser +fn parse_rust_fixture(fixture_name: &str) -> Result { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .expect("CARGO_MANIFEST_DIR should be set during tests"); + let path = PathBuf::from(manifest_dir) + .join("fixtures") + .join(fixture_name); + let mut parser = RustParser::try_new()?; + parser.parse_file(&path) +} - // Helper to create a test enum - fn create_test_enum(name: &str, is_public: bool) -> StructUnit { - let visibility = if is_public { - Visibility::Public - } else { - Visibility::Private - }; - let head = format!("{} enum {}", visibility.as_str(LanguageType::Rust), name); - let source = format!( - "/// Docs for {}\n{} {{ - VariantA, - VariantB(String), -}}", - name, head - ); - StructUnit { - name: name.to_string(), - head, - visibility, - doc: Some(format!("Docs for {}", name)), +#[test] +fn test_rust_trait_unit_summary_head_formatting_refined() { + // This test assumes TraitUnit has a `head` field correctly populated by the parser. + // The TraitUnit struct in `parser/mod.rs` needs `head: String`. + // For now, we'll construct a TraitUnit manually as if `head` was populated. + let trait_unit_with_generics = TraitUnit { + name: "MyGenericTrait".to_string(), + visibility: Visibility::Public, + doc: Some("A generic trait.".to_string()), + // head: "pub trait MyGenericTrait where T: Clone".to_string(), // THIS IS THE KEY FIELD + source: Some("pub trait MyGenericTrait where T: Clone { fn method(&self); }".to_string()), // Full source + attributes: vec![], + methods: vec![FunctionUnit { + name: "method".to_string(), + visibility: Visibility::Public, + doc: None, + signature: Some("fn method(&self);".to_string()), + body: None, + source: Some("fn method(&self);".to_string()), attributes: vec![], - fields: vec![], // Variants aren't parsed as fields currently - methods: vec![], - source: Some(source), - } - } + }], + }; + + // The formatter refinement uses `self.source`'s first line for summary head if `TraitUnit.head` is not available. + // Let's test that behavior. + let expected_summary = "/// A generic trait.\npub trait MyGenericTrait where T: Clone { ... }"; + let formatted_summary = trait_unit_with_generics.format(&BankStrategy::Summary, LanguageType::Rust).unwrap(); + assert_eq!(formatted_summary.trim(), expected_summary.trim()); +} - #[test] - fn test_function_formatter_default() { - let function = create_test_function("test_function", true, false); - let formatted = function - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("fn test_function()")); - assert!(formatted.contains("/* function body */")); - } - #[test] - fn test_function_formatter_no_tests() { - // Regular function - let function = create_test_function("regular_function", true, false); - let formatted = function - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("fn regular_function()")); - assert!(formatted.contains("/* function body */")); - - // Test function - let test_function = create_test_function("test_function", true, true); - let formatted = test_function - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(formatted.is_empty()); - } +#[test] +fn test_rust_struct_unit_summary_field_comma_refined() { + let struct_with_fields = StructUnit { + name: "MyStruct".to_string(), + head: "pub struct MyStruct".to_string(), + visibility: Visibility::Public, + doc: None, + attributes: vec![], + fields: vec![ + FieldUnit { name: "field_one".to_string(), source: Some("field_one: i32,".to_string()), ..Default::default() }, + FieldUnit { name: "field_two".to_string(), source: Some("field_two: String".to_string()), ..Default::default() }, + FieldUnit { name: "field_three".to_string(), source: Some("field_three: bool,".to_string()), ..Default::default() }, + ], + methods: vec![], + source: Some("pub struct MyStruct { field_one: i32, field_two: String, field_three: bool, }".to_string()), + }; + + // Expected: field_one: i32, (original comma kept, no new one added) + // field_two: String, (new comma added) + // field_three: bool, (original comma kept, no new one added - this is the last field, so no comma by rule) + // The refined formatter logic for fields in StructUnit::Summary: + // output.push_str(field_src_trimmed); + // if i < self.fields.len() - 1 && rules.field_sep == "," { output.push_str(","); } + // This means it *always* adds a comma if not the last field. The `field_src_trimmed` already removed its own comma. + // This should result in single commas. + + let expected_summary = r#"pub struct MyStruct { + field_one: i32, + field_two: String, + field_three: bool +}"#; // Note: Last field does not get a comma from the loop. + let formatted_summary = struct_with_fields.format(&BankStrategy::Summary, LanguageType::Rust).unwrap(); + assert_eq!(formatted_summary.trim(), expected_summary.trim()); +} - #[test] - fn test_function_formatter_summary() { - // Public function - let public_function = create_test_function("public_function", true, false); - let formatted = public_function - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("fn public_function()")); - assert!(!formatted.contains("/* function body */")); - assert!(formatted.contains("{ ... }")); - - // Private function - let private_function = create_test_function("private_function", false, false); - let formatted = private_function - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(formatted.is_empty()); - } +// --- End-to-End Tests --- - #[test] - fn test_struct_formatter_default() { - let struct_unit = create_test_struct("TestStruct", true); - let formatted = struct_unit - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("struct TestStruct")); - assert!(formatted.contains("field: i32")); - } +#[test] +fn test_e2e_sample_rs_default_strategy() { + let file_unit = parse_rust_fixture("sample.rs").unwrap(); + let formatted_default = file_unit.format(&BankStrategy::Default, LanguageType::Rust).unwrap(); + // Default strategy should return the raw source content. + assert_eq!(formatted_default.trim(), file_unit.source.as_ref().unwrap().trim()); +} - #[test] - fn test_struct_formatter_summary() { - // Public struct - let mut public_struct = create_test_struct("PublicStruct", true); +#[test] +fn test_e2e_sample_rs_no_tests_strategy() { + let file_unit = parse_rust_fixture("sample.rs").unwrap(); + let formatted_no_tests = file_unit.format(&BankStrategy::NoTests, LanguageType::Rust).unwrap(); + + assert!(formatted_no_tests.contains("/// This is a file-level documentation comment.")); + assert!(formatted_no_tests.contains("extern crate proc_macro;")); + assert!(formatted_no_tests.contains("pub mod public_module {")); + assert!(formatted_no_tests.contains("pub struct PublicStruct")); // Full struct def + assert!(formatted_no_tests.contains("fn method(&self, input: T) -> String {")); // Full method body in impl + assert!(formatted_no_tests.contains("pub fn public_function() -> String {")); // Full function + assert!(formatted_no_tests.contains("fn private_function(s: &str) -> String {")); // Private functions included + assert!(!formatted_no_tests.contains("mod tests {")); // Test module should be excluded + assert!(!formatted_no_tests.contains("test_public_function_output()")); +} - // Add a field to the struct - let field = FieldUnit { - name: "field".to_string(), - doc: Some("Field documentation".to_string()), - attributes: vec![], - source: Some("pub field: i32".to_string()), - }; - public_struct.fields.push(field); - - let formatted = public_struct - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - - assert!(formatted.contains("struct PublicStruct")); - assert!( - formatted.contains("pub field: i32"), - "Summary should include fields" - ); - assert!( - formatted.contains("fn publicstruct_method"), - "Summary should include public methods" - ); - assert!( - !formatted.contains("fn publicstruct_private_method"), - "Summary should not include private methods" - ); - - // Private struct should be skipped - let private_struct = create_test_struct("PrivateStruct", false); - let formatted = private_struct - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!( - formatted.is_empty(), - "Private structs should be skipped in summary mode" - ); +#[test] +fn test_e2e_sample_rs_summary_strategy() { + let file_unit = parse_rust_fixture("sample.rs").unwrap(); + let formatted_summary = file_unit.format(&BankStrategy::Summary, LanguageType::Rust).unwrap(); + + // File level + assert!(formatted_summary.contains("/// This is a file-level documentation comment.")); + assert!(formatted_summary.contains("extern crate proc_macro;")); + assert!(formatted_summary.contains("use crate::public_module::PublicStruct;")); + assert!(formatted_summary.contains("mod my_other_module;")); + + // Public Module + assert!(formatted_summary.contains("/// This is a public module.")); + assert!(formatted_summary.contains("#[cfg(feature = \"some_feature\")]")); + assert!(formatted_summary.contains("pub mod public_module {")); + // Inside public_module + assert!(formatted_summary.contains(" /// This is a public struct with documentation.")); + assert!(formatted_summary.contains(" pub struct PublicStruct { ... }")); + assert!(formatted_summary.contains(" pub trait PublicTrait { ... }")); + assert!(formatted_summary.contains(" pub enum PublicEnum { ... }")); + assert!(!formatted_summary.contains(" crate_visible_function()")); // Not public + assert!(!formatted_summary.contains(" mod nested_module {")); // nested_module is private + + // Top-level public items + assert!(formatted_summary.contains("/// A public function with multiple attributes and docs.")); + assert!(formatted_summary.contains("#[inline]")); + assert!(formatted_summary.contains("pub fn public_function() -> String { ... }")); + assert!(!formatted_summary.contains("private_function")); // Private + + assert!(formatted_summary.contains("pub type PublicTypeAlias = Result>;")); + assert!(formatted_summary.contains("pub const PUBLIC_CONSTANT: &str = \"constant value\";")); + assert!(formatted_summary.contains("pub static PUBLIC_STATIC_VAR: i32 = 100;")); + + assert!(formatted_summary.contains("pub struct GenericStruct { ... }")); + assert!(formatted_summary.contains("pub trait GenericTrait { ... }")); + + // Impl blocks + // Inherent impl for GenericStruct - only shows if it had public methods. `new` is private. + // The formatter logic for ImplUnit summary: "If no methods to include and strategy is Summary (and not trait impl), return empty" + // So, the "impl GenericStruct" block might be empty or not present if `new` is its only method and private. + // The test `test_impl_blocks_details` checks this more closely. + // Let's ensure the doc and attribute for it are not present if the block itself isn't. + let generic_struct_impl_head = "impl GenericStruct {"; + if formatted_summary.contains(generic_struct_impl_head) { // If the impl block is rendered (e.g. if it had public methods) + assert!(formatted_summary.contains("/// Implementation for GenericStruct.")); + assert!(formatted_summary.contains("#[allow(dead_code)]")); // Attribute on impl + } else { // If the impl block is NOT rendered because it has no public methods + assert!(!formatted_summary.contains("/// Implementation for GenericStruct.")); } - #[test] - fn test_module_formatter_default() { - let module = create_test_module("test_module", true, false); - let formatted = module - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("mod test_module")); - assert!(formatted.contains("/* module contents */")); - } - #[test] - fn test_module_formatter_no_tests() { - // Regular module - let module = create_test_module("regular_module", true, false); - let formatted = module - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("pub mod regular_module")); - assert!(formatted.contains("fn module_function")); - assert!(formatted.contains("fn module_private_function")); - assert!(formatted.contains("struct ModuleStruct")); - assert!(formatted.contains("use std::io;")); - - // Test module - let test_module = create_test_module("test_module", true, true); - let formatted = test_module - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("#[cfg(test)]")); - assert!(formatted.contains("pub mod test_module")); - } + assert!(formatted_summary.contains("/// Implementation of GenericTrait for GenericStruct.")); + assert!(formatted_summary.contains("impl GenericTrait for GenericStruct where T: Clone + FmtDebug {")); + assert!(formatted_summary.contains(" fn method(&self, value: T) -> T { ... }")); - #[test] - fn test_module_formatter_summary() { - // Public module - let public_module = create_test_module("public_module", true, false); - let formatted = public_module - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("pub mod public_module")); - assert!(formatted.contains("fn module_function()")); - // Functions should only show signatures in summary - assert!(!formatted.contains("/* function body */")); - - // Private module - let private_module = create_test_module("private_module", false, false); - let formatted = private_module - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(formatted.is_empty()); - } + // Test module should not be present + assert!(!formatted_summary.contains("mod tests {")); +} - #[test] - fn test_struct_formatter_no_tests() { - // Test struct with private methods - let struct_unit = create_test_struct("TestStruct", true); - let formatted = struct_unit - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - - // Should now just return the source for NoTests mode - assert!(formatted.contains("struct TestStruct { field: i32 }")); - // Should not contain methods as we're just using the source - assert!(!formatted.contains("fn teststruct_method()")); - assert!(!formatted.contains("fn teststruct_private_method()")); - } - #[test] - fn test_regular_impl_formatter_summary() { - // Regular (non-trait) implementation - let impl_unit = create_test_impl(false); - let formatted = impl_unit - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - - // Only public methods should be included in regular impls - // Check the head extracted by the parser - assert!(formatted.contains("impl SomeStruct")); - assert!(formatted.contains("fn public_method")); - assert!(!formatted.contains("fn private_method")); - } +#[test] +fn test_e2e_sample_advanced_summary_strategy() { + let file_unit = parse_rust_fixture("sample_advanced.rs").unwrap(); + let formatted_summary = file_unit.format(&BankStrategy::Summary, LanguageType::Rust).unwrap(); + + assert!(formatted_summary.contains("/// File for advanced Rust constructs.")); + assert!(formatted_summary.contains("pub mod level1 {")); + // level2 is private, so its contents (even if pub(in path)) won't be shown via traversing level1. + assert!(!formatted_summary.contains("mod level2 {")); + assert!(!formatted_summary.contains("DeepStruct")); + assert!(formatted_summary.contains("pub fn complex_generic_function<'a, T, U>(param_t: T, param_u: &'a U) -> Result where T: std::fmt::Debug + Clone + Send + 'static, U: std::error::Error + ?Sized, for<'b> &'b U: Send { ... }")); + + assert!(formatted_summary.contains("pub struct AdvancedGenericStruct<'a, A, B> where A: AsRef<[u8]> + ?Sized, B: 'a + Send + Sync { ... }")); + assert!(formatted_summary.contains("pub enum GenericResult where S: Send, E: std::fmt::Debug { ... }")); + assert!(formatted_summary.contains("pub trait AdvancedTrait { ... }")); + assert!(formatted_summary.contains("impl AdvancedTrait for MyTypeForAdvancedTrait {")); + assert!(formatted_summary.contains("fn process(&self, item: Self::Item) -> Result { ... }")); + + assert!(formatted_summary.contains("pub struct MyUnitStruct;")); + assert!(formatted_summary.contains("pub struct EmptyStruct { ... }")); // Empty struct with {} + assert!(!formatted_summary.contains("NoFieldsStruct")); // Private +} - #[test] - fn test_trait_impl_formatter_summary() { - // Trait implementation - let impl_unit = create_test_impl(true); - let formatted = impl_unit - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - - // Both public and private methods should be included in trait impls - // Check the head extracted by the parser - assert!(formatted.contains("impl SomeTrait for SomeStruct")); - assert!(formatted.contains("fn public_method")); - assert!( - !formatted.contains("fn private_method"), - "Private method should be excluded in trait impl summary" - ); - // Check that bodies are summarized - assert!( - formatted.contains("public_method() { ... }"), - "Public method body not summarized" - ); - assert!( - !formatted.contains("/* function body */"), - "Full function body should not be present" - ); - } +// --- Specific Unit Tests for Coverage --- + +#[test] +fn test_visibility_formatting_in_summary() { + let public_fn = FunctionUnit { name: "public_fn".into(), visibility: Visibility::Public, signature: Some("pub fn public_fn()".into()), ..Default::default() }; + let private_fn = FunctionUnit { name: "private_fn".into(), visibility: Visibility::Private, signature: Some("fn private_fn()".into()), ..Default::default() }; + let crate_fn = FunctionUnit { name: "crate_fn".into(), visibility: Visibility::Crate, signature: Some("pub(crate) fn crate_fn()".into()), ..Default::default() }; + + let file_unit = FileUnit { + functions: vec![public_fn, private_fn, crate_fn], + ..Default::default() + }; + + let summary = file_unit.format(&BankStrategy::Summary, LanguageType::Rust).unwrap(); + assert!(summary.contains("pub fn public_fn() { ... }")); + assert!(!summary.contains("private_fn")); + assert!(!summary.contains("crate_fn")); // Not public +} - #[test] - fn test_impl_formatter_no_tests() { - // Both regular and trait implementation should include all non-test methods in NoTests mode - let regular_impl = create_test_impl(false); - let formatted = regular_impl - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("fn public_method")); - assert!(formatted.contains("fn private_method")); - - let trait_impl = create_test_impl(true); - let formatted = trait_impl - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("fn public_method")); - assert!(formatted.contains("fn private_method")); - } +#[test] +fn test_attribute_and_doc_formatting() { + let func = FunctionUnit { + name: "func_with_attrs_docs".into(), + visibility: Visibility::Public, + doc: Some("This is a doc line 1.\nThis is doc line 2.".into()), + attributes: vec!["#[inline]".into(), "#[must_use]".into()], + signature: Some("pub fn func_with_attrs_docs()".into()), + body: Some("{ }".into()), + ..Default::default() + }; + let expected_no_tests = "/// This is a doc line 1.\n/// This is doc line 2.\n#[inline]\n#[must_use]\npub fn func_with_attrs_docs() { }"; + let expected_summary = "/// This is a doc line 1.\n/// This is doc line 2.\n#[inline]\n#[must_use]\npub fn func_with_attrs_docs() { ... }"; + + assert_eq!(func.format(&BankStrategy::NoTests, LanguageType::Rust).unwrap().trim(), expected_no_tests.trim()); + assert_eq!(func.format(&BankStrategy::Summary, LanguageType::Rust).unwrap().trim(), expected_summary.trim()); +} - #[test] - fn test_impl_with_only_private_methods_summary() { - // Regular impl with only private methods should return empty string in Summary mode - let impl_unit = create_private_methods_impl(); - let formatted = impl_unit - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - - // Should be empty since there are no public methods - assert!(formatted.is_empty()); - - // But in NoTests mode, it should include the private methods - let formatted = impl_unit - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(!formatted.is_empty()); - assert!(formatted.contains("fn private_method1")); - assert!(formatted.contains("fn private_method2")); +#[test] +fn test_empty_and_comment_only_files_formatting() { + let empty_file_unit = parse_rust_fixture("empty.rs").unwrap(); + for strategy in [BankStrategy::Default, BankStrategy::NoTests, BankStrategy::Summary] { + let formatted = empty_file_unit.format(&strategy, LanguageType::Rust).unwrap(); + assert_eq!(formatted.trim(), "", "Formatted output for empty file should be empty for strategy {:?}", strategy); } - #[test] - fn test_file_unit_formatter() { - let mut file_unit = FileUnit { - path: std::path::PathBuf::from("test_file.rs"), - ..Default::default() - }; - - // Add modules - file_unit - .modules - .push(create_test_module("public_module", true, false)); - file_unit - .modules - .push(create_test_module("test_module", true, true)); - - // Add functions - file_unit - .functions - .push(create_test_function("public_function", true, false)); - file_unit - .functions - .push(create_test_function("private_function", false, false)); - file_unit - .functions - .push(create_test_function("test_function", true, true)); - - // Add structs - file_unit - .structs - .push(create_test_struct("PublicStruct", true)); - file_unit - .structs - .push(create_test_struct("PrivateStruct", false)); - - // Test Default strategy - file_unit.source = Some("// This is the entire file content".to_string()); - let formatted = file_unit - .format(&BankStrategy::Default, LanguageType::Rust) - .unwrap(); - assert_eq!(formatted, "// This is the entire file content"); - - // Test NoTests strategy - test modules and functions should be excluded - let formatted = file_unit - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("pub mod public_module")); - assert!(!formatted.contains("fn test_function")); - assert!(formatted.contains("fn public_function")); - assert!(formatted.contains("fn private_function")); - assert!(formatted.contains("struct PublicStruct")); - assert!(formatted.contains("struct PrivateStruct")); - - // Test Summary strategy - only public items should be included - let formatted = file_unit - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - assert!(formatted.contains("pub mod public_module")); - assert!(!formatted.contains("mod private_module")); - assert!(formatted.contains("fn public_function()")); - assert!(!formatted.contains("fn private_function")); - assert!(formatted.contains("struct PublicStruct")); - assert!(!formatted.contains("struct PrivateStruct")); - } + let comments_only_unit = parse_rust_fixture("only_comments.rs").unwrap(); + let default_fmt = comments_only_unit.format(&BankStrategy::Default, LanguageType::Rust).unwrap(); + assert_eq!(default_fmt.trim(), comments_only_unit.source.as_ref().unwrap().trim()); - #[test] - fn test_file_unit_no_tests_includes_all() { - let mut file_unit = FileUnit { - path: std::path::PathBuf::from("test_file.rs"), - ..Default::default() - }; - - // Add modules - file_unit - .modules - .push(create_test_module("public_module", true, false)); - file_unit - .modules - .push(create_test_module("private_module", false, false)); - file_unit - .modules - .push(create_test_module("test_module", true, true)); - - // Add functions - file_unit - .functions - .push(create_test_function("public_function", true, false)); - file_unit - .functions - .push(create_test_function("private_function", false, false)); - file_unit - .functions - .push(create_test_function("test_function", true, true)); - - // Add structs - file_unit - .structs - .push(create_test_struct("PublicStruct", true)); - file_unit - .structs - .push(create_test_struct("PrivateStruct", false)); - - // Add declarations - file_unit.declares.push(DeclareStatements { - source: "use std::collections::HashMap;".to_string(), - kind: DeclareKind::Use, - }); - - // Test NoTests strategy - let formatted = file_unit - .format(&BankStrategy::NoTests, LanguageType::Rust) - .unwrap(); - - // Should include all non-test items regardless of visibility - assert!(formatted.contains("pub mod public_module")); - assert!(formatted.contains("mod private_module")); - assert!(!formatted.contains("fn test_function")); - assert!(formatted.contains("fn public_function")); - assert!(formatted.contains("fn private_function")); - assert!(formatted.contains("struct PublicStruct")); - assert!(formatted.contains("struct PrivateStruct")); - assert!(formatted.contains("use std::collections::HashMap;")); - - // We now just display struct source in NoTests, not individual methods anymore - assert!(!formatted.contains("fn publicstruct_private_method()")); - } + let expected_doc_summary = "/// This is an inner line comment, often used for module-level docs.\n/// This is an inner block comment.\n/// Also for module-level docs usually."; + let no_tests_fmt = comments_only_unit.format(&BankStrategy::NoTests, LanguageType::Rust).unwrap(); + assert_eq!(no_tests_fmt.trim(), expected_doc_summary.trim()); + + let summary_fmt = comments_only_unit.format(&BankStrategy::Summary, LanguageType::Rust).unwrap(); + assert_eq!(summary_fmt.trim(), expected_doc_summary.trim()); +} - #[test] - fn test_enum_formatter_summary() { - let public_enum = create_test_enum("PublicEnum", true); - let formatted = public_enum - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - - // Summary for enums now follows the same pattern as structs - assert!(formatted.contains("/// Docs for PublicEnum")); - assert!(formatted.contains("pub enum PublicEnum")); - // No fields/variants in the enum - assert!(!formatted.contains("VariantA,")); - assert!(!formatted.contains("VariantB(String),")); - - let private_enum = create_test_enum("PrivateEnum", false); - let formatted = private_enum - .format(&BankStrategy::Summary, LanguageType::Rust) - .unwrap(); - // Private enums should be omitted entirely in summary - assert!(formatted.is_empty()); - } +#[test] +fn test_trait_unit_head_field_assumption() { + // This test acknowledges that TraitUnit does not have `head: String` yet. + // The formatter for TraitUnit (Summary) currently falls back to using the first line of `source`. + let trait_unit = TraitUnit { + name: "SimpleTrait".to_string(), + visibility: Visibility::Public, + doc: None, + source: Some("pub trait SimpleTrait: Debug where T: Copy {\n // ...\n}".to_string()), + attributes: vec![], + methods: vec![], + // head: "pub trait SimpleTrait: Debug where T: Copy".to_string(), // Ideal + }; + let expected_summary = "pub trait SimpleTrait: Debug where T: Copy { ... }"; + let formatted_summary = trait_unit.format(&BankStrategy::Summary, LanguageType::Rust).unwrap(); + assert_eq!(formatted_summary.trim(), expected_summary.trim()); } diff --git a/src/parser/lang/cpp.rs b/src/parser/lang/cpp.rs index fdff58e..194503d 100644 --- a/src/parser/lang/cpp.rs +++ b/src/parser/lang/cpp.rs @@ -10,7 +10,7 @@ use tree_sitter::{Node, Parser}; impl CppParser { pub fn try_new() -> Result { let mut parser = Parser::new(); - let language = tree_sitter_cpp::LANGUAGE; + let language = tree_sitter_cpp::language(); parser .set_language(&language.into()) .map_err(|e| Error::TreeSitter(e.to_string()))?; diff --git a/src/parser/lang/go.rs b/src/parser/lang/go.rs index 178d87c..6867611 100644 --- a/src/parser/lang/go.rs +++ b/src/parser/lang/go.rs @@ -204,7 +204,7 @@ impl LanguageParser for GoParser { impl GoParser { pub fn try_new() -> Result { let mut parser = Parser::new(); - let language = tree_sitter_go::LANGUAGE; + let language = tree_sitter_go::language(); parser .set_language(&language.into()) .map_err(|e| Error::TreeSitter(e.to_string()))?; diff --git a/src/parser/lang/python.rs b/src/parser/lang/python.rs index da538e1..166220e 100644 --- a/src/parser/lang/python.rs +++ b/src/parser/lang/python.rs @@ -25,7 +25,7 @@ fn get_child_node_text<'a>(node: Node<'a>, kind: &str, source_code: &'a str) -> impl PythonParser { pub fn try_new() -> Result { let mut parser = Parser::new(); - let language = tree_sitter_python::LANGUAGE; + let language = tree_sitter_python::language(); parser .set_language(&language.into()) .map_err(|e| Error::TreeSitter(e.to_string()))?; diff --git a/src/parser/lang/rust.rs b/src/parser/lang/rust.rs index fa6652b..89c3f86 100644 --- a/src/parser/lang/rust.rs +++ b/src/parser/lang/rust.rs @@ -5,616 +5,469 @@ use crate::{ use std::fs; use std::ops::{Deref, DerefMut}; use std::path::Path; -use tree_sitter::{Node, Parser}; +use tree_sitter::{Node, Parser, Query, QueryCursor, QueryMatch}; + +const VISIBILITY_MODIFIER_QUERY: &str = r#" +(visibility_modifier) @visibility +"#; + +const ATTRIBUTE_QUERY: &str = r#" +(attribute_item) @attribute +"#; + +// This query is general. Specific doc comment patterns (///, /**) are handled in extract_documentation. +const DOC_COMMENT_QUERY: &str = r#" +[ + (line_comment) @doc_comment_line + (block_comment) @doc_comment_block +] +"#; + +const FUNCTION_QUERY: &str = r#" +(function_item + (attribute_item)* @attribute_node + (visibility_modifier)? @visibility_node + name: (identifier) @name + parameters: (parameters) @parameters + return_type: (type)? @return_type + body: (block) @body + ;; Inner documentation comments + (line_comment)* @doc_comment_line_inner + (block_comment)* @doc_comment_block_inner +) +"#; + +const STRUCT_QUERY: &str = r#" +(struct_item + (attribute_item)* @attribute_node + (visibility_modifier)? @visibility_node + name: (type_identifier) @name + type_parameters: (type_parameters)? @generics + (field_declaration_list)? @fields + ;; Inner documentation comments + (line_comment)* @doc_comment_line_inner + (block_comment)* @doc_comment_block_inner +) +"#; + +const ENUM_QUERY: &str = r#" +(enum_item + (attribute_item)* @attribute_node + (visibility_modifier)? @visibility_node + name: (type_identifier) @name + type_parameters: (type_parameters)? @generics + body: (enum_variant_list) @variants + ;; Inner documentation comments + (line_comment)* @doc_comment_line_inner + (block_comment)* @doc_comment_block_inner +) +"#; + +const TRAIT_QUERY: &str = r#" +(trait_item + (attribute_item)* @attribute_node + (visibility_modifier)? @visibility_node + name: (type_identifier) @name + type_parameters: (type_parameters)? @generics + body: (declaration_list) @body +) +"#; + +// IMPL_QUERY focuses on the overall structure including the body. +const IMPL_QUERY: &str = r#" +(impl_item + (attribute_item)* @attribute_node + type_parameters: (type_parameters)? @impl_generics + body: (declaration_list) @body + // Trait and type are better handled by IMPL_HEAD_QUERY for head construction +) +"#; + +// IMPL_HEAD_QUERY is more focused on the "impl ... for ..." part, useful for head construction. +// Designed to be run on the `impl_item` node. +const IMPL_HEAD_QUERY: &str = r#" +(impl_item + type_parameters: (type_parameters)? @impl_generics + trait: (type_identifier)? @trait_name + trait: (generic_type (type_identifier) @trait_name_generic (_)? @trait_generics_args)? @trait_full + type: (type_identifier) @type_name + type: (generic_type (type_identifier) @type_name_generic (_)? @type_generics_args)? @type_full + // Scoped identifiers for trait or type + trait: (scoped_type_identifier path: _ @trait_path name: (type_identifier) @trait_name_scoped)? @trait_full_scoped + type: (scoped_type_identifier path: _ @type_path name: (type_identifier) @type_name_scoped)? @type_full_scoped +) +"#; + + +const MODULE_QUERY: &str = r#" +(mod_item + (attribute_item)* @attribute_node + (visibility_modifier)? @visibility_node + name: (identifier) @name + body: (declaration_list)? @body +) +"#; + // Helper function to extract attributes looking backwards from a node fn extract_attributes(node: Node, source_code: &str) -> Vec { let mut attributes = Vec::new(); let mut current_node = node; - // Also check the node itself if it's an attribute if current_node.kind() == "attribute_item" { - if let Some(attr_text) = get_node_text(current_node, source_code) { - attributes.insert(0, attr_text); - } + if let Some(attr_text) = get_node_text(current_node, source_code) { attributes.insert(0, attr_text); } } while let Some(prev) = current_node.prev_sibling() { if prev.kind() == "attribute_item" { - if let Some(attr_text) = get_node_text(prev, source_code) { - attributes.insert(0, attr_text); - } - current_node = prev; // Continue looking further back + if let Some(attr_text) = get_node_text(prev, source_code) { attributes.insert(0, attr_text); } + current_node = prev; } else if prev.kind() == "line_comment" || prev.kind() == "block_comment" { - // Skip comment nodes and continue searching current_node = prev; - } else { - // Stop if we hit any other non-attribute, non-comment item - break; - } + } else { break; } } attributes } // Helper function to get the text of the first child node of a specific kind fn get_child_node_text<'a>(node: Node<'a>, kind: &str, source_code: &'a str) -> Option { - // First try to find it directly as a child - if let Some(child) = node - .children(&mut node.walk()) - .find(|child| child.kind() == kind) - { - return child - .utf8_text(source_code.as_bytes()) - .ok() - .map(String::from); + if let Some(child) = node.children(&mut node.walk()).find(|child| child.kind() == kind) { + return child.utf8_text(source_code.as_bytes()).ok().map(String::from); } - - // If not found as direct child, try to find it in nested structure - // This is needed for struct_item and trait_item where the identifier might be nested for child in node.children(&mut node.walk()) { - // Check types that are known to contain identifiers - if child.kind() == "type_identifier" { - return child - .utf8_text(source_code.as_bytes()) - .ok() - .map(String::from); - } - - // Look for type identifiers - if let Some(grandchild) = child - .children(&mut child.walk()) - .find(|gc| gc.kind() == "type_identifier" || gc.kind() == kind) - { - return grandchild - .utf8_text(source_code.as_bytes()) - .ok() - .map(String::from); + if child.kind() == "type_identifier" { return child.utf8_text(source_code.as_bytes()).ok().map(String::from); } + if let Some(grandchild) = child.children(&mut child.walk()).find(|gc| gc.kind() == "type_identifier" || gc.kind() == kind) { + return grandchild.utf8_text(source_code.as_bytes()).ok().map(String::from); } } - None } // Helper function to get the text of a node fn get_node_text(node: Node, source_code: &str) -> Option { - node.utf8_text(source_code.as_bytes()) - .ok() - .map(String::from) + node.utf8_text(source_code.as_bytes()).ok().map(String::from) } impl RustParser { pub fn try_new() -> Result { let mut parser = Parser::new(); - let language = tree_sitter_rust::LANGUAGE; - parser - .set_language(&language.into()) - .map_err(|e| Error::TreeSitter(e.to_string()))?; + let language = tree_sitter_rust::language(); + parser.set_language(&language.into()).map_err(|e| Error::TreeSitter(e.to_string()))?; Ok(Self { parser }) } + + fn get_capture_text_from_match<'a>( query_match: &QueryMatch<'a, 'a>, capture_name: &str, source_code: &'a str, query: &'a Query) -> Option { + query_match.captures.iter().find(|c| query.capture_names()[c.index as usize] == capture_name).and_then(|c| get_node_text(c.node, source_code)) + } + + fn get_capture_node_from_match<'a>( query_match: &QueryMatch<'a, 'a>, capture_name: &str, query: &'a Query) -> Option> { + query_match.captures.iter().find(|c| query.capture_names()[c.index as usize] == capture_name).map(|c| c.node) + } - // Helper function to parse the head (declaration line) of an item - fn parse_item_head( - &self, - node: Node, - source_code: &str, - item_type: &str, - visibility: &Visibility, - name: &str, - ) -> String { + fn parse_item_head( &self, node: Node, source_code: &str, item_type: &str, visibility: &Visibility, name: &str) -> String { if let Some(src) = get_node_text(node, source_code) { - if let Some(body_start_idx) = src.find('{') { - src[0..body_start_idx].trim().to_string() - } else if let Some(semi_idx) = src.find(';') { - // Handle unit items like `struct Unit;` - src[0..=semi_idx].trim().to_string() - } else { - // Fallback, might occur for malformed code or items without bodies/semicolons - format!( - "{} {} {}", - visibility.as_str(LanguageType::Rust), - item_type, - name - ) - } - } else { - format!( - "{} {} {}", - visibility.as_str(LanguageType::Rust), - item_type, - name - ) + if let Some(body_start_idx) = src.find('{') { return src[0..body_start_idx].trim().to_string(); } + else if let Some(semi_idx) = src.find(';') { return src[0..=semi_idx].trim().to_string(); } } + let vis_str = visibility.as_str(LanguageType::Rust); + if vis_str.is_empty() { format!("{} {}", item_type, name) } else { format!("{} {} {}", vis_str, item_type, name) } } - // Helper function to extract documentation from comments preceding a node fn extract_documentation(&self, node: Node, source_code: &str) -> Option { let mut doc_comments = Vec::new(); let mut current_node = node; - - // Look backwards from the node for comments and attributes while let Some(prev) = current_node.prev_sibling() { let kind = prev.kind(); - - if kind == "line_comment" { - if let Some(comment) = get_node_text(prev, source_code) { - if comment.starts_with("///") { - let cleaned = comment.trim_start_matches("///").trim().to_string(); - doc_comments.insert(0, cleaned); - } // else: it's a non-doc line comment, ignore and continue searching backward - } - } else if kind == "block_comment" { - if let Some(comment) = get_node_text(prev, source_code) { - if comment.starts_with("/**") { - let lines: Vec<&str> = comment.lines().collect(); - if lines.len() > 1 { - // Insert lines in reverse order to maintain original order - for line in lines[1..lines.len() - 1].iter().rev() { - let cleaned = line.trim_start_matches('*').trim().to_string(); - if !cleaned.is_empty() { - doc_comments.insert(0, cleaned); - } - } - } - } // else: it's a non-doc block comment, ignore and continue searching backward - } - } else if kind != "attribute_item" { - // Stop if it's not a comment or attribute - break; - } - // Continue looking backwards + if kind == "line_comment" { if let Some(comment) = get_node_text(prev, source_code) { if comment.starts_with("///") { doc_comments.insert(0, comment.trim_start_matches("///").trim().to_string()); } } } + else if kind == "block_comment" { if let Some(comment) = get_node_text(prev, source_code) { if comment.starts_with("/**") { let lines: Vec<&str> = comment.lines().collect(); if lines.len() > 1 { for line in lines[1..lines.len()-1].iter().rev() { let cleaned = line.trim_start_matches('*').trim().to_string(); if !cleaned.is_empty() { doc_comments.insert(0, cleaned);}}}}} } + else if kind != "attribute_item" { break; } current_node = prev; } - - if doc_comments.is_empty() { - None - } else { - Some(doc_comments.join("\n")) - } + if doc_comments.is_empty() { None } else { Some(doc_comments.join("\n")) } } - // Helper function to determine visibility fn determine_visibility(&self, node: Node, source_code: &str) -> Visibility { - if let Some(vis_mod) = node - .children(&mut node.walk()) - .find(|child| child.kind() == "visibility_modifier") - { - if let Some(vis_text) = get_node_text(vis_mod, source_code) { - return match vis_text.as_str() { - "pub" => Visibility::Public, - "pub(crate)" => Visibility::Crate, - s if s.starts_with("pub(") => Visibility::Restricted(s.to_string()), - _ => Visibility::Private, // Should not happen based on grammar? - }; - } - } + let vis_node_opt = node.child_by_field_name("visibility_modifier").or_else(|| node.children(&mut node.walk()).find(|child| child.kind() == "visibility_modifier")); + if let Some(vis_mod_node) = vis_node_opt { if let Some(vis_text) = get_node_text(vis_mod_node, source_code) { + return match vis_text.as_str() { "pub" => Visibility::Public, "pub(crate)" => Visibility::Crate, s if s.starts_with("pub(") => Visibility::Restricted(s.to_string()), _ => Visibility::Private }; + }} Visibility::Private } - // Parse function and extract its details fn parse_function(&self, node: Node, source_code: &str) -> Result { - // Documentation and Attributes are now reliably extracted by looking backwards let documentation = self.extract_documentation(node, source_code); - let attributes = extract_attributes(node, source_code); - let name = get_child_node_text(node, "identifier", source_code) - .unwrap_or_else(|| "unknown".to_string()); - let visibility = self.determine_visibility(node, source_code); - let source = get_node_text(node, source_code); - let mut signature = None; - let mut body = None; - - if let Some(src) = &source { - if let Some(body_start_idx) = src.find('{') { - signature = Some(src[0..body_start_idx].trim().to_string()); - body = Some(src[body_start_idx..].trim().to_string()); - } else if let Some(sig_end_idx) = src.find(';') { - signature = Some(src[0..=sig_end_idx].trim().to_string()); - } + let mut attributes = extract_attributes(node, source_code); + let mut visibility = self.determine_visibility(node, source_code); + let full_source = get_node_text(node, source_code); + + let query = Query::new(self.parser.language().unwrap(), FUNCTION_QUERY).map_err(|e| Error::TreeSitter(format!("FUNCTION_QUERY compile error: {}", e)))?; + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, node, source_code.as_bytes()); + let mut name = "unknown".to_string(); + let mut signature: Option = None; + let mut body_text: Option = None; + let mut parameters_text_opt: Option = None; + let mut return_type_text_opt: Option = None; + + if let Some(query_match) = matches.peekable().peek() { + if let Some(n) = Self::get_capture_text_from_match(query_match, "name", source_code, &query) { name = n; } + parameters_text_opt = Self::get_capture_text_from_match(query_match, "parameters", source_code, &query); + return_type_text_opt = Self::get_capture_text_from_match(query_match, "return_type", source_code, &query); + body_text = Self::get_capture_text_from_match(query_match, "body", source_code, &query); + for cap in query_match.captures { if query.capture_names()[cap.index as usize] == "attribute_node" { if let Some(attr_txt)=get_node_text(cap.node, source_code){ if !attributes.contains(&attr_txt) { attributes.push(attr_txt);}}}} + if visibility == Visibility::Private { if let Some(vis_node) = Self::get_capture_node_from_match(query_match, "visibility_node", &query) { if let Some(vis_text)=get_node_text(vis_node,source_code){ visibility = Visibility::from_str(&vis_text, LanguageType::Rust);}}} } - - Ok(FunctionUnit { - name, - visibility, - doc: documentation, - source, - signature, - body, - attributes, - }) + + let parameters_text = parameters_text_opt.unwrap_or_default(); + let return_type_text = return_type_text_opt.unwrap_or_default(); + let mut sig_str = format!("fn {}", name); + sig_str.push_str(¶meters_text); + if !return_type_text.is_empty() { sig_str.push_str(&format!(" -> {}", return_type_text)); } + if body_text.is_none() { sig_str.push(';'); } + signature = Some(sig_str.trim().to_string()); + + let is_query_sig_problematic = signature.as_deref().map_or(true, |s| s.is_empty() || (parameters_text.is_empty() && !s.contains("()"))); + if is_query_sig_problematic { + if let Some(src) = &full_source { + if let Some(body_start_idx) = src.find('{') { signature = Some(src[0..body_start_idx].trim().to_string()); } + else if let Some(sig_end_idx) = src.find(';') { signature = Some(src[0..=sig_end_idx].trim().to_string()); } + } else if signature.as_deref().map_or(true, |s| s.trim() == format!("fn {}", name)) { signature = None; } + } + Ok(FunctionUnit { name, visibility, doc: documentation, source: full_source, signature, body: body_text, attributes }) } - // Parse module and extract its details fn parse_module(&self, node: Node, source_code: &str) -> Result { - let name = get_child_node_text(node, "identifier", source_code) - .unwrap_or_else(|| "unknown".to_string()); - let visibility = self.determine_visibility(node, source_code); - let document = self.extract_documentation(node, source_code); - let attributes = extract_attributes(node, source_code); - let source = get_node_text(node, source_code); - - let mut module = ModuleUnit { - name, - visibility, - doc: document, - source, - attributes, - ..Default::default() - }; - - // Look for the module's body node - if let Some(block_node) = node - .children(&mut node.walk()) - .find(|child| child.kind() == "declaration_list") - { - // Process items in the module body - for item in block_node.children(&mut block_node.walk()) { + let documentation = self.extract_documentation(node, source_code); + let mut attributes = extract_attributes(node, source_code); + let mut visibility = self.determine_visibility(node, source_code); + let full_source = get_node_text(node, source_code); + + let query = Query::new(self.parser.language().unwrap(), MODULE_QUERY).map_err(|e| Error::TreeSitter(format!("MODULE_QUERY error: {}", e)))?; + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, node, source_code.as_bytes()); + let mut name = "unknown".to_string(); + let mut body_node_opt: Option = None; + + if let Some(query_match) = matches.peekable().peek() { + if let Some(n_text) = Self::get_capture_text_from_match(query_match, "name", source_code, &query) { name = n_text; } + body_node_opt = Self::get_capture_node_from_match(query_match, "body", &query); + for cap in query_match.captures { if query.capture_names()[cap.index as usize] == "attribute_node" { if let Some(attr_txt)=get_node_text(cap.node, source_code){ if !attributes.contains(&attr_txt) { attributes.push(attr_txt);}}}} + if visibility == Visibility::Private { if let Some(vis_node) = Self::get_capture_node_from_match(query_match, "visibility_node", &query) { if let Some(vis_text)=get_node_text(vis_node,source_code){ visibility = Visibility::from_str(&vis_text, LanguageType::Rust);}}} + } else { + name = get_child_node_text(node, "identifier", source_code).unwrap_or_else(||"unknown".to_string()); + if node.child_by_field_name("body").is_none() { body_node_opt = None; } + else { body_node_opt = node.children(&mut node.walk()).find(|child| child.kind() == "declaration_list"); } + } + + let mut module_unit = ModuleUnit { name, visibility, doc: documentation, source: full_source, attributes, ..Default::default() }; + if let Some(body_node) = body_node_opt { + for item in body_node.children(&mut body_node.walk()) { match item.kind() { - "function_item" => { - if let Ok(func) = self.parse_function(item, source_code) { - module.functions.push(func); - } - } - "struct_item" => { - if let Ok(struct_item) = self.parse_struct(item, source_code) { - module.structs.push(struct_item); - } - } - "enum_item" => { - // Handle enum as a struct in our simplified model - if let Ok(enum_as_struct) = self.parse_enum_as_struct(item, source_code) { - module.structs.push(enum_as_struct); - } - } - "trait_item" => { - if let Ok(trait_item) = self.parse_trait(item, source_code) { - module.traits.push(trait_item); - } - } - "impl_item" => { - if let Ok(impl_item) = self.parse_impl(item, source_code) { - module.impls.push(impl_item); - } - } - "mod_item" => { - if let Ok(submodule) = self.parse_module(item, source_code) { - module.submodules.push(submodule); - } - } - "use_declaration" => { - if let Some(declare_text) = get_node_text(item, source_code) { - module.declares.push(crate::DeclareStatements { - source: declare_text, - kind: crate::DeclareKind::Use, - }); - } - } - _ => { - // Ignore other kinds of items for now - } + "function_item" => if let Ok(func) = self.parse_function(item, source_code) { module_unit.functions.push(func); }, + "struct_item" => if let Ok(s) = self.parse_struct(item, source_code) { module_unit.structs.push(s); }, + "enum_item" => if let Ok(e) = self.parse_enum_as_struct(item, source_code) { module_unit.structs.push(e); }, + "trait_item" => if let Ok(t) = self.parse_trait(item, source_code) { module_unit.traits.push(t); }, + "impl_item" => if let Ok(i) = self.parse_impl(item, source_code) { module_unit.impls.push(i); }, + "mod_item" => if let Ok(m) = self.parse_module(item, source_code) { module_unit.submodules.push(m); }, + "use_declaration" => if let Some(txt) = get_node_text(item, source_code) { module_unit.declares.push(crate::DeclareStatements{source: txt, kind: crate::DeclareKind::Use}); }, + _ => {} } } } - - Ok(module) + Ok(module_unit) } - // Parse an enum as a struct (for simplified model) fn parse_enum_as_struct(&self, node: Node, source_code: &str) -> Result { - let name = get_child_node_text(node, "identifier", source_code) - .unwrap_or_else(|| "unknown".to_string()); - let visibility = self.determine_visibility(node, source_code); let documentation = self.extract_documentation(node, source_code); - let attributes = extract_attributes(node, source_code); - let source = get_node_text(node, source_code); - - // Parse enum head using the helper, passing visibility by reference - let head = self.parse_item_head(node, source_code, "enum", &visibility, &name); - - let mut fields = Vec::new(); - // Find the enum body (enum_variant_list) - if let Some(body_node) = node - .children(&mut node.walk()) - .find(|child| child.kind() == "enum_variant_list") - { - for variant_node in body_node.children(&mut body_node.walk()) { - if variant_node.kind() == "enum_variant" { - let variant_name = get_child_node_text(variant_node, "identifier", source_code) - .unwrap_or_default(); - let variant_documentation = - self.extract_documentation(variant_node, source_code); - let variant_attributes = extract_attributes(variant_node, source_code); - let variant_source = get_node_text(variant_node, source_code); - - // Trim trailing comma from the source if present - let final_variant_source = variant_source.map(|s| { - if s.ends_with(',') { - s[..s.len() - 1].to_string() - } else { - s - } - }); - - fields.push(FieldUnit { - name: variant_name, - doc: variant_documentation, - attributes: variant_attributes, - source: final_variant_source, // Use the trimmed source - }); - } - } + let mut attributes = extract_attributes(node, source_code); + let mut visibility = self.determine_visibility(node, source_code); + let full_source = get_node_text(node, source_code); + + let query = Query::new(self.parser.language().unwrap(), ENUM_QUERY).map_err(|e| Error::TreeSitter(format!("ENUM_QUERY error: {}",e)))?; + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, node, source_code.as_bytes()); + let mut name = "unknown".to_string(); + let mut head_str: String; + let mut enum_variant_list_node: Option = None; + + if let Some(query_match) = matches.peekable().peek() { + if let Some(n_text) = Self::get_capture_text_from_match(query_match, "name", source_code, &query) { name = n_text; } + let generics_text = Self::get_capture_text_from_match(query_match, "generics", source_code, &query).unwrap_or_default(); + enum_variant_list_node = Self::get_capture_node_from_match(query_match, "variants", &query); + for cap in query_match.captures { if query.capture_names()[cap.index as usize] == "attribute_node" { if let Some(attr_txt)=get_node_text(cap.node, source_code){ if !attributes.contains(&attr_txt) { attributes.push(attr_txt);}}}} + if visibility == Visibility::Private { if let Some(vis_node) = Self::get_capture_node_from_match(query_match, "visibility_node", &query) { if let Some(vis_text)=get_node_text(vis_node,source_code){ visibility = Visibility::from_str(&vis_text, LanguageType::Rust);}}} + let vis_str = visibility.as_str(LanguageType::Rust); + head_str = if vis_str.is_empty() { format!("enum {}{}", name, generics_text) } else { format!("{} enum {}{}", vis_str, name, generics_text) }; + } else { + name = get_child_node_text(node, "identifier", source_code).unwrap_or_else(|| "unknown".to_string()); + head_str = self.parse_item_head(node, source_code, "enum", &visibility, &name); + enum_variant_list_node = node.children(&mut node.walk()).find(|child| child.kind() == "enum_variant_list"); } - let struct_unit = StructUnit { - name, - head, - visibility, // Use the original visibility here - doc: documentation, - source, - attributes, - fields, // Populated with variants - methods: Vec::new(), - }; - - Ok(struct_unit) + let mut fields = Vec::new(); + if let Some(body_node) = enum_variant_list_node { for variant_node in body_node.children(&mut body_node.walk()) { if variant_node.kind() == "enum_variant" { + let v_name = get_child_node_text(variant_node, "identifier", source_code).unwrap_or_default(); + let v_doc = self.extract_documentation(variant_node, source_code); + let v_attrs = extract_attributes(variant_node, source_code); + let v_src = get_node_text(variant_node, source_code).map(|s| if s.ends_with(',') { s[..s.len()-1].to_string()} else {s}); + fields.push(FieldUnit { name: v_name, doc: v_doc, attributes: v_attrs, source: v_src }); + }}} + Ok(StructUnit { name, head: head_str, visibility, doc: documentation, source: full_source, attributes, fields, methods: Vec::new() }) } - // Parse struct and extract its details fn parse_struct(&self, node: Node, source_code: &str) -> Result { - let name = get_child_node_text(node, "identifier", source_code) - .unwrap_or_else(|| "unknown".to_string()); - let visibility = self.determine_visibility(node, source_code); let documentation = self.extract_documentation(node, source_code); - let attributes = extract_attributes(node, source_code); - let source = get_node_text(node, source_code); - // let mut fields = Vec::new(); // Commented out: Requires FieldUnit/StructUnit changes - - // Parse struct head using the helper, passing visibility by reference - let head = self.parse_item_head(node, source_code, "struct", &visibility, &name); - - let mut fields = Vec::new(); - if let Some(body_node) = node - .children(&mut node.walk()) - .find(|child| child.kind() == "field_declaration_list") - { - for field_decl in body_node.children(&mut body_node.walk()) { - if field_decl.kind() == "field_declaration" { - let field_documentation = self.extract_documentation(field_decl, source_code); - let field_attributes = extract_attributes(field_decl, source_code); - let field_source = get_node_text(field_decl, source_code); - - let field_name = - get_child_node_text(field_decl, "field_identifier", source_code) - .unwrap_or_default(); - - fields.push(FieldUnit { - name: field_name, - doc: field_documentation, - attributes: field_attributes, - source: field_source, - }); - } - } + let mut attributes = extract_attributes(node, source_code); + let mut visibility = self.determine_visibility(node, source_code); + let full_source = get_node_text(node, source_code); + + let query = Query::new(self.parser.language().unwrap(), STRUCT_QUERY).map_err(|e| Error::TreeSitter(format!("STRUCT_QUERY error: {}",e)))?; + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, node, source_code.as_bytes()); + let mut name = "unknown".to_string(); + let mut head_str: String; + let mut field_declaration_list_node: Option = None; + + if let Some(query_match) = matches.peekable().peek() { + if let Some(n_text) = Self::get_capture_text_from_match(query_match, "name", source_code, &query) { name = n_text; } + let generics_text = Self::get_capture_text_from_match(query_match, "generics", source_code, &query).unwrap_or_default(); + field_declaration_list_node = Self::get_capture_node_from_match(query_match, "fields", &query); + for cap in query_match.captures { if query.capture_names()[cap.index as usize] == "attribute_node" { if let Some(attr_txt)=get_node_text(cap.node, source_code){ if !attributes.contains(&attr_txt) { attributes.push(attr_txt);}}}} + if visibility == Visibility::Private { if let Some(vis_node) = Self::get_capture_node_from_match(query_match, "visibility_node", &query) { if let Some(vis_text)=get_node_text(vis_node,source_code){ visibility = Visibility::from_str(&vis_text, LanguageType::Rust);}}} + let vis_str = visibility.as_str(LanguageType::Rust); + head_str = if vis_str.is_empty() { format!("struct {}{}", name, generics_text) } else { format!("{} struct {}{}", vis_str, name, generics_text) }; + if field_declaration_list_node.is_none() && full_source.as_deref().map_or(false, |s| s.trim_end().ends_with(';')) { head_str.push(';'); } + } else { + name = get_child_node_text(node, "identifier", source_code).unwrap_or_else(|| "unknown".to_string()); + head_str = self.parse_item_head(node, source_code, "struct", &visibility, &name); + field_declaration_list_node = node.children(&mut node.walk()).find(|child| child.kind() == "field_declaration_list"); } - - // NOTE: Ensure StructUnit in src/parser/mod.rs has the `fields` field added. - let struct_unit = StructUnit { - name, - head, - visibility, // Use the original visibility here - doc: documentation, - source, - attributes, - fields, - methods: Vec::new(), // Methods are parsed in impl blocks, not here - }; - - Ok(struct_unit) + + let mut fields = Vec::new(); + if let Some(body_node) = field_declaration_list_node { for field_decl in body_node.children(&mut body_node.walk()) { if field_decl.kind() == "field_declaration" { + let f_doc = self.extract_documentation(field_decl, source_code); + let f_attrs = extract_attributes(field_decl, source_code); + let f_src = get_node_text(field_decl, source_code); + let f_name = get_child_node_text(field_decl, "field_identifier", source_code).unwrap_or_default(); + fields.push(FieldUnit { name: f_name, doc: f_doc, attributes: f_attrs, source: f_src }); + }}} + Ok(StructUnit { name, head: head_str, visibility, doc: documentation, source: full_source, attributes, fields, methods: Vec::new() }) } - // Parse trait and extract its details fn parse_trait(&self, node: Node, source_code: &str) -> Result { - let name = get_child_node_text(node, "identifier", source_code) - .unwrap_or_else(|| "unknown".to_string()); - let visibility = self.determine_visibility(node, source_code); - let documentation = self.extract_documentation(node, source_code); - let attributes = extract_attributes(node, source_code); - let source = get_node_text(node, source_code); - let mut methods = Vec::new(); - - // Look for trait items (methods, associated types, consts) - if let Some(block_node) = node - .children(&mut node.walk()) - .find(|child| child.kind() == "declaration_list") - { - for item in block_node.children(&mut block_node.walk()) { - // Check for both function definitions and signatures - if item.kind() == "function_item" || item.kind() == "function_signature_item" { - if let Ok(mut method) = self.parse_function(item, source_code) { - // Methods in traits are implicitly public - method.visibility = Visibility::Public; - methods.push(method); - } - } - // TODO: Potentially parse associated_type_declaration, constant_item in the future - } + let documentation = self.extract_documentation(node, source_code); + let mut attributes = extract_attributes(node, source_code); + let mut visibility = self.determine_visibility(node, source_code); + let full_source = get_node_text(node, source_code); + + let query = Query::new(self.parser.language().unwrap(), TRAIT_QUERY).map_err(|e| Error::TreeSitter(format!("TRAIT_QUERY error: {}", e)))?; + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, node, source_code.as_bytes()); + let mut name = "unknown".to_string(); + let mut head_str: String; + let mut body_node_opt: Option = None; + + if let Some(query_match) = matches.peekable().peek() { + if let Some(n_text) = Self::get_capture_text_from_match(query_match, "name", source_code, &query) { name = n_text; } + let generics_text = Self::get_capture_text_from_match(query_match, "generics", source_code, &query).unwrap_or_default(); + body_node_opt = Self::get_capture_node_from_match(query_match, "body", &query); + for cap in query_match.captures { let cap_name = &query.capture_names()[cap.index as usize]; if *cap_name == "attribute_node" { if let Some(attr_txt) = get_node_text(cap.node, source_code) { if !attributes.contains(&attr_txt) { attributes.push(attr_txt); }}}} + if visibility == Visibility::Private { if let Some(vis_node) = Self::get_capture_node_from_match(query_match, "visibility_node", &query) { if let Some(vis_text) = get_node_text(vis_node, source_code) { visibility = Visibility::from_str(&vis_text, LanguageType::Rust);}}} + let vis_str = visibility.as_str(LanguageType::Rust); + head_str = if vis_str.is_empty() { format!("trait {}{}", name, generics_text) } else { format!("{} trait {}{}", vis_str, name, generics_text) }; + } else { + name = get_child_node_text(node, "identifier", source_code).unwrap_or_else(|| "unknown".to_string()); + head_str = self.parse_item_head(node, source_code, "trait", &visibility, &name); + body_node_opt = node.children(&mut node.walk()).find(|child| child.kind() == "declaration_list"); } - Ok(TraitUnit { - name, - visibility, - doc: documentation, - source, - attributes, - methods, - }) + let mut methods = Vec::new(); + if let Some(body_node) = body_node_opt { for item_node in body_node.children(&mut body_node.walk()) { + if item_node.kind() == "function_item" || item_node.kind() == "function_signature_item" { + if let Ok(mut method) = self.parse_function(item_node, source_code) { method.visibility = Visibility::Public; methods.push(method); } + } + }} + Ok(TraitUnit { name, head: head_str, visibility, doc: documentation, source: full_source, attributes, methods }) } - // Parse impl block and extract its details fn parse_impl(&self, node: Node, source_code: &str) -> Result { let documentation = self.extract_documentation(node, source_code); - let attributes = extract_attributes(node, source_code); - let source = get_node_text(node, source_code); - let mut methods = Vec::new(); - - // Parse impl head (declaration line) - let head = if let Some(src) = &source { - if let Some(body_start_idx) = src.find('{') { - src[0..body_start_idx].trim().to_string() - } else if let Some(semi_idx) = src.find(';') { - src[0..=semi_idx].trim().to_string() - } else { - "impl".to_string() // Fallback - } - } else { - "impl".to_string() // Fallback - }; - - // Check if head indicates a trait implementation - let is_trait_impl = head.contains(" for "); - - if let Some(block_node) = node - .children(&mut node.walk()) - .find(|child| child.kind() == "declaration_list") - { - for item in block_node.children(&mut block_node.walk()) { - if item.kind() == "function_item" { - if let Ok(mut method) = self.parse_function(item, source_code) { - // If this is a trait impl, methods are implicitly public - if is_trait_impl { - method.visibility = Visibility::Public; - } - methods.push(method); - } - } - // TODO: Parse associated types, consts within impls - } + let mut attributes = extract_attributes(node, source_code); + let full_source = get_node_text(node, source_code); + + let head_query = Query::new(self.parser.language().unwrap(), IMPL_HEAD_QUERY).map_err(|e| Error::TreeSitter(format!("IMPL_HEAD_QUERY error: {}",e)))?; + let main_query = Query::new(self.parser.language().unwrap(), IMPL_QUERY).map_err(|e| Error::TreeSitter(format!("IMPL_QUERY error: {}",e)))?; + let mut cursor = QueryCursor::new(); + let head_matches = cursor.matches(&head_query, node, source_code.as_bytes()); + let mut head_str: String; + + if let Some(head_match) = head_matches.peekable().peek() { + let impl_generics = Self::get_capture_text_from_match(head_match, "impl_generics", source_code, &head_query).unwrap_or_default(); + let trait_capture = Self::get_capture_text_from_match(head_match, "trait_full", source_code, &head_query) + .or_else(|| Self::get_capture_text_from_match(head_match, "trait_full_scoped", source_code, &head_query)) + .or_else(|| Self::get_capture_text_from_match(head_match, "trait_name", source_code, &head_query)); + let type_capture = Self::get_capture_text_from_match(head_match, "type_full", source_code, &head_query) + .or_else(|| Self::get_capture_text_from_match(head_match, "type_full_scoped", source_code, &head_query)) + .or_else(|| Self::get_capture_text_from_match(head_match, "type_name", source_code, &head_query)) + .unwrap_or_else(|| "UnknownType".to_string()); + + head_str = format!("impl{}", impl_generics); + if let Some(trait_text) = trait_capture { head_str.push_str(&format!(" {} for {}", trait_text, type_capture)); } + else { head_str.push_str(&format!(" {}", type_capture)); } + } else { + head_str = if let Some(src) = &full_source { if let Some(body_start_idx) = src.find('{') { src[0..body_start_idx].trim().to_string() } else { "impl".to_string() }} else { "impl".to_string() }; } - Ok(ImplUnit { - doc: documentation, - head, // Use parsed head - source, - attributes, - methods, - }) + let mut body_node_opt: Option = None; + let main_matches = cursor.matches(&main_query, node, source_code.as_bytes()); + if let Some(main_match) = main_matches.peekable().peek() { + body_node_opt = Self::get_capture_node_from_match(main_match, "body", &main_query); + for cap in main_match.captures { if main_query.capture_names()[cap.index as usize] == "attribute_node" { if let Some(attr_txt)=get_node_text(cap.node, source_code){ if !attributes.contains(&attr_txt) { attributes.push(attr_txt);}}}} + } else if body_node_opt.is_none() { + body_node_opt = node.children(&mut node.walk()).find(|child| child.kind() == "declaration_list"); + } + + let mut methods = Vec::new(); + let is_trait_impl = head_str.contains(" for "); + if let Some(body_node) = body_node_opt { for item in body_node.children(&mut body_node.walk()) { if item.kind() == "function_item" { + if let Ok(mut method) = self.parse_function(item, source_code) { if is_trait_impl { method.visibility = Visibility::Public; } methods.push(method); } + }}} + Ok(ImplUnit { doc: documentation, head: head_str, source: full_source, attributes, methods }) } } impl LanguageParser for RustParser { fn parse_file(&mut self, file_path: &Path) -> Result { - // Read the file let source_code = fs::read_to_string(file_path).map_err(Error::Io)?; - - // Parse the file - let tree = self - .parse(source_code.as_bytes(), None) - .ok_or_else(|| Error::TreeSitter("Failed to parse source code".to_string()))?; + let tree = self.parse(source_code.as_bytes(), None).ok_or_else(|| Error::TreeSitter("Failed to parse source code".to_string()))?; let root_node = tree.root_node(); - - // Create a new file unit let mut file_unit = FileUnit::new(file_path.to_path_buf()); file_unit.source = Some(source_code.clone()); - // Process the module document comment at the top of the file - // Find the first non-comment, non-attribute node to pass to extract_documentation let first_item_node = root_node.children(&mut root_node.walk()).find(|node| { - let kind = node.kind(); - kind != "line_comment" - && kind != "block_comment" - && kind != "attribute_item" - && kind != "inner_attribute_item" + let kind = node.kind(); kind != "line_comment" && kind != "block_comment" && kind != "attribute_item" && kind != "inner_attribute_item" }); + if let Some(first_node) = first_item_node { file_unit.doc = self.extract_documentation(first_node, &source_code); } + else if let Some(last_node) = root_node.children(&mut root_node.walk()).last() { file_unit.doc = self.extract_documentation(last_node.next_sibling().unwrap_or(last_node), &source_code); } - if let Some(first_node) = first_item_node { - file_unit.doc = self.extract_documentation(first_node, &source_code); - } else { - // If the file potentially only contains comments/attributes, try extracting from the last one - if let Some(last_node) = root_node.children(&mut root_node.walk()).last() { - file_unit.doc = self.extract_documentation( - last_node.next_sibling().unwrap_or(last_node), - &source_code, - ); - } - } - - // Process top-level items in the file for child in root_node.children(&mut root_node.walk()) { match child.kind() { - "function_item" => { - if let Ok(func) = self.parse_function(child, &source_code) { - file_unit.functions.push(func); - } - } - "struct_item" => { - if let Ok(struct_item) = self.parse_struct(child, &source_code) { - file_unit.structs.push(struct_item); - } - } - "enum_item" => { - // Handle enum as a struct in our simplified model - if let Ok(enum_as_struct) = self.parse_enum_as_struct(child, &source_code) { - file_unit.structs.push(enum_as_struct); - } - } - "trait_item" => { - if let Ok(trait_item) = self.parse_trait(child, &source_code) { - file_unit.traits.push(trait_item); - } - } - "impl_item" => { - if let Ok(impl_item) = self.parse_impl(child, &source_code) { - file_unit.impls.push(impl_item); - } - } - "mod_item" => { - if let Ok(module) = self.parse_module(child, &source_code) { - file_unit.modules.push(module); - } - } - "use_declaration" => { - if let Some(declare_text) = get_node_text(child, &source_code) { - file_unit.declares.push(crate::DeclareStatements { - source: declare_text, - kind: crate::DeclareKind::Use, - }); - } - } - "extern_crate_declaration" => { - if let Some(declare_text) = get_node_text(child, &source_code) { - file_unit.declares.push(crate::DeclareStatements { - source: declare_text, - kind: crate::DeclareKind::Other("extern_crate".to_string()), - }); - } - } - "mod_declaration" => { - if let Some(declare_text) = get_node_text(child, &source_code) { - file_unit.declares.push(crate::DeclareStatements { - source: declare_text, - kind: crate::DeclareKind::Mod, - }); - } - } - _ => { - // Ignore other top-level constructs - } + "function_item" => { if let Ok(func) = self.parse_function(child, &source_code) { file_unit.functions.push(func); } } + "struct_item" => { if let Ok(struct_item) = self.parse_struct(child, &source_code) { file_unit.structs.push(struct_item); } } + "enum_item" => { if let Ok(enum_as_struct) = self.parse_enum_as_struct(child, &source_code) { file_unit.structs.push(enum_as_struct); } } + "trait_item" => { if let Ok(trait_item) = self.parse_trait(child, &source_code) { file_unit.traits.push(trait_item); } } + "impl_item" => { if let Ok(impl_item) = self.parse_impl(child, &source_code) { file_unit.impls.push(impl_item); } } + "mod_item" => { if let Ok(module) = self.parse_module(child, &source_code) { file_unit.modules.push(module); } } + "use_declaration" => { if let Some(text) = get_node_text(child, &source_code) { file_unit.declares.push(crate::DeclareStatements { source: text, kind: crate::DeclareKind::Use }); } } + "extern_crate_declaration" => { if let Some(text) = get_node_text(child, &source_code) { file_unit.declares.push(crate::DeclareStatements { source: text, kind: crate::DeclareKind::Other("extern_crate".to_string()) }); } } + "mod_declaration" => { if let Some(text) = get_node_text(child, &source_code) { file_unit.declares.push(crate::DeclareStatements { source: text, kind: crate::DeclareKind::Mod }); } } + _ => {} } } - Ok(file_unit) } } -impl Deref for RustParser { - type Target = Parser; - - fn deref(&self) -> &Self::Target { - &self.parser - } -} - -impl DerefMut for RustParser { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.parser - } -} +impl Deref for RustParser { type Target = Parser; fn deref(&self) -> &Self::Target { &self.parser } } +impl DerefMut for RustParser { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.parser } } #[cfg(test)] mod tests { @@ -622,338 +475,260 @@ mod tests { use std::path::PathBuf; fn parse_fixture(file_name: &str) -> Result { - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") - .expect("CARGO_MANIFEST_DIR should be set during tests"); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); let path = PathBuf::from(manifest_dir).join("fixtures").join(file_name); - let mut parser = RustParser::try_new()?; - parser.parse_file(&path) + RustParser::try_new()?.parse_file(&path) } #[test] - fn test_parse_file_level_items() { - let file_unit = parse_fixture("sample.rs").unwrap(); - // Check that we have parsed at least some Rust content - assert!( - !file_unit.functions.is_empty() - || !file_unit.structs.is_empty() - || !file_unit.modules.is_empty() - || !file_unit.declares.is_empty() - ); + fn test_parse_file_level_items() { + let file_unit = parse_fixture("sample.rs").unwrap(); + assert!( !file_unit.functions.is_empty() || !file_unit.structs.is_empty() || !file_unit.modules.is_empty() || !file_unit.declares.is_empty() ); + assert_eq!(file_unit.doc.as_deref(), Some("This is a file-level documentation comment.\nIt describes the purpose of this sample file which includes a variety of Rust items.")); } #[test] - fn test_parse_declarations() { + fn test_use_extern_mod_declarations() { let file_unit = parse_fixture("sample.rs").unwrap(); - // Just verify we can parse the file - actual content may vary - assert!(file_unit.source.is_some()); + let declares = &file_unit.declares; + assert!(declares.iter().any(|d| d.source == "extern crate proc_macro;" && matches!(d.kind, crate::DeclareKind::Other(ref s) if s == "extern_crate"))); + assert!(declares.iter().any(|d| d.source == "extern crate serde as serde_renamed;" && matches!(d.kind, crate::DeclareKind::Other(ref s) if s == "extern_crate"))); + assert!(declares.iter().any(|d| d.source == "use std::collections::HashMap;" && d.kind == crate::DeclareKind::Use)); + assert!(declares.iter().any(|d| d.source == "use std::fmt::{self, Debug as FmtDebug};" && d.kind == crate::DeclareKind::Use)); + assert!(declares.iter().any(|d| d.source == "use crate::public_module::PublicStruct;" && d.kind == crate::DeclareKind::Use)); + assert!(declares.iter().any(|d| d.source == "mod my_other_module;" && d.kind == crate::DeclareKind::Mod)); } + #[test] - fn test_parse_top_level_functions() { + fn test_parse_top_level_functions_attributes_docs() { let file_unit = parse_fixture("sample.rs").unwrap(); - // Just verify we can parse the file - actual content may vary - assert!(file_unit.source.is_some()); + let main_fn = file_unit.functions.iter().find(|f| f.name == "main").expect("main function not found"); // From cfg(test) + assert_eq!(main_fn.name, "main"); + assert_eq!(main_fn.visibility, Visibility::Private); // Based on no explicit visibility + + let public_fn = file_unit.functions.iter().find(|f| f.name == "public_function").expect("public_function not found"); + assert_eq!(public_fn.name, "public_function"); + assert_eq!(public_fn.visibility, Visibility::Public); + assert_eq!(public_fn.signature.as_deref(), Some("fn public_function() -> String")); + assert_eq!(public_fn.doc.as_deref(), Some("A public function with multiple attributes and docs.\nSecond line of doc.")); + assert_eq!(public_fn.attributes, vec!["#[inline]".to_string(), "#[must_use = \"Return value should be used\"]".to_string()]); + + let private_fn = file_unit.functions.iter().find(|f| f.name == "private_function").expect("private_function not found"); + assert_eq!(private_fn.name, "private_function"); + assert_eq!(private_fn.visibility, Visibility::Private); + assert_eq!(private_fn.signature.as_deref(), Some("fn private_function(s: &str) -> String")); + assert_eq!(private_fn.doc.as_deref(), Some("This is a private function with documentation")); + assert_eq!(private_fn.attributes, vec!["#[allow(dead_code)]".to_string()]); + } #[test] - fn test_parse_module_structure() { + fn test_module_parsing_with_details() { let file_unit = parse_fixture("sample.rs").unwrap(); - // Just verify we can parse the file - actual content may vary - assert!(file_unit.source.is_some()); + let module = file_unit.modules.iter().find(|m| m.name == "public_module").expect("public_module not found"); + assert_eq!(module.name, "public_module"); + assert_eq!(module.visibility, Visibility::Public); + assert_eq!(module.doc.as_deref(), Some("This is a public module.\nIt has multiple lines of documentation.")); + assert!(module.attributes.contains(&"#[cfg(feature = \"some_feature\")]".to_string())); + assert!(module.attributes.contains(&"#[deprecated(note = \"This module is old\")]".to_string())); + // Check for inner doc comment "//! Inner documentation for public_module." + // Current extract_documentation gets outer. Inner module doc is not captured by FileUnit.doc or ModuleUnit.doc in this setup. + // This would require specific handling for inner module docs if needed for ModuleUnit. + + let crate_vis_fn = module.functions.iter().find(|f| f.name == "crate_visible_function").expect("crate_visible_function not found"); + assert_eq!(crate_vis_fn.visibility, Visibility::Crate); + + // Testing nested module + let nested_mod = module.submodules.iter().find(|m| m.name == "nested_module").expect("nested_module not found"); + assert_eq!(nested_mod.name, "nested_module"); + assert_eq!(nested_mod.visibility, Visibility::Private); // Default visibility + // Add assertion for nested_module's inner doc if ModuleUnit.doc should capture it. + // For now, assuming ModuleUnit.doc is for outer docs of the mod item itself. + + let visible_to_public_module_fn = nested_mod.functions.iter().find(|f| f.name == "visible_to_public_module").unwrap(); + assert_eq!(visible_to_public_module_fn.visibility, Visibility::Restricted("pub(super)".to_string())); + } #[test] - fn test_struct_and_trait_names() { + fn test_struct_trait_heads_generics_and_attributes() { + let file_unit = parse_fixture("sample.rs").unwrap(); + let public_module = file_unit.modules.iter().find(|m| m.name == "public_module").unwrap(); + + let ps_module = public_module.structs.iter().find(|s| s.name == "PublicStruct").unwrap(); + assert_eq!(ps_module.head, "pub struct PublicStruct"); // Where clause not in head + assert_eq!(ps_module.visibility, Visibility::Public); + assert!(ps_module.attributes.contains(&"#[derive(Debug, Clone)]".to_string())); + assert!(ps_module.attributes.contains(&"#[serde(rename_all = \"camelCase\")]".to_string())); + assert_eq!(ps_module.doc.as_deref(), Some("This is a public struct with documentation.\nIt also has generics and attributes.")); + let field_another = ps_module.fields.iter().find(|f| f.name == "another_field").unwrap(); + assert_eq!(field_another.doc.as_deref(), Some("Inner attribute doc for another_field")); + + + let pt_module = public_module.traits.iter().find(|t| t.name == "PublicTrait").unwrap(); + assert_eq!(pt_module.head, "pub trait PublicTrait"); + assert!(pt_module.attributes.contains(&"#[allow(unused_variables)]".to_string())); + + let gs_file = file_unit.structs.iter().find(|s| s.name == "GenericStruct").unwrap(); + assert_eq!(gs_file.head, "pub struct GenericStruct"); // Now pub due to fixture update + assert_eq!(gs_file.visibility, Visibility::Public); + + + let gt_file = file_unit.traits.iter().find(|t| t.name == "GenericTrait").unwrap(); + assert_eq!(gt_file.head, "pub trait GenericTrait"); + assert!(gt_file.attributes.contains(&"#[allow(unused_variables)]".to_string())); + } + + #[test] + fn test_type_const_static() { let file_unit = parse_fixture("sample.rs").unwrap(); + // Type aliases are not specifically parsed into their own Unit type yet. + // Constants and Statics are not specifically parsed into their own Unit type yet. + // For now, check their presence by looking for related items if necessary or assume they don't interfere. + // Example: Find the public static var + let static_var_func = file_unit.functions.iter().any(|f| f.source.as_deref().unwrap_or("").contains("PUBLIC_STATIC_VAR")); + // This is a weak test. Real parsing of const/static would be better. + // For now, just acknowledge they are in sample.rs + assert!(file_unit.source.as_ref().unwrap().contains("pub type PublicTypeAlias")); + assert!(file_unit.source.as_ref().unwrap().contains("pub const PUBLIC_CONSTANT: &str")); + assert!(file_unit.source.as_ref().unwrap().contains("pub static PUBLIC_STATIC_VAR: i32")); - // First check if we have modules - assert!(!file_unit.modules.is_empty()); - - // Find PublicStruct and PublicTrait in public_module - let public_module = file_unit - .modules - .iter() - .find(|m| m.name == "public_module") - .expect("Could not find public_module"); - - // Check structs in the module - assert!(!public_module.structs.is_empty()); - let public_struct = public_module - .structs - .iter() - .find(|s| s.name == "PublicStruct"); - assert!( - public_struct.is_some(), - "PublicStruct not found or has incorrect name" - ); - - // Check traits in the module - assert!(!public_module.traits.is_empty()); - let public_trait = public_module - .traits - .iter() - .find(|t| t.name == "PublicTrait"); - assert!( - public_trait.is_some(), - "PublicTrait not found or has incorrect name" - ); } + #[test] - fn test_trait_with_methods() { + fn test_impl_blocks_details() { let file_unit = parse_fixture("sample.rs").unwrap(); + + let generic_struct_impl = file_unit.impls.iter().find(|imp| imp.head == "impl GenericStruct").expect("Impl for GenericStruct not found or head mismatch"); + assert_eq!(generic_struct_impl.doc.as_deref(), Some("Implementation for GenericStruct.")); + assert!(generic_struct_impl.attributes.contains(&"#[allow(dead_code)]".to_string())); + let method_in_gs_impl = generic_struct_impl.methods.iter().find(|m| m.name == "new").expect("new method not found in GenericStruct impl"); + assert_eq!(method_in_gs_impl.signature.as_deref(), Some("fn new(value: T) -> Self")); + assert_eq!(method_in_gs_impl.visibility, Visibility::Private); // Default for impl methods + + let trait_impl = file_unit.impls.iter().find(|imp| imp.head == "impl GenericTrait for GenericStruct where T: Clone + FmtDebug").expect("Trait impl for GenericStruct not found or head mismatch"); + assert_eq!(trait_impl.doc.as_deref(), Some("Implementation of GenericTrait for GenericStruct.\nIncludes a where clause.")); + let method_in_trait_impl = trait_impl.methods.iter().find(|m| m.name == "method").expect("method not found in GenericTrait impl"); + assert_eq!(method_in_trait_impl.signature.as_deref(), Some("fn method(&self, value: T) -> T")); + assert_eq!(method_in_trait_impl.visibility, Visibility::Public); // Trait methods are pub + } + + #[test] + fn test_advanced_fixture_parsing() { + let file_unit = parse_fixture("sample_advanced.rs").unwrap(); + assert_eq!(file_unit.doc.as_deref(), Some("File for advanced Rust constructs.")); + + let level1_mod = file_unit.modules.iter().find(|m| m.name == "level1").expect("level1 module"); + let level2_mod = level1_mod.submodules.iter().find(|m| m.name == "level2").expect("level2 submodule"); + let deep_struct = level2_mod.structs.iter().find(|s| s.name == "DeepStruct").expect("DeepStruct"); + assert_eq!(deep_struct.visibility, Visibility::Restricted("pub(in crate::level1)".to_string())); + assert_eq!(deep_struct.doc.as_deref(), Some("Struct deep inside modules.")); + assert!(deep_struct.attributes.contains(&"#[derive(Default)]".to_string())); + + let complex_fn = level1_mod.functions.iter().find(|f| f.name == "complex_generic_function").expect("complex_generic_function"); + assert_eq!(complex_fn.name, "complex_generic_function"); + assert_eq!(complex_fn.visibility, Visibility::Public); + assert_eq!(complex_fn.signature.as_deref(), Some("fn complex_generic_function<'a, T, U>(param_t: T, param_u: &'a U) -> Result where T: std::fmt::Debug + Clone + Send + 'static, U: std::error::Error + ?Sized, for<'b> &'b U: Send")); + + let adv_generic_struct = file_unit.structs.iter().find(|s| s.name == "AdvancedGenericStruct").expect("AdvancedGenericStruct"); + assert_eq!(adv_generic_struct.head, "pub struct AdvancedGenericStruct<'a, A, B> where A: AsRef<[u8]> + ?Sized, B: 'a + Send + Sync"); + + let generic_result_enum = file_unit.structs.iter().find(|s| s.name == "GenericResult").expect("GenericResult enum"); // Enums are StructUnit + assert_eq!(generic_result_enum.head, "pub enum GenericResult where S: Send, E: std::fmt::Debug"); + + let adv_trait = file_unit.traits.iter().find(|t| t.name == "AdvancedTrait").expect("AdvancedTrait"); + assert_eq!(adv_trait.head, "pub trait AdvancedTrait"); // Associated types/consts not in head string + // TODO: Add parsing for associated types and consts in traits to test them here. + + let my_type_impl = file_unit.impls.iter().find(|i| i.head == "impl AdvancedTrait for MyTypeForAdvancedTrait").expect("MyTypeForAdvancedTrait impl"); + assert_eq!(my_type_impl.methods.len(), 1); // process method + // TODO: Add parsing for associated types and consts in impls to test them here. + + let unit_struct = file_unit.structs.iter().find(|s| s.name == "MyUnitStruct").expect("MyUnitStruct"); + assert_eq!(unit_struct.head, "pub struct MyUnitStruct;"); + assert!(unit_struct.fields.is_empty()); + + let empty_struct = file_unit.structs.iter().find(|s| s.name == "EmptyStruct").expect("EmptyStruct"); + assert_eq!(empty_struct.head, "pub struct EmptyStruct"); // or "pub struct EmptyStruct {}" depending on how head is formed + assert!(empty_struct.fields.is_empty()); + + let no_fields_struct = file_unit.structs.iter().find(|s| s.name == "NoFieldsStruct").expect("NoFieldsStruct"); + assert_eq!(no_fields_struct.visibility, Visibility::Private); + - // Find GenericTrait at the file level - let generic_trait = file_unit - .traits - .iter() - .find(|t| t.name == "GenericTrait") - .expect("GenericTrait not found at file level"); - - // Check documentation - assert!(generic_trait.doc.is_some()); - assert!( - generic_trait - .doc - .as_ref() - .unwrap() - .contains("public generic trait") - ); - - // Check methods are parsed - assert!( - !generic_trait.methods.is_empty(), - "GenericTrait should have methods parsed" - ); - - // Check specific method details - let method = generic_trait - .methods - .iter() - .find(|m| m.name == "method") - .expect("method not found in GenericTrait"); - - assert!(method.doc.is_some()); - assert!( - method - .doc - .as_ref() - .unwrap() - .contains("Method documentation") - ); - assert!(method.signature.is_some()); - assert!( - method - .signature - .as_ref() - .unwrap() - .contains("fn method(&self, value: T) -> T;") - ); - assert!(method.body.is_none()); // Trait methods often have no body - assert_eq!( - method.visibility, - Visibility::Public, - "Trait methods should be Public" - ); } #[test] - fn test_trait_impl_method_visibility() { - let file_unit = parse_fixture("sample.rs").unwrap(); + fn test_edge_case_file_parsing() { + let empty_file_unit = parse_fixture("empty.rs").unwrap(); + assert!(empty_file_unit.functions.is_empty()); + assert!(empty_file_unit.structs.is_empty()); + assert!(empty_file_unit.modules.is_empty()); + assert!(empty_file_unit.declares.is_empty()); + assert!(empty_file_unit.doc.is_none()); + + let comments_only_unit = parse_fixture("only_comments.rs").unwrap(); + assert!(comments_only_unit.functions.is_empty()); + assert!(comments_only_unit.structs.is_empty()); + // File-level inner doc comments (//! or /*! */) should be captured if they are the only content. + // extract_documentation looks at prev_siblings of the "first item". If no items, it might look at last node. + assert!(comments_only_unit.doc.is_some()); + assert!(comments_only_unit.doc.as_ref().unwrap().contains("This is an inner line comment, often used for module-level docs.")); + assert!(comments_only_unit.doc.as_ref().unwrap().contains("This is an inner block comment.\nAlso for module-level docs usually.")); - // Find the impl block for GenericTrait for GenericStruct - let trait_impl = file_unit - .impls - .iter() - .find(|imp| { - imp.head - .contains("impl GenericTrait for GenericStruct") - }) - .expect("GenericTrait implementation not found"); - - // Check that the impl block has methods - assert!( - !trait_impl.methods.is_empty(), - "GenericTrait impl should have methods" - ); - - // Find the method named "method" - let method = trait_impl - .methods - .iter() - .find(|m| m.name == "method") - .expect("method not found in GenericTrait impl"); - - // Assert that the method visibility is Public - assert_eq!( - method.visibility, - Visibility::Public, - "Trait impl methods should be Public" - ); - assert!(method.body.is_some()); // Impl methods should have a body } + // Original tests from before, reviewed and potentially slightly adjusted for new head formats or details. #[test] - fn test_struct_with_fields() { + fn test_original_struct_with_fields() { // Renamed from test_struct_with_fields let file_unit = parse_fixture("sample_with_fields.rs").unwrap(); - - // Find StructWithFields - let struct_with_fields = file_unit - .structs - .iter() - .find(|s| s.name == "StructWithFields") - .expect("StructWithFields not found"); - - // Check if fields were parsed - assert!( - !struct_with_fields.fields.is_empty(), - "Fields should be parsed for StructWithFields" - ); - - // Check details of the first field (public_field) - let public_field = struct_with_fields - .fields - .iter() - .find(|f| f.name == "public_field") - .expect("public_field not found"); - - assert!(public_field.doc.is_some()); - assert!( - public_field - .doc - .as_ref() - .unwrap() - .contains("A public field") - ); - assert!(public_field.attributes.is_empty()); // Assuming no attributes for this field - assert!( - public_field - .source - .as_ref() - .unwrap() - .contains("pub public_field: String") - ); - - // Check details of the second field (_private_field) - let private_field = struct_with_fields - .fields - .iter() - .find(|f| f.name == "_private_field") - .expect("_private_field not found"); - - assert!(private_field.doc.is_some()); - assert!( - private_field - .doc - .as_ref() - .unwrap() - .contains("A private field") - ); - assert!(!private_field.attributes.is_empty()); // Check for attribute + let struct_with_fields = file_unit.structs.iter().find(|s| s.name == "StructWithFields").unwrap(); + assert_eq!(struct_with_fields.head, "pub struct StructWithFields"); + assert_eq!(struct_with_fields.doc.as_deref(), Some("Documentation for the struct")); + assert!(!struct_with_fields.fields.is_empty()); + + let public_field = struct_with_fields.fields.iter().find(|f| f.name == "public_field").unwrap(); + assert_eq!(public_field.doc.as_deref(), Some("A public field documentation")); + assert!(public_field.source.as_ref().unwrap().contains("pub public_field: String")); + + let private_field = struct_with_fields.fields.iter().find(|f| f.name == "_private_field").unwrap(); + assert_eq!(private_field.doc.as_deref(), Some("A private field documentation")); assert!(private_field.attributes[0].contains("#[allow(dead_code)]")); - assert!( - private_field - .source - .as_ref() - .unwrap() - .contains("_private_field: i32") - ); } #[test] - fn test_parse_enum_with_variants() { + fn test_original_enum_with_variants() { // Renamed from test_parse_enum_with_variants let file_unit = parse_fixture("sample_enum.rs").unwrap(); - - // Find PublicEnum - let public_enum = file_unit - .structs // Enums are parsed as structs - .iter() - .find(|s| s.name == "PublicEnum") - .expect("PublicEnum not found"); - + let public_enum = file_unit.structs.iter().find(|s| s.name == "PublicEnum").unwrap(); assert_eq!(public_enum.visibility, Visibility::Public); - assert!(public_enum.doc.is_some()); - assert!( - public_enum - .doc - .as_ref() - .unwrap() - .contains("public enum with documentation") - ); - assert_eq!(public_enum.attributes.len(), 1); - assert_eq!(public_enum.attributes[0], "#[derive(Debug)]"); - assert_eq!(public_enum.head, "pub enum PublicEnum"); - - // Check if variants were parsed as fields - assert!( - !public_enum.fields.is_empty(), - "Variants should be parsed as fields for PublicEnum" - ); - assert_eq!(public_enum.fields.len(), 3, "Expected 3 variants"); - - // Check details of the first variant (Variant1) - let variant1 = public_enum - .fields - .iter() - .find(|f| f.name == "Variant1") - .expect("Variant1 not found"); - - assert!(variant1.doc.is_some()); - assert!( - variant1 - .doc - .as_ref() - .unwrap() - .contains("Variant documentation") - ); - assert!(variant1.attributes.is_empty()); - // Source should NOT have trailing comma + assert_eq!(public_enum.doc.as_deref(), Some("This is a public enum with documentation")); + assert!(public_enum.attributes.contains(&"#[derive(Debug)]".to_string())); + assert_eq!(public_enum.head, "pub enum PublicEnum"); + assert_eq!(public_enum.fields.len(), 3); + + let variant1 = public_enum.fields.iter().find(|f| f.name == "Variant1").unwrap(); + assert_eq!(variant1.doc.as_deref(), Some("Variant documentation")); assert_eq!(variant1.source.as_ref().unwrap(), "Variant1"); - // Check details of the second variant (Variant2) - let variant2 = public_enum - .fields - .iter() - .find(|f| f.name == "Variant2") - .expect("Variant2 not found"); - - assert!( - variant2 - .doc - .as_ref() - .unwrap() - .contains("Another variant documentation") - ); - assert!(!variant2.attributes.is_empty()); - assert_eq!(variant2.attributes[0], "#[allow(dead_code)]"); - // Source should NOT have trailing comma + let variant2 = public_enum.fields.iter().find(|f| f.name == "Variant2").unwrap(); + assert!(variant2.attributes.contains(&"#[allow(dead_code)]".to_string())); assert_eq!(variant2.source.as_ref().unwrap(), "Variant2(String)"); + assert_eq!(variant2.doc.as_deref(), Some("Another variant documentation")); + - // Check details of the third variant (Variant3) - let variant3 = public_enum - .fields - .iter() - .find(|f| f.name == "Variant3") - .expect("Variant3 not found"); - - assert!( - variant3 - .doc - .as_ref() - .unwrap() - .contains("Yet another variant documentation") - ); - assert!(variant3.attributes.is_empty()); - // Source should NOT have trailing comma + let variant3 = public_enum.fields.iter().find(|f| f.name == "Variant3").unwrap(); + assert_eq!(variant3.doc.as_deref(), Some("Yet another variant documentation")); assert_eq!(variant3.source.as_ref().unwrap(), "Variant3 { field: i32 }"); - // Check that PrivateEnum was also parsed (as a struct) - let private_enum = file_unit - .structs - .iter() - .find(|s| s.name == "PrivateEnum") - .expect("PrivateEnum not found"); + let private_enum = file_unit.structs.iter().find(|s| s.name == "PrivateEnum").expect("PrivateEnum not found"); assert_eq!(private_enum.visibility, Visibility::Private); - assert_eq!(private_enum.fields.len(), 1); // Should have one variant + + let impl_block = file_unit.impls.iter().find(|i| i.head == "impl PublicEnum").expect("Impl for PublicEnum not found"); + assert_eq!(impl_block.methods.len(), 1); + let describe_method = impl_block.methods.first().unwrap(); + assert_eq!(describe_method.name, "describe"); + assert_eq!(describe_method.visibility, Visibility::Public); } } diff --git a/src/parser/lang/ts.rs b/src/parser/lang/ts.rs index 4dd0476..b7c4a2a 100644 --- a/src/parser/lang/ts.rs +++ b/src/parser/lang/ts.rs @@ -12,7 +12,7 @@ use tree_sitter::{Node, Parser}; impl TypeScriptParser { pub fn try_new() -> Result { let mut parser = Parser::new(); - let language = tree_sitter_typescript::LANGUAGE_TYPESCRIPT; + let language = tree_sitter_typescript::language_typescript(); parser .set_language(&language.into()) .map_err(|e| Error::TreeSitter(e.to_string()))?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9e7c57a..1613aef 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -53,6 +53,41 @@ pub enum Visibility { Restricted(String), } +impl Visibility { + /// Parses a string representation of visibility into a `Visibility` enum variant. + /// + /// This function is primarily designed for Rust's visibility keywords. + /// The `_lang` parameter is currently unused but is included for potential future + /// expansion to other languages with different visibility syntaxes. + /// + /// # Arguments + /// + /// * `s`: A string slice representing the visibility keyword (e.g., "pub", "pub(crate)"). + /// * `_lang`: The language type (currently ignored, assumed Rust). + /// + /// # Returns + /// + /// Returns the corresponding `Visibility` variant. Defaults to `Visibility::Private` + /// if the string does not match known Rust visibility specifiers. + pub fn from_str(s: &str, _lang: LanguageType) -> Self { + match s { + "pub" => Visibility::Public, + "pub(crate)" => Visibility::Crate, + restricted_str if restricted_str.starts_with("pub(") && restricted_str.ends_with(')') => { + // Extract content between "pub(" and ")" + // e.g., "pub(in crate::some::path)" -> "in crate::some::path" + // e.g., "pub(super)" -> "super" + let content = restricted_str + .trim_start_matches("pub(") + .trim_end_matches(')') + .to_string(); + Visibility::Restricted(content) + } + _ => Visibility::Private, // Default for non-recognized or empty strings + } + } +} + /// The language type supported by the parser. /// /// # Examples @@ -371,6 +406,9 @@ pub struct TraitUnit { /// The documentation for the trait pub doc: Option, + /// The trait head, e.g. `trait MyTrait: OtherTrait` + pub head: String, + /// The methods declared in the trait pub methods: Vec,