use std::collections::HashMap;
use syn::{self, visit::Visit, File, ImplItem, Item, ItemFn, ItemImpl, ItemStruct};
#[derive(Debug, Clone, Default)]
pub struct StructOwnershipAnalyzer {
struct_to_methods: HashMap<String, Vec<String>>,
method_to_struct: HashMap<String, String>,
standalone_functions: Vec<String>,
struct_locations: HashMap<String, (usize, usize)>,
}
impl StructOwnershipAnalyzer {
pub fn analyze_file(parsed: &File) -> Self {
let mut visitor = StructVisitor::default();
visitor.visit_file(parsed);
visitor.analyzer
}
pub fn get_struct_methods(&self, struct_name: &str) -> &[String] {
self.struct_to_methods
.get(struct_name)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn get_method_struct(&self, method_name: &str) -> Option<&str> {
self.method_to_struct.get(method_name).map(|s| s.as_str())
}
pub fn get_standalone_functions(&self) -> &[String] {
&self.standalone_functions
}
pub fn get_struct_names(&self) -> Vec<&str> {
self.struct_to_methods.keys().map(|s| s.as_str()).collect()
}
pub fn get_struct_location(&self, struct_name: &str) -> Option<(usize, usize)> {
self.struct_locations.get(struct_name).copied()
}
pub fn total_method_count(&self) -> usize {
self.struct_to_methods.values().map(|v| v.len()).sum()
}
}
#[derive(Default)]
struct StructVisitor {
analyzer: StructOwnershipAnalyzer,
}
impl StructVisitor {
fn extract_type_name(ty: &syn::Type) -> Option<String> {
match ty {
syn::Type::Path(type_path) => type_path
.path
.segments
.last()
.map(|segment| segment.ident.to_string()),
_ => None,
}
}
fn track_struct(&mut self, item_struct: &ItemStruct) {
let struct_name = item_struct.ident.to_string();
self.analyzer
.struct_to_methods
.entry(struct_name.clone())
.or_default();
self.analyzer.struct_locations.insert(struct_name, (0, 0));
}
fn track_impl(&mut self, item_impl: &ItemImpl) {
if item_impl.trait_.is_some() {
return;
}
let struct_name = match Self::extract_type_name(&item_impl.self_ty) {
Some(name) => name,
None => return,
};
let methods = self
.analyzer
.struct_to_methods
.entry(struct_name.clone())
.or_default();
for item in &item_impl.items {
if let ImplItem::Fn(method) = item {
let method_name = method.sig.ident.to_string();
methods.push(method_name.clone());
self.analyzer
.method_to_struct
.insert(method_name, struct_name.clone());
}
}
}
fn track_standalone_function(&mut self, item_fn: &ItemFn) {
let fn_name = item_fn.sig.ident.to_string();
self.analyzer.standalone_functions.push(fn_name);
}
}
impl<'ast> Visit<'ast> for StructVisitor {
fn visit_item(&mut self, item: &'ast Item) {
match item {
Item::Struct(item_struct) => {
self.track_struct(item_struct);
}
Item::Impl(item_impl) => {
self.track_impl(item_impl);
}
Item::Fn(item_fn) => {
self.track_standalone_function(item_fn);
}
_ => {}
}
syn::visit::visit_item(self, item);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_struct_ownership() {
let code = r#"
struct Config {
value: String,
}
impl Config {
fn new() -> Self {
Config { value: String::new() }
}
fn get_value(&self) -> &str {
&self.value
}
}
fn standalone_helper() { }
"#;
let parsed = syn::parse_file(code).unwrap();
let analyzer = StructOwnershipAnalyzer::analyze_file(&parsed);
assert_eq!(analyzer.get_struct_methods("Config"), &["new", "get_value"]);
assert_eq!(analyzer.get_standalone_functions(), &["standalone_helper"]);
assert_eq!(analyzer.get_method_struct("new"), Some("Config"));
assert_eq!(analyzer.get_method_struct("get_value"), Some("Config"));
}
#[test]
fn test_multiple_impl_blocks() {
let code = r#"
struct Config { }
impl Config {
fn new() -> Self { Config {} }
}
impl Config {
fn get_value(&self) -> i32 { 42 }
}
"#;
let parsed = syn::parse_file(code).unwrap();
let analyzer = StructOwnershipAnalyzer::analyze_file(&parsed);
let methods = analyzer.get_struct_methods("Config");
assert_eq!(methods.len(), 2);
assert!(methods.contains(&"new".to_string()));
assert!(methods.contains(&"get_value".to_string()));
}
#[test]
fn test_trait_impl_exclusion() {
let code = r#"
use std::fmt::{Display, Formatter, Result};
struct Config { }
impl Config {
fn new() -> Self { Config {} }
}
impl Display for Config {
fn fmt(&self, f: &mut Formatter) -> Result {
Ok(())
}
}
"#;
let parsed = syn::parse_file(code).unwrap();
let analyzer = StructOwnershipAnalyzer::analyze_file(&parsed);
assert_eq!(analyzer.get_struct_methods("Config"), &["new"]);
assert_eq!(analyzer.get_method_struct("fmt"), None);
}
#[test]
fn test_generic_impl_blocks() {
let code = r#"
struct Container<T> {
value: T,
}
impl<T> Container<T> {
fn new(value: T) -> Self { Container { value } }
fn get(&self) -> &T { &self.value }
}
"#;
let parsed = syn::parse_file(code).unwrap();
let analyzer = StructOwnershipAnalyzer::analyze_file(&parsed);
assert_eq!(analyzer.get_struct_methods("Container"), &["new", "get"]);
}
#[test]
fn test_empty_impl_block() {
let code = r#"
struct Config { }
impl Config { }
"#;
let parsed = syn::parse_file(code).unwrap();
let analyzer = StructOwnershipAnalyzer::analyze_file(&parsed);
assert_eq!(analyzer.get_struct_methods("Config"), &[] as &[String]);
}
#[test]
fn test_multiple_structs() {
let code = r#"
struct ConfigA { }
struct ConfigB { }
impl ConfigA {
fn method_a(&self) { }
}
impl ConfigB {
fn method_b(&self) { }
}
"#;
let parsed = syn::parse_file(code).unwrap();
let analyzer = StructOwnershipAnalyzer::analyze_file(&parsed);
assert_eq!(analyzer.get_struct_methods("ConfigA"), &["method_a"]);
assert_eq!(analyzer.get_struct_methods("ConfigB"), &["method_b"]);
assert_eq!(analyzer.get_method_struct("method_a"), Some("ConfigA"));
assert_eq!(analyzer.get_method_struct("method_b"), Some("ConfigB"));
let struct_names = analyzer.get_struct_names();
assert_eq!(struct_names.len(), 2);
assert!(struct_names.contains(&"ConfigA"));
assert!(struct_names.contains(&"ConfigB"));
}
}