baobao_codegen/pipeline/phases/validate/
mod.rs

1//! Validate phase - runs lints on the manifest.
2
3mod lint;
4pub mod lints;
5
6use eyre::{Result, bail};
7pub use lint::{Lint, LintInfo};
8pub use lints::{CommandNamingLint, DuplicateCommandLint, EmptyDescriptionLint};
9
10use crate::pipeline::{CompilationContext, Phase};
11
12/// Phase that validates the manifest using configurable lints.
13pub struct ValidatePhase {
14    lints: Vec<Box<dyn Lint>>,
15}
16
17impl ValidatePhase {
18    /// Create a new validate phase with default lints.
19    pub fn new() -> Self {
20        Self {
21            lints: vec![
22                Box::new(CommandNamingLint),
23                Box::new(DuplicateCommandLint),
24                Box::new(EmptyDescriptionLint),
25            ],
26        }
27    }
28
29    /// Create a validate phase with no lints.
30    pub fn empty() -> Self {
31        Self { lints: Vec::new() }
32    }
33
34    /// Add a custom lint to the validation phase.
35    pub fn with_lint(mut self, lint: impl Lint + 'static) -> Self {
36        self.lints.push(Box::new(lint));
37        self
38    }
39}
40
41impl Default for ValidatePhase {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl ValidatePhase {
48    /// Get the names of all lints that will be run.
49    pub fn lint_names(&self) -> Vec<&'static str> {
50        self.lints.iter().map(|l| l.name()).collect()
51    }
52
53    /// Get information about all lints that will be run.
54    pub fn lint_info(&self) -> Vec<LintInfo> {
55        self.lints.iter().map(|l| l.info()).collect()
56    }
57}
58
59impl Phase for ValidatePhase {
60    fn name(&self) -> &'static str {
61        "validate"
62    }
63
64    fn description(&self) -> &'static str {
65        "Check manifest integrity and collect diagnostics"
66    }
67
68    fn run(&self, ctx: &mut CompilationContext) -> Result<()> {
69        // Run all lints
70        for lint in &self.lints {
71            lint.check(&ctx.manifest, &mut ctx.diagnostics);
72        }
73
74        // Fail if there are any errors (warnings are allowed)
75        if ctx.has_errors() {
76            bail!("Validation failed with {} error(s)", ctx.error_count());
77        }
78
79        Ok(())
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use baobao_manifest::Manifest;
86
87    use super::*;
88    use crate::pipeline::Diagnostic;
89
90    fn parse_manifest(content: &str) -> Manifest {
91        toml::from_str(content).expect("Failed to parse test manifest")
92    }
93
94    fn make_test_manifest() -> Manifest {
95        parse_manifest(
96            r#"
97            [cli]
98            name = "test"
99            language = "rust"
100        "#,
101        )
102    }
103
104    #[test]
105    fn test_with_errors() {
106        struct AlwaysErrorLint;
107        impl Lint for AlwaysErrorLint {
108            fn name(&self) -> &'static str {
109                "always-error"
110            }
111            fn description(&self) -> &'static str {
112                "Always produces an error"
113            }
114            fn check(&self, _manifest: &Manifest, diagnostics: &mut Vec<Diagnostic>) {
115                diagnostics.push(Diagnostic::error("test", "forced error"));
116            }
117        }
118
119        let manifest = make_test_manifest();
120        let mut ctx = CompilationContext::new(manifest);
121
122        let phase = ValidatePhase::empty().with_lint(AlwaysErrorLint);
123        let result = phase.run(&mut ctx);
124
125        assert!(result.is_err());
126        assert!(ctx.has_errors());
127    }
128
129    #[test]
130    fn test_warnings_allowed() {
131        let manifest = parse_manifest(
132            r#"
133            [cli]
134            name = "test"
135            language = "rust"
136
137            [commands.deploy]
138            description = ""
139        "#,
140        );
141
142        let mut ctx = CompilationContext::new(manifest);
143
144        // Only use EmptyDescriptionLint which produces warnings
145        let phase = ValidatePhase::empty().with_lint(EmptyDescriptionLint);
146        let result = phase.run(&mut ctx);
147
148        // Warnings don't cause failure
149        assert!(result.is_ok());
150        assert!(ctx.has_warnings());
151        assert!(!ctx.has_errors());
152    }
153}