Skip to main content

es_fluent_cli/commands/check/
mod.rs

1//! Check command for validating FTL files against inventory-registered types.
2//!
3//! This module provides functionality to check FTL files by:
4//! - Running a temp crate that collects inventory registrations (expected keys/variables)
5//! - Parsing FTL files directly using fluent-syntax (for proper ParserError handling)
6//! - Comparing FTL files against the expected keys and variables from Rust code
7//! - Reporting missing keys as errors
8//! - Reporting missing variables as warnings
9
10mod inventory;
11mod validation;
12
13use crate::commands::{WorkspaceArgs, WorkspaceCrates};
14use crate::core::{CliError, ValidationIssue, ValidationReport};
15use crate::generation::{prepare_monolithic_runner_crate, run_monolithic};
16use crate::utils::ui;
17use clap::Parser;
18use std::collections::HashSet;
19
20/// Arguments for the check command.
21#[derive(Debug, Parser)]
22pub struct CheckArgs {
23    #[command(flatten)]
24    pub workspace: WorkspaceArgs,
25
26    /// Check all locales, not just the fallback language.
27    #[arg(long)]
28    pub all: bool,
29
30    /// Crates to skip during validation. Can be specified multiple times
31    /// (e.g., --ignore foo --ignore bar) or comma-separated (e.g., --ignore foo,bar).
32    #[arg(long, value_delimiter = ',')]
33    pub ignore: Vec<String>,
34
35    /// Force rebuild of the runner, ignoring the staleness cache.
36    #[arg(long)]
37    pub force_run: bool,
38}
39
40/// Run the check command.
41pub fn run_check(args: CheckArgs) -> Result<(), CliError> {
42    let workspace = WorkspaceCrates::discover(args.workspace)?;
43
44    if !workspace.print_discovery(ui::print_check_header) {
45        ui::print_no_crates_found();
46        return Ok(());
47    }
48
49    // Convert ignore list to a HashSet for efficient lookups
50    let ignore_crates: HashSet<String> = args.ignore.into_iter().collect();
51    let force_run = args.force_run;
52
53    // Filter out ignored crates
54    let crates_to_check: Vec<_> = workspace
55        .valid
56        .iter()
57        .filter(|k| !ignore_crates.contains(&k.name))
58        .collect();
59
60    // Validate that all ignored crates are known
61    if !ignore_crates.is_empty() {
62        let all_crate_names: HashSet<String> =
63            workspace.valid.iter().map(|k| k.name.clone()).collect();
64
65        let mut unknown_crates: Vec<&String> = ignore_crates
66            .iter()
67            .filter(|c| !all_crate_names.contains(*c))
68            .collect();
69
70        if !unknown_crates.is_empty() {
71            // Sort for deterministic error messages
72            unknown_crates.sort();
73
74            return Err(CliError::Other(format!(
75                "Unknown crates passed to --ignore: {}",
76                unknown_crates
77                    .iter()
78                    .map(|c| format!("'{}'", c))
79                    .collect::<Vec<_>>()
80                    .join(", ")
81            )));
82        }
83    }
84
85    if crates_to_check.is_empty() {
86        ui::print_no_crates_found();
87        return Ok(());
88    }
89
90    // Prepare monolithic temp crate once for all checks
91    prepare_monolithic_runner_crate(&workspace.workspace_info)
92        .map_err(|e| CliError::Other(e.to_string()))?;
93
94    // First pass: collect all expected keys from crates
95    let temp_dir =
96        es_fluent_derive_core::get_es_fluent_temp_dir(&workspace.workspace_info.root_dir);
97
98    let pb = ui::create_progress_bar(crates_to_check.len() as u64, "Collecting keys...");
99
100    for krate in &crates_to_check {
101        pb.set_message(format!("Scanning {}", krate.name));
102        run_monolithic(
103            &workspace.workspace_info,
104            "check",
105            &krate.name,
106            &[],
107            force_run,
108        )
109        .map_err(|e| CliError::Other(e.to_string()))?;
110        pb.inc(1);
111    }
112
113    pb.finish_and_clear();
114
115    // Second pass: validate FTL files
116    let mut all_issues: Vec<ValidationIssue> = Vec::new();
117
118    let pb = ui::create_progress_bar(crates_to_check.len() as u64, "Checking crates...");
119
120    for krate in &crates_to_check {
121        pb.set_message(format!("Checking {}", krate.name));
122
123        match validation::validate_crate(
124            krate,
125            &workspace.workspace_info.root_dir,
126            &temp_dir,
127            args.all,
128        ) {
129            Ok(issues) => {
130                all_issues.extend(issues);
131            },
132            Err(e) => {
133                // If error, print above progress bar
134                pb.suspend(|| {
135                    ui::print_check_error(&krate.name, &e.to_string());
136                });
137            },
138        }
139        pb.inc(1);
140    }
141
142    pb.finish_and_clear();
143
144    // Sort issues for deterministic output
145    all_issues.sort_by_cached_key(|issue| issue.sort_key());
146
147    let error_count = all_issues
148        .iter()
149        .filter(|i| {
150            matches!(
151                i,
152                ValidationIssue::MissingKey(_) | ValidationIssue::SyntaxError(_)
153            )
154        })
155        .count();
156    let warning_count = all_issues
157        .iter()
158        .filter(|i| matches!(i, ValidationIssue::MissingVariable(_)))
159        .count();
160
161    if all_issues.is_empty() {
162        ui::print_check_success();
163        Ok(())
164    } else {
165        Err(CliError::Validation(ValidationReport {
166            error_count,
167            warning_count,
168            issues: all_issues,
169        }))
170    }
171}