// tools/src/main.rs use serde::Serialize; use std::env; use std::fs; use std::process; use syn::spanned::Spanned; use syn::{ FnArg, Item, ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, ItemTrait, Pat, Type, Visibility, }; #[derive(Serialize)] struct RustDecl { name: String, type_: String, start_line: usize, end_line: usize, visibility: String, is_async: bool, is_unsafe: bool, generics: Vec, traits: Vec, fields: Vec, methods: Vec, return_type: Option, parameters: Vec, } fn main() { let args: Vec = env::args().collect(); if args.len() < 2 { eprintln!("Usage: {} ", args[0]); process::exit(1); } let filename = &args[1]; let content = match fs::read_to_string(filename) { Ok(content) => content, Err(e) => { eprintln!("ERR: Failed to read file {}: {}", filename, e); process::exit(2); } }; let syntax = match syn::parse_file(&content) { Ok(syntax) => syntax, Err(e) => { eprintln!("ERR: Failed to parse Rust file {}: {}", filename, e); process::exit(3); } }; let mut decls = Vec::new(); // Package comment (first comment group before package decl) let pkg_comment = ""; for cg in &syntax.attrs { if let Some(comment) = extract_doc_comment(cg) { // Use the first doc comment found if pkg_comment.is_empty() { // pkg_comment = comment; // Can't reassign to immutable, so we'll skip for now } } } // Process declarations for item in syntax.items { match item { Item::Fn(item_fn) => { if let Some(decl) = parse_function(item_fn, &content) { decls.push(decl); } } Item::Struct(item_struct) => { if let Some(decl) = parse_struct(item_struct, &content) { decls.push(decl); } } Item::Enum(item_enum) => { if let Some(decl) = parse_enum(item_enum, &content) { decls.push(decl); } } Item::Trait(item_trait) => { if let Some(decl) = parse_trait(item_trait, &content) { decls.push(decl); } } Item::Impl(item_impl) => { if let Some(decl) = parse_impl(item_impl, &content) { decls.push(decl); } } Item::Mod(item_mod) => { if let Some(decl) = parse_mod(item_mod, &content) { decls.push(decl); } } _ => {} } } let output = serde_json::to_string(&decls).unwrap_or_else(|_| "[]".to_string()); println!("{}", output); } fn parse_function(item_fn: ItemFn, _content: &str) -> Option { let start_line = item_fn.span().start().line; let end_line = item_fn.span().end().line; let mut parameters = Vec::new(); for input in &item_fn.sig.inputs { if let FnArg::Typed(pat_type) = input { if let Pat::Ident(pat_ident) = &*pat_type.pat { parameters.push(pat_ident.ident.to_string()); } } } let return_type = match &item_fn.sig.output { syn::ReturnType::Default => None, syn::ReturnType::Type(_, ty) => Some(type_to_string(ty)), }; Some(RustDecl { name: item_fn.sig.ident.to_string(), type_: "function".to_string(), start_line, end_line, visibility: visibility_to_string(&item_fn.vis), is_async: item_fn.sig.asyncness.is_some(), is_unsafe: item_fn.sig.unsafety.is_some(), generics: item_fn .sig .generics .params .iter() .map(generic_param_to_string) .collect(), traits: Vec::new(), fields: Vec::new(), methods: Vec::new(), return_type, parameters, }) } fn parse_struct(item_struct: ItemStruct, _content: &str) -> Option { let start_line = item_struct.span().start().line; let end_line = item_struct.span().end().line; let fields: Vec = item_struct .fields .iter() .filter_map(|field| field.ident.as_ref().map(|ident| ident.to_string())) .collect(); Some(RustDecl { name: item_struct.ident.to_string(), type_: "struct".to_string(), start_line, end_line, visibility: visibility_to_string(&item_struct.vis), is_async: false, is_unsafe: false, generics: item_struct .generics .params .iter() .map(generic_param_to_string) .collect(), traits: Vec::new(), fields, methods: Vec::new(), return_type: None, parameters: Vec::new(), }) } fn parse_enum(item_enum: ItemEnum, _content: &str) -> Option { let start_line = item_enum.span().start().line; let end_line = item_enum.span().end().line; let variants: Vec = item_enum .variants .iter() .map(|variant| variant.ident.to_string()) .collect(); Some(RustDecl { name: item_enum.ident.to_string(), type_: "enum".to_string(), start_line, end_line, visibility: visibility_to_string(&item_enum.vis), is_async: false, is_unsafe: false, generics: item_enum .generics .params .iter() .map(generic_param_to_string) .collect(), traits: Vec::new(), fields: variants, methods: Vec::new(), return_type: None, parameters: Vec::new(), }) } fn parse_trait(item_trait: ItemTrait, _content: &str) -> Option { let start_line = item_trait.span().start().line; let end_line = item_trait.span().end().line; let methods: Vec = item_trait .items .iter() .filter_map(|item| { if let syn::TraitItem::Fn(method) = item { Some(method.sig.ident.to_string()) } else { None } }) .collect(); Some(RustDecl { name: item_trait.ident.to_string(), type_: "trait".to_string(), start_line, end_line, visibility: visibility_to_string(&item_trait.vis), is_async: false, is_unsafe: false, generics: item_trait .generics .params .iter() .map(generic_param_to_string) .collect(), traits: Vec::new(), fields: Vec::new(), methods, return_type: None, parameters: Vec::new(), }) } fn parse_impl(item_impl: ItemImpl, _content: &str) -> Option { let start_line = item_impl.span().start().line; let end_line = item_impl.span().end().line; let target = type_to_string(&item_impl.self_ty); let methods: Vec = item_impl .items .iter() .filter_map(|item| { if let syn::ImplItem::Fn(method) = item { Some(method.sig.ident.to_string()) } else { None } }) .collect(); let traits: Vec = item_impl .trait_ .as_ref() .map(|(_, path, _)| path_to_string(path)) .into_iter() .collect(); Some(RustDecl { name: format!("impl_{}", target), type_: "impl".to_string(), start_line, end_line, visibility: "".to_string(), is_async: false, is_unsafe: item_impl.unsafety.is_some(), generics: item_impl .generics .params .iter() .map(generic_param_to_string) .collect(), traits, fields: Vec::new(), methods, return_type: None, parameters: Vec::new(), }) } fn parse_mod(item_mod: ItemMod, _content: &str) -> Option { let start_line = item_mod.span().start().line; let end_line = item_mod.span().end().line; Some(RustDecl { name: item_mod.ident.to_string(), type_: "module".to_string(), start_line, end_line, visibility: visibility_to_string(&item_mod.vis), is_async: false, is_unsafe: false, generics: Vec::new(), traits: Vec::new(), fields: Vec::new(), methods: Vec::new(), return_type: None, parameters: Vec::new(), }) } fn visibility_to_string(vis: &Visibility) -> String { match vis { Visibility::Public(_) => "pub".to_string(), _ => "private".to_string(), } } fn type_to_string(ty: &Type) -> String { // Simple string representation for common types match ty { Type::Path(type_path) => type_path .path .segments .last() .map(|seg| seg.ident.to_string()) .unwrap_or_else(|| "unknown".to_string()), Type::Reference(type_ref) => { format!("&{}", type_to_string(&type_ref.elem)) } _ => "unknown".to_string(), } } fn path_to_string(path: &syn::Path) -> String { path.segments .iter() .map(|seg| seg.ident.to_string()) .collect::>() .join("::") } fn generic_param_to_string(param: &syn::GenericParam) -> String { match param { syn::GenericParam::Type(ty) => ty.ident.to_string(), syn::GenericParam::Lifetime(lf) => lf.lifetime.to_string(), syn::GenericParam::Const(cnst) => cnst.ident.to_string(), } } fn extract_doc_comment(attr: &syn::Attribute) -> Option { if attr.path().is_ident("doc") { if let syn::Meta::NameValue(meta) = &attr.meta { if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. }) = &meta.value { return Some(lit_str.value().trim().to_string()); } } } None }