cargo_coupling/
connascence.rs

1//! Connascence type detection based on Meilir Page-Jones' taxonomy
2//!
3//! Connascence is a software quality metric that describes the degree to which
4//! changes in one component require changes in another. This module provides
5//! detection for various connascence types through static analysis.
6//!
7//! ## Connascence Types (from weakest to strongest)
8//!
9//! ### Static Connascence (detectable at compile time)
10//!
11//! 1. **Name** - Components must agree on the name of something
12//! 2. **Type** - Components must agree on the type of something
13//! 3. **Meaning** - Components must agree on the meaning of particular values
14//! 4. **Position** - Components must agree on the order of elements
15//! 5. **Algorithm** - Components must agree on a particular algorithm
16//!
17//! ### Dynamic Connascence (only visible at runtime)
18//!
19//! 6. **Execution** - Components must be executed in a particular order
20//! 7. **Timing** - Components must be timed in relation to each other
21//! 8. **Value** - Components must agree on specific values at runtime
22//! 9. **Identity** - Components must reference the same object
23//!
24//! ## References
25//!
26//! - Meilir Page-Jones, "What Every Programmer Should Know About OOD"
27//! - Jim Weirich, "Grand Unified Theory of Software Design" (talk)
28
29use std::collections::HashMap;
30
31/// Types of connascence that can be detected through static analysis
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum ConnascenceType {
34    /// Connascence of Name - Agreement on names
35    ///
36    /// Example: Using a function/type by its name
37    /// Strength: Weakest (easy to refactor with rename)
38    Name,
39
40    /// Connascence of Type - Agreement on types
41    ///
42    /// Example: Function parameter types, struct field types
43    /// Strength: Weak-Medium
44    Type,
45
46    /// Connascence of Meaning - Agreement on semantic values
47    ///
48    /// Example: Magic numbers, special string values
49    /// Strength: Medium
50    Meaning,
51
52    /// Connascence of Position - Agreement on ordering
53    ///
54    /// Example: Function argument order, tuple element order
55    /// Strength: Medium-Strong
56    Position,
57
58    /// Connascence of Algorithm - Agreement on algorithms
59    ///
60    /// Example: Encoding/decoding pairs, hash functions
61    /// Strength: Strong
62    Algorithm,
63}
64
65impl ConnascenceType {
66    /// Get the strength value (0.0 - 1.0, higher = stronger coupling)
67    pub fn strength(&self) -> f64 {
68        match self {
69            ConnascenceType::Name => 0.2,
70            ConnascenceType::Type => 0.4,
71            ConnascenceType::Meaning => 0.6,
72            ConnascenceType::Position => 0.7,
73            ConnascenceType::Algorithm => 0.9,
74        }
75    }
76
77    /// Get human-readable description
78    pub fn description(&self) -> &'static str {
79        match self {
80            ConnascenceType::Name => "Agreement on names (renaming affects both)",
81            ConnascenceType::Type => "Agreement on types (type changes affect both)",
82            ConnascenceType::Meaning => "Agreement on semantic values (magic values)",
83            ConnascenceType::Position => "Agreement on ordering (positional coupling)",
84            ConnascenceType::Algorithm => "Agreement on algorithm (algorithm changes affect both)",
85        }
86    }
87
88    /// Get refactoring suggestion
89    pub fn refactoring_suggestion(&self) -> &'static str {
90        match self {
91            ConnascenceType::Name => "Use IDE rename refactoring to change safely",
92            ConnascenceType::Type => "Consider using traits/generics to reduce type coupling",
93            ConnascenceType::Meaning => "Replace magic values with named constants or enums",
94            ConnascenceType::Position => "Use named parameters or builder pattern",
95            ConnascenceType::Algorithm => {
96                "Extract algorithm into shared module with clear contract"
97            }
98        }
99    }
100}
101
102/// Detected connascence instance
103#[derive(Debug, Clone)]
104pub struct ConnascenceInstance {
105    /// Type of connascence
106    pub connascence_type: ConnascenceType,
107    /// Source location (module/file)
108    pub source: String,
109    /// Target location (module/file/item)
110    pub target: String,
111    /// Additional context (e.g., which name, which type)
112    pub context: String,
113    /// Line number if available
114    pub line: Option<usize>,
115}
116
117impl ConnascenceInstance {
118    pub fn new(
119        connascence_type: ConnascenceType,
120        source: String,
121        target: String,
122        context: String,
123    ) -> Self {
124        Self {
125            connascence_type,
126            source,
127            target,
128            context,
129            line: None,
130        }
131    }
132
133    pub fn with_line(mut self, line: usize) -> Self {
134        self.line = Some(line);
135        self
136    }
137}
138
139/// Statistics about connascence types in a project
140#[derive(Debug, Clone, Default)]
141pub struct ConnascenceStats {
142    /// Count by type
143    pub by_type: HashMap<ConnascenceType, usize>,
144    /// Total instances
145    pub total: usize,
146    /// Weighted strength score
147    pub weighted_strength: f64,
148}
149
150impl ConnascenceStats {
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Add a connascence instance
156    pub fn add(&mut self, connascence_type: ConnascenceType) {
157        *self.by_type.entry(connascence_type).or_insert(0) += 1;
158        self.total += 1;
159        self.weighted_strength += connascence_type.strength();
160    }
161
162    /// Get average strength (0.0 - 1.0)
163    pub fn average_strength(&self) -> f64 {
164        if self.total == 0 {
165            0.0
166        } else {
167            self.weighted_strength / self.total as f64
168        }
169    }
170
171    /// Get count for a specific type
172    pub fn count(&self, connascence_type: ConnascenceType) -> usize {
173        self.by_type.get(&connascence_type).copied().unwrap_or(0)
174    }
175
176    /// Get percentage for a specific type
177    pub fn percentage(&self, connascence_type: ConnascenceType) -> f64 {
178        if self.total == 0 {
179            0.0
180        } else {
181            (self.count(connascence_type) as f64 / self.total as f64) * 100.0
182        }
183    }
184}
185
186/// Analyzer for detecting connascence patterns
187#[derive(Debug, Default, Clone)]
188pub struct ConnascenceAnalyzer {
189    /// Detected instances
190    pub instances: Vec<ConnascenceInstance>,
191    /// Statistics
192    pub stats: ConnascenceStats,
193    /// Current module being analyzed
194    current_module: String,
195    /// Function signatures for position analysis (fn_name -> arg_count)
196    function_signatures: HashMap<String, usize>,
197    /// Magic number patterns detected
198    magic_numbers: Vec<(String, String)>, // (location, value)
199}
200
201impl ConnascenceAnalyzer {
202    pub fn new() -> Self {
203        Self::default()
204    }
205
206    /// Set current module context
207    pub fn set_module(&mut self, module: String) {
208        self.current_module = module;
209    }
210
211    /// Record a name dependency (Connascence of Name)
212    pub fn record_name_dependency(&mut self, target: &str, context: &str) {
213        let instance = ConnascenceInstance::new(
214            ConnascenceType::Name,
215            self.current_module.clone(),
216            target.to_string(),
217            context.to_string(),
218        );
219        self.instances.push(instance);
220        self.stats.add(ConnascenceType::Name);
221    }
222
223    /// Record a type dependency (Connascence of Type)
224    pub fn record_type_dependency(&mut self, type_name: &str, usage_context: &str) {
225        let instance = ConnascenceInstance::new(
226            ConnascenceType::Type,
227            self.current_module.clone(),
228            type_name.to_string(),
229            usage_context.to_string(),
230        );
231        self.instances.push(instance);
232        self.stats.add(ConnascenceType::Type);
233    }
234
235    /// Record a positional dependency (Connascence of Position)
236    ///
237    /// This is detected when a function has many positional arguments
238    pub fn record_position_dependency(&mut self, fn_name: &str, arg_count: usize) {
239        // Only flag as positional coupling if there are 4+ arguments
240        if arg_count >= 4 {
241            let instance = ConnascenceInstance::new(
242                ConnascenceType::Position,
243                self.current_module.clone(),
244                fn_name.to_string(),
245                format!("Function with {} positional arguments", arg_count),
246            );
247            self.instances.push(instance);
248            self.stats.add(ConnascenceType::Position);
249        }
250        self.function_signatures
251            .insert(fn_name.to_string(), arg_count);
252    }
253
254    /// Record a magic number (Connascence of Meaning)
255    pub fn record_magic_number(&mut self, location: &str, value: &str) {
256        // Skip common acceptable values
257        if is_acceptable_literal(value) {
258            return;
259        }
260
261        let instance = ConnascenceInstance::new(
262            ConnascenceType::Meaning,
263            self.current_module.clone(),
264            location.to_string(),
265            format!("Magic value: {}", value),
266        );
267        self.instances.push(instance);
268        self.stats.add(ConnascenceType::Meaning);
269        self.magic_numbers
270            .push((location.to_string(), value.to_string()));
271    }
272
273    /// Record an algorithm dependency (Connascence of Algorithm)
274    ///
275    /// This is detected heuristically for known patterns like:
276    /// - encode/decode pairs
277    /// - serialize/deserialize
278    /// - hash functions used in multiple places
279    pub fn record_algorithm_dependency(&mut self, pattern: &str, context: &str) {
280        let instance = ConnascenceInstance::new(
281            ConnascenceType::Algorithm,
282            self.current_module.clone(),
283            pattern.to_string(),
284            context.to_string(),
285        );
286        self.instances.push(instance);
287        self.stats.add(ConnascenceType::Algorithm);
288    }
289
290    /// Get summary report
291    pub fn summary(&self) -> String {
292        let mut report = String::new();
293        report.push_str("## Connascence Analysis\n\n");
294        report.push_str(&format!("**Total Instances**: {}\n", self.stats.total));
295        report.push_str(&format!(
296            "**Average Strength**: {:.2}\n\n",
297            self.stats.average_strength()
298        ));
299
300        report.push_str("| Type | Count | % | Strength | Description |\n");
301        report.push_str("|------|-------|---|----------|-------------|\n");
302
303        for conn_type in [
304            ConnascenceType::Name,
305            ConnascenceType::Type,
306            ConnascenceType::Meaning,
307            ConnascenceType::Position,
308            ConnascenceType::Algorithm,
309        ] {
310            let count = self.stats.count(conn_type);
311            if count > 0 {
312                report.push_str(&format!(
313                    "| {:?} | {} | {:.1}% | {:.1} | {} |\n",
314                    conn_type,
315                    count,
316                    self.stats.percentage(conn_type),
317                    conn_type.strength(),
318                    conn_type.description()
319                ));
320            }
321        }
322
323        report
324    }
325
326    /// Get high-strength instances that should be reviewed
327    pub fn high_strength_instances(&self) -> Vec<&ConnascenceInstance> {
328        self.instances
329            .iter()
330            .filter(|i| i.connascence_type.strength() >= 0.6)
331            .collect()
332    }
333}
334
335/// Check if a literal value is acceptable (not a magic number)
336fn is_acceptable_literal(value: &str) -> bool {
337    // Common acceptable numeric values
338    let acceptable_numbers = [
339        "0", "1", "2", "-1", "0.0", "1.0", "0.5", "100", "1000", "true", "false",
340    ];
341
342    if acceptable_numbers.contains(&value) {
343        return true;
344    }
345
346    // Check for common patterns
347    if value.starts_with('"') || value.starts_with('\'') {
348        // String literals - check for common acceptable patterns
349        let inner = value.trim_matches(|c| c == '"' || c == '\'');
350        // Empty string, single char, common separators
351        return inner.is_empty()
352            || inner.len() == 1
353            || inner == " "
354            || inner == "\n"
355            || inner == ","
356            || inner == ":"
357            || inner == "/"
358            || inner.starts_with("http")
359            || inner.starts_with("https");
360    }
361
362    false
363}
364
365/// Detect potential algorithm connascence patterns in code
366pub fn detect_algorithm_patterns(content: &str) -> Vec<(&'static str, String)> {
367    let mut patterns = Vec::new();
368
369    // Check for encode/decode pairs
370    if content.contains("encode") && content.contains("decode") {
371        patterns.push(("encode/decode", "Encoding algorithm must match".to_string()));
372    }
373
374    // Check for serialize/deserialize
375    if content.contains("serialize") && content.contains("deserialize") {
376        patterns.push((
377            "serialize/deserialize",
378            "Serialization format must match".to_string(),
379        ));
380    }
381
382    // Check for hash patterns
383    if (content.contains("hash") || content.contains("Hash"))
384        && (content.contains("sha") || content.contains("md5") || content.contains("blake"))
385    {
386        patterns.push((
387            "hash algorithm",
388            "Hash algorithm must be consistent".to_string(),
389        ));
390    }
391
392    // Check for compression patterns
393    if content.contains("compress") && content.contains("decompress") {
394        patterns.push((
395            "compression",
396            "Compression algorithm must match".to_string(),
397        ));
398    }
399
400    // Check for encryption patterns
401    if content.contains("encrypt") && content.contains("decrypt") {
402        patterns.push(("encryption", "Encryption algorithm must match".to_string()));
403    }
404
405    patterns
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn test_connascence_type_strength() {
414        assert!(ConnascenceType::Name.strength() < ConnascenceType::Type.strength());
415        assert!(ConnascenceType::Type.strength() < ConnascenceType::Meaning.strength());
416        assert!(ConnascenceType::Position.strength() < ConnascenceType::Algorithm.strength());
417    }
418
419    #[test]
420    fn test_connascence_stats() {
421        let mut stats = ConnascenceStats::new();
422        stats.add(ConnascenceType::Name);
423        stats.add(ConnascenceType::Name);
424        stats.add(ConnascenceType::Type);
425
426        assert_eq!(stats.total, 3);
427        assert_eq!(stats.count(ConnascenceType::Name), 2);
428        assert_eq!(stats.count(ConnascenceType::Type), 1);
429    }
430
431    #[test]
432    fn test_analyzer_name_dependency() {
433        let mut analyzer = ConnascenceAnalyzer::new();
434        analyzer.set_module("test_module".to_string());
435        analyzer.record_name_dependency("SomeType", "use statement");
436
437        assert_eq!(analyzer.instances.len(), 1);
438        assert_eq!(analyzer.stats.count(ConnascenceType::Name), 1);
439    }
440
441    #[test]
442    fn test_position_dependency_threshold() {
443        let mut analyzer = ConnascenceAnalyzer::new();
444        analyzer.set_module("test_module".to_string());
445
446        // 3 args should not be flagged
447        analyzer.record_position_dependency("small_fn", 3);
448        assert_eq!(analyzer.stats.count(ConnascenceType::Position), 0);
449
450        // 4+ args should be flagged
451        analyzer.record_position_dependency("large_fn", 5);
452        assert_eq!(analyzer.stats.count(ConnascenceType::Position), 1);
453    }
454
455    #[test]
456    fn test_magic_number_detection() {
457        let mut analyzer = ConnascenceAnalyzer::new();
458        analyzer.set_module("test_module".to_string());
459
460        // Acceptable values should not be flagged
461        analyzer.record_magic_number("test", "0");
462        analyzer.record_magic_number("test", "1");
463        analyzer.record_magic_number("test", "true");
464        assert_eq!(analyzer.stats.count(ConnascenceType::Meaning), 0);
465
466        // Magic numbers should be flagged
467        analyzer.record_magic_number("test", "42");
468        analyzer.record_magic_number("test", "3.14159");
469        assert_eq!(analyzer.stats.count(ConnascenceType::Meaning), 2);
470    }
471
472    #[test]
473    fn test_algorithm_pattern_detection() {
474        let code_with_encoding = "fn encode() {} fn decode() {}";
475        let patterns = detect_algorithm_patterns(code_with_encoding);
476        assert!(!patterns.is_empty());
477
478        let code_without_patterns = "fn process() { let x = 1; }";
479        let patterns = detect_algorithm_patterns(code_without_patterns);
480        assert!(patterns.is_empty());
481    }
482
483    #[test]
484    fn test_acceptable_literals() {
485        assert!(is_acceptable_literal("0"));
486        assert!(is_acceptable_literal("1"));
487        assert!(is_acceptable_literal("true"));
488        assert!(is_acceptable_literal("\"\""));
489        assert!(!is_acceptable_literal("42"));
490        assert!(!is_acceptable_literal("3.14159"));
491    }
492}