Skip to main content

rust_diff_analyzer/classifier/
attr_classifier.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use crate::{config::Config, types::SemanticUnit};
5
6/// Checks if unit is a test function
7///
8/// # Arguments
9///
10/// * `unit` - Semantic unit to check
11///
12/// # Returns
13///
14/// `true` if unit has #[test] attribute
15///
16/// # Examples
17///
18/// ```
19/// use rust_diff_analyzer::{
20///     classifier::attr_classifier::is_test_unit,
21///     types::{LineSpan, SemanticUnit, SemanticUnitKind, Visibility},
22/// };
23///
24/// let unit = SemanticUnit::new(
25///     SemanticUnitKind::Function,
26///     "test_it".to_string(),
27///     Visibility::Private,
28///     LineSpan::new(1, 10),
29///     vec!["test".to_string()],
30/// );
31///
32/// assert!(is_test_unit(&unit));
33/// ```
34pub fn is_test_unit(unit: &SemanticUnit) -> bool {
35    unit.has_attribute("test")
36}
37
38/// Checks if unit is a benchmark function
39///
40/// # Arguments
41///
42/// * `unit` - Semantic unit to check
43///
44/// # Returns
45///
46/// `true` if unit has #[bench] attribute
47///
48/// # Examples
49///
50/// ```
51/// use rust_diff_analyzer::{
52///     classifier::attr_classifier::is_bench_unit,
53///     types::{LineSpan, SemanticUnit, SemanticUnitKind, Visibility},
54/// };
55///
56/// let unit = SemanticUnit::new(
57///     SemanticUnitKind::Function,
58///     "bench_it".to_string(),
59///     Visibility::Private,
60///     LineSpan::new(1, 10),
61///     vec!["bench".to_string()],
62/// );
63///
64/// assert!(is_bench_unit(&unit));
65/// ```
66pub fn is_bench_unit(unit: &SemanticUnit) -> bool {
67    unit.has_attribute("bench")
68}
69
70/// Checks if unit is inside a #[cfg(test)] module
71///
72/// # Arguments
73///
74/// * `unit` - Semantic unit to check
75///
76/// # Returns
77///
78/// `true` if unit has cfg_test marker
79///
80/// # Examples
81///
82/// ```
83/// use rust_diff_analyzer::{
84///     classifier::attr_classifier::is_in_test_module,
85///     types::{LineSpan, SemanticUnit, SemanticUnitKind, Visibility},
86/// };
87///
88/// let unit = SemanticUnit::new(
89///     SemanticUnitKind::Function,
90///     "helper".to_string(),
91///     Visibility::Private,
92///     LineSpan::new(1, 10),
93///     vec!["cfg_test".to_string()],
94/// );
95///
96/// assert!(is_in_test_module(&unit));
97/// ```
98pub fn is_in_test_module(unit: &SemanticUnit) -> bool {
99    unit.has_attribute("cfg_test")
100}
101
102/// Checks if unit has a test-related feature attribute
103///
104/// The AST visitor records `#[cfg(feature = "name")]` gates as
105/// `cfg_feature:name` attribute markers; this matches those markers against
106/// the configured test features.
107///
108/// # Arguments
109///
110/// * `unit` - Semantic unit to check
111/// * `config` - Configuration with test features
112///
113/// # Returns
114///
115/// `true` if unit is gated behind a configured test feature
116///
117/// # Examples
118///
119/// ```
120/// use rust_diff_analyzer::{
121///     classifier::attr_classifier::has_test_feature,
122///     config::Config,
123///     types::{LineSpan, SemanticUnit, SemanticUnitKind, Visibility},
124/// };
125///
126/// let unit = SemanticUnit::new(
127///     SemanticUnitKind::Function,
128///     "mock_fn".to_string(),
129///     Visibility::Private,
130///     LineSpan::new(1, 10),
131///     vec!["cfg".to_string(), "cfg_feature:mock".to_string()],
132/// );
133///
134/// let config = Config::default();
135/// assert!(has_test_feature(&unit, &config));
136/// ```
137pub fn has_test_feature(unit: &SemanticUnit, config: &Config) -> bool {
138    unit.attributes.iter().any(|attr| {
139        attr.strip_prefix("cfg_feature:")
140            .map(|name| {
141                config
142                    .classification
143                    .test_features
144                    .iter()
145                    .any(|feature| feature == name)
146            })
147            .unwrap_or(false)
148    })
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::types::{LineSpan, SemanticUnitKind, Visibility};
155
156    fn make_unit(attrs: Vec<&str>) -> SemanticUnit {
157        SemanticUnit::new(
158            SemanticUnitKind::Function,
159            "test".to_string(),
160            Visibility::Private,
161            LineSpan::new(1, 10),
162            attrs.into_iter().map(String::from).collect(),
163        )
164    }
165
166    #[test]
167    fn test_is_test_unit() {
168        assert!(is_test_unit(&make_unit(vec!["test"])));
169        assert!(!is_test_unit(&make_unit(vec!["inline"])));
170    }
171
172    #[test]
173    fn test_is_bench_unit() {
174        assert!(is_bench_unit(&make_unit(vec!["bench"])));
175        assert!(!is_bench_unit(&make_unit(vec!["test"])));
176    }
177
178    #[test]
179    fn test_is_in_test_module() {
180        assert!(is_in_test_module(&make_unit(vec!["cfg_test"])));
181        assert!(!is_in_test_module(&make_unit(vec!["test"])));
182    }
183
184    #[test]
185    fn test_has_test_feature_matches_configured_feature() {
186        let config = Config::default();
187        assert!(has_test_feature(
188            &make_unit(vec!["cfg", "cfg_feature:mock"]),
189            &config
190        ));
191        assert!(has_test_feature(
192            &make_unit(vec!["cfg", "cfg_feature:test-utils"]),
193            &config
194        ));
195    }
196
197    #[test]
198    fn test_has_test_feature_ignores_other_features() {
199        let config = Config::default();
200        assert!(!has_test_feature(
201            &make_unit(vec!["cfg", "cfg_feature:serde"]),
202            &config
203        ));
204        assert!(!has_test_feature(&make_unit(vec!["cfg"]), &config));
205    }
206}