use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use regex::Regex;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use crate::ast::parser::parse;
use crate::error::TldrError;
use crate::types::Language;
use crate::TldrResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentInfo {
pub methods: Vec<String>,
pub fields: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CohesionVerdict {
Cohesive,
SplitCandidate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassCohesion {
pub name: String,
pub file: PathBuf,
pub line: usize,
pub method_count: usize,
pub field_count: usize,
pub lcom4: usize,
pub components: Vec<ComponentInfo>,
pub verdict: CohesionVerdict,
#[serde(skip_serializing_if = "Option::is_none")]
pub split_suggestion: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CohesionSummary {
pub total_classes: usize,
pub cohesive: usize,
pub split_candidates: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub avg_lcom4: Option<f64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CohesionReport {
pub classes_analyzed: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub avg_lcom4: Option<f64>,
pub low_cohesion_count: usize,
pub classes: Vec<ClassCohesion>,
pub summary: CohesionSummary,
}
#[derive(Debug, Clone)]
pub struct CohesionOptions {
pub include_dunder: bool,
pub low_cohesion_threshold: usize,
}
impl Default for CohesionOptions {
fn default() -> Self {
Self {
include_dunder: false,
low_cohesion_threshold: 2,
}
}
}
struct UnionFind {
parent: Vec<usize>,
rank: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
Self {
parent: (0..n).collect(),
rank: vec![0; n],
}
}
fn find(&mut self, x: usize) -> usize {
let mut root = x;
while self.parent[root] != root {
root = self.parent[root];
}
let mut node = x;
while self.parent[node] != root {
let next = self.parent[node];
self.parent[node] = root;
node = next;
}
root
}
fn union(&mut self, x: usize, y: usize) {
let rx = self.find(x);
let ry = self.find(y);
if rx != ry {
if self.rank[rx] < self.rank[ry] {
self.parent[rx] = ry;
} else if self.rank[rx] > self.rank[ry] {
self.parent[ry] = rx;
} else {
self.parent[ry] = rx;
self.rank[rx] += 1;
}
}
}
fn count_components(&mut self) -> usize {
let n = self.parent.len();
if n == 0 {
return 0;
}
(0..n)
.map(|i| self.find(i))
.collect::<HashSet<_>>()
.len()
}
fn get_components(&mut self) -> Vec<usize> {
let n = self.parent.len();
(0..n).map(|i| self.find(i)).collect()
}
}
struct MethodInfo {
name: String,
start_byte: usize,
end_byte: usize,
}
struct ClassInfo {
name: String,
line: usize,
methods: Vec<MethodInfo>,
}
pub fn analyze_cohesion(
path: &Path,
language: Option<Language>,
threshold: usize,
) -> TldrResult<CohesionReport> {
let options = CohesionOptions {
include_dunder: false,
low_cohesion_threshold: threshold,
};
analyze_cohesion_with_options(path, language, options)
}
pub fn analyze_cohesion_with_options(
path: &Path,
language: Option<Language>,
options: CohesionOptions,
) -> TldrResult<CohesionReport> {
let file_paths: Vec<PathBuf> = if path.is_file() {
vec![path.to_path_buf()]
} else {
WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.filter(|e| {
let detected = Language::from_path(e.path());
match (detected, language) {
(Some(d), Some(l)) => d == l,
(Some(_), None) => true,
_ => false,
}
})
.map(|e| e.path().to_path_buf())
.collect()
};
let mut all_classes: Vec<ClassCohesion> = Vec::new();
for file_path in &file_paths {
if let Ok(classes) = analyze_file_cohesion(file_path, &options) {
all_classes.extend(classes);
}
}
all_classes.sort_by(|a, b| b.lcom4.cmp(&a.lcom4));
let total_classes = all_classes.len();
let total_lcom4: usize = all_classes.iter().map(|c| c.lcom4).sum();
let avg_lcom4 = if total_classes > 0 {
Some(total_lcom4 as f64 / total_classes as f64)
} else {
None
};
let low_cohesion_count = all_classes
.iter()
.filter(|c| c.lcom4 > options.low_cohesion_threshold)
.count();
let cohesive_count = all_classes
.iter()
.filter(|c| c.verdict == CohesionVerdict::Cohesive)
.count();
let summary = CohesionSummary {
total_classes,
cohesive: cohesive_count,
split_candidates: low_cohesion_count,
avg_lcom4,
};
Ok(CohesionReport {
classes_analyzed: total_classes,
avg_lcom4,
low_cohesion_count,
classes: all_classes,
summary,
})
}
fn analyze_file_cohesion(
file_path: &Path,
options: &CohesionOptions,
) -> TldrResult<Vec<ClassCohesion>> {
let source = std::fs::read_to_string(file_path)?;
let language = Language::from_path(file_path)
.ok_or_else(|| TldrError::UnsupportedLanguage(
file_path.extension()
.and_then(|e| e.to_str())
.unwrap_or("unknown")
.to_string()
))?;
let tree = parse(&source, language)?;
let root = tree.root_node();
let class_infos = extract_classes(root, &source, language);
let mut results = Vec::new();
for class_info in class_infos {
let cohesion = compute_class_cohesion(
&class_info,
&source,
file_path,
options,
);
results.push(cohesion);
}
Ok(results)
}
fn extract_classes(
root: tree_sitter::Node,
source: &str,
language: Language,
) -> Vec<ClassInfo> {
match language {
Language::Python => extract_python_classes(root, source),
Language::TypeScript | Language::JavaScript => extract_typescript_classes(root, source),
Language::Go => extract_go_structs(root, source),
Language::Rust => extract_rust_structs(root, source),
Language::Java => extract_java_classes(root, source),
Language::Ruby => extract_ruby_classes(root, source),
Language::CSharp => extract_csharp_classes(root, source),
Language::Scala => extract_scala_classes(root, source),
Language::Php => extract_php_classes(root, source),
_ => vec![], }
}
fn extract_python_classes(root: tree_sitter::Node, source: &str) -> Vec<ClassInfo> {
let mut classes = Vec::new();
extract_python_classes_recursive(root, source, &mut classes);
classes
}
fn extract_python_classes_recursive(
node: tree_sitter::Node,
source: &str,
classes: &mut Vec<ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"class_definition" => {
if let Some(class_info) = extract_python_class_info(&child, source) {
classes.push(class_info);
}
if let Some(body) = child.child_by_field_name("body") {
extract_python_classes_recursive(body, source, classes);
}
}
"decorated_definition" => {
if let Some(def) = child.child_by_field_name("definition") {
if def.kind() == "class_definition" {
if let Some(class_info) = extract_python_class_info(&def, source) {
classes.push(class_info);
}
if let Some(body) = def.child_by_field_name("body") {
extract_python_classes_recursive(body, source, classes);
}
}
}
}
_ => {
extract_python_classes_recursive(child, source, classes);
}
}
}
}
fn extract_python_class_info(node: &tree_sitter::Node, source: &str) -> Option<ClassInfo> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source.as_bytes()).ok()?.to_string();
let line = node.start_position().row + 1;
let body = node.child_by_field_name("body")?;
let methods = extract_python_methods(&body, source);
Some(ClassInfo { name, line, methods })
}
fn extract_python_methods(body: &tree_sitter::Node, source: &str) -> Vec<MethodInfo> {
let mut methods = Vec::new();
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
match child.kind() {
"function_definition" => {
if let Some(method) = extract_python_method(&child, source) {
methods.push(method);
}
}
"decorated_definition" => {
if let Some(def) = child.child_by_field_name("definition") {
if def.kind() == "function_definition" {
if let Some(method) = extract_python_method(&def, source) {
methods.push(method);
}
}
}
}
_ => {}
}
}
methods
}
fn extract_python_method(node: &tree_sitter::Node, source: &str) -> Option<MethodInfo> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source.as_bytes()).ok()?.to_string();
Some(MethodInfo {
name,
start_byte: node.start_byte(),
end_byte: node.end_byte(),
})
}
fn extract_typescript_classes(root: tree_sitter::Node, source: &str) -> Vec<ClassInfo> {
let mut classes = Vec::new();
extract_typescript_classes_recursive(root, source, &mut classes);
classes
}
fn extract_typescript_classes_recursive(
node: tree_sitter::Node,
source: &str,
classes: &mut Vec<ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "class_declaration" || child.kind() == "class" {
if let Some(class_info) = extract_typescript_class_info(&child, source) {
classes.push(class_info);
}
}
extract_typescript_classes_recursive(child, source, classes);
}
}
fn extract_typescript_class_info(node: &tree_sitter::Node, source: &str) -> Option<ClassInfo> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source.as_bytes()).ok()?.to_string();
let line = node.start_position().row + 1;
let body = node.child_by_field_name("body")?;
let methods = extract_typescript_methods(&body, source);
Some(ClassInfo { name, line, methods })
}
fn extract_typescript_methods(body: &tree_sitter::Node, source: &str) -> Vec<MethodInfo> {
let mut methods = Vec::new();
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "method_definition" || child.kind() == "public_field_definition" {
if let Some(name_node) = child.child_by_field_name("name") {
if let Ok(name) = name_node.utf8_text(source.as_bytes()) {
if name != "constructor" {
methods.push(MethodInfo {
name: name.to_string(),
start_byte: child.start_byte(),
end_byte: child.end_byte(),
});
}
}
}
}
}
methods
}
fn extract_java_classes(root: tree_sitter::Node, source: &str) -> Vec<ClassInfo> {
let mut classes = Vec::new();
extract_java_classes_recursive(root, source, &mut classes);
classes
}
fn extract_java_classes_recursive(
node: tree_sitter::Node,
source: &str,
classes: &mut Vec<ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"class_declaration" | "interface_declaration" | "enum_declaration" => {
if let Some(class_info) = extract_java_class_info(&child, source) {
classes.push(class_info);
}
if let Some(body) = child.child_by_field_name("body") {
extract_java_classes_recursive(body, source, classes);
}
}
_ => {
extract_java_classes_recursive(child, source, classes);
}
}
}
}
fn extract_java_class_info(node: &tree_sitter::Node, source: &str) -> Option<ClassInfo> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source.as_bytes()).ok()?.to_string();
let line = node.start_position().row + 1;
let body = node.child_by_field_name("body")?;
let methods = extract_java_methods(&body, source);
Some(ClassInfo { name, line, methods })
}
fn extract_java_methods(body: &tree_sitter::Node, source: &str) -> Vec<MethodInfo> {
let mut methods = Vec::new();
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "method_declaration" {
if let Some(name_node) = child.child_by_field_name("name") {
if let Ok(name) = name_node.utf8_text(source.as_bytes()) {
methods.push(MethodInfo {
name: name.to_string(),
start_byte: child.start_byte(),
end_byte: child.end_byte(),
});
}
}
}
}
methods
}
fn extract_go_structs(root: tree_sitter::Node, source: &str) -> Vec<ClassInfo> {
let mut structs: HashMap<String, ClassInfo> = HashMap::new();
collect_go_structs(root, source, &mut structs);
collect_go_methods(root, source, &mut structs);
structs.into_values().collect()
}
fn collect_go_structs(
node: tree_sitter::Node,
source: &str,
structs: &mut HashMap<String, ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "type_declaration" {
let mut type_cursor = child.walk();
for type_child in child.children(&mut type_cursor) {
if type_child.kind() == "type_spec" {
if let Some(name_node) = type_child.child_by_field_name("name") {
if let Some(type_node) = type_child.child_by_field_name("type") {
if type_node.kind() == "struct_type" {
if let Ok(name) = name_node.utf8_text(source.as_bytes()) {
let line = type_child.start_position().row + 1;
structs.insert(
name.to_string(),
ClassInfo {
name: name.to_string(),
line,
methods: Vec::new(),
},
);
}
}
}
}
}
}
}
collect_go_structs(child, source, structs);
}
}
fn collect_go_methods(
node: tree_sitter::Node,
source: &str,
structs: &mut HashMap<String, ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "method_declaration" {
if let Some(receiver) = child.child_by_field_name("receiver") {
if let Some(struct_name) = extract_go_receiver_type(&receiver, source) {
if let Some(name_node) = child.child_by_field_name("name") {
if let Ok(method_name) = name_node.utf8_text(source.as_bytes()) {
if let Some(class_info) = structs.get_mut(&struct_name) {
class_info.methods.push(MethodInfo {
name: method_name.to_string(),
start_byte: child.start_byte(),
end_byte: child.end_byte(),
});
}
}
}
}
}
}
collect_go_methods(child, source, structs);
}
}
fn extract_go_receiver_type(receiver: &tree_sitter::Node, source: &str) -> Option<String> {
let mut cursor = receiver.walk();
for child in receiver.children(&mut cursor) {
if child.kind() == "parameter_declaration" {
if let Some(type_node) = child.child_by_field_name("type") {
if type_node.kind() == "pointer_type" {
if let Some(elem) = type_node.named_child(0) {
return elem.utf8_text(source.as_bytes()).ok().map(|s| s.to_string());
}
} else {
return type_node.utf8_text(source.as_bytes()).ok().map(|s| s.to_string());
}
}
}
}
None
}
fn extract_rust_structs(root: tree_sitter::Node, source: &str) -> Vec<ClassInfo> {
let mut structs: HashMap<String, ClassInfo> = HashMap::new();
collect_rust_structs(root, source, &mut structs);
collect_rust_impl_methods(root, source, &mut structs);
structs.into_values().collect()
}
fn collect_rust_structs(
node: tree_sitter::Node,
source: &str,
structs: &mut HashMap<String, ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "struct_item" {
if let Some(name_node) = child.child_by_field_name("name") {
if let Ok(name) = name_node.utf8_text(source.as_bytes()) {
let line = child.start_position().row + 1;
structs.insert(
name.to_string(),
ClassInfo {
name: name.to_string(),
line,
methods: Vec::new(),
},
);
}
}
}
collect_rust_structs(child, source, structs);
}
}
fn collect_rust_impl_methods(
node: tree_sitter::Node,
source: &str,
structs: &mut HashMap<String, ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "impl_item" {
if let Some(type_node) = child.child_by_field_name("type") {
if let Ok(type_name) = type_node.utf8_text(source.as_bytes()) {
let type_name = type_name.to_string();
if let Some(body) = child.child_by_field_name("body") {
let mut body_cursor = body.walk();
for body_child in body.children(&mut body_cursor) {
if body_child.kind() == "function_item" {
if !rust_function_has_self(&body_child) {
continue;
}
if let Some(name_node) = body_child.child_by_field_name("name") {
if let Ok(method_name) = name_node.utf8_text(source.as_bytes()) {
if let Some(class_info) = structs.get_mut(&type_name) {
class_info.methods.push(MethodInfo {
name: method_name.to_string(),
start_byte: body_child.start_byte(),
end_byte: body_child.end_byte(),
});
}
}
}
}
}
}
}
}
}
collect_rust_impl_methods(child, source, structs);
}
}
fn rust_function_has_self(function_node: &tree_sitter::Node) -> bool {
if let Some(params) = function_node.child_by_field_name("parameters") {
let mut cursor = params.walk();
for param_child in params.children(&mut cursor) {
if param_child.kind() == "self_parameter" {
return true;
}
}
}
false
}
fn extract_ruby_classes(root: tree_sitter::Node, source: &str) -> Vec<ClassInfo> {
let mut classes = Vec::new();
extract_ruby_classes_recursive(root, source, &mut classes);
classes
}
fn extract_ruby_classes_recursive(
node: tree_sitter::Node,
source: &str,
classes: &mut Vec<ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"class" => {
if let Some(class_info) = extract_ruby_class_info(&child, source) {
classes.push(class_info);
}
if let Some(body) = child.child_by_field_name("body") {
extract_ruby_classes_recursive(body, source, classes);
}
}
_ => {
extract_ruby_classes_recursive(child, source, classes);
}
}
}
}
fn extract_ruby_class_info(node: &tree_sitter::Node, source: &str) -> Option<ClassInfo> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source.as_bytes()).ok()?.to_string();
let line = node.start_position().row + 1;
let body = node.child_by_field_name("body")?;
let methods = extract_ruby_methods(&body, source);
Some(ClassInfo { name, line, methods })
}
fn extract_ruby_methods(body: &tree_sitter::Node, source: &str) -> Vec<MethodInfo> {
let mut methods = Vec::new();
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "method" || child.kind() == "singleton_method" {
if let Some(name_node) = child.child_by_field_name("name") {
if let Ok(name) = name_node.utf8_text(source.as_bytes()) {
methods.push(MethodInfo {
name: name.to_string(),
start_byte: child.start_byte(),
end_byte: child.end_byte(),
});
}
}
}
}
methods
}
fn extract_csharp_classes(root: tree_sitter::Node, source: &str) -> Vec<ClassInfo> {
let mut classes = Vec::new();
extract_csharp_classes_recursive(root, source, &mut classes);
classes
}
fn extract_csharp_classes_recursive(
node: tree_sitter::Node,
source: &str,
classes: &mut Vec<ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"class_declaration" | "struct_declaration" | "interface_declaration" => {
if let Some(class_info) = extract_csharp_class_info(&child, source) {
classes.push(class_info);
}
if let Some(body) = child.child_by_field_name("body") {
extract_csharp_classes_recursive(body, source, classes);
}
}
_ => {
extract_csharp_classes_recursive(child, source, classes);
}
}
}
}
fn extract_csharp_class_info(node: &tree_sitter::Node, source: &str) -> Option<ClassInfo> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source.as_bytes()).ok()?.to_string();
let line = node.start_position().row + 1;
let body = node.child_by_field_name("body")?;
let methods = extract_csharp_methods(&body, source);
Some(ClassInfo { name, line, methods })
}
fn extract_csharp_methods(body: &tree_sitter::Node, source: &str) -> Vec<MethodInfo> {
let mut methods = Vec::new();
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "method_declaration" {
if let Some(name_node) = child.child_by_field_name("name") {
if let Ok(name) = name_node.utf8_text(source.as_bytes()) {
methods.push(MethodInfo {
name: name.to_string(),
start_byte: child.start_byte(),
end_byte: child.end_byte(),
});
}
}
}
}
methods
}
fn extract_scala_classes(root: tree_sitter::Node, source: &str) -> Vec<ClassInfo> {
let mut classes = Vec::new();
extract_scala_classes_recursive(root, source, &mut classes);
classes
}
fn extract_scala_classes_recursive(
node: tree_sitter::Node,
source: &str,
classes: &mut Vec<ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"class_definition" | "object_definition" | "trait_definition" => {
if let Some(class_info) = extract_scala_class_info(&child, source) {
classes.push(class_info);
}
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "template_body" || inner_child.kind() == "body" {
extract_scala_classes_recursive(inner_child, source, classes);
}
}
}
_ => {
extract_scala_classes_recursive(child, source, classes);
}
}
}
}
fn extract_scala_class_info(node: &tree_sitter::Node, source: &str) -> Option<ClassInfo> {
let name = node
.child_by_field_name("name")
.and_then(|n| n.utf8_text(source.as_bytes()).ok().map(|s| s.to_string()))
.or_else(|| {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier" {
return child.utf8_text(source.as_bytes()).ok().map(|s| s.to_string());
}
}
None
})?;
let line = node.start_position().row + 1;
let methods = extract_scala_methods(node, source);
Some(ClassInfo { name, line, methods })
}
fn extract_scala_methods(node: &tree_sitter::Node, source: &str) -> Vec<MethodInfo> {
let mut methods = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "template_body" || child.kind() == "body" {
let mut body_cursor = child.walk();
for body_child in child.children(&mut body_cursor) {
if body_child.kind() == "function_definition"
|| body_child.kind() == "function_declaration"
{
if let Some(name_node) = body_child.child_by_field_name("name") {
if let Ok(name) = name_node.utf8_text(source.as_bytes()) {
methods.push(MethodInfo {
name: name.to_string(),
start_byte: body_child.start_byte(),
end_byte: body_child.end_byte(),
});
}
}
}
}
}
}
methods
}
fn extract_php_classes(root: tree_sitter::Node, source: &str) -> Vec<ClassInfo> {
let mut classes = Vec::new();
extract_php_classes_recursive(root, source, &mut classes);
classes
}
fn extract_php_classes_recursive(
node: tree_sitter::Node,
source: &str,
classes: &mut Vec<ClassInfo>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"class_declaration" | "interface_declaration" | "trait_declaration" => {
if let Some(class_info) = extract_php_class_info(&child, source) {
classes.push(class_info);
}
if let Some(body) = child.child_by_field_name("body") {
extract_php_classes_recursive(body, source, classes);
}
}
_ => {
extract_php_classes_recursive(child, source, classes);
}
}
}
}
fn extract_php_class_info(node: &tree_sitter::Node, source: &str) -> Option<ClassInfo> {
let name_node = node.child_by_field_name("name")?;
let name = name_node.utf8_text(source.as_bytes()).ok()?.to_string();
let line = node.start_position().row + 1;
let body = node.child_by_field_name("body")?;
let methods = extract_php_methods(&body, source);
Some(ClassInfo { name, line, methods })
}
fn extract_php_methods(body: &tree_sitter::Node, source: &str) -> Vec<MethodInfo> {
let mut methods = Vec::new();
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "method_declaration" {
if let Some(name_node) = child.child_by_field_name("name") {
if let Ok(name) = name_node.utf8_text(source.as_bytes()) {
methods.push(MethodInfo {
name: name.to_string(),
start_byte: child.start_byte(),
end_byte: child.end_byte(),
});
}
}
}
}
methods
}
fn is_dunder_method(name: &str) -> bool {
name.starts_with("__") && name.ends_with("__")
}
pub(crate) fn extract_self_accesses(method_source: &str) -> HashSet<String> {
let mut fields = HashSet::new();
let pattern = Regex::new(r"self\.([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
for cap in pattern.captures_iter(method_source) {
if let Some(field) = cap.get(1) {
fields.insert(field.as_str().to_string());
}
}
fields
}
fn extract_this_accesses(method_source: &str) -> HashSet<String> {
let mut fields = HashSet::new();
let pattern = Regex::new(r"this\.([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
for cap in pattern.captures_iter(method_source) {
if let Some(field) = cap.get(1) {
fields.insert(field.as_str().to_string());
}
}
fields
}
fn extract_go_receiver_accesses(method_source: &str, receiver_name: &str) -> HashSet<String> {
let mut fields = HashSet::new();
let pattern_str = format!(r"{}\.([a-zA-Z_][a-zA-Z0-9_]*)", regex::escape(receiver_name));
if let Ok(pattern) = Regex::new(&pattern_str) {
for cap in pattern.captures_iter(method_source) {
if let Some(field) = cap.get(1) {
fields.insert(field.as_str().to_string());
}
}
}
let short_pattern = Regex::new(r"\b([a-z])\.([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
for cap in short_pattern.captures_iter(method_source) {
if let Some(field) = cap.get(2) {
fields.insert(field.as_str().to_string());
}
}
fields
}
fn extract_rust_self_accesses(method_source: &str) -> HashSet<String> {
let mut fields = HashSet::new();
let pattern = Regex::new(r"self\.([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
for cap in pattern.captures_iter(method_source) {
if let Some(field) = cap.get(1) {
fields.insert(field.as_str().to_string());
}
}
fields
}
fn extract_ruby_instance_var_accesses(method_source: &str) -> HashSet<String> {
let mut fields = HashSet::new();
let pattern = Regex::new(r"(@?)@([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
for cap in pattern.captures_iter(method_source) {
if cap.get(1).is_some_and(|m| !m.as_str().is_empty()) {
continue;
}
if let Some(field) = cap.get(2) {
fields.insert(field.as_str().to_string());
}
}
fields
}
fn extract_php_this_accesses(method_source: &str) -> HashSet<String> {
let mut fields = HashSet::new();
let pattern = Regex::new(r"\$this->([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
for cap in pattern.captures_iter(method_source) {
if let Some(field) = cap.get(1) {
fields.insert(field.as_str().to_string());
}
}
fields
}
fn compute_class_cohesion(
class_info: &ClassInfo,
source: &str,
file_path: &Path,
options: &CohesionOptions,
) -> ClassCohesion {
let methods: Vec<&MethodInfo> = class_info
.methods
.iter()
.filter(|m| options.include_dunder || !is_dunder_method(&m.name))
.collect();
let method_count = methods.len();
if method_count == 0 {
return ClassCohesion {
name: class_info.name.clone(),
file: file_path.to_path_buf(),
line: class_info.line,
method_count: 0,
field_count: 0,
lcom4: 0,
components: vec![],
verdict: CohesionVerdict::Cohesive,
split_suggestion: None,
};
}
if method_count == 1 {
let method = methods[0];
let method_source = &source[method.start_byte..method.end_byte];
let fields = extract_field_accesses(method_source, file_path);
let field_vec: Vec<String> = fields.into_iter().collect();
return ClassCohesion {
name: class_info.name.clone(),
file: file_path.to_path_buf(),
line: class_info.line,
method_count: 1,
field_count: field_vec.len(),
lcom4: 1,
components: vec![ComponentInfo {
methods: vec![method.name.clone()],
fields: field_vec,
}],
verdict: CohesionVerdict::Cohesive,
split_suggestion: None,
};
}
let method_fields: Vec<HashSet<String>> = methods
.iter()
.map(|m| {
let method_source = &source[m.start_byte..m.end_byte];
extract_field_accesses(method_source, file_path)
})
.collect();
let all_fields: HashSet<String> = method_fields.iter().flatten().cloned().collect();
let field_count = all_fields.len();
if all_fields.is_empty() {
let lcom4 = method_count;
let components: Vec<ComponentInfo> = methods
.iter()
.map(|m| ComponentInfo {
methods: vec![m.name.clone()],
fields: vec![],
})
.collect();
let verdict = if lcom4 > options.low_cohesion_threshold {
CohesionVerdict::SplitCandidate
} else {
CohesionVerdict::Cohesive
};
let split_suggestion = if verdict == CohesionVerdict::SplitCandidate {
Some(format!(
"Class has {} disconnected methods with no shared state",
method_count
))
} else {
None
};
return ClassCohesion {
name: class_info.name.clone(),
file: file_path.to_path_buf(),
line: class_info.line,
method_count,
field_count: 0,
lcom4,
components,
verdict,
split_suggestion,
};
}
let mut uf = UnionFind::new(method_count);
for i in 0..method_count {
for j in (i + 1)..method_count {
if !method_fields[i].is_disjoint(&method_fields[j]) {
uf.union(i, j);
}
}
}
let lcom4 = uf.count_components();
let component_ids = uf.get_components();
let mut component_map: HashMap<usize, (Vec<String>, HashSet<String>)> = HashMap::new();
for (i, &comp_id) in component_ids.iter().enumerate() {
let entry = component_map.entry(comp_id).or_insert_with(|| (Vec::new(), HashSet::new()));
entry.0.push(methods[i].name.clone());
entry.1.extend(method_fields[i].iter().cloned());
}
let components: Vec<ComponentInfo> = component_map
.into_values()
.map(|(methods, fields)| ComponentInfo {
methods,
fields: fields.into_iter().collect(),
})
.collect();
let verdict = if lcom4 > options.low_cohesion_threshold {
CohesionVerdict::SplitCandidate
} else {
CohesionVerdict::Cohesive
};
let split_suggestion = if verdict == CohesionVerdict::SplitCandidate {
Some(format!(
"Consider splitting into {} classes based on {} disconnected method groups",
lcom4, lcom4
))
} else {
None
};
ClassCohesion {
name: class_info.name.clone(),
file: file_path.to_path_buf(),
line: class_info.line,
method_count,
field_count,
lcom4,
components,
verdict,
split_suggestion,
}
}
fn extract_field_accesses(method_source: &str, file_path: &Path) -> HashSet<String> {
let lang = Language::from_path(file_path);
match lang {
Some(language) => {
extract_field_accesses_ast(method_source, language, None)
}
None => {
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"py" => extract_self_accesses(method_source),
"ts" | "tsx" | "js" | "jsx" => extract_this_accesses(method_source),
"go" => extract_go_receiver_accesses(method_source, ""),
"rs" => extract_rust_self_accesses(method_source),
"rb" => extract_ruby_instance_var_accesses(method_source),
"cs" => extract_this_accesses(method_source),
"scala" | "sc" => extract_this_accesses(method_source),
"php" => extract_php_this_accesses(method_source),
_ => HashSet::new(),
}
}
}
}
pub fn extract_field_accesses_ast(
method_source: &str,
language: Language,
receiver_name: Option<&str>,
) -> HashSet<String> {
use crate::security::ast_utils::field_access_info;
let tree = match parse(method_source, language) {
Ok(t) => t,
Err(_) => {
return extract_field_accesses_regex(method_source, language, receiver_name);
}
};
let mut fields = HashSet::new();
let source = method_source.as_bytes();
let patterns = field_access_info(language);
walk_and_extract_fields(
&tree.root_node(),
source,
language,
receiver_name,
patterns,
&mut fields,
);
if fields.is_empty() {
let regex_fields = extract_field_accesses_regex(method_source, language, receiver_name);
if !regex_fields.is_empty() {
return regex_fields;
}
}
fields
}
fn walk_and_extract_fields(
node: &tree_sitter::Node,
source: &[u8],
language: Language,
receiver_name: Option<&str>,
patterns: &[crate::security::ast_utils::FieldAccessPattern],
fields: &mut HashSet<String>,
) {
use crate::security::ast_utils::{is_in_comment, is_in_string};
let node_kind = node.kind();
for pattern in patterns {
if node_kind == pattern.node_kind {
if is_in_comment(node, language) || is_in_string(node, language) {
continue;
}
if let Some(field_name) = extract_field_from_pattern(
node, source, language, receiver_name, pattern,
) {
fields.insert(field_name);
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_and_extract_fields(&child, source, language, receiver_name, patterns, fields);
}
}
fn extract_field_from_pattern(
node: &tree_sitter::Node,
source: &[u8],
language: Language,
receiver_name: Option<&str>,
_pattern: &crate::security::ast_utils::FieldAccessPattern,
) -> Option<String> {
match language {
Language::Python => extract_field_with_named_receiver(
node,
source,
"object",
"attribute",
"self",
"call",
"function",
),
Language::TypeScript | Language::JavaScript => extract_field_with_named_receiver(
node,
source,
"object",
"property",
"this",
"call_expression",
"function",
),
Language::Go => extract_go_field_access(node, source, receiver_name),
Language::Rust => extract_field_with_named_receiver(
node,
source,
"value",
"field",
"self",
"call_expression",
"function",
),
Language::Java => extract_field_with_named_receiver(
node,
source,
"object",
"field",
"this",
"method_invocation",
"object",
),
Language::CSharp => extract_field_with_positional_receiver(
node,
source,
0,
"name",
"this",
"invocation_expression",
0,
),
Language::Cpp => extract_field_with_named_receiver(
node,
source,
"argument",
"field",
"this",
"call_expression",
"function",
),
Language::C => extract_c_field_access(node, source),
Language::Ruby => extract_ruby_instance_field(node, source),
Language::Kotlin => extract_navigation_field_access(
node,
source,
"this_expression",
"this",
"call_expression",
),
Language::Swift => extract_navigation_field_access(
node,
source,
"self_expression",
"self",
"call_expression",
),
Language::Scala => extract_scala_this_field_access(node, source),
Language::Php => extract_php_this_field_access(node, source),
Language::Lua | Language::Luau => extract_lua_self_field_access(node, source),
Language::Elixir => extract_elixir_module_attribute(node, source),
Language::Ocaml => None,
}
}
fn extract_field_with_named_receiver(
node: &tree_sitter::Node,
source: &[u8],
receiver_field: &str,
field_name: &str,
expected_receiver: &str,
call_parent_kind: &str,
call_target_field: &str,
) -> Option<String> {
use crate::security::ast_utils::node_text;
let receiver = node.child_by_field_name(receiver_field)?;
if node_text(&receiver, source) != expected_receiver {
return None;
}
if parent_field_matches_node(node, call_parent_kind, call_target_field) {
return None;
}
Some(node_text(&node.child_by_field_name(field_name)?, source).to_string())
}
fn extract_field_with_positional_receiver(
node: &tree_sitter::Node,
source: &[u8],
receiver_index: usize,
field_name: &str,
expected_receiver: &str,
call_parent_kind: &str,
call_target_index: usize,
) -> Option<String> {
use crate::security::ast_utils::node_text;
let receiver = node.child(receiver_index)?;
if node_text(&receiver, source) != expected_receiver {
return None;
}
if parent_child_matches_node(node, call_parent_kind, call_target_index) {
return None;
}
Some(node_text(&node.child_by_field_name(field_name)?, source).to_string())
}
fn extract_go_field_access(
node: &tree_sitter::Node,
source: &[u8],
receiver_name: Option<&str>,
) -> Option<String> {
use crate::security::ast_utils::node_text;
let operand = node.child_by_field_name("operand")?;
let operand_text = node_text(&operand, source);
if !is_go_receiver_match(operand_text, receiver_name) {
return None;
}
if parent_field_matches_node(node, "call_expression", "function") {
return None;
}
Some(node_text(&node.child_by_field_name("field")?, source).to_string())
}
fn is_go_receiver_match(operand_text: &str, receiver_name: Option<&str>) -> bool {
match receiver_name {
Some("") | None => is_single_lowercase_identifier(operand_text),
Some(recv) => operand_text == recv,
}
}
fn is_single_lowercase_identifier(text: &str) -> bool {
text.len() == 1
&& text
.chars()
.next()
.is_some_and(|c| c.is_ascii_lowercase())
}
fn extract_c_field_access(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
use crate::security::ast_utils::node_text;
node.child_by_field_name("argument")?;
Some(node_text(&node.child_by_field_name("field")?, source).to_string())
}
fn extract_ruby_instance_field(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
use crate::security::ast_utils::node_text;
let text = node_text(node, source);
if text.starts_with('@') && !text.starts_with("@@") {
return Some(text[1..].to_string());
}
None
}
fn extract_navigation_field_access(
node: &tree_sitter::Node,
source: &[u8],
self_kind: &str,
self_text: &str,
call_parent_kind: &str,
) -> Option<String> {
use crate::security::ast_utils::node_text;
let target = node.child(0)?;
if target.kind() != self_kind && node_text(&target, source) != self_text {
return None;
}
for i in 1..node.child_count() {
let child = node.child(i)?;
if child.kind() == "identifier" || child.kind() == "simple_identifier" {
if parent_child_matches_node(node, call_parent_kind, 0) {
return None;
}
return Some(node_text(&child, source).to_string());
}
if child.kind() == "navigation_suffix" {
if let Some(identifier) = extract_suffix_identifier(&child, source) {
if parent_child_matches_node(node, call_parent_kind, 0) {
return None;
}
return Some(identifier);
}
}
}
None
}
fn extract_suffix_identifier(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
use crate::security::ast_utils::node_text;
for i in 0..node.child_count() {
let child = node.child(i)?;
if child.kind() == "simple_identifier" || child.kind() == "identifier" {
return Some(node_text(&child, source).to_string());
}
}
None
}
fn extract_scala_this_field_access(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
use crate::security::ast_utils::node_text;
let mut identifiers = Vec::new();
for i in 0..node.child_count() {
let child = node.child(i)?;
match child.kind() {
"identifier" | "type_identifier" => {
identifiers.push(node_text(&child, source).to_string());
}
"this" => identifiers.push("this".to_string()),
_ => {}
}
}
if identifiers.len() >= 2 && identifiers[0] == "this" {
return Some(identifiers[1].clone());
}
None
}
fn extract_php_this_field_access(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
use crate::security::ast_utils::node_text;
let object = node.child_by_field_name("object")?;
if node_text(&object, source) != "$this" {
return None;
}
Some(node_text(&node.child_by_field_name("name")?, source).to_string())
}
fn extract_lua_self_field_access(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
use crate::security::ast_utils::node_text;
let first = node.child(0)?;
if node_text(&first, source) != "self" {
return None;
}
for i in (0..node.child_count()).rev() {
let child = node.child(i)?;
if child.kind() == "identifier" {
if parent_child_matches_node(node, "function_call", 0) {
return None;
}
return Some(node_text(&child, source).to_string());
}
}
None
}
fn extract_elixir_module_attribute(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
use crate::security::ast_utils::node_text;
let operator = node.child(0)?;
if node_text(&operator, source) != "@" {
return None;
}
let name_node = node.child(1)?;
if name_node.kind() == "call" {
return Some(node_text(&name_node.child(0)?, source).to_string());
}
Some(node_text(&name_node, source).to_string())
}
fn parent_field_matches_node(
node: &tree_sitter::Node,
parent_kind: &str,
field_name: &str,
) -> bool {
let Some(parent) = node.parent() else {
return false;
};
if parent.kind() != parent_kind {
return false;
}
parent
.child_by_field_name(field_name)
.is_some_and(|child| child.id() == node.id())
}
fn parent_child_matches_node(
node: &tree_sitter::Node,
parent_kind: &str,
child_index: usize,
) -> bool {
let Some(parent) = node.parent() else {
return false;
};
if parent.kind() != parent_kind {
return false;
}
parent
.child(child_index)
.is_some_and(|child| child.id() == node.id())
}
fn extract_field_accesses_regex(
method_source: &str,
language: Language,
receiver_name: Option<&str>,
) -> HashSet<String> {
match language {
Language::Python => extract_self_accesses(method_source),
Language::TypeScript | Language::JavaScript => extract_this_accesses(method_source),
Language::Go => {
let recv = receiver_name.unwrap_or("");
extract_go_receiver_accesses(method_source, recv)
}
Language::Rust => extract_rust_self_accesses(method_source),
Language::Ruby => extract_ruby_instance_var_accesses(method_source),
Language::CSharp => extract_this_accesses(method_source),
Language::Scala => extract_this_accesses(method_source),
Language::Php => extract_php_this_accesses(method_source),
_ => HashSet::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_union_find_single_component() {
let mut uf = UnionFind::new(3);
uf.union(0, 1);
uf.union(1, 2);
assert_eq!(uf.count_components(), 1);
}
#[test]
fn test_union_find_multiple_components() {
let mut uf = UnionFind::new(4);
uf.union(0, 1); uf.union(2, 3); assert_eq!(uf.count_components(), 2);
}
#[test]
fn test_union_find_all_separate() {
let mut uf = UnionFind::new(4);
assert_eq!(uf.count_components(), 4);
}
#[test]
fn test_union_find_empty() {
let mut uf = UnionFind::new(0);
assert_eq!(uf.count_components(), 0);
}
#[test]
fn test_is_dunder_method() {
assert!(is_dunder_method("__init__"));
assert!(is_dunder_method("__str__"));
assert!(is_dunder_method("__repr__"));
assert!(!is_dunder_method("__private"));
assert!(!is_dunder_method("public__"));
assert!(!is_dunder_method("regular_method"));
}
#[test]
fn test_extract_self_accesses() {
let source = r#"
def method(self):
self.value = 1
self._private = 2
x = self.other
return self.value + self.other
"#;
let fields = extract_self_accesses(source);
assert!(fields.contains("value"));
assert!(fields.contains("_private"));
assert!(fields.contains("other"));
assert_eq!(fields.len(), 3);
}
#[test]
fn test_extract_this_accesses() {
let source = r#"
getValue() {
return this.value + this.other;
}
"#;
let fields = extract_this_accesses(source);
assert!(fields.contains("value"));
assert!(fields.contains("other"));
assert_eq!(fields.len(), 2);
}
#[test]
fn test_cohesion_verdict_serialization() {
let cohesive = CohesionVerdict::Cohesive;
let split = CohesionVerdict::SplitCandidate;
assert_eq!(
serde_json::to_string(&cohesive).unwrap(),
"\"cohesive\""
);
assert_eq!(
serde_json::to_string(&split).unwrap(),
"\"split_candidate\""
);
}
#[test]
fn test_ast_python_field_access() {
let source = "def method(self):\n x = self.name\n y = self.age\n z = self.name";
let fields = extract_field_accesses_ast(source, Language::Python, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
assert_eq!(fields.len(), 2, "Expected 2 unique fields, got {:?}", fields);
}
#[test]
fn test_ast_python_excludes_method_calls() {
let source = "def method(self):\n self.do_thing()\n x = self.name";
let fields = extract_field_accesses_ast(source, Language::Python, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(!fields.contains("do_thing"), "Should not contain method call 'do_thing': {:?}", fields);
}
#[test]
fn test_ast_python_string_not_detected() {
let source = r#"def method(self):
x = "self.fake_field"
y = self.real_field"#;
let fields = extract_field_accesses_ast(source, Language::Python, None);
assert!(fields.contains("real_field"), "Expected 'real_field' in {:?}", fields);
assert!(!fields.contains("fake_field"), "Should not detect field in string literal: {:?}", fields);
}
#[test]
fn test_ast_python_comment_not_detected() {
let source = "def method(self):\n # self.commented_field\n x = self.real_field";
let fields = extract_field_accesses_ast(source, Language::Python, None);
assert!(fields.contains("real_field"), "Expected 'real_field' in {:?}", fields);
assert!(!fields.contains("commented_field"), "Should not detect field in comment: {:?}", fields);
}
#[test]
fn test_ast_typescript_field_access() {
let source = "method() {\n const x = this.name;\n const y = this.age;\n}";
let fields = extract_field_accesses_ast(source, Language::TypeScript, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
assert_eq!(fields.len(), 2, "Expected 2 fields, got {:?}", fields);
}
#[test]
fn test_ast_javascript_field_access() {
let source = "function method() {\n const x = this.value;\n this.count = 0;\n}";
let fields = extract_field_accesses_ast(source, Language::JavaScript, None);
assert!(fields.contains("value"), "Expected 'value' in {:?}", fields);
assert!(fields.contains("count"), "Expected 'count' in {:?}", fields);
}
#[test]
fn test_ast_go_field_access() {
let source = "func (s *Server) method() {\n x := s.host\n y := s.port\n}";
let fields = extract_field_accesses_ast(source, Language::Go, Some("s"));
assert!(fields.contains("host"), "Expected 'host' in {:?}", fields);
assert!(fields.contains("port"), "Expected 'port' in {:?}", fields);
}
#[test]
fn test_ast_go_single_letter_receiver_heuristic() {
let source = "func method() {\n x := s.host\n y := s.port\n}";
let fields = extract_field_accesses_ast(source, Language::Go, None);
assert!(fields.contains("host"), "Expected 'host' in {:?}", fields);
assert!(fields.contains("port"), "Expected 'port' in {:?}", fields);
}
#[test]
fn test_ast_rust_field_access() {
let source = "fn method(&self) {\n let x = self.name;\n let y = self.age;\n}";
let fields = extract_field_accesses_ast(source, Language::Rust, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
assert_eq!(fields.len(), 2, "Expected 2 fields, got {:?}", fields);
}
#[test]
fn test_ast_java_field_access() {
let source = "void method() {\n int x = this.value;\n this.count = 0;\n}";
let fields = extract_field_accesses_ast(source, Language::Java, None);
assert!(fields.contains("value"), "Expected 'value' in {:?}", fields);
assert!(fields.contains("count"), "Expected 'count' in {:?}", fields);
}
#[test]
fn test_ast_kotlin_field_access() {
let source = "fun method() {\n val x = this.name\n this.age = 25\n}";
let fields = extract_field_accesses_ast(source, Language::Kotlin, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
}
#[test]
fn test_ast_swift_field_access() {
let source = "func method() {\n let x = self.name\n self.age = 25\n}";
let fields = extract_field_accesses_ast(source, Language::Swift, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
}
#[test]
fn test_ast_csharp_field_access() {
let source = "void Method() {\n var x = this.Name;\n this.Count = 0;\n}";
let fields = extract_field_accesses_ast(source, Language::CSharp, None);
assert!(fields.contains("Name"), "Expected 'Name' in {:?}", fields);
assert!(fields.contains("Count"), "Expected 'Count' in {:?}", fields);
}
#[test]
fn test_ast_cpp_field_access() {
let source = "void method() {\n int x = this->name;\n this->age = 25;\n}";
let fields = extract_field_accesses_ast(source, Language::Cpp, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
}
#[test]
fn test_ast_c_field_access() {
let source = "void method(struct Server* s) {\n int x = s->host;\n s->port = 80;\n}";
let fields = extract_field_accesses_ast(source, Language::C, None);
assert!(fields.contains("host"), "Expected 'host' in {:?}", fields);
assert!(fields.contains("port"), "Expected 'port' in {:?}", fields);
}
#[test]
fn test_ast_ruby_instance_variable() {
let source = "def method\n x = @name\n @age = 25\nend";
let fields = extract_field_accesses_ast(source, Language::Ruby, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
}
#[test]
fn test_ast_scala_field_access() {
let source = "def method(): Unit = {\n val x = this.name\n this.age = 25\n}";
let fields = extract_field_accesses_ast(source, Language::Scala, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
}
#[test]
fn test_ast_php_field_access() {
let source = "<?php\nfunction method() {\n $x = $this->name;\n $this->age = 25;\n}";
let fields = extract_field_accesses_ast(source, Language::Php, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
}
#[test]
fn test_ast_lua_field_access() {
let source = "function MyClass:method()\n local x = self.name\n self.age = 25\nend";
let fields = extract_field_accesses_ast(source, Language::Lua, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
}
#[test]
fn test_ast_luau_field_access() {
let source = "function MyClass:method()\n local x = self.name\n self.age = 25\nend";
let fields = extract_field_accesses_ast(source, Language::Luau, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
}
#[test]
fn test_ast_elixir_module_attribute() {
let source = "defmodule MyModule do\n @name \"test\"\n @age 25\nend";
let fields = extract_field_accesses_ast(source, Language::Elixir, None);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
}
#[test]
fn test_ast_ocaml_no_self() {
let source = "let method x = x.name + x.age";
let fields = extract_field_accesses_ast(source, Language::Ocaml, None);
assert!(fields.is_empty(), "OCaml should return empty set for LCOM4: {:?}", fields);
}
#[test]
fn test_ast_regex_fallback_on_parse_failure() {
let source = "self.name = 1\nself.age = 2";
let fields = extract_field_accesses_regex(source, Language::Python, None);
assert!(fields.contains("name"), "Regex fallback should find 'name': {:?}", fields);
assert!(fields.contains("age"), "Regex fallback should find 'age': {:?}", fields);
}
#[test]
fn test_extract_java_classes_basic() {
let source = r#"
public class MyService {
private String name;
public MyService(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
"#;
let tree = parse(source, Language::Java).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Java);
assert_eq!(classes.len(), 1, "Expected 1 class, got {:?}", classes.iter().map(|c| &c.name).collect::<Vec<_>>());
assert_eq!(classes[0].name, "MyService");
let non_ctor_methods: Vec<_> = classes[0].methods.iter().filter(|m| m.name != "MyService").collect();
assert_eq!(non_ctor_methods.len(), 2, "Expected 2 non-constructor methods, got {:?}", classes[0].methods.iter().map(|m| &m.name).collect::<Vec<_>>());
}
#[test]
fn test_extract_java_classes_multiple() {
let source = r#"
public class First {
public void doA() {}
}
class Second {
public void doB() {}
}
"#;
let tree = parse(source, Language::Java).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Java);
assert_eq!(classes.len(), 2, "Expected 2 classes, got {:?}", classes.iter().map(|c| &c.name).collect::<Vec<_>>());
let names: Vec<&str> = classes.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"First"), "Expected 'First' in {:?}", names);
assert!(names.contains(&"Second"), "Expected 'Second' in {:?}", names);
}
#[test]
fn test_extract_java_interface_and_enum() {
let source = r#"
interface Describable {
String describe();
}
enum Color {
RED, GREEN, BLUE;
public String label() {
return this.name();
}
}
"#;
let tree = parse(source, Language::Java).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Java);
let names: Vec<&str> = classes.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"Color"), "Expected 'Color' enum in {:?}", names);
}
#[test]
fn test_extract_java_methods_exclude_constructors() {
let source = r#"
public class Widget {
private int size;
public Widget() {
this.size = 0;
}
public Widget(int size) {
this.size = size;
}
public int getSize() {
return this.size;
}
}
"#;
let tree = parse(source, Language::Java).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Java);
assert_eq!(classes.len(), 1);
let widget = &classes[0];
let method_names: Vec<&str> = widget.methods.iter().map(|m| m.name.as_str()).collect();
assert!(method_names.contains(&"getSize"), "Expected 'getSize' in {:?}", method_names);
assert!(!method_names.contains(&"Widget"), "Constructors should not be extracted: {:?}", method_names);
}
#[test]
fn test_extract_rust_structs_basic() {
let source = r#"
pub struct Foo {
bar: String,
baz: i32,
}
impl Foo {
pub fn get_bar(&self) -> &str {
&self.bar
}
pub fn get_baz(&self) -> i32 {
self.baz
}
pub fn set_bar(&mut self, val: String) {
self.bar = val;
}
}
"#;
let tree = parse(source, Language::Rust).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Rust);
assert_eq!(classes.len(), 1, "Expected 1 struct, got {}", classes.len());
assert_eq!(classes[0].name, "Foo");
assert_eq!(
classes[0].methods.len(), 3,
"Expected 3 methods, got {}: {:?}",
classes[0].methods.len(),
classes[0].methods.iter().map(|m| &m.name).collect::<Vec<_>>()
);
}
#[test]
fn test_rust_lcom4_cohesive_struct() {
let source = r#"
pub struct Foo {
bar: String,
baz: i32,
}
impl Foo {
pub fn get_bar(&self) -> &str {
&self.bar
}
pub fn get_baz(&self) -> i32 {
self.baz
}
pub fn set_bar(&mut self, val: String) {
self.bar = val;
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("foo.rs");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let foo = results.iter().find(|c| c.name == "Foo").unwrap();
assert_eq!(foo.method_count, 3, "Expected 3 methods, got {}", foo.method_count);
assert!(foo.field_count > 0, "Expected fields to be detected, got {}", foo.field_count);
assert_eq!(foo.lcom4, 2, "Expected LCOM4=2 (two components), got {}", foo.lcom4);
}
#[test]
fn test_rust_lcom4_fully_cohesive() {
let source = r#"
pub struct Counter {
count: i32,
}
impl Counter {
pub fn increment(&mut self) {
self.count += 1;
}
pub fn decrement(&mut self) {
self.count -= 1;
}
pub fn get(&self) -> i32 {
self.count
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("counter.rs");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let counter = results.iter().find(|c| c.name == "Counter").unwrap();
assert_eq!(counter.method_count, 3);
assert_eq!(counter.field_count, 1, "Expected 1 field (count), got {}", counter.field_count);
assert_eq!(counter.lcom4, 1, "Fully cohesive class should have LCOM4=1, got {}", counter.lcom4);
}
#[test]
fn test_rust_multiple_structs() {
let source = r#"
pub struct Alpha {
x: i32,
}
impl Alpha {
pub fn get_x(&self) -> i32 {
self.x
}
}
pub struct Beta {
y: String,
}
impl Beta {
pub fn get_y(&self) -> &str {
&self.y
}
}
"#;
let tree = parse(source, Language::Rust).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Rust);
assert_eq!(classes.len(), 2, "Expected 2 structs, got {}", classes.len());
let names: Vec<&str> = classes.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"Alpha"), "Expected 'Alpha' in {:?}", names);
assert!(names.contains(&"Beta"), "Expected 'Beta' in {:?}", names);
}
#[test]
fn test_rust_struct_with_multiple_impl_blocks() {
let source = r#"
pub struct MyType {
a: i32,
b: String,
}
impl MyType {
pub fn get_a(&self) -> i32 {
self.a
}
}
impl MyType {
pub fn get_b(&self) -> &str {
&self.b
}
}
"#;
let tree = parse(source, Language::Rust).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Rust);
assert_eq!(classes.len(), 1, "Expected 1 struct (merged impl blocks), got {}", classes.len());
let my_type = &classes[0];
assert_eq!(
my_type.methods.len(), 2,
"Expected 2 methods from merged impl blocks, got {}: {:?}",
my_type.methods.len(),
my_type.methods.iter().map(|m| &m.name).collect::<Vec<_>>()
);
}
#[test]
fn test_rust_trait_impl_methods_included() {
let source = r#"
pub struct Config {
name: String,
count: i32,
}
impl Config {
pub fn get_name(&self) -> &str {
&self.name
}
}
impl Default for Config {
fn default() -> Self {
Self {
name: String::new(),
count: 0,
}
}
}
"#;
let tree = parse(source, Language::Rust).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Rust);
assert_eq!(classes.len(), 1, "Expected 1 struct");
let config = &classes[0];
let method_names: Vec<&str> = config.methods.iter().map(|m| m.name.as_str()).collect();
assert!(
method_names.contains(&"get_name"),
"Expected 'get_name' in methods: {:?}", method_names
);
assert!(
!method_names.contains(&"default"),
"Static 'default()' should be excluded from instance methods: {:?}", method_names
);
assert_eq!(
config.methods.len(), 1,
"Expected 1 instance method (get_name only, default() excluded), got {}: {:?}",
config.methods.len(), method_names
);
}
#[test]
fn test_rust_static_method_not_inflating_lcom4() {
let source = r#"
pub struct Builder {
name: String,
count: i32,
}
impl Builder {
pub fn new() -> Self {
Self {
name: String::new(),
count: 0,
}
}
pub fn get_name(&self) -> &str {
&self.name
}
pub fn get_count(&self) -> i32 {
self.count
}
pub fn inc(&mut self) {
self.count += 1;
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("builder.rs");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
let builder = results.iter().find(|c| c.name == "Builder").unwrap();
assert_eq!(
builder.method_count, 3,
"Expected 3 instance methods (excluding static new()), got {}",
builder.method_count
);
assert_eq!(
builder.lcom4, 2,
"Expected LCOM4=2 (two components: {{get_name}} and {{get_count, inc}}), got {}",
builder.lcom4
);
}
#[test]
fn test_rust_field_accesses_detected_in_methods() {
let method_source = r#"pub fn process(&mut self) {
let x = self.name;
self.count += 1;
self.data.push(x);
}"#;
let fields = extract_rust_self_accesses(method_source);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("count"), "Expected 'count' in {:?}", fields);
assert!(fields.contains("data"), "Expected 'data' in {:?}", fields);
assert_eq!(fields.len(), 3, "Expected 3 fields, got {:?}", fields);
}
#[test]
fn test_rust_analyze_file_cohesion_on_coupling_rs() {
let coupling_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src/quality/coupling.rs");
if coupling_path.exists() {
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&coupling_path, &options).unwrap();
let names: Vec<&str> = results.iter().map(|c| c.name.as_str()).collect();
assert!(
results.len() >= 3,
"Expected at least 3 structs in coupling.rs, got {}: {:?}",
results.len(),
names
);
for r in &results {
assert_eq!(
r.method_count, 0,
"Struct {} should have 0 instance methods (default() is static), got {}",
r.name, r.method_count
);
}
}
}
#[test]
fn test_rust_analyze_file_cohesion_on_real_file() {
let source = r#"
use std::collections::HashMap;
/// A report structure
#[derive(Debug, Clone)]
pub struct Report {
pub title: String,
pub items: Vec<String>,
pub metadata: HashMap<String, String>,
}
impl Report {
pub fn new(title: String) -> Self {
Self {
title,
items: Vec::new(),
metadata: HashMap::new(),
}
}
pub fn add_item(&mut self, item: String) {
self.items.push(item);
}
pub fn get_title(&self) -> &str {
&self.title
}
pub fn item_count(&self) -> usize {
self.items.len()
}
pub fn set_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("report.rs");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected structs to be found in realistic Rust file");
let report = results.iter().find(|c| c.name == "Report")
.expect("Expected 'Report' struct to be found");
assert!(report.method_count >= 4, "Expected at least 4 methods, got {}", report.method_count);
assert!(report.field_count >= 2, "Expected at least 2 fields, got {}", report.field_count);
}
#[test]
fn test_extract_ruby_classes_basic() {
let source = r#"
class Dog
def initialize(name, age)
@name = name
@age = age
end
def bark
@name
end
def age
@age
end
end
"#;
let tree = parse(source, Language::Ruby).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Ruby);
assert_eq!(classes.len(), 1, "Expected 1 class, got {:?}", classes.iter().map(|c| &c.name).collect::<Vec<_>>());
assert_eq!(classes[0].name, "Dog");
assert_eq!(
classes[0].methods.len(), 3,
"Expected 3 methods, got {:?}",
classes[0].methods.iter().map(|m| &m.name).collect::<Vec<_>>()
);
}
#[test]
fn test_extract_ruby_classes_with_inheritance() {
let source = r#"
class Animal
def speak
@sound
end
end
class Cat < Animal
def purr
@purr_volume
end
end
"#;
let tree = parse(source, Language::Ruby).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Ruby);
assert_eq!(classes.len(), 2, "Expected 2 classes, got {:?}", classes.iter().map(|c| &c.name).collect::<Vec<_>>());
let names: Vec<&str> = classes.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"Animal"), "Expected 'Animal' in {:?}", names);
assert!(names.contains(&"Cat"), "Expected 'Cat' in {:?}", names);
}
#[test]
fn test_ruby_lcom4_cohesive_class() {
let source = r#"
class Counter
def initialize
@count = 0
end
def increment
@count += 1
end
def decrement
@count -= 1
end
def value
@count
end
end
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("counter.rb");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let counter = results.iter().find(|c| c.name == "Counter").unwrap();
assert_eq!(counter.method_count, 4, "Expected 4 methods, got {}", counter.method_count);
assert_eq!(counter.lcom4, 1, "Fully cohesive class should have LCOM4=1, got {}", counter.lcom4);
}
#[test]
fn test_ruby_lcom4_split_candidate() {
let source = r#"
class Mixed
def get_name
@name
end
def set_name(n)
@name = n
end
def get_age
@age
end
def set_age(a)
@age = a
end
end
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("mixed.rb");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let mixed = results.iter().find(|c| c.name == "Mixed").unwrap();
assert_eq!(mixed.method_count, 4, "Expected 4 methods, got {}", mixed.method_count);
assert_eq!(mixed.lcom4, 2, "Expected LCOM4=2 (two components), got {}", mixed.lcom4);
}
#[test]
fn test_extract_csharp_classes_basic() {
let source = r#"
class UserService {
private string name;
public string GetName() {
return this.name;
}
public void SetName(string n) {
this.name = n;
}
}
"#;
let tree = parse(source, Language::CSharp).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::CSharp);
assert_eq!(classes.len(), 1, "Expected 1 class, got {:?}", classes.iter().map(|c| &c.name).collect::<Vec<_>>());
assert_eq!(classes[0].name, "UserService");
assert_eq!(
classes[0].methods.len(), 2,
"Expected 2 methods, got {:?}",
classes[0].methods.iter().map(|m| &m.name).collect::<Vec<_>>()
);
}
#[test]
fn test_extract_csharp_classes_multiple() {
let source = r#"
class First {
public void DoA() {}
}
class Second {
public void DoB() {}
}
"#;
let tree = parse(source, Language::CSharp).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::CSharp);
assert_eq!(classes.len(), 2, "Expected 2 classes, got {:?}", classes.iter().map(|c| &c.name).collect::<Vec<_>>());
let names: Vec<&str> = classes.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"First"), "Expected 'First' in {:?}", names);
assert!(names.contains(&"Second"), "Expected 'Second' in {:?}", names);
}
#[test]
fn test_csharp_lcom4_cohesive_class() {
let source = r#"
class Counter {
private int count;
public void Increment() {
this.count += 1;
}
public void Decrement() {
this.count -= 1;
}
public int GetValue() {
return this.count;
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("counter.cs");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let counter = results.iter().find(|c| c.name == "Counter").unwrap();
assert_eq!(counter.method_count, 3, "Expected 3 methods, got {}", counter.method_count);
assert_eq!(counter.lcom4, 1, "Fully cohesive class should have LCOM4=1, got {}", counter.lcom4);
}
#[test]
fn test_csharp_lcom4_split_candidate() {
let source = r#"
class Mixed {
private string name;
private int age;
public string GetName() {
return this.name;
}
public void SetName(string n) {
this.name = n;
}
public int GetAge() {
return this.age;
}
public void SetAge(int a) {
this.age = a;
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("mixed.cs");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let mixed = results.iter().find(|c| c.name == "Mixed").unwrap();
assert_eq!(mixed.method_count, 4, "Expected 4 methods, got {}", mixed.method_count);
assert_eq!(mixed.lcom4, 2, "Expected LCOM4=2 (two components), got {}", mixed.lcom4);
}
#[test]
fn test_extract_scala_classes_basic() {
let source = r#"
class UserService {
def getName(): String = {
this.name
}
def setName(n: String): Unit = {
this.name = n
}
}
"#;
let tree = parse(source, Language::Scala).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Scala);
assert_eq!(classes.len(), 1, "Expected 1 class, got {:?}", classes.iter().map(|c| &c.name).collect::<Vec<_>>());
assert_eq!(classes[0].name, "UserService");
assert_eq!(
classes[0].methods.len(), 2,
"Expected 2 methods, got {:?}",
classes[0].methods.iter().map(|m| &m.name).collect::<Vec<_>>()
);
}
#[test]
fn test_extract_scala_object_and_trait() {
let source = r#"
object Config {
def getValue(): String = {
this.value
}
}
trait Describable {
def describe(): String
}
"#;
let tree = parse(source, Language::Scala).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Scala);
let names: Vec<&str> = classes.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"Config"), "Expected 'Config' object in {:?}", names);
assert!(names.contains(&"Describable"), "Expected 'Describable' trait in {:?}", names);
}
#[test]
fn test_scala_lcom4_cohesive_class() {
let source = r#"
class Counter {
def increment(): Unit = {
this.count += 1
}
def decrement(): Unit = {
this.count -= 1
}
def getValue(): Int = {
this.count
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("counter.scala");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let counter = results.iter().find(|c| c.name == "Counter").unwrap();
assert_eq!(counter.method_count, 3, "Expected 3 methods, got {}", counter.method_count);
assert_eq!(counter.lcom4, 1, "Fully cohesive class should have LCOM4=1, got {}", counter.lcom4);
}
#[test]
fn test_scala_lcom4_split_candidate() {
let source = r#"
class Mixed {
def getName(): String = {
this.name
}
def setName(n: String): Unit = {
this.name = n
}
def getAge(): Int = {
this.age
}
def setAge(a: Int): Unit = {
this.age = a
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("mixed.scala");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let mixed = results.iter().find(|c| c.name == "Mixed").unwrap();
assert_eq!(mixed.method_count, 4, "Expected 4 methods, got {}", mixed.method_count);
assert_eq!(mixed.lcom4, 2, "Expected LCOM4=2 (two components), got {}", mixed.lcom4);
}
#[test]
fn test_extract_php_classes_basic() {
let source = r#"<?php
class UserService {
private $name;
public function getName() {
return $this->name;
}
public function setName($n) {
$this->name = $n;
}
}
"#;
let tree = parse(source, Language::Php).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Php);
assert_eq!(classes.len(), 1, "Expected 1 class, got {:?}", classes.iter().map(|c| &c.name).collect::<Vec<_>>());
assert_eq!(classes[0].name, "UserService");
assert_eq!(
classes[0].methods.len(), 2,
"Expected 2 methods, got {:?}",
classes[0].methods.iter().map(|m| &m.name).collect::<Vec<_>>()
);
}
#[test]
fn test_extract_php_classes_multiple() {
let source = r#"<?php
class First {
public function doA() {}
}
class Second {
public function doB() {}
}
"#;
let tree = parse(source, Language::Php).unwrap();
let classes = extract_classes(tree.root_node(), source, Language::Php);
assert_eq!(classes.len(), 2, "Expected 2 classes, got {:?}", classes.iter().map(|c| &c.name).collect::<Vec<_>>());
let names: Vec<&str> = classes.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"First"), "Expected 'First' in {:?}", names);
assert!(names.contains(&"Second"), "Expected 'Second' in {:?}", names);
}
#[test]
fn test_php_lcom4_cohesive_class() {
let source = r#"<?php
class Counter {
private $count;
public function increment() {
$this->count += 1;
}
public function decrement() {
$this->count -= 1;
}
public function getValue() {
return $this->count;
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("counter.php");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let counter = results.iter().find(|c| c.name == "Counter").unwrap();
assert_eq!(counter.method_count, 3, "Expected 3 methods, got {}", counter.method_count);
assert_eq!(counter.lcom4, 1, "Fully cohesive class should have LCOM4=1, got {}", counter.lcom4);
}
#[test]
fn test_php_lcom4_split_candidate() {
let source = r#"<?php
class Mixed {
private $name;
private $age;
public function getName() {
return $this->name;
}
public function setName($n) {
$this->name = $n;
}
public function getAge() {
return $this->age;
}
public function setAge($a) {
$this->age = $a;
}
}
"#;
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("mixed.php");
std::fs::write(&file_path, source).unwrap();
let options = CohesionOptions::default();
let results = analyze_file_cohesion(&file_path, &options).unwrap();
assert!(!results.is_empty(), "Expected at least 1 class in results");
let mixed = results.iter().find(|c| c.name == "Mixed").unwrap();
assert_eq!(mixed.method_count, 4, "Expected 4 methods, got {}", mixed.method_count);
assert_eq!(mixed.lcom4, 2, "Expected LCOM4=2 (two components), got {}", mixed.lcom4);
}
#[test]
fn test_extract_ruby_instance_var_no_panic() {
let source = "@name = 'Alice'\n@@class_var = 1\n@age = 30";
let fields = extract_ruby_instance_var_accesses(source);
assert!(fields.contains("name"), "Expected 'name' in {:?}", fields);
assert!(fields.contains("age"), "Expected 'age' in {:?}", fields);
assert!(!fields.contains("class_var"), "@@class_var should not be matched as instance var, got {:?}", fields);
}
#[test]
fn test_extract_ruby_instance_var_inline() {
let source = "puts @value + @@counter";
let fields = extract_ruby_instance_var_accesses(source);
assert!(fields.contains("value"), "Expected 'value' in {:?}", fields);
assert!(!fields.contains("counter"), "@@counter should not match as instance var, got {:?}", fields);
}
}