1use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::path::Path;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RuleResult {
13 pub passed: bool,
15 pub violations: Vec<RuleViolation>,
17 pub suggestions: Vec<Suggestion>,
19 pub context: Option<String>,
21}
22
23impl RuleResult {
24 pub fn pass() -> Self {
26 Self { passed: true, violations: Vec::new(), suggestions: Vec::new(), context: None }
27 }
28
29 pub fn pass_with_suggestions(suggestions: Vec<Suggestion>) -> Self {
31 Self { passed: true, violations: Vec::new(), suggestions, context: None }
32 }
33
34 pub fn fail(violations: Vec<RuleViolation>) -> Self {
36 Self { passed: false, violations, suggestions: Vec::new(), context: None }
37 }
38
39 pub fn with_context(mut self, context: impl Into<String>) -> Self {
41 self.context = Some(context.into());
42 self
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct RuleViolation {
49 pub code: String,
51 pub message: String,
53 pub severity: ViolationLevel,
55 pub location: Option<String>,
57 pub line: Option<usize>,
59 pub expected: Option<String>,
61 pub actual: Option<String>,
63 pub fixable: bool,
65}
66
67impl RuleViolation {
68 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
70 Self {
71 code: code.into(),
72 message: message.into(),
73 severity: ViolationLevel::Error,
74 location: None,
75 line: None,
76 expected: None,
77 actual: None,
78 fixable: false,
79 }
80 }
81
82 pub fn with_severity(mut self, severity: ViolationLevel) -> Self {
84 self.severity = severity;
85 self
86 }
87
88 pub fn with_location(mut self, location: impl Into<String>) -> Self {
90 self.location = Some(location.into());
91 self
92 }
93
94 pub fn with_line(mut self, line: usize) -> Self {
96 self.line = Some(line);
97 self
98 }
99
100 pub fn with_diff(mut self, expected: impl Into<String>, actual: impl Into<String>) -> Self {
102 self.expected = Some(expected.into());
103 self.actual = Some(actual.into());
104 self
105 }
106
107 pub fn fixable(mut self) -> Self {
109 self.fixable = true;
110 self
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116pub enum ViolationLevel {
117 Info,
119 Warning,
121 Error,
123 Critical,
125}
126
127impl fmt::Display for ViolationLevel {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 ViolationLevel::Info => write!(f, "INFO"),
131 ViolationLevel::Warning => write!(f, "WARN"),
132 ViolationLevel::Error => write!(f, "ERROR"),
133 ViolationLevel::Critical => write!(f, "CRITICAL"),
134 }
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Suggestion {
141 pub message: String,
143 pub location: Option<String>,
145 pub fix: Option<String>,
147}
148
149impl Suggestion {
150 pub fn new(message: impl Into<String>) -> Self {
152 Self { message: message.into(), location: None, fix: None }
153 }
154
155 pub fn with_location(mut self, location: impl Into<String>) -> Self {
157 self.location = Some(location.into());
158 self
159 }
160
161 pub fn with_fix(mut self, fix: impl Into<String>) -> Self {
163 self.fix = Some(fix.into());
164 self
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct FixResult {
171 pub success: bool,
173 pub fixed_count: usize,
175 pub failed_count: usize,
177 pub details: Vec<FixDetail>,
179}
180
181impl FixResult {
182 pub fn success(fixed: usize) -> Self {
184 Self { success: true, fixed_count: fixed, failed_count: 0, details: Vec::new() }
185 }
186
187 pub fn partial(fixed: usize, failed: usize, details: Vec<FixDetail>) -> Self {
189 Self { success: false, fixed_count: fixed, failed_count: failed, details }
190 }
191
192 pub fn failure(error: impl Into<String>) -> Self {
194 Self {
195 success: false,
196 fixed_count: 0,
197 failed_count: 0,
198 details: vec![FixDetail::Error(error.into())],
199 }
200 }
201
202 pub fn with_detail(mut self, detail: FixDetail) -> Self {
204 self.details.push(detail);
205 self
206 }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub enum FixDetail {
212 Fixed { code: String, description: String },
214 FailedToFix { code: String, reason: String },
216 Error(String),
218}
219
220pub trait StackComplianceRule: Send + Sync + std::fmt::Debug {
224 fn id(&self) -> &str;
226
227 fn description(&self) -> &str;
229
230 fn help(&self) -> Option<&str> {
232 None
233 }
234
235 fn check(&self, project_path: &Path) -> anyhow::Result<RuleResult>;
237
238 fn can_fix(&self) -> bool {
240 false
241 }
242
243 fn fix(&self, project_path: &Path) -> anyhow::Result<FixResult> {
245 let _ = project_path;
246 Ok(FixResult::failure("Auto-fix not supported for this rule"))
247 }
248
249 fn category(&self) -> RuleCategory {
251 RuleCategory::General
252 }
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
257pub enum RuleCategory {
258 General,
260 Build,
262 Ci,
264 Code,
266 Docs,
268}
269
270impl fmt::Display for RuleCategory {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 match self {
273 RuleCategory::General => write!(f, "General"),
274 RuleCategory::Build => write!(f, "Build"),
275 RuleCategory::Ci => write!(f, "CI"),
276 RuleCategory::Code => write!(f, "Code"),
277 RuleCategory::Docs => write!(f, "Docs"),
278 }
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_rule_result_pass() {
288 let result = RuleResult::pass();
289 assert!(result.passed);
290 assert!(result.violations.is_empty());
291 }
292
293 #[test]
294 fn test_rule_result_fail() {
295 let violations = vec![RuleViolation::new("TEST-001", "Test violation")];
296 let result = RuleResult::fail(violations);
297 assert!(!result.passed);
298 assert_eq!(result.violations.len(), 1);
299 }
300
301 #[test]
302 fn test_violation_builder() {
303 let violation = RuleViolation::new("MK-001", "Missing target")
304 .with_severity(ViolationLevel::Error)
305 .with_location("Makefile")
306 .with_line(10)
307 .with_diff("test-fast", "test")
308 .fixable();
309
310 assert_eq!(violation.code, "MK-001");
311 assert_eq!(violation.severity, ViolationLevel::Error);
312 assert_eq!(violation.location, Some("Makefile".to_string()));
313 assert_eq!(violation.line, Some(10));
314 assert!(violation.fixable);
315 }
316
317 #[test]
318 fn test_fix_result() {
319 let result = FixResult::success(5);
320 assert!(result.success);
321 assert_eq!(result.fixed_count, 5);
322 }
323
324 #[test]
325 fn test_violation_level_display() {
326 assert_eq!(format!("{}", ViolationLevel::Info), "INFO");
327 assert_eq!(format!("{}", ViolationLevel::Warning), "WARN");
328 assert_eq!(format!("{}", ViolationLevel::Error), "ERROR");
329 assert_eq!(format!("{}", ViolationLevel::Critical), "CRITICAL");
330 }
331
332 #[test]
333 fn test_rule_result_pass_with_suggestions() {
334 let suggestions =
335 vec![Suggestion::new("Consider adding tests"), Suggestion::new("Could improve docs")];
336 let result = RuleResult::pass_with_suggestions(suggestions);
337 assert!(result.passed);
338 assert!(result.violations.is_empty());
339 assert_eq!(result.suggestions.len(), 2);
340 }
341
342 #[test]
343 fn test_rule_result_with_context() {
344 let result = RuleResult::pass().with_context("Checked 10 files");
345 assert_eq!(result.context, Some("Checked 10 files".to_string()));
346 }
347
348 #[test]
349 fn test_suggestion_new() {
350 let suggestion = Suggestion::new("Add more tests");
351 assert_eq!(suggestion.message, "Add more tests");
352 assert!(suggestion.location.is_none());
353 assert!(suggestion.fix.is_none());
354 }
355
356 #[test]
357 fn test_suggestion_with_location() {
358 let suggestion = Suggestion::new("Fix formatting").with_location("src/lib.rs");
359 assert_eq!(suggestion.location, Some("src/lib.rs".to_string()));
360 }
361
362 #[test]
363 fn test_suggestion_with_fix() {
364 let suggestion = Suggestion::new("Add license").with_fix("Add MIT license file");
365 assert_eq!(suggestion.fix, Some("Add MIT license file".to_string()));
366 }
367
368 #[test]
369 fn test_suggestion_builder_chain() {
370 let suggestion = Suggestion::new("Update config")
371 .with_location("Cargo.toml")
372 .with_fix("Add edition = \"2024\"");
373 assert_eq!(suggestion.message, "Update config");
374 assert!(suggestion.location.is_some());
375 assert!(suggestion.fix.is_some());
376 }
377
378 #[test]
379 fn test_fix_result_partial() {
380 let details = vec![
381 FixDetail::Fixed {
382 code: "MK-001".to_string(),
383 description: "Added target".to_string(),
384 },
385 FixDetail::FailedToFix {
386 code: "MK-002".to_string(),
387 reason: "File not writable".to_string(),
388 },
389 ];
390 let result = FixResult::partial(1, 1, details);
391 assert!(!result.success);
392 assert_eq!(result.fixed_count, 1);
393 assert_eq!(result.failed_count, 1);
394 assert_eq!(result.details.len(), 2);
395 }
396
397 #[test]
398 fn test_fix_result_failure() {
399 let result = FixResult::failure("Cannot write to disk");
400 assert!(!result.success);
401 assert_eq!(result.fixed_count, 0);
402 assert_eq!(result.failed_count, 0);
403 assert_eq!(result.details.len(), 1);
404 }
405
406 #[test]
407 fn test_fix_result_with_detail() {
408 let result = FixResult::success(1).with_detail(FixDetail::Fixed {
409 code: "TEST".to_string(),
410 description: "Test fix".to_string(),
411 });
412 assert_eq!(result.details.len(), 1);
413 }
414
415 #[test]
416 fn test_fix_detail_error_variant() {
417 let detail = FixDetail::Error("Something went wrong".to_string());
418 match detail {
419 FixDetail::Error(msg) => assert_eq!(msg, "Something went wrong"),
420 _ => panic!("Expected Error variant"),
421 }
422 }
423
424 #[test]
425 fn test_rule_category_display() {
426 assert_eq!(format!("{}", RuleCategory::General), "General");
427 assert_eq!(format!("{}", RuleCategory::Build), "Build");
428 assert_eq!(format!("{}", RuleCategory::Ci), "CI");
429 assert_eq!(format!("{}", RuleCategory::Code), "Code");
430 assert_eq!(format!("{}", RuleCategory::Docs), "Docs");
431 }
432
433 #[test]
434 fn test_violation_default_values() {
435 let violation = RuleViolation::new("TEST", "Test message");
436 assert_eq!(violation.severity, ViolationLevel::Error);
437 assert!(violation.location.is_none());
438 assert!(violation.line.is_none());
439 assert!(violation.expected.is_none());
440 assert!(violation.actual.is_none());
441 assert!(!violation.fixable);
442 }
443
444 #[test]
445 fn test_violation_with_severity_warning() {
446 let violation =
447 RuleViolation::new("WARN-001", "Warning").with_severity(ViolationLevel::Warning);
448 assert_eq!(violation.severity, ViolationLevel::Warning);
449 }
450
451 #[test]
452 fn test_violation_with_severity_critical() {
453 let violation = RuleViolation::new("CRIT-001", "Critical issue")
454 .with_severity(ViolationLevel::Critical);
455 assert_eq!(violation.severity, ViolationLevel::Critical);
456 }
457
458 #[test]
459 fn test_violation_with_severity_info() {
460 let violation =
461 RuleViolation::new("INFO-001", "Info message").with_severity(ViolationLevel::Info);
462 assert_eq!(violation.severity, ViolationLevel::Info);
463 }
464
465 #[test]
466 fn test_rule_category_equality() {
467 assert_eq!(RuleCategory::General, RuleCategory::General);
468 assert_eq!(RuleCategory::Build, RuleCategory::Build);
469 assert_eq!(RuleCategory::Ci, RuleCategory::Ci);
470 assert_ne!(RuleCategory::General, RuleCategory::Build);
471 }
472
473 #[test]
474 fn test_violation_level_equality() {
475 assert_eq!(ViolationLevel::Info, ViolationLevel::Info);
476 assert_eq!(ViolationLevel::Warning, ViolationLevel::Warning);
477 assert_ne!(ViolationLevel::Info, ViolationLevel::Warning);
478 }
479
480 #[test]
481 fn test_fix_detail_fixed_variant() {
482 let detail = FixDetail::Fixed {
483 code: "MK-001".to_string(),
484 description: "Added test target".to_string(),
485 };
486 match detail {
487 FixDetail::Fixed { code, description } => {
488 assert_eq!(code, "MK-001");
489 assert_eq!(description, "Added test target");
490 }
491 _ => panic!("Expected Fixed variant"),
492 }
493 }
494
495 #[test]
496 fn test_fix_detail_failed_variant() {
497 let detail = FixDetail::FailedToFix {
498 code: "MK-002".to_string(),
499 reason: "Permission denied".to_string(),
500 };
501 match detail {
502 FixDetail::FailedToFix { code, reason } => {
503 assert_eq!(code, "MK-002");
504 assert_eq!(reason, "Permission denied");
505 }
506 _ => panic!("Expected FailedToFix variant"),
507 }
508 }
509
510 #[test]
511 fn test_rule_result_fields() {
512 let result = RuleResult::fail(vec![RuleViolation::new("A", "B")]);
513 assert!(!result.passed);
514 assert_eq!(result.violations.len(), 1);
515 assert!(result.suggestions.is_empty());
516 assert!(result.context.is_none());
517 }
518
519 #[test]
520 fn test_violation_full_chain() {
521 let violation = RuleViolation::new("FULL-001", "Full test")
522 .with_severity(ViolationLevel::Warning)
523 .with_location("test.rs")
524 .with_line(42)
525 .with_diff("expected", "actual")
526 .fixable();
527
528 assert_eq!(violation.code, "FULL-001");
529 assert_eq!(violation.message, "Full test");
530 assert_eq!(violation.severity, ViolationLevel::Warning);
531 assert_eq!(violation.location, Some("test.rs".to_string()));
532 assert_eq!(violation.line, Some(42));
533 assert_eq!(violation.expected, Some("expected".to_string()));
534 assert_eq!(violation.actual, Some("actual".to_string()));
535 assert!(violation.fixable);
536 }
537
538 #[test]
539 fn test_rule_result_serialization() {
540 let result = RuleResult::pass().with_context("test context");
541 let json = serde_json::to_string(&result).expect("json serialize failed");
542 assert!(json.contains("\"passed\":true"));
543 assert!(json.contains("test context"));
544 }
545
546 #[test]
547 fn test_violation_serialization() {
548 let violation = RuleViolation::new("SER-001", "Serialize test");
549 let json = serde_json::to_string(&violation).expect("json serialize failed");
550 assert!(json.contains("SER-001"));
551 assert!(json.contains("Serialize test"));
552 }
553
554 #[test]
555 fn test_suggestion_serialization() {
556 let suggestion = Suggestion::new("Test suggestion").with_location("file.rs");
557 let json = serde_json::to_string(&suggestion).expect("json serialize failed");
558 assert!(json.contains("Test suggestion"));
559 assert!(json.contains("file.rs"));
560 }
561
562 #[test]
563 fn test_fix_result_serialization() {
564 let result = FixResult::success(3);
565 let json = serde_json::to_string(&result).expect("json serialize failed");
566 assert!(json.contains("\"success\":true"));
567 assert!(json.contains("\"fixed_count\":3"));
568 }
569
570 #[test]
571 fn test_rule_category_serialization() {
572 let category = RuleCategory::Build;
573 let json = serde_json::to_string(&category).expect("json serialize failed");
574 assert!(json.contains("Build"));
575 }
576
577 #[test]
578 fn test_violation_level_serialization() {
579 let level = ViolationLevel::Critical;
580 let json = serde_json::to_string(&level).expect("json serialize failed");
581 assert!(json.contains("Critical"));
582 }
583}