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}