Skip to main content

oparry_cli/commands/
check.rs

1//! Check command - validate codebase
2
3use crate::{CliContext, OutputFormatter, HumanFormatter, JsonFormatter, SarifFormatter};
4use oparry_core::{OutputFormat, Result, ValidationResult};
5use oparry_parser::parser_for_path;
6use oparry_validators::{Validators, TailwindValidator, ImportValidator, RustValidator, ComponentValidator};
7use oparry_validators::{A11yValidator, SecurityValidator, PerformanceValidator, TypeScriptValidator, TestingValidator};
8use oparry_validators::{tailwind::TailwindConfig, imports::ImportConfig, rust::RustConfig, components::ComponentConfig};
9use oparry_validators::{accessibility::A11yConfig, security::SecurityConfig, performance::PerformanceConfig, typescript::TypeScriptConfig, testing::TestingConfig};
10use std::path::PathBuf;
11use glob::glob;
12
13/// Check command
14pub struct CheckCommand {
15    /// Paths to check
16    paths: Vec<PathBuf>,
17    /// Validators to run
18    validators: Vec<String>,
19    /// Output format
20    format: OutputFormat,
21    /// Auto-fix
22    fix: bool,
23    /// Strict mode
24    strict: bool,
25}
26
27impl CheckCommand {
28    /// Create new check command
29    pub fn new(
30        paths: Vec<PathBuf>,
31        validators: Vec<String>,
32        format: OutputFormat,
33        fix: bool,
34        strict: bool,
35    ) -> Self {
36        Self {
37            paths,
38            validators,
39            format,
40            fix,
41            strict,
42        }
43    }
44
45    /// Run the check command
46    pub fn run(&self, ctx: &CliContext) -> Result<()> {
47        // Build validators
48        let validators = self.build_validators(ctx)?;
49
50        // Collect files
51        let files = self.collect_files()?;
52
53        let mut total_result = ValidationResult::new();
54
55        // Check each file
56        for file in files {
57            let content = std::fs::read_to_string(&file)
58                .map_err(|e| oparry_core::Error::File {
59                    path: file.clone(),
60                    source: e,
61                })?;
62
63            let parser = parser_for_path(&file);
64            let parsed = parser.parse(&content)?;
65
66            let result = validators.validate(&parsed, &file)?;
67            total_result.merge(result);
68        }
69
70        // Format and print output
71        let formatter = self.create_formatter();
72        println!("{}", formatter.format_result(&total_result));
73
74        // Apply strict mode if enabled
75        total_result.finalize_with_strict_mode(self.strict);
76
77        // Return appropriate exit code
78        if total_result.is_passing_with_strict(self.strict) {
79            Ok(())
80        } else {
81            Err(oparry_core::Error::Validation(
82                "Validation failed".to_string()
83            ))
84        }
85    }
86
87    /// Build validators from context
88    fn build_validators(&self, ctx: &CliContext) -> Result<Validators> {
89        let mut validators = Validators::new();
90
91        // Add Tailwind validator
92        if self.should_run_validator("tailwind") && ctx.config.tailwind.enabled {
93            let tw_config = TailwindConfig {
94                safe_list: ctx.config.tailwind.safe_list.clone(),
95                block_list: ctx.config.tailwind.block_list.clone(),
96                max_arbitrary: ctx.config.tailwind.max_arbitrary_values,
97                ..Default::default()
98            };
99            validators = validators.with_tailwind(TailwindValidator::new(tw_config));
100        }
101
102        // Add Import validator
103        if self.should_run_validator("imports") {
104            let imp_config = ImportConfig {
105                enforce_alias: ctx.config.imports.enforce_alias,
106                alias_map: ctx.config.imports.alias_map.clone(),
107                require_extensions: ctx.config.imports.require_extensions,
108                ..Default::default()
109            };
110            validators = validators.with_imports(ImportValidator::new(imp_config));
111        }
112
113        // Add Rust validator
114        if self.should_run_validator("rust") && ctx.config.rust.enabled {
115            let rust_config = RustConfig {
116                deny_unsafe: ctx.config.rust.deny_unsafe.is_some(),
117                warn_unwrap: ctx.config.rust.warn_unwrap,
118                enforce_result_handling: ctx.config.rust.enforce_result_handling,
119            };
120            validators = validators.with_rust(RustValidator::new(rust_config));
121        }
122
123        // Add Component validator
124        if self.should_run_validator("components") && ctx.config.components.enforce_shadcn {
125            let comp_config = ComponentConfig {
126                enforce_shadcn: ctx.config.components.enforce_shadcn,
127                shadcn_path: ctx.config.components.shadcn_path.clone(),
128                ..Default::default()
129            };
130            validators = validators.with_components(ComponentValidator::new(comp_config));
131        }
132
133        // Add Accessibility validator
134        if self.should_run_validator("accessibility") || self.should_run_validator("a11y") {
135            let a11y_config = A11yConfig::default();
136            validators = validators.with_accessibility(A11yValidator::new(a11y_config));
137        }
138
139        // Add Security validator
140        if self.should_run_validator("security") {
141            let sec_config = SecurityConfig::default();
142            validators = validators.with_security(SecurityValidator::new(sec_config));
143        }
144
145        // Add Performance validator
146        if self.should_run_validator("performance") || self.should_run_validator("perf") {
147            let perf_config = PerformanceConfig::default();
148            validators = validators.with_performance(PerformanceValidator::new(perf_config));
149        }
150
151        // Add TypeScript validator
152        if self.should_run_validator("typescript") || self.should_run_validator("ts") {
153            let ts_config = TypeScriptConfig::default();
154            validators = validators.with_typescript(TypeScriptValidator::new(ts_config));
155        }
156
157        // Add Testing validator
158        if self.should_run_validator("testing") || self.should_run_validator("test") {
159            let test_config = TestingConfig::default();
160            validators = validators.with_testing(TestingValidator::new(test_config));
161        }
162
163        Ok(validators)
164    }
165
166    /// Check if validator should run
167    fn should_run_validator(&self, name: &str) -> bool {
168        self.validators.is_empty() || self.validators.iter().any(|v| v == name)
169    }
170
171    /// Collect all files to check
172    fn collect_files(&self) -> Result<Vec<PathBuf>> {
173        let mut files = Vec::new();
174
175        let patterns = if self.paths.is_empty() {
176            vec!["**/*.{ts,tsx,js,jsx,rs}"]
177        } else {
178            self.paths.iter().map(|p| p.to_str().unwrap()).collect()
179        };
180
181        for pattern in patterns {
182            if let Ok(entries) = glob(pattern) {
183                for entry in entries.flatten() {
184                    if entry.is_file() {
185                        files.push(entry);
186                    }
187                }
188            } else {
189                // Single file
190                files.push(PathBuf::from(pattern));
191            }
192        }
193
194        Ok(files)
195    }
196
197    /// Create output formatter
198    fn create_formatter(&self) -> Box<dyn OutputFormatter> {
199        match self.format {
200            OutputFormat::Human => Box::new(HumanFormatter::new(true, true)),
201            OutputFormat::Json => Box::new(JsonFormatter::new()),
202            OutputFormat::Sarif => Box::new(SarifFormatter::new()),
203        }
204    }
205}