structural.rs•40.7 kB
//! Structural pattern detection and analysis
#[cfg(feature = "napi-bindings")]
use napi_derive::napi;
use crate::patterns::types::{Pattern, PatternExample, StructuralPattern, PatternExtractor};
use crate::types::{ParseError, SemanticConcept, LineRange};
use std::collections::{HashMap, HashSet};
use walkdir::WalkDir;
use std::fs;
use std::path::Path;
/// Analyzer for detecting architectural and structural patterns
#[cfg_attr(feature = "napi-bindings", napi)]
pub struct StructuralPatternAnalyzer {
patterns: HashMap<String, StructuralPattern>,
architecture_signatures: HashMap<String, ArchitectureSignature>,
}
#[derive(Debug, Clone)]
struct ArchitectureSignature {
pattern_name: String,
required_components: Vec<String>,
directory_structure: Vec<String>,
file_patterns: Vec<String>,
confidence_threshold: f64,
}
#[derive(Debug, Clone)]
struct DirectoryAnalysis {
path: String,
subdirectories: Vec<String>,
file_types: HashMap<String, usize>,
depth: usize,
}
#[cfg_attr(feature = "napi-bindings", napi)]
impl StructuralPatternAnalyzer {
#[cfg_attr(feature = "napi-bindings", napi(constructor))]
pub fn new() -> Self {
let mut analyzer = StructuralPatternAnalyzer {
patterns: HashMap::new(),
architecture_signatures: HashMap::new(),
};
analyzer.initialize_signatures();
analyzer
}
/// Initialize common architectural pattern signatures
fn initialize_signatures(&mut self) {
// MVC Pattern
self.architecture_signatures.insert("MVC".to_string(), ArchitectureSignature {
pattern_name: "Model-View-Controller".to_string(),
required_components: vec!["model".to_string(), "view".to_string(), "controller".to_string()],
directory_structure: vec!["models/".to_string(), "views/".to_string(), "controllers/".to_string()],
file_patterns: vec!["*Controller.*".to_string(), "*Model.*".to_string(), "*View.*".to_string()],
confidence_threshold: 0.7,
});
// Clean Architecture
self.architecture_signatures.insert("Clean".to_string(), ArchitectureSignature {
pattern_name: "Clean Architecture".to_string(),
required_components: vec!["domain".to_string(), "application".to_string(), "infrastructure".to_string(), "presentation".to_string()],
directory_structure: vec!["domain/".to_string(), "application/".to_string(), "infrastructure/".to_string(), "presentation/".to_string()],
file_patterns: vec!["*Service.*".to_string(), "*Repository.*".to_string(), "*UseCase.*".to_string()],
confidence_threshold: 0.8,
});
// Layered Architecture
self.architecture_signatures.insert("Layered".to_string(), ArchitectureSignature {
pattern_name: "Layered Architecture".to_string(),
required_components: vec!["api".to_string(), "service".to_string(), "data".to_string()],
directory_structure: vec!["api/".to_string(), "service/".to_string(), "data/".to_string()],
file_patterns: vec!["*Api.*".to_string(), "*Service.*".to_string(), "*Repository.*".to_string()],
confidence_threshold: 0.6,
});
// Microservices
self.architecture_signatures.insert("Microservices".to_string(), ArchitectureSignature {
pattern_name: "Microservices Architecture".to_string(),
required_components: vec!["service".to_string(), "gateway".to_string()],
directory_structure: vec!["services/".to_string(), "gateway/".to_string()],
file_patterns: vec!["*Service.*".to_string(), "docker*".to_string(), "*Gateway.*".to_string()],
confidence_threshold: 0.7,
});
// Modular Monolith
self.architecture_signatures.insert("Modular".to_string(), ArchitectureSignature {
pattern_name: "Modular Architecture".to_string(),
required_components: vec!["modules".to_string(), "shared".to_string()],
directory_structure: vec!["modules/".to_string(), "shared/".to_string()],
file_patterns: vec!["mod.*".to_string(), "index.*".to_string()],
confidence_threshold: 0.5,
});
// Event-Driven Architecture
self.architecture_signatures.insert("EventDriven".to_string(), ArchitectureSignature {
pattern_name: "Event-Driven Architecture".to_string(),
required_components: vec!["events".to_string(), "handlers".to_string(), "publishers".to_string()],
directory_structure: vec!["events/".to_string(), "handlers/".to_string()],
file_patterns: vec!["*Event.*".to_string(), "*Handler.*".to_string(), "*Publisher.*".to_string()],
confidence_threshold: 0.7,
});
}
/// Analyze structural patterns from codebase organization
pub fn analyze_codebase_structure(&mut self, path: &str) -> Result<Vec<Pattern>, ParseError> {
let directory_analysis = self.analyze_directory_structure(path)?;
let file_analysis = self.analyze_file_patterns(path)?;
let mut detected_patterns = Vec::new();
// Check each architectural signature
for (pattern_key, signature) in &self.architecture_signatures {
let confidence = self.calculate_structure_confidence(
&directory_analysis,
&file_analysis,
signature,
);
if confidence >= signature.confidence_threshold {
let examples = self.collect_structure_examples(path, signature)?;
let example_count = examples.len() as u32;
let pattern = Pattern {
id: format!("structural_{}", pattern_key),
pattern_type: "structural".to_string(),
description: format!(
"{} detected with {:.1}% confidence",
signature.pattern_name,
confidence * 100.0
),
frequency: example_count,
confidence,
examples,
contexts: vec!["architecture".to_string()],
};
detected_patterns.push(pattern);
// Store in internal patterns
let structural_pattern = StructuralPattern {
pattern_type: signature.pattern_name.clone(),
frequency: example_count,
characteristics: signature.required_components.clone(),
confidence,
};
self.patterns.insert(pattern_key.clone(), structural_pattern);
}
}
Ok(detected_patterns)
}
/// Analyze concepts for structural relationships
pub fn analyze_concept_structures(&mut self, concepts: &[SemanticConcept]) -> Result<Vec<Pattern>, ParseError> {
let mut detected_patterns = Vec::new();
// Analyze file organization patterns
let file_organization = self.analyze_file_organization(concepts);
// Analyze dependency patterns
let dependency_patterns = self.analyze_dependency_patterns(concepts);
// Analyze naming structure patterns
let naming_structure = self.analyze_naming_structure_patterns(concepts);
detected_patterns.extend(file_organization);
detected_patterns.extend(dependency_patterns);
detected_patterns.extend(naming_structure);
Ok(detected_patterns)
}
/// Detect violations of structural patterns
pub fn detect_structural_violations(&self, concepts: &[SemanticConcept]) -> Vec<String> {
let mut violations = Vec::new();
// Check for common structural anti-patterns
violations.extend(self.detect_god_object_violations(concepts));
violations.extend(self.detect_circular_dependency_violations(concepts));
violations.extend(self.detect_layer_violations(concepts));
violations.extend(self.detect_coupling_violations(concepts));
violations
}
/// Generate structural recommendations
pub fn generate_structural_recommendations(&self, concepts: &[SemanticConcept]) -> Vec<String> {
let mut recommendations = Vec::new();
// File organization recommendations
let file_metrics = self.calculate_file_metrics(concepts);
if file_metrics.avg_concepts_per_file > 20.0 {
recommendations.push("Consider breaking down large files into smaller, more focused modules".to_string());
}
if file_metrics.max_concepts_per_file > 50 {
recommendations.push(format!(
"One file contains {} concepts - consider splitting this monolithic file",
file_metrics.max_concepts_per_file
));
}
if file_metrics.total_files < 5 && concepts.len() > 100 {
recommendations.push(format!(
"Only {} files for {} concepts - consider better separation of concerns",
file_metrics.total_files, concepts.len()
));
}
// Coupling recommendations
let coupling_metrics = self.calculate_coupling_metrics(concepts);
if coupling_metrics.high_coupling_count > 5 {
recommendations.push("Reduce tight coupling between components using dependency injection or interfaces".to_string());
}
// Layer separation recommendations
if self.has_layer_violations(concepts) {
recommendations.push("Establish clear layer boundaries and enforce dependency directions".to_string());
}
// Modularity recommendations
let modularity_score = self.calculate_modularity_score(concepts);
if modularity_score < 0.6 {
recommendations.push("Consider refactoring into more modular components with clear responsibilities".to_string());
}
if recommendations.is_empty() {
recommendations.push("Structural patterns look good! Consider documenting architectural decisions".to_string());
}
recommendations
}
/// Analyze directory structure recursively
fn analyze_directory_structure(&self, path: &str) -> Result<Vec<DirectoryAnalysis>, ParseError> {
let mut analyses = Vec::new();
for entry in WalkDir::new(path).max_depth(5).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_dir() {
let dir_path = entry.path();
let mut analysis = DirectoryAnalysis {
path: dir_path.to_string_lossy().to_string(),
subdirectories: Vec::new(),
file_types: HashMap::new(),
depth: entry.depth(),
};
// Analyze immediate children
if let Ok(entries) = fs::read_dir(dir_path) {
for child_entry in entries.filter_map(|e| e.ok()) {
if child_entry.file_type().ok().is_some_and(|ft| ft.is_dir()) {
if let Some(name) = child_entry.file_name().to_str() {
analysis.subdirectories.push(name.to_string());
}
} else if let Some(extension) = child_entry.path().extension().and_then(|s| s.to_str()) {
*analysis.file_types.entry(extension.to_string()).or_insert(0) += 1;
}
}
}
analyses.push(analysis);
}
}
Ok(analyses)
}
/// Analyze file patterns in the codebase
fn analyze_file_patterns(&self, path: &str) -> Result<HashMap<String, Vec<String>>, ParseError> {
let mut file_patterns: HashMap<String, Vec<String>> = HashMap::new();
for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let file_path = entry.path();
if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
// Categorize files by patterns
if file_name.contains("Controller") {
file_patterns.entry("controller".to_string()).or_default().push(file_name.to_string());
}
if file_name.contains("Model") {
file_patterns.entry("model".to_string()).or_default().push(file_name.to_string());
}
if file_name.contains("View") {
file_patterns.entry("view".to_string()).or_default().push(file_name.to_string());
}
if file_name.contains("Service") {
file_patterns.entry("service".to_string()).or_default().push(file_name.to_string());
}
if file_name.contains("Repository") {
file_patterns.entry("repository".to_string()).or_default().push(file_name.to_string());
}
if file_name.contains("Handler") {
file_patterns.entry("handler".to_string()).or_default().push(file_name.to_string());
}
}
}
}
Ok(file_patterns)
}
/// Calculate confidence score for a structural pattern
fn calculate_structure_confidence(
&self,
directory_analysis: &[DirectoryAnalysis],
file_patterns: &HashMap<String, Vec<String>>,
signature: &ArchitectureSignature,
) -> f64 {
let mut score = 0.0;
let mut max_score = 0.0;
// Check directory structure matches
max_score += 0.4;
let mut dir_matches = 0;
let mut depth_penalty = 0.0;
for required_dir in &signature.directory_structure {
for analysis in directory_analysis {
if analysis.path.contains(required_dir) ||
analysis.subdirectories.iter().any(|d| d.contains(&required_dir.replace("/", ""))) {
dir_matches += 1;
// Apply depth-based scoring - deeper structures get slight penalty for complexity
if analysis.depth > 4 {
depth_penalty += 0.1;
}
break;
}
}
}
if !signature.directory_structure.is_empty() {
let base_score = dir_matches as f64 / signature.directory_structure.len() as f64;
let depth_adjusted_score = (base_score - (depth_penalty / signature.directory_structure.len() as f64)).max(0.0);
score += 0.4 * depth_adjusted_score;
}
// Check component existence
max_score += 0.3;
let mut component_matches = 0;
for component in &signature.required_components {
if file_patterns.contains_key(component) {
component_matches += 1;
}
}
if !signature.required_components.is_empty() {
score += 0.3 * (component_matches as f64 / signature.required_components.len() as f64);
}
// Check file patterns
max_score += 0.3;
let mut pattern_matches = 0;
for pattern in &signature.file_patterns {
let pattern_key = pattern.replace("*", "").replace(".", "").to_lowercase();
if file_patterns.keys().any(|k| k.contains(&pattern_key)) {
pattern_matches += 1;
}
}
if !signature.file_patterns.is_empty() {
score += 0.3 * (pattern_matches as f64 / signature.file_patterns.len() as f64);
}
if max_score > 0.0 {
score / max_score
} else {
0.0
}
}
/// Collect examples of structural patterns
fn collect_structure_examples(&self, path: &str, signature: &ArchitectureSignature) -> Result<Vec<PatternExample>, ParseError> {
let mut examples = Vec::new();
for entry in WalkDir::new(path).max_depth(3).into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_dir() {
let dir_name = entry.file_name().to_string_lossy().to_string();
// Check if directory name matches required components
for component in &signature.required_components {
if dir_name.to_lowercase().contains(&component.to_lowercase()) {
examples.push(PatternExample {
code: format!("Directory: {}", dir_name),
file_path: entry.path().to_string_lossy().to_string(),
line_range: LineRange { start: 1, end: 1 },
});
break;
}
}
}
}
// Limit examples to avoid overwhelming output
examples.truncate(10);
Ok(examples)
}
/// Analyze file organization patterns from concepts
fn analyze_file_organization(&self, concepts: &[SemanticConcept]) -> Vec<Pattern> {
let mut patterns = Vec::new();
let mut file_concept_map: HashMap<String, Vec<&SemanticConcept>> = HashMap::new();
// Group concepts by file
for concept in concepts {
file_concept_map.entry(concept.file_path.clone()).or_default().push(concept);
}
// Check for single responsibility principle violations
let large_files: Vec<_> = file_concept_map.iter()
.filter(|(_, concepts)| concepts.len() > 10)
.collect();
if !large_files.is_empty() {
patterns.push(Pattern {
id: "structural_large_files".to_string(),
pattern_type: "structural".to_string(),
description: format!("Files with too many concepts detected ({} files)", large_files.len()),
frequency: large_files.len() as u32,
confidence: 0.8,
examples: large_files.into_iter().take(5).map(|(file_path, concepts)| {
PatternExample {
code: format!("File contains {} concepts", concepts.len()),
file_path: file_path.clone(),
line_range: LineRange { start: 1, end: 1 },
}
}).collect(),
contexts: vec!["organization".to_string()],
});
}
patterns
}
/// Analyze dependency patterns from concepts
fn analyze_dependency_patterns(&self, concepts: &[SemanticConcept]) -> Vec<Pattern> {
let mut patterns = Vec::new();
let mut dependencies: HashMap<String, HashSet<String>> = HashMap::new();
// Build dependency graph from relationships
for concept in concepts {
let concept_file = Path::new(&concept.file_path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(&concept.file_path);
for (relationship_type, target) in &concept.relationships {
if relationship_type.contains("import") || relationship_type.contains("depends") {
dependencies.entry(concept_file.to_string()).or_default().insert(target.clone());
}
}
}
// Detect circular dependencies
let cycles = self.detect_cycles(&dependencies);
if !cycles.is_empty() {
patterns.push(Pattern {
id: "structural_circular_dependencies".to_string(),
pattern_type: "structural".to_string(),
description: format!("Circular dependencies detected ({} cycles)", cycles.len()),
frequency: cycles.len() as u32,
confidence: 0.9,
examples: cycles.into_iter().take(3).map(|cycle| {
PatternExample {
code: format!("Circular dependency: {}", cycle.join(" -> ")),
file_path: "multiple_files".to_string(),
line_range: LineRange { start: 1, end: 1 },
}
}).collect(),
contexts: vec!["dependency".to_string()],
});
}
patterns
}
/// Analyze naming structure patterns
fn analyze_naming_structure_patterns(&self, concepts: &[SemanticConcept]) -> Vec<Pattern> {
let mut patterns = Vec::new();
let mut namespace_patterns: HashMap<String, u32> = HashMap::new();
// Analyze namespace/module patterns
for concept in concepts {
if let Some(namespace) = self.extract_namespace_from_path(&concept.file_path) {
*namespace_patterns.entry(namespace).or_insert(0) += 1;
}
}
if !namespace_patterns.is_empty() {
let most_common = namespace_patterns.iter()
.max_by_key(|(_, count)| *count)
.map(|(ns, count)| (ns.clone(), *count));
if let Some((namespace, count)) = most_common {
patterns.push(Pattern {
id: "structural_namespace_organization".to_string(),
pattern_type: "structural".to_string(),
description: format!("Consistent namespace organization detected ({})", namespace),
frequency: count,
confidence: 0.7,
examples: vec![PatternExample {
code: format!("Namespace pattern: {}", namespace),
file_path: "multiple_files".to_string(),
line_range: LineRange { start: 1, end: 1 },
}],
contexts: vec!["organization".to_string()],
});
}
}
patterns
}
/// Detect God Object violations
fn detect_god_object_violations(&self, concepts: &[SemanticConcept]) -> Vec<String> {
let mut violations = Vec::new();
let mut class_method_counts: HashMap<String, u32> = HashMap::new();
for concept in concepts {
if concept.concept_type == "class" || concept.concept_type == "struct" {
// Count methods in this class
let method_count = concepts.iter()
.filter(|c| c.concept_type == "method" || c.concept_type == "function")
.filter(|c| c.file_path == concept.file_path)
.filter(|c| c.line_range.start >= concept.line_range.start && c.line_range.end <= concept.line_range.end)
.count() as u32;
class_method_counts.insert(concept.name.clone(), method_count);
if method_count > 20 {
violations.push(format!(
"Potential God Object: '{}' has {} methods ({}:{})",
concept.name,
method_count,
concept.file_path,
concept.line_range.start
));
}
}
}
violations
}
/// Detect circular dependency violations
fn detect_circular_dependency_violations(&self, concepts: &[SemanticConcept]) -> Vec<String> {
let mut violations = Vec::new();
let mut dependencies: HashMap<String, HashSet<String>> = HashMap::new();
// Build dependency graph
for concept in concepts {
for (rel_type, target) in &concept.relationships {
if rel_type.contains("depends") || rel_type.contains("import") {
dependencies.entry(concept.name.clone()).or_default().insert(target.clone());
}
}
}
// Simple cycle detection
let cycles = self.detect_cycles(&dependencies);
for cycle in cycles {
violations.push(format!(
"Circular dependency detected: {}",
cycle.join(" -> ")
));
}
violations
}
/// Detect layer violations
fn detect_layer_violations(&self, concepts: &[SemanticConcept]) -> Vec<String> {
let mut violations = Vec::new();
// Define layer hierarchy (lower numbers = higher layers)
let layer_hierarchy = [
("presentation", 0),
("api", 0),
("application", 1),
("domain", 2),
("infrastructure", 3),
("data", 3),
].iter().cloned().collect::<HashMap<&str, u32>>();
for concept in concepts {
let concept_layer = self.determine_layer(&concept.file_path, &layer_hierarchy);
for (rel_type, target) in &concept.relationships {
if rel_type.contains("depends") || rel_type.contains("import") {
// Find target concept to determine its layer
if let Some(target_concept) = concepts.iter().find(|c| c.name == *target) {
let target_layer = self.determine_layer(&target_concept.file_path, &layer_hierarchy);
// Check for violations (higher layer depending on lower layer)
if let (Some(concept_level), Some(target_level)) = (concept_layer, target_layer) {
if concept_level < target_level {
violations.push(format!(
"Layer violation: {} depends on {} (higher layer depending on lower layer)",
concept.name,
target
));
}
}
}
}
}
}
violations
}
/// Detect coupling violations
fn detect_coupling_violations(&self, concepts: &[SemanticConcept]) -> Vec<String> {
let mut violations = Vec::new();
let mut coupling_counts: HashMap<String, u32> = HashMap::new();
for concept in concepts {
let coupling_count = concept.relationships.len() as u32;
coupling_counts.insert(concept.name.clone(), coupling_count);
if coupling_count > 10 {
violations.push(format!(
"High coupling detected: '{}' has {} dependencies ({}:{})",
concept.name,
coupling_count,
concept.file_path,
concept.line_range.start
));
}
}
violations
}
/// Helper methods
fn extract_namespace_from_path(&self, path: &str) -> Option<String> {
let path_obj = Path::new(path);
path_obj.parent()?.file_name()?.to_str().map(String::from)
}
fn detect_cycles(&self, dependencies: &HashMap<String, HashSet<String>>) -> Vec<Vec<String>> {
// Simple DFS-based cycle detection
let mut cycles = Vec::new();
let mut visited = HashSet::new();
let mut path = Vec::new();
for node in dependencies.keys() {
if !visited.contains(node) {
Self::dfs_cycle_detection(node, dependencies, &mut visited, &mut path, &mut cycles);
}
}
cycles
}
fn dfs_cycle_detection(
node: &str,
dependencies: &HashMap<String, HashSet<String>>,
visited: &mut HashSet<String>,
path: &mut Vec<String>,
cycles: &mut Vec<Vec<String>>,
) {
if path.contains(&node.to_string()) {
// Found a cycle
if let Some(cycle_start) = path.iter().position(|n| n == node) {
let cycle = path[cycle_start..].to_vec();
cycles.push(cycle);
}
return;
}
if visited.contains(node) {
return;
}
visited.insert(node.to_string());
path.push(node.to_string());
if let Some(deps) = dependencies.get(node) {
for dep in deps {
Self::dfs_cycle_detection(dep, dependencies, visited, path, cycles);
}
}
path.pop();
}
fn determine_layer(&self, file_path: &str, layer_hierarchy: &HashMap<&str, u32>) -> Option<u32> {
for (layer_name, level) in layer_hierarchy {
if file_path.to_lowercase().contains(layer_name) {
return Some(*level);
}
}
None
}
fn calculate_file_metrics(&self, concepts: &[SemanticConcept]) -> FileMetrics {
let mut file_concept_counts: HashMap<String, u32> = HashMap::new();
for concept in concepts {
*file_concept_counts.entry(concept.file_path.clone()).or_insert(0) += 1;
}
let total_concepts = concepts.len() as f64;
let file_count = file_concept_counts.len() as f64;
let avg_concepts_per_file = if file_count > 0.0 { total_concepts / file_count } else { 0.0 };
FileMetrics {
avg_concepts_per_file,
max_concepts_per_file: file_concept_counts.values().max().copied().unwrap_or(0),
total_files: file_count as u32,
}
}
fn calculate_coupling_metrics(&self, concepts: &[SemanticConcept]) -> CouplingMetrics {
let mut high_coupling_count = 0;
let mut total_coupling = 0;
for concept in concepts {
let coupling = concept.relationships.len();
total_coupling += coupling;
if coupling > 8 {
high_coupling_count += 1;
}
}
CouplingMetrics {
high_coupling_count,
avg_coupling: if !concepts.is_empty() { total_coupling as f64 / concepts.len() as f64 } else { 0.0 },
}
}
fn has_layer_violations(&self, concepts: &[SemanticConcept]) -> bool {
!self.detect_layer_violations(concepts).is_empty()
}
fn calculate_modularity_score(&self, concepts: &[SemanticConcept]) -> f64 {
// Enhanced modularity score based on file organization, coupling, and distribution
let file_metrics = self.calculate_file_metrics(concepts);
let coupling_metrics = self.calculate_coupling_metrics(concepts);
// Base file organization score
let file_score: f64 = if file_metrics.avg_concepts_per_file <= 15.0 { 0.4 } else { 0.1 };
// Penalize files that are too large (monolithic)
let max_file_penalty: f64 = if file_metrics.max_concepts_per_file > 50 { 0.2 } else { 0.0 };
// Reward reasonable file distribution
let distribution_bonus: f64 = if file_metrics.total_files >= 3 &&
(concepts.len() as f64 / file_metrics.total_files as f64) < 25.0 {
0.2
} else {
0.0
};
let coupling_score: f64 = if coupling_metrics.avg_coupling <= 5.0 { 0.4 } else { 0.1 };
(file_score + distribution_bonus + coupling_score - max_file_penalty).clamp(0.0_f64, 1.0_f64)
}
}
#[derive(Debug)]
struct FileMetrics {
avg_concepts_per_file: f64,
max_concepts_per_file: u32,
total_files: u32,
}
#[derive(Debug)]
struct CouplingMetrics {
high_coupling_count: u32,
avg_coupling: f64,
}
impl PatternExtractor for StructuralPatternAnalyzer {
fn extract_patterns(&self, path: &str) -> Result<Vec<Pattern>, ParseError> {
let mut analyzer = self.clone();
analyzer.analyze_codebase_structure(path)
}
}
impl Clone for StructuralPatternAnalyzer {
fn clone(&self) -> Self {
StructuralPatternAnalyzer {
patterns: self.patterns.clone(),
architecture_signatures: self.architecture_signatures.clone(),
}
}
}
impl Default for StructuralPatternAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use tempfile::TempDir;
use std::fs;
fn create_test_concept(name: &str, concept_type: &str, file_path: &str, start: u32, end: u32) -> SemanticConcept {
SemanticConcept {
id: format!("test_{}", name),
name: name.to_string(),
concept_type: concept_type.to_string(),
confidence: 0.8,
file_path: file_path.to_string(),
line_range: LineRange { start, end },
relationships: HashMap::new(),
metadata: HashMap::new(),
}
}
#[test]
fn test_structural_pattern_analyzer_creation() {
let analyzer = StructuralPatternAnalyzer::new();
assert!(!analyzer.architecture_signatures.is_empty());
}
#[test]
fn test_mvc_pattern_detection() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
// Create MVC directory structure
fs::create_dir_all(base_path.join("models")).unwrap();
fs::create_dir_all(base_path.join("views")).unwrap();
fs::create_dir_all(base_path.join("controllers")).unwrap();
// Create some files
fs::write(base_path.join("models/UserModel.js"), "// User model").unwrap();
fs::write(base_path.join("views/UserView.js"), "// User view").unwrap();
fs::write(base_path.join("controllers/UserController.js"), "// User controller").unwrap();
let mut analyzer = StructuralPatternAnalyzer::new();
let patterns = analyzer.analyze_codebase_structure(base_path.to_str().unwrap()).unwrap();
let mvc_pattern = patterns.iter().find(|p| p.id.contains("MVC"));
assert!(mvc_pattern.is_some());
if let Some(pattern) = mvc_pattern {
assert!(pattern.confidence >= 0.7);
assert_eq!(pattern.pattern_type, "structural");
}
}
#[test]
fn test_god_object_detection() {
let mut concepts = Vec::new();
// Create a class with many methods (God Object)
let god_class = create_test_concept("GodClass", "class", "GodClass.js", 1, 100);
concepts.push(god_class);
// Add many methods to the same file
for i in 0..25 {
let method = create_test_concept(
&format!("method{}", i),
"method",
"GodClass.js",
i * 3 + 2,
i * 3 + 4,
);
concepts.push(method);
}
let analyzer = StructuralPatternAnalyzer::new();
let violations = analyzer.detect_god_object_violations(&concepts);
assert!(!violations.is_empty());
assert!(violations[0].contains("God Object"));
}
#[test]
fn test_circular_dependency_detection() {
let mut concepts = Vec::new();
// Create concepts with circular dependencies
let mut concept_a = create_test_concept("ModuleA", "class", "ModuleA.js", 1, 10);
concept_a.relationships.insert("depends_on".to_string(), "ModuleB".to_string());
concepts.push(concept_a);
let mut concept_b = create_test_concept("ModuleB", "class", "ModuleB.js", 1, 10);
concept_b.relationships.insert("depends_on".to_string(), "ModuleA".to_string());
concepts.push(concept_b);
let analyzer = StructuralPatternAnalyzer::new();
let violations = analyzer.detect_circular_dependency_violations(&concepts);
assert!(!violations.is_empty());
assert!(violations[0].contains("Circular dependency"));
}
#[test]
fn test_high_coupling_detection() {
let mut concept = create_test_concept("HighlyCoupled", "class", "test.js", 1, 10);
// Add many dependencies
for i in 0..15 {
concept.relationships.insert(format!("depends_{}", i), format!("Dependency{}", i));
}
let concepts = vec![concept];
let analyzer = StructuralPatternAnalyzer::new();
let violations = analyzer.detect_coupling_violations(&concepts);
assert!(!violations.is_empty());
assert!(violations[0].contains("High coupling"));
}
#[test]
fn test_file_organization_analysis() {
let concepts = vec![
// Many concepts in one file (violation)
create_test_concept("Concept1", "class", "large_file.js", 1, 10),
create_test_concept("Concept2", "class", "large_file.js", 11, 20),
create_test_concept("Concept3", "function", "large_file.js", 21, 25),
];
// Add more concepts to trigger the pattern
let mut all_concepts = concepts;
for i in 4..15 {
all_concepts.push(create_test_concept(
&format!("Concept{}", i),
"function",
"large_file.js",
i * 5,
i * 5 + 3,
));
}
let analyzer = StructuralPatternAnalyzer::new();
let patterns = analyzer.analyze_file_organization(&all_concepts);
let large_file_pattern = patterns.iter().find(|p| p.id.contains("large_files"));
assert!(large_file_pattern.is_some());
}
#[test]
fn test_namespace_pattern_analysis() {
let concepts = vec![
create_test_concept("Service1", "class", "services/UserService.js", 1, 10),
create_test_concept("Service2", "class", "services/OrderService.js", 1, 10),
create_test_concept("Service3", "class", "services/PaymentService.js", 1, 10),
];
let analyzer = StructuralPatternAnalyzer::new();
let patterns = analyzer.analyze_naming_structure_patterns(&concepts);
let namespace_pattern = patterns.iter().find(|p| p.id.contains("namespace"));
assert!(namespace_pattern.is_some());
if let Some(pattern) = namespace_pattern {
assert!(pattern.description.contains("services"));
}
}
#[test]
fn test_modularity_score_calculation() {
let good_concepts = vec![
create_test_concept("Service1", "class", "service1.js", 1, 10),
create_test_concept("Service2", "class", "service2.js", 1, 10),
];
let bad_concepts = vec![
create_test_concept("GodClass", "class", "god.js", 1, 10),
];
// Add many concepts to the god class file
let mut all_bad_concepts = bad_concepts;
for i in 1..25 {
all_bad_concepts.push(create_test_concept(
&format!("Method{}", i),
"method",
"god.js",
i * 2,
i * 2 + 1,
));
}
let analyzer = StructuralPatternAnalyzer::new();
let good_score = analyzer.calculate_modularity_score(&good_concepts);
let bad_score = analyzer.calculate_modularity_score(&all_bad_concepts);
assert!(good_score > bad_score);
assert!(good_score >= 0.8);
assert!(bad_score <= 0.5);
}
#[test]
fn test_layer_violation_detection() {
let mut presentation_concept = create_test_concept("UI", "class", "presentation/UI.js", 1, 10);
presentation_concept.relationships.insert("depends_on".to_string(), "DatabaseLayer".to_string());
let data_concept = create_test_concept("DatabaseLayer", "class", "data/Database.js", 1, 10);
let concepts = vec![presentation_concept, data_concept];
let analyzer = StructuralPatternAnalyzer::new();
let violations = analyzer.detect_layer_violations(&concepts);
assert!(!violations.is_empty());
assert!(violations[0].contains("Layer violation"));
}
}