smelt_validator/rules/
visibility.rs1use super::ValidationRule;
4use crate::config::SemanticConfig;
5use crate::validator::{ValidationSeverity, Violation};
6use smelt_core::{IntentRecord, SemanticChange, SemanticDelta, Visibility};
7
8pub struct VisibilityChecker {
10 config: SemanticConfig,
11}
12
13impl VisibilityChecker {
14 pub fn new(config: SemanticConfig) -> Self {
16 Self { config }
17 }
18
19 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 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}