Skip to main content

rust_diff_analyzer/classifier/
rules.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use crate::{
5    config::Config,
6    types::{Change, SemanticUnit, SemanticUnitKind},
7};
8
9/// Calculates the weight score for a semantic unit
10///
11/// # Arguments
12///
13/// * `unit` - Semantic unit to calculate weight for
14/// * `config` - Configuration with weight settings
15///
16/// # Returns
17///
18/// Weight score for the unit
19///
20/// # Examples
21///
22/// ```
23/// use rust_diff_analyzer::{
24///     classifier::rules::calculate_weight,
25///     config::Config,
26///     types::{LineSpan, SemanticUnit, SemanticUnitKind, Visibility},
27/// };
28///
29/// let unit = SemanticUnit::new(
30///     SemanticUnitKind::Function,
31///     "public_fn".to_string(),
32///     Visibility::Public,
33///     LineSpan::new(1, 10),
34///     vec![],
35/// );
36///
37/// let config = Config::default();
38/// let weight = calculate_weight(&unit, &config);
39/// assert_eq!(weight, 3); // default public function weight
40/// ```
41pub fn calculate_weight(unit: &SemanticUnit, config: &Config) -> usize {
42    let weights = &config.weights;
43
44    match unit.kind {
45        SemanticUnitKind::Function => {
46            if unit.visibility.is_public() {
47                weights.public_function
48            } else {
49                weights.private_function
50            }
51        }
52        SemanticUnitKind::Struct | SemanticUnitKind::Enum => {
53            if unit.visibility.is_public() {
54                weights.public_struct
55            } else {
56                weights.private_struct
57            }
58        }
59        SemanticUnitKind::Impl => weights.impl_block,
60        SemanticUnitKind::Trait => weights.trait_definition,
61        SemanticUnitKind::Const | SemanticUnitKind::Static => weights.const_static,
62        SemanticUnitKind::TypeAlias => weights.const_static,
63        SemanticUnitKind::Macro => weights.private_function,
64        SemanticUnitKind::Module => weights.const_static,
65    }
66}
67
68/// Returns every production unit kind exceeding its per-type limit
69///
70/// # Arguments
71///
72/// * `changes` - Analyzed changes
73/// * `config` - Configuration with optional per-type limits
74///
75/// # Returns
76///
77/// Tuples of `(kind name, count, limit)` for each exceeded limit; empty when
78/// no per-type limits are configured or none are exceeded
79///
80/// # Examples
81///
82/// ```
83/// use rust_diff_analyzer::{classifier::rules::exceeded_per_type_limits, config::Config};
84///
85/// let config = Config::default();
86/// assert!(exceeded_per_type_limits(&[], &config).is_empty());
87/// ```
88pub fn exceeded_per_type_limits(
89    changes: &[Change],
90    config: &Config,
91) -> Vec<(&'static str, usize, usize)> {
92    let per_type = match &config.limits.per_type {
93        Some(limits) => limits,
94        None => return Vec::new(),
95    };
96
97    let kinds = [
98        SemanticUnitKind::Function,
99        SemanticUnitKind::Struct,
100        SemanticUnitKind::Enum,
101        SemanticUnitKind::Trait,
102        SemanticUnitKind::Impl,
103        SemanticUnitKind::Const,
104        SemanticUnitKind::Static,
105        SemanticUnitKind::TypeAlias,
106        SemanticUnitKind::Macro,
107        SemanticUnitKind::Module,
108    ];
109    let limits = [
110        per_type.functions,
111        per_type.structs,
112        per_type.enums,
113        per_type.traits,
114        per_type.impl_blocks,
115        per_type.consts,
116        per_type.statics,
117        per_type.type_aliases,
118        per_type.macros,
119        per_type.modules,
120    ];
121
122    let mut counts = [0usize; 10];
123    for change in changes {
124        if !change.classification.is_production() {
125            continue;
126        }
127        if let Some(index) = kinds.iter().position(|k| *k == change.unit.kind) {
128            counts[index] += 1;
129        }
130    }
131
132    kinds
133        .iter()
134        .zip(limits)
135        .zip(counts)
136        .filter_map(|((kind, limit), count)| {
137            limit.and_then(|limit| (count > limit).then_some((kind.as_str(), count, limit)))
138        })
139        .collect()
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::types::{LineSpan, Visibility};
146
147    #[test]
148    fn test_weight_calculation() {
149        let config = Config::default();
150
151        let pub_fn = SemanticUnit::new(
152            SemanticUnitKind::Function,
153            "public".to_string(),
154            Visibility::Public,
155            LineSpan::new(1, 10),
156            vec![],
157        );
158        assert_eq!(calculate_weight(&pub_fn, &config), 3);
159
160        let priv_fn = SemanticUnit::new(
161            SemanticUnitKind::Function,
162            "private".to_string(),
163            Visibility::Private,
164            LineSpan::new(1, 10),
165            vec![],
166        );
167        assert_eq!(calculate_weight(&priv_fn, &config), 1);
168
169        let trait_def = SemanticUnit::new(
170            SemanticUnitKind::Trait,
171            "MyTrait".to_string(),
172            Visibility::Public,
173            LineSpan::new(1, 10),
174            vec![],
175        );
176        assert_eq!(calculate_weight(&trait_def, &config), 4);
177    }
178
179    #[test]
180    fn test_exceeded_per_type_limits() {
181        use std::path::PathBuf;
182
183        use crate::{
184            config::PerTypeLimits,
185            types::{Change, CodeType},
186        };
187
188        let make_change = |kind: SemanticUnitKind| {
189            Change::new(
190                PathBuf::from("src/lib.rs"),
191                SemanticUnit::new(
192                    kind,
193                    "unit".to_string(),
194                    Visibility::Public,
195                    LineSpan::new(1, 5),
196                    vec![],
197                ),
198                CodeType::Production,
199                3,
200                0,
201            )
202        };
203
204        let mut config = Config::default();
205        config.limits.per_type = Some(PerTypeLimits {
206            functions: Some(1),
207            ..PerTypeLimits::default()
208        });
209
210        let changes = vec![
211            make_change(SemanticUnitKind::Function),
212            make_change(SemanticUnitKind::Function),
213            make_change(SemanticUnitKind::Struct),
214        ];
215
216        let exceeded = exceeded_per_type_limits(&changes, &config);
217        assert_eq!(exceeded, vec![("function", 2, 1)]);
218
219        config.limits.per_type = None;
220        assert!(exceeded_per_type_limits(&changes, &config).is_empty());
221    }
222}