use crate::model::Language;
#[derive(Debug, Clone, Default)]
pub struct InheritanceAnalysis {
pub classes: Vec<ClassDepth>,
}
#[derive(Debug, Clone)]
pub struct ClassDepth {
#[allow(dead_code)] pub name: String,
pub depth: u32,
#[allow(dead_code)] pub parent: Option<String>,
#[allow(dead_code)] pub cross_file: bool,
}
pub fn analyze(source: &str, language: &Language) -> InheritanceAnalysis {
let declarations = match language {
Language::Java => extract_java_classes(source),
Language::Python => extract_python_classes(source),
Language::JavaScript | Language::TypeScript => extract_ts_classes(source),
Language::Cpp => extract_cpp_classes(source),
Language::CSharp => extract_csharp_classes(source),
Language::Ruby => extract_ruby_classes(source),
Language::Rust => extract_rust_traits(source),
_ => Vec::new(),
};
let mut class_map: std::collections::HashMap<String, ClassDecl> =
std::collections::HashMap::new();
for decl in &declarations {
class_map.insert(decl.name.clone(), decl.clone());
}
let mut classes = Vec::new();
for decl in &declarations {
let (depth, cross_file) = compute_depth(
&decl.name,
&class_map,
&mut std::collections::HashSet::new(),
0,
);
classes.push(ClassDepth {
name: decl.name.clone(),
depth,
parent: decl.parent.clone(),
cross_file,
});
}
InheritanceAnalysis { classes }
}
#[derive(Debug, Clone)]
struct ClassDecl {
name: String,
parent: Option<String>,
}
fn compute_depth(
name: &str,
class_map: &std::collections::HashMap<String, ClassDecl>,
visited: &mut std::collections::HashSet<String>,
depth: u32,
) -> (u32, bool) {
if let Some(decl) = class_map.get(name) {
if let Some(ref parent) = decl.parent {
if visited.contains(parent) {
return (depth + 1, false);
}
visited.insert(parent.clone());
let (parent_depth, cross_file) = compute_depth(parent, class_map, visited, depth + 1);
(parent_depth, cross_file)
} else {
(depth, false)
}
} else if depth > 0 {
(depth, true)
} else {
(0, false)
}
}
fn extract_java_classes(source: &str) -> Vec<ClassDecl> {
let mut classes = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("class ") {
let name = rest.split_whitespace().next().unwrap_or("").to_string();
if name.is_empty() || name.starts_with('{') {
continue;
}
let parent = if let Some(extends_pos) = rest.find(" extends ") {
rest[extends_pos + 9..]
.split_whitespace()
.next()
.map(|s| s.trim_end_matches('{').trim().to_string())
} else {
None
};
classes.push(ClassDecl { name, parent });
}
}
classes
}
fn extract_python_classes(source: &str) -> Vec<ClassDecl> {
let mut classes = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("class ") {
if let Some(paren) = rest.find('(') {
let name = rest[..paren].trim().to_string();
let parent_part = rest[paren + 1..].trim_end_matches(':').trim();
let parent = parent_part.split(',').next().map(|s| s.trim().to_string());
if !name.is_empty() && parent.as_deref() != Some("object") {
classes.push(ClassDecl { name, parent });
}
}
else if let Some(end) = rest.find(':') {
let name = rest[..end].trim().to_string();
if !name.is_empty() {
classes.push(ClassDecl { name, parent: None });
}
}
}
}
classes
}
fn extract_ts_classes(source: &str) -> Vec<ClassDecl> {
let mut classes = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("class ") {
if let Some(extends_pos) = rest.find(" extends ") {
let name = rest[..extends_pos]
.split_whitespace()
.next()
.unwrap_or("")
.to_string();
let parent = rest[extends_pos + 9..].split_whitespace().next().map(|s| {
s.trim_matches(|c: char| c.is_whitespace() || c == '{' || c == ',')
.to_string()
});
if !name.is_empty() {
classes.push(ClassDecl { name, parent });
}
}
}
}
classes
}
fn extract_cpp_classes(source: &str) -> Vec<ClassDecl> {
let mut classes = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("class ") {
let name = rest.split_whitespace().next().unwrap_or("").to_string();
if !name.is_empty() && !name.starts_with(';') {
if let Some(colon_pos) = rest.find(" : ") {
let visibility = ["public ", "private ", "protected "];
let after_colon = rest[colon_pos + 3..].trim();
let parent = visibility
.iter()
.find_map(|v| after_colon.strip_prefix(v))
.or(Some(after_colon))
.and_then(|s| {
s.split_whitespace()
.next()
.map(|s| s.trim_matches(|c: char| c == '{' || c == ',').to_string())
});
classes.push(ClassDecl { name, parent });
}
}
}
}
classes
}
fn extract_csharp_classes(source: &str) -> Vec<ClassDecl> {
extract_java_classes(source)
}
fn extract_ruby_classes(source: &str) -> Vec<ClassDecl> {
let mut classes = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("class ") {
if let Some(lt_pos) = rest.find(" < ") {
let name = rest[..lt_pos].trim().to_string();
let parent = rest[lt_pos + 3..]
.split_whitespace()
.next()
.map(|s| s.to_string());
if !name.is_empty() {
classes.push(ClassDecl { name, parent });
}
}
}
}
classes
}
fn extract_rust_traits(source: &str) -> Vec<ClassDecl> {
let mut classes = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("trait ") {
let parts: Vec<&str> = rest.trim().splitn(2, ": ").collect();
let name = parts[0].split_whitespace().next().unwrap_or("").to_string();
if name.is_empty() {
continue;
}
if parts.len() > 1 {
let bounds = parts[1].trim_end_matches(['{', '}']).trim();
let parent = bounds
.split('+')
.map(|s| s.trim())
.find(|b| {
!matches!(
*b,
"Display"
| "Debug"
| "Clone"
| "Copy"
| "Send"
| "Sync"
| "Sized"
| "Default"
| "Eq"
| "PartialEq"
| "Ord"
| "PartialOrd"
| "Hash"
| "Into"
| "From"
)
})
.map(|s| s.to_string());
classes.push(ClassDecl { name, parent });
} else {
classes.push(ClassDecl { name, parent: None });
}
}
}
classes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn java_single_inheritance() {
let source = r#"
class A {}
class B extends A {}
class C extends B {}
"#;
let analysis = analyze(source, &Language::Java);
let c = analysis.classes.iter().find(|c| c.name == "C").unwrap();
assert_eq!(c.depth, 2);
}
#[test]
fn java_exceeds_threshold() {
let source = r#"
class A {}
class B extends A {}
class C extends B {}
class D extends C {}
"#;
let analysis = analyze(source, &Language::Java);
let d = analysis.classes.iter().find(|c| c.name == "D").unwrap();
assert_eq!(d.depth, 3);
}
#[test]
fn no_parent_depth_zero() {
let source = "class A {}";
let analysis = analyze(source, &Language::Java);
let a = analysis.classes.iter().find(|c| c.name == "A").unwrap();
assert_eq!(a.depth, 0);
}
#[test]
fn python_class_inheritance() {
let source = r#"
class A:
pass
class B(A):
pass
"#;
let analysis = analyze(source, &Language::Python);
let b = analysis.classes.iter().find(|c| c.name == "B").unwrap();
assert_eq!(b.depth, 1);
}
#[test]
fn rust_stdlib_trait_excluded() {
let source = r#"
trait Foo: Display + Debug {}
"#;
let analysis = analyze(source, &Language::Rust);
let f = analysis.classes.iter().find(|c| c.name == "Foo").unwrap();
assert_eq!(f.depth, 0);
}
#[test]
fn rust_custom_trait() {
let source = r#"
trait Bar {}
trait Foo: Bar {}
"#;
let analysis = analyze(source, &Language::Rust);
let f = analysis.classes.iter().find(|c| c.name == "Foo").unwrap();
assert_eq!(f.depth, 1);
}
#[test]
fn cross_file_parent_depth_one() {
let source = "class Foo extends Bar {}";
let analysis = analyze(source, &Language::Java);
let f = analysis.classes.iter().find(|c| c.name == "Foo").unwrap();
assert_eq!(f.depth, 1);
assert!(f.cross_file);
}
#[test]
fn unsupported_language_empty() {
let analysis = analyze("nothing", &Language::Julia);
assert!(analysis.classes.is_empty());
}
}