Skip to main content

cargo_coupling/metrics/
module.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use crate::analyzer::ItemDependency;
5use crate::volatility::Volatility;
6
7use super::dimensions::{Distance, IntegrationStrength, Subdomain, Visibility};
8
9#[derive(Debug, Clone)]
10pub struct TypeDefinition {
11    /// Name of the type
12    pub name: String,
13    /// Visibility of the type
14    pub visibility: Visibility,
15    /// Whether this is a trait (vs struct/enum)
16    pub is_trait: bool,
17    /// Whether this is a newtype pattern (tuple struct with single field)
18    pub is_newtype: bool,
19    /// Inner type for newtypes (e.g., "u64" for `struct UserId(u64)`)
20    pub inner_type: Option<String>,
21    /// Whether this type has #[derive(Serialize)] or #[derive(Deserialize)]
22    pub has_serde_derive: bool,
23    /// Number of public fields (for pub field exposure detection)
24    pub public_field_count: usize,
25    /// Total number of fields
26    pub total_field_count: usize,
27}
28
29/// Information about a function definition in a module
30#[derive(Debug, Clone)]
31pub struct FunctionDefinition {
32    /// Name of the function
33    pub name: String,
34    /// Visibility of the function
35    pub visibility: Visibility,
36    /// Number of parameters
37    pub param_count: usize,
38    /// Number of primitive type parameters (String, u32, bool, etc.)
39    pub primitive_param_count: usize,
40    /// Parameter types (for primitive obsession detection)
41    pub param_types: Vec<String>,
42}
43
44/// Khononov's balance classification for couplings
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum BalanceClassification {
47    /// High strength + Low distance = High cohesion (ideal)
48    HighCohesion,
49    /// Low strength + High distance = Loose coupling (ideal)
50    LooseCoupling,
51    /// High strength + High distance + Low volatility = Acceptable
52    Acceptable,
53    /// High strength + High distance + High volatility = Pain (needs refactoring)
54    Pain,
55    /// Low strength + Low distance = Local complexity (review needed)
56    LocalComplexity,
57}
58
59impl BalanceClassification {
60    /// Classify a coupling based on Khononov's formula
61    pub fn classify(
62        strength: IntegrationStrength,
63        distance: Distance,
64        volatility: Volatility,
65    ) -> Self {
66        let is_strong = strength.value() >= 0.5;
67        let is_far = distance.value() >= 0.5;
68        let is_volatile = volatility == Volatility::High;
69
70        match (is_strong, is_far, is_volatile) {
71            (true, false, _) => BalanceClassification::HighCohesion,
72            (false, true, _) => BalanceClassification::LooseCoupling,
73            (false, false, _) => BalanceClassification::LocalComplexity,
74            (true, true, false) => BalanceClassification::Acceptable,
75            (true, true, true) => BalanceClassification::Pain,
76        }
77    }
78
79    /// Get Japanese description
80    pub fn description_ja(&self) -> &'static str {
81        match self {
82            BalanceClassification::HighCohesion => "高凝集 (強+近)",
83            BalanceClassification::LooseCoupling => "疎結合 (弱+遠)",
84            BalanceClassification::Acceptable => "許容可能 (強+遠+安定)",
85            BalanceClassification::Pain => "要改善 (強+遠+変動)",
86            BalanceClassification::LocalComplexity => "局所複雑性 (弱+近)",
87        }
88    }
89
90    /// Get English description
91    pub fn description_en(&self) -> &'static str {
92        match self {
93            BalanceClassification::HighCohesion => "High Cohesion",
94            BalanceClassification::LooseCoupling => "Loose Coupling",
95            BalanceClassification::Acceptable => "Acceptable",
96            BalanceClassification::Pain => "Needs Refactoring",
97            BalanceClassification::LocalComplexity => "Local Complexity",
98        }
99    }
100
101    /// Is this classification ideal?
102    pub fn is_ideal(&self) -> bool {
103        matches!(
104            self,
105            BalanceClassification::HighCohesion | BalanceClassification::LooseCoupling
106        )
107    }
108
109    /// Does this need attention?
110    pub fn needs_attention(&self) -> bool {
111        matches!(
112            self,
113            BalanceClassification::Pain | BalanceClassification::LocalComplexity
114        )
115    }
116}
117
118/// Statistics for 3-dimensional coupling analysis
119#[derive(Debug, Clone, Default)]
120pub struct DimensionStats {
121    /// Strength distribution
122    pub strength_counts: StrengthCounts,
123    /// Distance distribution
124    pub distance_counts: DistanceCounts,
125    /// Volatility distribution
126    pub volatility_counts: VolatilityCounts,
127    /// Balance classification counts
128    pub balance_counts: BalanceCounts,
129}
130
131impl DimensionStats {
132    /// Total number of couplings analyzed
133    pub fn total(&self) -> usize {
134        self.strength_counts.total()
135    }
136
137    /// Get percentage of each strength level
138    pub fn strength_percentages(&self) -> (f64, f64, f64, f64) {
139        let total = self.total() as f64;
140        if total == 0.0 {
141            return (0.0, 0.0, 0.0, 0.0);
142        }
143        (
144            self.strength_counts.intrusive as f64 / total * 100.0,
145            self.strength_counts.functional as f64 / total * 100.0,
146            self.strength_counts.model as f64 / total * 100.0,
147            self.strength_counts.contract as f64 / total * 100.0,
148        )
149    }
150
151    /// Get percentage of each distance level
152    pub fn distance_percentages(&self) -> (f64, f64, f64) {
153        let total = self.total() as f64;
154        if total == 0.0 {
155            return (0.0, 0.0, 0.0);
156        }
157        (
158            self.distance_counts.same_module as f64 / total * 100.0,
159            self.distance_counts.different_module as f64 / total * 100.0,
160            self.distance_counts.different_crate as f64 / total * 100.0,
161        )
162    }
163
164    /// Get percentage of each volatility level
165    pub fn volatility_percentages(&self) -> (f64, f64, f64) {
166        let total = self.total() as f64;
167        if total == 0.0 {
168            return (0.0, 0.0, 0.0);
169        }
170        (
171            self.volatility_counts.low as f64 / total * 100.0,
172            self.volatility_counts.medium as f64 / total * 100.0,
173            self.volatility_counts.high as f64 / total * 100.0,
174        )
175    }
176
177    /// Count of ideal couplings (High Cohesion + Loose Coupling)
178    pub fn ideal_count(&self) -> usize {
179        self.balance_counts.high_cohesion + self.balance_counts.loose_coupling
180    }
181
182    /// Count of problematic couplings (Pain + Local Complexity)
183    pub fn problematic_count(&self) -> usize {
184        self.balance_counts.pain + self.balance_counts.local_complexity
185    }
186
187    /// Percentage of ideal couplings
188    pub fn ideal_percentage(&self) -> f64 {
189        let total = self.total() as f64;
190        if total == 0.0 {
191            return 0.0;
192        }
193        self.ideal_count() as f64 / total * 100.0
194    }
195}
196
197/// Counts for each strength level
198#[derive(Debug, Clone, Default)]
199pub struct StrengthCounts {
200    /// Number of intrusive-strength couplings.
201    pub intrusive: usize,
202    /// Number of functional-strength couplings.
203    pub functional: usize,
204    /// Number of model-strength couplings.
205    pub model: usize,
206    /// Number of contract-strength couplings.
207    pub contract: usize,
208}
209
210impl StrengthCounts {
211    /// Total count across all strength levels
212    pub fn total(&self) -> usize {
213        self.intrusive + self.functional + self.model + self.contract
214    }
215}
216
217/// Counts for each distance level
218#[derive(Debug, Clone, Default)]
219pub struct DistanceCounts {
220    /// Couplings within one module or function.
221    pub same_module: usize,
222    /// Couplings across modules in the same crate/workspace.
223    pub different_module: usize,
224    /// Couplings to external crates.
225    pub different_crate: usize,
226}
227
228/// Counts for each volatility level
229#[derive(Debug, Clone, Default)]
230pub struct VolatilityCounts {
231    /// Couplings whose target rarely changes.
232    pub low: usize,
233    /// Couplings whose target changes occasionally.
234    pub medium: usize,
235    /// Couplings whose target changes frequently.
236    pub high: usize,
237}
238
239/// Counts for each balance classification
240#[derive(Debug, Clone, Default)]
241pub struct BalanceCounts {
242    /// Strong and close couplings.
243    pub high_cohesion: usize,
244    /// Weak and distant couplings.
245    pub loose_coupling: usize,
246    /// Strong and distant couplings neutralized by low volatility.
247    pub acceptable: usize,
248    /// Strong, distant, and volatile couplings.
249    pub pain: usize,
250    /// Weak couplings kept unnecessarily close.
251    pub local_complexity: usize,
252}
253
254/// Aggregated metrics for a module
255#[derive(Debug, Clone, Default)]
256pub struct ModuleMetrics {
257    /// Module path
258    pub path: PathBuf,
259    /// Module name
260    pub name: String,
261    /// Number of trait implementations (contract coupling)
262    pub trait_impl_count: usize,
263    /// Number of inherent implementations (intrusive coupling)
264    pub inherent_impl_count: usize,
265    /// Number of function calls
266    pub function_call_count: usize,
267    /// Number of struct/enum usages
268    pub type_usage_count: usize,
269    /// External crate dependencies
270    pub external_deps: Vec<String>,
271    /// Internal module dependencies
272    pub internal_deps: Vec<String>,
273    /// Type definitions in this module with visibility info
274    pub type_definitions: HashMap<String, TypeDefinition>,
275    /// Function definitions in this module with visibility info
276    pub function_definitions: HashMap<String, FunctionDefinition>,
277    /// Item-level dependencies (function → function, function → type, etc.)
278    pub item_dependencies: Vec<ItemDependency>,
279    /// Whether this module is a test module (mod tests or #[cfg(test)])
280    pub is_test_module: bool,
281    /// Number of test functions (#[test])
282    pub test_function_count: usize,
283    /// DDD subdomain classification from config, if configured.
284    pub subdomain: Option<Subdomain>,
285}
286
287impl ModuleMetrics {
288    /// Create empty metrics for a module path/name pair.
289    pub fn new(path: PathBuf, name: String) -> Self {
290        Self {
291            path,
292            name,
293            ..Default::default()
294        }
295    }
296
297    /// Add a type definition to this module (simple version for backward compatibility)
298    pub fn add_type_definition(&mut self, name: String, visibility: Visibility, is_trait: bool) {
299        self.type_definitions.insert(
300            name.clone(),
301            TypeDefinition {
302                name,
303                visibility,
304                is_trait,
305                is_newtype: false,
306                inner_type: None,
307                has_serde_derive: false,
308                public_field_count: 0,
309                total_field_count: 0,
310            },
311        );
312    }
313
314    /// Add a type definition with full details
315    #[allow(clippy::too_many_arguments)]
316    pub fn add_type_definition_full(
317        &mut self,
318        name: String,
319        visibility: Visibility,
320        is_trait: bool,
321        is_newtype: bool,
322        inner_type: Option<String>,
323        has_serde_derive: bool,
324        public_field_count: usize,
325        total_field_count: usize,
326    ) {
327        self.type_definitions.insert(
328            name.clone(),
329            TypeDefinition {
330                name,
331                visibility,
332                is_trait,
333                is_newtype,
334                inner_type,
335                has_serde_derive,
336                public_field_count,
337                total_field_count,
338            },
339        );
340    }
341
342    /// Add a function definition to this module (simple version for backward compatibility)
343    pub fn add_function_definition(&mut self, name: String, visibility: Visibility) {
344        self.function_definitions.insert(
345            name.clone(),
346            FunctionDefinition {
347                name,
348                visibility,
349                param_count: 0,
350                primitive_param_count: 0,
351                param_types: Vec::new(),
352            },
353        );
354    }
355
356    /// Add a function definition with full details
357    pub fn add_function_definition_full(
358        &mut self,
359        name: String,
360        visibility: Visibility,
361        param_count: usize,
362        primitive_param_count: usize,
363        param_types: Vec<String>,
364    ) {
365        self.function_definitions.insert(
366            name.clone(),
367            FunctionDefinition {
368                name,
369                visibility,
370                param_count,
371                primitive_param_count,
372                param_types,
373            },
374        );
375    }
376
377    /// Get visibility of a type defined in this module
378    pub fn get_type_visibility(&self, name: &str) -> Option<Visibility> {
379        self.type_definitions.get(name).map(|t| t.visibility)
380    }
381
382    /// Count public types
383    pub fn public_type_count(&self) -> usize {
384        self.type_definitions
385            .values()
386            .filter(|t| t.visibility == Visibility::Public)
387            .count()
388    }
389
390    /// Count non-public types
391    pub fn private_type_count(&self) -> usize {
392        self.type_definitions
393            .values()
394            .filter(|t| t.visibility != Visibility::Public)
395            .count()
396    }
397
398    /// Calculate average integration strength
399    pub fn average_strength(&self) -> f64 {
400        let total = self.trait_impl_count + self.inherent_impl_count;
401        if total == 0 {
402            return 0.0;
403        }
404
405        let contract_weight = self.trait_impl_count as f64 * IntegrationStrength::Contract.value();
406        let intrusive_weight =
407            self.inherent_impl_count as f64 * IntegrationStrength::Intrusive.value();
408
409        (contract_weight + intrusive_weight) / total as f64
410    }
411
412    /// Count newtypes in this module
413    pub fn newtype_count(&self) -> usize {
414        self.type_definitions
415            .values()
416            .filter(|t| t.is_newtype)
417            .count()
418    }
419
420    /// Count types with serde derives
421    pub fn serde_type_count(&self) -> usize {
422        self.type_definitions
423            .values()
424            .filter(|t| t.has_serde_derive)
425            .count()
426    }
427
428    /// Calculate newtype usage ratio (newtypes / total non-trait types)
429    pub fn newtype_ratio(&self) -> f64 {
430        let non_trait_types = self
431            .type_definitions
432            .values()
433            .filter(|t| !t.is_trait)
434            .count();
435        if non_trait_types == 0 {
436            return 0.0;
437        }
438        self.newtype_count() as f64 / non_trait_types as f64
439    }
440
441    /// Count types with public fields
442    pub fn types_with_public_fields(&self) -> usize {
443        self.type_definitions
444            .values()
445            .filter(|t| t.public_field_count > 0)
446            .count()
447    }
448
449    /// Total function count
450    pub fn function_count(&self) -> usize {
451        self.function_definitions.len()
452    }
453
454    /// Count functions with high primitive parameter ratio
455    /// (potential Primitive Obsession)
456    pub fn functions_with_primitive_obsession(&self) -> Vec<&FunctionDefinition> {
457        self.function_definitions
458            .values()
459            .filter(|f| {
460                f.param_count >= 3 && f.primitive_param_count as f64 / f.param_count as f64 >= 0.6
461            })
462            .collect()
463    }
464
465    /// Check if this module is a potential "God Module"
466    /// (too many functions, types, or implementations)
467    pub fn is_god_module(&self, max_functions: usize, max_types: usize, max_impls: usize) -> bool {
468        self.function_count() > max_functions
469            || self.type_definitions.len() > max_types
470            || (self.trait_impl_count + self.inherent_impl_count) > max_impls
471    }
472}
473
474// ===== Project Aggregates =====