Skip to main content

omni_dev/data/
scopes_lint.rs

1//! Report types for `omni-dev config scopes lint` (issue #1475).
2
3use serde::{Deserialize, Serialize};
4
5/// A `file_patterns` entry that matches no tracked file.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct DeadPattern {
8    /// Name of the scope the pattern belongs to.
9    pub scope: String,
10    /// The dead pattern itself.
11    pub pattern: String,
12}
13
14/// Full report produced by `omni-dev config scopes lint`.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ScopesLintReport {
17    /// Roots checked for scope coverage.
18    pub roots: Vec<String>,
19    /// Whether ecosystem-injected default scopes were excluded.
20    pub project_only: bool,
21    /// Number of scopes validated against.
22    pub scopes_checked: usize,
23    /// Number of tracked files considered under `roots`, after `--allow`.
24    pub files_checked: usize,
25    /// `file_patterns` entries matching no tracked file.
26    pub dead_patterns: Vec<DeadPattern>,
27    /// Tracked files under `roots` matched by no scope.
28    pub unscoped_files: Vec<String>,
29}
30
31impl ScopesLintReport {
32    /// Whether the report found any violation.
33    #[must_use]
34    pub fn has_violations(&self) -> bool {
35        !self.dead_patterns.is_empty() || !self.unscoped_files.is_empty()
36    }
37
38    /// Exit code for this report: `0` clean, `1` any violation.
39    ///
40    /// Unlike `data::check::CheckReport`, there is no severity gradation
41    /// (both assertions are unconditional structural violations), so there
42    /// is no `--strict` tier to toggle.
43    #[must_use]
44    pub fn exit_code(&self) -> i32 {
45        i32::from(self.has_violations())
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    fn clean_report() -> ScopesLintReport {
54        ScopesLintReport {
55            roots: vec!["src".to_string()],
56            project_only: true,
57            scopes_checked: 1,
58            files_checked: 1,
59            dead_patterns: Vec::new(),
60            unscoped_files: Vec::new(),
61        }
62    }
63
64    #[test]
65    fn clean_report_has_no_violations() {
66        let report = clean_report();
67        assert!(!report.has_violations());
68        assert_eq!(report.exit_code(), 0);
69    }
70
71    #[test]
72    fn dead_pattern_only_is_a_violation() {
73        let mut report = clean_report();
74        report.dead_patterns.push(DeadPattern {
75            scope: "cli".to_string(),
76            pattern: "src/nonexistent/**".to_string(),
77        });
78        assert!(report.has_violations());
79        assert_eq!(report.exit_code(), 1);
80    }
81
82    #[test]
83    fn unscoped_file_only_is_a_violation() {
84        let mut report = clean_report();
85        report.unscoped_files.push("src/worktrees.rs".to_string());
86        assert!(report.has_violations());
87        assert_eq!(report.exit_code(), 1);
88    }
89}