use crate::models::{Class, Function};
use crate::parsers::{ImportInfo, ParseResult};
use anyhow::{Context, Result};
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;
use std::sync::OnceLock;
use tree_sitter::{Node, Parser, Query, QueryCursor, StreamingIterator};
thread_local! {
static RS_PARSER: RefCell<Parser> = RefCell::new({
let mut p = Parser::new();
p.set_language(&tree_sitter_rust::LANGUAGE.into()).expect("Rust language");
p
});
}
const FUNC_QUERY_STR: &str = r#"
(function_item
name: (identifier) @func_name
parameters: (parameters) @params
return_type: (_)? @return_type
) @func
(function_signature_item
name: (identifier) @func_name
parameters: (parameters) @params
return_type: (_)? @return_type
) @func
"#;
const IMPORT_QUERY_STR: &str = r#"
(use_declaration
argument: (_) @import_path
)
"#;
static RS_FUNC_QUERY: OnceLock<Query> = OnceLock::new();
static RS_IMPORT_QUERY: OnceLock<Query> = OnceLock::new();
#[allow(dead_code)]
pub fn parse(path: &Path) -> Result<ParseResult> {
let source = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path.display()))?;
parse_source(&source, path)
}
pub fn parse_source(source: &str, path: &Path) -> Result<ParseResult> {
parse_source_with_tree(source, path).map(|(r, _)| r)
}
pub fn parse_source_with_tree(source: &str, path: &Path) -> Result<(ParseResult, tree_sitter::Tree)> {
let tree = RS_PARSER.with(|cell| {
cell.borrow_mut().parse(source, None)
}).context("Failed to parse Rust source")?;
let root = tree.root_node();
let source_bytes = source.as_bytes();
let mut result = ParseResult::default();
extract_functions(&root, source_bytes, path, &mut result)?;
extract_structs_and_traits(&root, source_bytes, path, &mut result)?;
extract_imports(&root, source_bytes, &mut result)?;
extract_calls(&root, source_bytes, path, &mut result)?;
Ok((result, tree))
}
fn extract_functions(
root: &Node,
source: &[u8],
path: &Path,
result: &mut ParseResult,
) -> Result<()> {
let query = RS_FUNC_QUERY.get_or_init(|| {
Query::new(&tree_sitter_rust::LANGUAGE.into(), FUNC_QUERY_STR)
.expect("valid function query")
});
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, *root, source);
while let Some(m) = matches.next() {
let mut func_node = None;
let mut name = String::new();
let mut params_node = None;
let mut return_type_node = None;
for capture in m.captures.iter() {
let capture_name = query.capture_names()[capture.index as usize];
match capture_name {
"func" => func_node = Some(capture.node),
"func_name" => {
name = capture.node.utf8_text(source).unwrap_or("").to_string();
}
"params" => params_node = Some(capture.node),
"return_type" => return_type_node = Some(capture.node),
_ => {}
}
}
if let Some(node) = func_node {
if is_inside_impl(&node) {
continue;
}
let is_async = has_async_modifier(&node, source);
let parameters = extract_parameters(params_node, source);
let return_type =
return_type_node.map(|n| n.utf8_text(source).unwrap_or("").to_string());
let line_start = node.start_position().row as u32 + 1;
let line_end = node.end_position().row as u32 + 1;
let qualified_name = format!("{}::{}:{}", path.display(), name, line_start);
let mut annotations = extract_rust_attributes(&node, source);
if has_pub_visibility(&node) {
annotations.push("exported".to_string());
}
result.functions.push(Function {
name: name.clone(),
qualified_name,
file_path: path.to_path_buf(),
line_start,
line_end,
parameters,
return_type,
is_async,
complexity: Some(calculate_complexity(&node, source)),
max_nesting: None,
doc_comment: None,
annotations,
});
}
}
Ok(())
}
fn has_async_modifier(node: &Node, source: &[u8]) -> bool {
for child in node.children(&mut node.walk()) {
if child.kind() == "async" {
return true;
}
if let Ok(text) = child.utf8_text(source) {
if text == "async" {
return true;
}
}
}
false
}
fn is_inside_impl(node: &Node) -> bool {
super::is_inside_ancestor(node, "impl_item")
}
fn extract_parameters(params_node: Option<Node>, source: &[u8]) -> Vec<String> {
let Some(node) = params_node else {
return vec![];
};
let mut params = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"parameter" => {
if let Some(pattern) = child.child_by_field_name("pattern") {
if let Ok(text) = pattern.utf8_text(source) {
params.push(text.to_string());
}
}
}
"self_parameter" => {
if let Ok(text) = child.utf8_text(source) {
params.push(text.to_string());
}
}
_ => {}
}
}
params
}
fn extract_structs_and_traits(
root: &Node,
source: &[u8],
path: &Path,
result: &mut ParseResult,
) -> Result<()> {
let mut cursor = root.walk();
for node in root.children(&mut cursor) {
match node.kind() {
"struct_item" => {
if let Some(class) = parse_struct_node(&node, source, path) {
result.classes.push(class);
}
}
"enum_item" => {
if let Some(class) = parse_enum_node(&node, source, path) {
result.classes.push(class);
}
}
"trait_item" => {
if let Some(class) = parse_trait_node(&node, source, path) {
result.classes.push(class);
}
}
"impl_item" => {
extract_impl_methods(&node, source, path, result)?;
}
_ => {}
}
}
Ok(())
}
fn parse_struct_node(node: &Node, source: &[u8], path: &Path) -> Option<Class> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source).ok()?.to_string();
let line_start = node.start_position().row as u32 + 1;
let line_end = node.end_position().row as u32 + 1;
let qualified_name = format!("{}::{}:{}", path.display(), name, line_start);
let mut annotations = extract_rust_attributes(node, source);
if has_pub_visibility(node) {
annotations.push("exported".to_string());
}
let field_count = count_struct_fields(node);
Some(Class {
name,
qualified_name,
file_path: path.to_path_buf(),
line_start,
line_end,
methods: vec![],
field_count,
bases: vec![],
doc_comment: None,
annotations,
})
}
fn count_struct_fields(node: &Node) -> usize {
if let Some(body) = node.child_by_field_name("body") {
match body.kind() {
"field_declaration_list" => {
let mut count = 0;
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "field_declaration" {
count += 1;
}
}
count
}
"ordered_field_declaration_list" => body.named_child_count(),
_ => 0,
}
} else {
0 }
}
fn parse_enum_node(node: &Node, source: &[u8], path: &Path) -> Option<Class> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source).ok()?.to_string();
let line_start = node.start_position().row as u32 + 1;
let line_end = node.end_position().row as u32 + 1;
let qualified_name = format!("{}::{}:{}", path.display(), name, line_start);
let mut annotations = extract_rust_attributes(node, source);
if has_pub_visibility(node) {
annotations.push("exported".to_string());
}
let field_count = node
.child_by_field_name("body")
.map(|body| {
let mut count = 0;
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "enum_variant" {
count += 1;
}
}
count
})
.unwrap_or(0);
Some(Class {
name,
qualified_name,
file_path: path.to_path_buf(),
line_start,
line_end,
methods: vec![],
field_count,
bases: vec![],
doc_comment: None,
annotations,
})
}
fn parse_trait_node(node: &Node, source: &[u8], path: &Path) -> Option<Class> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source).ok()?.to_string();
let line_start = node.start_position().row as u32 + 1;
let line_end = node.end_position().row as u32 + 1;
let qualified_name = format!("{}::trait::{}:{}", path.display(), name, line_start);
let methods = extract_trait_methods(node, source);
let bases = extract_trait_bounds(node, source);
let mut annotations = extract_rust_attributes(node, source);
if has_pub_visibility(node) {
annotations.push("exported".to_string());
}
Some(Class {
name,
qualified_name,
file_path: path.to_path_buf(),
line_start,
line_end,
methods,
field_count: 0,
bases,
doc_comment: None,
annotations,
})
}
fn extract_trait_methods(trait_node: &Node, source: &[u8]) -> Vec<String> {
let mut methods = Vec::new();
if let Some(body) = trait_node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
if child.kind() == "function_item" || child.kind() == "function_signature_item" {
if let Some(name_node) = child.child_by_field_name("name") {
if let Ok(name) = name_node.utf8_text(source) {
methods.push(name.to_string());
}
}
}
}
}
methods
}
fn extract_trait_bounds(trait_node: &Node, source: &[u8]) -> Vec<String> {
let mut bounds = Vec::new();
if let Some(bounds_node) = trait_node.child_by_field_name("bounds") {
for child in bounds_node.children(&mut bounds_node.walk()) {
if child.kind() == "type_identifier" || child.kind() == "generic_type" {
if let Ok(text) = child.utf8_text(source) {
bounds.push(text.to_string());
}
}
}
}
bounds
}
fn extract_impl_methods(
impl_node: &Node,
source: &[u8],
path: &Path,
result: &mut ParseResult,
) -> Result<()> {
let type_name = impl_node
.child_by_field_name("type")
.and_then(|n| n.utf8_text(source).ok())
.map(|s| s.to_string())
.unwrap_or_else(|| "Unknown".to_string());
let trait_name = impl_node
.child_by_field_name("trait")
.and_then(|n| n.utf8_text(source).ok())
.map(|s| s.to_string());
let impl_line = impl_node.start_position().row as u32 + 1;
if let Some(ref trait_n) = trait_name {
result.trait_impls.push((type_name.clone(), trait_n.clone()));
}
if let Some(body) = impl_node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
if child.kind() == "function_item" {
if let Some(func) = parse_impl_method(
&child,
source,
path,
&type_name,
trait_name.as_deref(),
impl_line,
) {
result.functions.push(func);
}
}
}
}
Ok(())
}
fn parse_impl_method(
node: &Node,
source: &[u8],
path: &Path,
type_name: &str,
trait_name: Option<&str>,
_impl_line: u32,
) -> Option<Function> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source).ok()?.to_string();
let params_node = node.child_by_field_name("parameters");
let parameters = extract_parameters(params_node, source);
let return_type = node
.child_by_field_name("return_type")
.and_then(|n| n.utf8_text(source).ok())
.map(|s| s.to_string());
let is_async = has_async_modifier(node, source);
let line_start = node.start_position().row as u32 + 1;
let line_end = node.end_position().row as u32 + 1;
let qualified_name = if let Some(trait_n) = trait_name {
format!(
"{}::impl<{} for {}>::{}:{}",
path.display(),
trait_n,
type_name,
name,
line_start
)
} else {
format!(
"{}::impl<{}>::{}:{}",
path.display(),
type_name,
name,
line_start
)
};
let mut annotations = extract_rust_attributes(node, source);
if has_pub_visibility(node) {
annotations.push("exported".to_string());
}
Some(Function {
name,
qualified_name,
file_path: path.to_path_buf(),
line_start,
line_end,
parameters,
return_type,
is_async,
complexity: Some(calculate_complexity(node, source)),
max_nesting: None,
doc_comment: None,
annotations,
})
}
fn extract_imports(root: &Node, source: &[u8], result: &mut ParseResult) -> Result<()> {
let query = RS_IMPORT_QUERY.get_or_init(|| {
Query::new(&tree_sitter_rust::LANGUAGE.into(), IMPORT_QUERY_STR)
.expect("valid import query")
});
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, *root, source);
while let Some(m) = matches.next() {
for capture in m.captures.iter() {
if let Ok(text) = capture.node.utf8_text(source) {
let import = text.trim().to_string();
if !import.is_empty() {
result.imports.push(ImportInfo::runtime(import));
}
}
}
}
Ok(())
}
fn extract_calls(root: &Node, source: &[u8], path: &Path, result: &mut ParseResult) -> Result<()> {
let mut scope_map: HashMap<(u32, u32), String> = HashMap::new();
for func in &result.functions {
scope_map.insert(
(func.line_start, func.line_end),
func.qualified_name.clone(),
);
}
extract_calls_recursive(root, source, path, &scope_map, result);
Ok(())
}
fn extract_calls_recursive(
node: &Node,
source: &[u8],
path: &Path,
scope_map: &HashMap<(u32, u32), String>,
result: &mut ParseResult,
) {
if node.kind() == "call_expression" {
let call_line = node.start_position().row as u32 + 1;
let caller = find_containing_scope(call_line, scope_map)
.unwrap_or_else(|| path.display().to_string());
if let Some(func_node) = node.child_by_field_name("function") {
if let Some(callee) = extract_call_target(&func_node, source) {
result.calls.push((caller, callee));
}
}
}
if node.kind() == "method_call_expression" {
let call_line = node.start_position().row as u32 + 1;
let caller = find_containing_scope(call_line, scope_map)
.unwrap_or_else(|| path.display().to_string());
if let Some(name_node) = node.child_by_field_name("name") {
if let Ok(callee) = name_node.utf8_text(source) {
result.calls.push((caller, callee.to_string()));
}
}
}
for child in node.children(&mut node.walk()) {
extract_calls_recursive(&child, source, path, scope_map, result);
}
}
fn find_containing_scope(line: u32, scope_map: &HashMap<(u32, u32), String>) -> Option<String> {
super::find_containing_scope(line, scope_map)
}
fn extract_call_target(node: &Node, source: &[u8]) -> Option<String> {
match node.kind() {
"identifier" => node.utf8_text(source).ok().map(|s| s.to_string()),
"scoped_identifier" => node.utf8_text(source).ok().map(|s| s.to_string()),
"field_expression" => node.utf8_text(source).ok().map(|s| s.to_string()),
"generic_function" => {
node.child_by_field_name("function")
.and_then(|n| extract_call_target(&n, source))
}
_ => node.utf8_text(source).ok().map(|s| s.to_string()),
}
}
fn has_pub_visibility(node: &Node) -> bool {
for child in node.children(&mut node.walk()) {
if child.kind() == "visibility_modifier" {
return true;
}
}
false
}
fn extract_rust_attributes(node: &Node, source: &[u8]) -> Vec<String> {
let mut attrs = Vec::new();
let mut sibling = node.prev_sibling();
while let Some(sib) = sibling {
if sib.kind() == "attribute_item" {
let text = sib.utf8_text(source).unwrap_or("");
let inner = text.trim_start_matches("#[").trim_end_matches(']').trim();
if !inner.is_empty() {
attrs.push(inner.to_string());
}
} else if sib.kind() == "line_comment" || sib.kind() == "block_comment" {
} else {
break;
}
sibling = sib.prev_sibling();
}
attrs.reverse(); attrs
}
fn calculate_complexity(node: &Node, _source: &[u8]) -> u32 {
let mut complexity = 1;
fn count_branches(node: &Node, complexity: &mut u32) {
match node.kind() {
"if_expression" | "else_clause" | "while_expression" | "for_expression"
| "loop_expression" => {
*complexity += 1;
}
"match_arm" => {
*complexity += 1;
}
"binary_expression" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "&&" || child.kind() == "||" {
*complexity += 1;
}
}
}
"?" => {
*complexity += 1;
}
_ => {}
}
for child in node.children(&mut node.walk()) {
count_branches(&child, complexity);
}
}
count_branches(node, &mut complexity);
complexity
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_parse_simple_function() {
let source = r#"
fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
"#;
let path = PathBuf::from("test.rs");
let result = parse_source(source, &path).expect("should parse Rust source");
assert_eq!(result.functions.len(), 1);
let func = &result.functions[0];
assert_eq!(func.name, "hello");
assert!(!func.is_async);
}
#[test]
fn test_parse_async_function() {
let source = r#"
async fn fetch_data(url: &str) -> Result<String, Error> {
Ok(String::new())
}
"#;
let path = PathBuf::from("test.rs");
let result = parse_source(source, &path).expect("should parse Rust source");
assert_eq!(result.functions.len(), 1);
let func = &result.functions[0];
assert_eq!(func.name, "fetch_data");
assert!(func.is_async);
}
#[test]
fn test_parse_struct() {
let source = r#"
struct MyStruct {
field: i32,
}
"#;
let path = PathBuf::from("test.rs");
let result = parse_source(source, &path).expect("should parse Rust source");
assert_eq!(result.classes.len(), 1);
let class = &result.classes[0];
assert_eq!(class.name, "MyStruct");
}
#[test]
fn test_parse_impl_methods() {
let source = r#"
struct MyStruct;
impl MyStruct {
fn new() -> Self {
MyStruct
}
fn method(&self, x: i32) -> i32 {
x * 2
}
}
"#;
let path = PathBuf::from("test.rs");
let result = parse_source(source, &path).expect("should parse Rust source");
assert_eq!(result.functions.len(), 2);
assert!(result.functions.iter().any(|f| f.name == "new"));
assert!(result.functions.iter().any(|f| f.name == "method"));
}
#[test]
fn test_parse_imports() {
let source = r#"
use std::collections::HashMap;
use crate::models::Function;
use super::{ImportInfo, ParseResult};
"#;
let path = PathBuf::from("test.rs");
let result = parse_source(source, &path).expect("should parse Rust source");
assert!(result.imports.len() >= 3);
}
#[test]
fn test_parse_trait() {
let source = r#"
trait MyTrait: Clone + Send {
fn required_method(&self);
fn provided_method(&self) -> i32 {
42
}
}
"#;
let path = PathBuf::from("test.rs");
let result = parse_source(source, &path).expect("should parse Rust source");
assert_eq!(result.classes.len(), 1);
let trait_def = &result.classes[0];
assert_eq!(trait_def.name, "MyTrait");
assert!(trait_def.methods.contains(&"required_method".to_string()));
assert!(trait_def.methods.contains(&"provided_method".to_string()));
}
#[test]
fn test_export_detection_rust() {
let code = r#"
pub fn public_func() {}
fn private_func() {}
pub struct PublicStruct {}
struct PrivateStruct {}
pub enum PublicEnum { A, B }
pub trait PublicTrait {}
"#;
let path = PathBuf::from("test.rs");
let result = parse_source(code, &path).expect("should parse Rust exports");
let public = result
.functions
.iter()
.find(|f| f.name == "public_func")
.unwrap();
assert!(
public.annotations.iter().any(|a| a == "exported"),
"pub fn should be exported, annotations: {:?}",
public.annotations
);
let private = result
.functions
.iter()
.find(|f| f.name == "private_func")
.unwrap();
assert!(
!private.annotations.iter().any(|a| a == "exported"),
"private fn should NOT be exported"
);
let pub_struct = result
.classes
.iter()
.find(|c| c.name == "PublicStruct")
.unwrap();
assert!(
pub_struct.annotations.iter().any(|a| a == "exported"),
"pub struct should be exported, annotations: {:?}",
pub_struct.annotations
);
let priv_struct = result
.classes
.iter()
.find(|c| c.name == "PrivateStruct")
.unwrap();
assert!(
!priv_struct.annotations.iter().any(|a| a == "exported"),
"private struct should NOT be exported"
);
let pub_enum = result
.classes
.iter()
.find(|c| c.name == "PublicEnum")
.unwrap();
assert!(
pub_enum.annotations.iter().any(|a| a == "exported"),
"pub enum should be exported, annotations: {:?}",
pub_enum.annotations
);
let pub_trait = result
.classes
.iter()
.find(|c| c.name == "PublicTrait")
.unwrap();
assert!(
pub_trait.annotations.iter().any(|a| a == "exported"),
"pub trait should be exported, annotations: {:?}",
pub_trait.annotations
);
}
#[test]
fn test_attribute_extraction() {
let code = r#"
#[test]
fn test_something() {
assert!(true);
}
#[derive(Debug, Clone)]
struct MyStruct {
field: i32,
}
#[tokio::main]
async fn main() {
println!("hello");
}
pub fn no_attrs() {}
"#;
let result = parse_source(code, Path::new("test.rs")).expect("should parse");
let test_fn = result
.functions
.iter()
.find(|f| f.name == "test_something")
.unwrap();
assert!(
test_fn.annotations.iter().any(|a| a.contains("test")),
"test_something should have #[test] annotation, got: {:?}",
test_fn.annotations
);
let main_fn = result.functions.iter().find(|f| f.name == "main").unwrap();
assert!(
main_fn.annotations.iter().any(|a| a.contains("tokio::main")),
"main should have #[tokio::main] annotation, got: {:?}",
main_fn.annotations
);
let my_struct = result
.classes
.iter()
.find(|c| c.name == "MyStruct")
.unwrap();
assert!(
my_struct.annotations.iter().any(|a| a.contains("derive")),
"MyStruct should have #[derive] annotation, got: {:?}",
my_struct.annotations
);
let no_attrs_fn = result
.functions
.iter()
.find(|f| f.name == "no_attrs")
.unwrap();
assert!(
no_attrs_fn
.annotations
.iter()
.all(|a| a == "exported"),
"no_attrs should only have 'exported' (no #[attr]), got: {:?}",
no_attrs_fn.annotations
);
}
#[test]
fn test_attribute_extraction_inside_cfg_test_module() {
let code = r#"
pub fn production_code() {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_something() {
assert!(true);
}
#[test]
fn test_another() {
assert_eq!(1, 1);
}
}
"#;
let result = parse_source(code, Path::new("test.rs")).expect("should parse");
let test_fn = result.functions.iter().find(|f| f.name == "test_something");
assert!(
test_fn.is_some(),
"test_something inside mod tests should be found. Functions: {:?}",
result.functions.iter().map(|f| (&f.name, &f.annotations)).collect::<Vec<_>>()
);
if let Some(tf) = test_fn {
assert!(
tf.annotations.iter().any(|a| a == "test"),
"test_something should have #[test] annotation, got: {:?}",
tf.annotations
);
}
let test_fn2 = result.functions.iter().find(|f| f.name == "test_another");
assert!(
test_fn2.is_some(),
"test_another inside mod tests should be found"
);
if let Some(tf) = test_fn2 {
assert!(
tf.annotations.iter().any(|a| a == "test"),
"test_another should have #[test] annotation, got: {:?}",
tf.annotations
);
}
}
#[test]
fn test_impl_method_export_detection() {
let code = r#"
struct MyStruct;
impl MyStruct {
pub fn public_method(&self) -> i32 {
42
}
fn private_method(&self) -> i32 {
0
}
}
"#;
let result = parse_source(code, Path::new("test.rs")).expect("should parse");
let public = result
.functions
.iter()
.find(|f| f.name == "public_method")
.unwrap();
assert!(
public.annotations.iter().any(|a| a == "exported"),
"pub impl method should be exported, got: {:?}",
public.annotations
);
let private = result
.functions
.iter()
.find(|f| f.name == "private_method")
.unwrap();
assert!(
!private.annotations.iter().any(|a| a == "exported"),
"private impl method should NOT be exported, got: {:?}",
private.annotations
);
}
#[test]
fn test_struct_field_count() {
let source = r#"
pub struct Config {
pub name: String,
pub value: i32,
pub enabled: bool,
}
struct Pair(i32, String);
struct Unit;
pub enum Color {
Red,
Green,
Blue,
Custom(u8, u8, u8),
}
"#;
let result = parse_source(source, Path::new("test.rs")).unwrap();
let config = result.classes.iter().find(|c| c.name == "Config").unwrap();
assert_eq!(config.field_count, 3, "Config has 3 named fields");
let pair = result.classes.iter().find(|c| c.name == "Pair").unwrap();
assert_eq!(pair.field_count, 2, "Pair tuple struct has 2 fields");
let unit = result.classes.iter().find(|c| c.name == "Unit").unwrap();
assert_eq!(unit.field_count, 0, "Unit struct has 0 fields");
let color = result.classes.iter().find(|c| c.name == "Color").unwrap();
assert_eq!(color.field_count, 4, "Color enum has 4 variants");
}
#[test]
fn test_trait_impl_method_qn_format() {
let source = r#"
struct GodClassDetector;
impl GodClassDetector {
fn new() -> Self { GodClassDetector }
}
trait Detector {
fn detect(&self) -> Vec<String>;
}
impl Detector for GodClassDetector {
fn detect(&self) -> Vec<String> { vec![] }
}
"#;
let result = parse_source(source, Path::new("src/detectors/god_class.rs")).unwrap();
for f in &result.functions {
eprintln!(" FUNC: {} -> {}", f.name, f.qualified_name);
}
let detect_impl = result.functions.iter()
.find(|f| f.name == "detect" && f.qualified_name.contains("impl<"))
.expect("Should find impl method for detect");
assert!(
detect_impl.qualified_name.contains("impl<Detector for GodClassDetector>"),
"Trait impl method QN should contain impl<Trait for Type>, got: {}",
detect_impl.qualified_name
);
let new_fn = result.functions.iter().find(|f| f.name == "new").unwrap();
assert!(
new_fn.qualified_name.contains("impl<GodClassDetector>"),
"Inherent impl QN should contain impl<Type>, got: {}",
new_fn.qualified_name
);
assert!(
!new_fn.qualified_name.contains(" for "),
"Inherent impl QN should NOT contain ' for ', got: {}",
new_fn.qualified_name
);
}
#[test]
fn test_trait_impl_relationships() {
let source = r#"
trait MyTrait {
fn do_thing(&self);
}
struct MyStruct;
impl MyTrait for MyStruct {
fn do_thing(&self) {}
}
impl MyStruct {
fn new() -> Self { MyStruct }
}
"#;
let result = parse_source(source, Path::new("test.rs")).unwrap();
assert_eq!(result.trait_impls.len(), 1);
assert_eq!(result.trait_impls[0].0, "MyStruct");
assert_eq!(result.trait_impls[0].1, "MyTrait");
}
}