1use std::collections::HashMap;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum ConnascenceType {
34 Name,
39
40 Type,
45
46 Meaning,
51
52 Position,
57
58 Algorithm,
63}
64
65impl ConnascenceType {
66 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 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 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#[derive(Debug, Clone)]
104pub struct ConnascenceInstance {
105 pub connascence_type: ConnascenceType,
107 pub source: String,
109 pub target: String,
111 pub context: String,
113 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#[derive(Debug, Clone, Default)]
141pub struct ConnascenceStats {
142 pub by_type: HashMap<ConnascenceType, usize>,
144 pub total: usize,
146 pub weighted_strength: f64,
148}
149
150impl ConnascenceStats {
151 pub fn new() -> Self {
152 Self::default()
153 }
154
155 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 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 pub fn count(&self, connascence_type: ConnascenceType) -> usize {
173 self.by_type.get(&connascence_type).copied().unwrap_or(0)
174 }
175
176 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#[derive(Debug, Default, Clone)]
188pub struct ConnascenceAnalyzer {
189 pub instances: Vec<ConnascenceInstance>,
191 pub stats: ConnascenceStats,
193 current_module: String,
195 function_signatures: HashMap<String, usize>,
197 magic_numbers: Vec<(String, String)>, }
200
201impl ConnascenceAnalyzer {
202 pub fn new() -> Self {
203 Self::default()
204 }
205
206 pub fn set_module(&mut self, module: String) {
208 self.current_module = module;
209 }
210
211 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 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 pub fn record_position_dependency(&mut self, fn_name: &str, arg_count: usize) {
239 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 pub fn record_magic_number(&mut self, location: &str, value: &str) {
256 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 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 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 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
335fn is_acceptable_literal(value: &str) -> bool {
337 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 if value.starts_with('"') || value.starts_with('\'') {
348 let inner = value.trim_matches(|c| c == '"' || c == '\'');
350 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
365pub fn detect_algorithm_patterns(content: &str) -> Vec<(&'static str, String)> {
367 let mut patterns = Vec::new();
368
369 if content.contains("encode") && content.contains("decode") {
371 patterns.push(("encode/decode", "Encoding algorithm must match".to_string()));
372 }
373
374 if content.contains("serialize") && content.contains("deserialize") {
376 patterns.push((
377 "serialize/deserialize",
378 "Serialization format must match".to_string(),
379 ));
380 }
381
382 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 if content.contains("compress") && content.contains("decompress") {
394 patterns.push((
395 "compression",
396 "Compression algorithm must match".to_string(),
397 ));
398 }
399
400 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 analyzer.record_position_dependency("small_fn", 3);
448 assert_eq!(analyzer.stats.count(ConnascenceType::Position), 0);
449
450 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 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 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}