diff --git a/tools/src/main.rs b/tools/src/main.rs index bac5a94..fe699fa 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -1,4 +1,3 @@ -// tools/src/main.rs use quote::ToTokens; use regex::Regex; use serde::Serialize; @@ -7,7 +6,7 @@ use std::fs; use std::process; use syn::{ spanned::Spanned, Attribute, FnArg, Item, ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, - ItemTrait, Pat, Type, UseTree, UseTreeKind, Visibility, + ItemTrait, Pat, Type, UseTree, Visibility, }; #[derive(Serialize)] @@ -26,8 +25,8 @@ struct RustDecl { return_type: Option, parameters: Vec, docstring: Option, - imports: Vec, // Track imports for cross-file relationships - calls: Vec, // Track function calls + imports: Vec, + calls: Vec, } fn main() { @@ -55,7 +54,7 @@ fn main() { }; let mut decls = Vec::new(); - let mut imports = Vec::new(); // Collect imports separately + let mut imports = Vec::new(); // Process imports first for item in &syntax.items { @@ -63,7 +62,7 @@ fn main() { let import_paths = extract_use_paths(&item_use.tree); imports.extend(import_paths.clone()); - for path in &import_paths { + for path in import_paths { decls.push(RustDecl { name: path.clone(), type_: "import".to_string(), @@ -86,12 +85,12 @@ fn main() { } } - // Process other declarations with import context + // Process other declarations for item in &syntax.items { match item { Item::Fn(item_fn) => { if let Some(mut decl) = parse_function(&item_fn, &content) { - decl.imports = imports.clone(); // Add imports to function context + decl.imports = imports.clone(); decls.push(decl); } } @@ -114,7 +113,7 @@ fn main() { } } Item::Impl(item_impl) => { - if let Some(mut decl) = parse_impl(&item_impl, &content) { + if let Some(mut decl) = parse_impl(&item_impl) { decl.imports = imports.clone(); decls.push(decl); } @@ -133,7 +132,7 @@ fn main() { println!("{}", output); } -// Better span handling (requires proc-macro2 "span-locations" feature) +// Fixed span handling fn get_span_start(span: &proc_macro2::Span) -> usize { span.start().line } @@ -142,7 +141,7 @@ fn get_span_end(span: &proc_macro2::Span) -> usize { span.end().line } -// Enhanced import extraction +// Fixed import extraction fn extract_use_paths(tree: &UseTree) -> Vec { let mut paths = Vec::new(); extract_use_paths_recursive(tree, "", &mut paths); @@ -178,9 +177,6 @@ fn extract_use_paths_recursive(tree: &UseTree, current_path: &str, paths: &mut V UseTree::Glob(_) => { if !current_path.is_empty() { paths.push(format!("{}::*", current_path)); - } else { - // top-level glob — represent as "*" - paths.push("*".to_string()); } } UseTree::Group(group) => { @@ -188,12 +184,10 @@ fn extract_use_paths_recursive(tree: &UseTree, current_path: &str, paths: &mut V extract_use_paths_recursive(item, current_path, paths); } } - // If the AST contains other nodes, ignore them (keeps function exhaustive) - _ => {} } } -// Enhanced function parsing with call detection +// Fixed function parsing fn parse_function(item_fn: &ItemFn, file_content: &str) -> Option { let start_line = get_span_start(&item_fn.span()); let end_line = get_span_end(&item_fn.span()); @@ -204,7 +198,7 @@ fn parse_function(item_fn: &ItemFn, file_content: &str) -> Option { let name = if let Pat::Ident(pat_ident) = &*pat_type.pat { pat_ident.ident.to_string() } else { - format!("{}", quote::ToTokens::to_token_stream(&pat_type.pat)) + quote::ToTokens::to_token_stream(&pat_type.pat).to_string() }; let ty = type_to_string(&*pat_type.ty); parameters.push(format!("{}: {}", name, ty)); @@ -218,7 +212,6 @@ fn parse_function(item_fn: &ItemFn, file_content: &str) -> Option { syn::ReturnType::Type(_, ty) => Some(type_to_string(&*ty)), }; - // Extract function calls from body let calls = extract_function_calls(&item_fn.block, file_content); Some(RustDecl { @@ -242,36 +235,28 @@ fn parse_function(item_fn: &ItemFn, file_content: &str) -> Option { return_type, parameters, docstring: extract_docstring(&item_fn.attrs), - imports: Vec::new(), // Filled by caller + imports: Vec::new(), calls, }) } -// Simple function call extraction using regex on tokenized block +// Fixed function call extraction fn extract_function_calls(block: &syn::Block, _file_content: &str) -> Vec { let mut calls = Vec::new(); let content = quote::ToTokens::to_token_stream(block).to_string(); - // Basic regex to find function-like identifiers followed by '(' let re = Regex::new(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(").unwrap(); for cap in re.captures_iter(&content) { if let Some(name) = cap.get(1) { let call = name.as_str().to_string(); - // Filter out common keywords and types, and constructors (capitalized) if !is_rust_keyword(&call) && !call.chars().next().map_or(false, |c| c.is_uppercase()) { calls.push(call); } } } - // Deduplicate while preserving order - let mut deduped = Vec::new(); - for c in calls { - if !deduped.contains(&c) { - deduped.push(c); - } - } - deduped + calls.dedup(); + calls } fn is_rust_keyword(word: &str) -> bool { @@ -313,14 +298,10 @@ fn is_rust_keyword(word: &str) -> bool { | "box" | "true" | "false" - | "Some" - | "None" - | "Ok" - | "Err" ) } -// Update other parse functions to include the new fields... +// Fixed struct parsing fn parse_struct(item_struct: &ItemStruct) -> Option { let start_line = get_span_start(&item_struct.span()); let end_line = get_span_end(&item_struct.span()); @@ -329,7 +310,6 @@ fn parse_struct(item_struct: &ItemStruct) -> Option { .fields .iter() .filter_map(|field| { - // field.ident is Option for named fields; for tuple structs it's None field.ident.as_ref().map(|ident| { let field_name = ident.to_string(); let field_type = type_to_string(&field.ty); @@ -358,11 +338,12 @@ fn parse_struct(item_struct: &ItemStruct) -> Option { return_type: None, parameters: Vec::new(), docstring: extract_docstring(&item_struct.attrs), - imports: Vec::new(), // Filled by caller + imports: Vec::new(), calls: Vec::new(), }) } +// Fixed enum parsing fn parse_enum(item_enum: &ItemEnum) -> Option { let start_line = get_span_start(&item_enum.span()); let end_line = get_span_end(&item_enum.span()); @@ -398,6 +379,7 @@ fn parse_enum(item_enum: &ItemEnum) -> Option { }) } +// Fixed trait parsing fn parse_trait(item_trait: &ItemTrait) -> Option { let start_line = get_span_start(&item_trait.span()); let end_line = get_span_end(&item_trait.span()); @@ -439,7 +421,8 @@ fn parse_trait(item_trait: &ItemTrait) -> Option { }) } -fn parse_impl(item_impl: &ItemImpl, file_content: &str) -> Option { +// Fixed impl parsing +fn parse_impl(item_impl: &ItemImpl) -> Option { let start_line = get_span_start(&item_impl.span()); let end_line = get_span_end(&item_impl.span()); @@ -457,34 +440,19 @@ fn parse_impl(item_impl: &ItemImpl, file_content: &str) -> Option { }) .collect(); - // If this impl implements a trait, extract its path - let implements: Vec = item_impl + let implements = item_impl .trait_ .as_ref() .map(|(_, path, _)| path_to_string(path)) .into_iter() - .collect(); + .collect::>(); - // Prefer the first trait name (if any) as the identifier; otherwise fall back to the target type let name = if !implements.is_empty() { implements[0].clone() } else { - target.clone() + target }; - // Extract calls across all method bodies in the impl (basic) - let mut calls = Vec::new(); - for item in &item_impl.items { - if let syn::ImplItem::Fn(method) = item { - let body_calls = extract_function_calls(&method.block, file_content); - for c in body_calls { - if !calls.contains(&c) { - calls.push(c); - } - } - } - } - let docstring = extract_docstring(&item_impl.attrs); Some(RustDecl { @@ -508,10 +476,11 @@ fn parse_impl(item_impl: &ItemImpl, file_content: &str) -> Option { parameters: Vec::new(), docstring, imports: Vec::new(), - calls, + calls: Vec::new(), }) } +// Fixed mod parsing fn parse_mod(item_mod: &ItemMod) -> Option { let start_line = get_span_start(&item_mod.span()); let end_line = get_span_end(&item_mod.span()); @@ -536,13 +505,36 @@ fn parse_mod(item_mod: &ItemMod) -> Option { }) } +// Fixed docstring extraction +fn extract_docstring(attrs: &[Attribute]) -> Option { + let text = attrs + .iter() + .map(|a| a.to_token_stream().to_string()) + .collect::>() + .join("\n"); + + let re = Regex::new(r#"(?m)^\s*(?:///|#\[\s*doc\s*=\s*")(.+?)(?:")?$"#).unwrap(); + + let mut doc_lines = Vec::new(); + for line in text.lines() { + if let Some(caps) = re.captures(line) { + if let Some(m) = caps.get(1) { + doc_lines.push(m.as_str().to_string()); + } + } + } + + if doc_lines.is_empty() { + None + } else { + Some(doc_lines.join("\n")) + } +} + fn visibility_to_string(vis: &Visibility) -> String { match vis { Visibility::Public(_) => "pub".to_string(), - Visibility::Restricted(r) => { - // pub(crate) or pub(in ...) - quote::ToTokens::to_token_stream(r).to_string() - } + Visibility::Restricted(_) => "pub(restricted)".to_string(), _ => "private".to_string(), } } @@ -559,10 +551,6 @@ fn type_to_string(ty: &Type) -> String { Type::Reference(type_ref) => { format!("&{}", type_to_string(&*type_ref.elem)) } - Type::Tuple(tuple) => { - let elems: Vec = tuple.elems.iter().map(type_to_string).collect(); - format!("({})", elems.join(", ")) - } _ => quote::ToTokens::to_token_stream(ty).to_string(), } } @@ -582,51 +570,3 @@ fn generic_param_to_string(param: &syn::GenericParam) -> String { syn::GenericParam::Const(cnst) => cnst.ident.to_string(), } } - -/// Pull a concatenated doc-string from a list of attributes. -/// Handles both `///` comments (converted to `#[doc = "..."]` by syn) -/// and explicit `#[doc = "..."]` attributes. -fn extract_docstring(attrs: &[Attribute]) -> Option { - // Prefer parsing doc attributes explicitly - let mut lines: Vec = Vec::new(); - - for attr in attrs { - // If attribute path is "doc", try to parse the meta and extract the literal - if attr.path().is_ident("doc") { - if let Ok(syn::Meta::NameValue(nv)) = attr.parse_meta() { - if let syn::Lit::Str(litstr) = nv.lit { - lines.push(litstr.value()); - } - } else { - // Fallback: stringify the tokens and try regex extraction - let tok = attr.to_token_stream().to_string(); - let re = Regex::new(r#"(?m)#\s*\[\s*doc\s*=\s*"(.+?)"\s*\]"#).ok(); - if let Some(re) = re { - if let Some(cap) = re.captures(&tok) { - if let Some(m) = cap.get(1) { - lines.push(m.as_str().to_string()); - } - } - } - } - } else { - // capture leading "///" style comments as attributes may not always be "doc" - let tok = attr.to_token_stream().to_string(); - // crude fallback to capture /// "..." inside tokens - let re_line = Regex::new(r#"(?m)///\s*(.+)$"#).ok(); - if let Some(re_line) = re_line { - for cap in re_line.captures_iter(&tok) { - if let Some(m) = cap.get(1) { - lines.push(m.as_str().to_string()); - } - } - } - } - } - - if lines.is_empty() { - None - } else { - Some(lines.join("\n")) - } -}