Skip to main content

smelt_validator/rules/
visibility.rs

1//! Visibility change detection
2
3use super::ValidationRule;
4use crate::config::SemanticConfig;
5use crate::validator::{ValidationSeverity, Violation};
6use smelt_core::{IntentRecord, SemanticChange, SemanticDelta, Visibility};
7
8/// Checks for visibility changes and new public API
9pub struct VisibilityChecker {
10    config: SemanticConfig,
11}
12
13impl VisibilityChecker {
14    /// Create a new visibility checker
15    pub fn new(config: SemanticConfig) -> Self {
16        Self { config }
17    }
18
19    /// Check for new public API additions
20    fn check_new_public_api(&self, change: &SemanticChange) -> Option<Violation> {
21        match change {
22            SemanticChange::FunctionAdded {
23                name,
24                file,
25                is_public,
26                ..
27            } => {
28                if *is_public && self.config.review_new_public_api {
29                    Some(Violation {
30                        rule: "new-public-api".to_string(),
31                        severity: ValidationSeverity::Warning,
32                        message: format!("New public function '{}' added", name),
33                        location: Some(file.clone()),
34                        suggestion: Some(
35                            "Ensure API design is reviewed before committing public interface"
36                                .to_string(),
37                        ),
38                    })
39                } else {
40                    None
41                }
42            }
43
44            SemanticChange::TypeAdded {
45                name,
46                file,
47                is_public,
48                kind,
49            } => {
50                if *is_public && self.config.review_new_public_api {
51                    Some(Violation {
52                        rule: "new-public-api".to_string(),
53                        severity: ValidationSeverity::Warning,
54                        message: format!("New public {} '{}' added", kind, name),
55                        location: Some(file.clone()),
56                        suggestion: Some(
57                            "Ensure type design is reviewed before committing public interface"
58                                .to_string(),
59                        ),
60                    })
61                } else {
62                    None
63                }
64            }
65
66            SemanticChange::VisibilityChanged {
67                name,
68                file,
69                old_visibility,
70                new_visibility,
71            } => {
72                // Check for visibility increase (internal -> public)
73                let is_increase = matches!(
74                    (old_visibility, new_visibility),
75                    (Visibility::Private, Visibility::Internal)
76                        | (Visibility::Private, Visibility::Public)
77                        | (Visibility::Internal, Visibility::Public)
78                );
79
80                if is_increase
81                    && *new_visibility == Visibility::Public
82                    && self.config.review_new_public_api
83                {
84                    Some(Violation {
85                        rule: "new-public-api".to_string(),
86                        severity: ValidationSeverity::Warning,
87                        message: format!(
88                            "'{}' visibility increased to public ({:?} -> {:?})",
89                            name, old_visibility, new_visibility
90                        ),
91                        location: Some(file.clone()),
92                        suggestion: Some("Ensure API is ready for public consumption".to_string()),
93                    })
94                } else {
95                    None
96                }
97            }
98
99            _ => None,
100        }
101    }
102}
103
104impl ValidationRule for VisibilityChecker {
105    fn name(&self) -> &'static str {
106        "visibility"
107    }
108
109    fn validate(&self, delta: &SemanticDelta, _intent: Option<&IntentRecord>) -> Vec<Violation> {
110        if !self.config.check_visibility {
111            return Vec::new();
112        }
113
114        delta
115            .changes
116            .iter()
117            .filter_map(|change| self.check_new_public_api(change))
118            .collect()
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use chrono::Utc;
126    use smelt_core::ImpactSummary;
127    use uuid::Uuid;
128
129    fn make_delta(changes: Vec<SemanticChange>) -> SemanticDelta {
130        SemanticDelta {
131            id: Uuid::new_v4(),
132            intent_id: Uuid::new_v4(),
133            timestamp: Utc::now(),
134            from_snapshot: Uuid::new_v4(),
135            to_snapshot: Uuid::new_v4(),
136            changes,
137            impact_summary: ImpactSummary::default(),
138        }
139    }
140
141    #[test]
142    fn test_new_public_function_flagged() {
143        let config = SemanticConfig {
144            review_new_public_api: true,
145            ..Default::default()
146        };
147
148        let checker = VisibilityChecker::new(config);
149
150        let delta = make_delta(vec![SemanticChange::FunctionAdded {
151            name: "new_api".to_string(),
152            file: "lib.rs".to_string(),
153            signature: "fn new_api()".to_string(),
154            is_public: true,
155        }]);
156
157        let violations = checker.validate(&delta, None);
158        assert_eq!(violations.len(), 1);
159        assert_eq!(violations[0].rule, "new-public-api");
160    }
161
162    #[test]
163    fn test_private_function_ok() {
164        let config = SemanticConfig {
165            review_new_public_api: true,
166            ..Default::default()
167        };
168
169        let checker = VisibilityChecker::new(config);
170
171        let delta = make_delta(vec![SemanticChange::FunctionAdded {
172            name: "helper".to_string(),
173            file: "lib.rs".to_string(),
174            signature: "fn helper()".to_string(),
175            is_public: false,
176        }]);
177
178        let violations = checker.validate(&delta, None);
179        assert!(violations.is_empty());
180    }
181
182    #[test]
183    fn test_review_disabled_no_warnings() {
184        let config = SemanticConfig {
185            review_new_public_api: false,
186            ..Default::default()
187        };
188
189        let checker = VisibilityChecker::new(config);
190
191        let delta = make_delta(vec![SemanticChange::FunctionAdded {
192            name: "new_api".to_string(),
193            file: "lib.rs".to_string(),
194            signature: "fn new_api()".to_string(),
195            is_public: true,
196        }]);
197
198        let violations = checker.validate(&delta, None);
199        assert!(violations.is_empty());
200    }
201}