Skip to main content

lintel_check/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::time::Instant;
4
5use anyhow::Result;
6use bpaf::Bpaf;
7
8use lintel_diagnostics::reporter::{CheckResult, CheckedFile, Reporter};
9
10// -----------------------------------------------------------------------
11// CheckArgs — CLI struct for the `lintel check` command
12// -----------------------------------------------------------------------
13
14#[derive(Debug, Clone, Bpaf)]
15#[bpaf(generate(check_args_inner))]
16pub struct CheckArgs {
17    /// Automatically fix formatting issues
18    #[bpaf(long("fix"), switch)]
19    pub fix: bool,
20
21    #[bpaf(external(lintel_validate::validate_args))]
22    pub validate: lintel_validate::ValidateArgs,
23}
24
25/// Construct the bpaf parser for `CheckArgs`.
26pub fn check_args() -> impl bpaf::Parser<CheckArgs> {
27    check_args_inner()
28}
29
30/// Run all checks and return the result without reporting.
31///
32/// Calls `on_file_checked` for each file as it is validated (for streaming
33/// progress). The caller is responsible for reporting the result.
34///
35/// # Errors
36///
37/// Returns an error if schema validation fails to run (e.g. network or I/O issues).
38pub async fn check(
39    args: &mut CheckArgs,
40    on_file_checked: impl FnMut(&CheckedFile),
41) -> Result<CheckResult> {
42    // Save original args before validate's merge_config modifies them.
43    let original_globs = args.validate.globs.clone();
44    let original_exclude = args.validate.exclude.clone();
45
46    lintel_validate::merge_config(&mut args.validate);
47
48    let lib_args = lintel_validate::validate::ValidateArgs::from(&args.validate);
49
50    // Collect and read files once.
51    let files = lintel_validate::validate::collect_files(&lib_args.globs, &lib_args.exclude)?;
52    let mut read_errors = Vec::new();
53    let file_contents = lintel_validate::validate::read_files(&files, &mut read_errors).await;
54
55    if args.fix {
56        let fixed = lintel_format::fix_format(&original_globs, &original_exclude)?;
57        if fixed > 0 {
58            eprintln!("Fixed formatting in {fixed} file(s).");
59        }
60
61        let mut result = lintel_validate::validate::run_with_contents(
62            &lib_args,
63            file_contents,
64            None,
65            on_file_checked,
66        )
67        .await?;
68        result.errors.extend(read_errors);
69        sort_errors(&mut result.errors);
70        Ok(result)
71    } else {
72        // Check formatting using pre-read contents (borrows, no extra I/O).
73        let format_errors = lintel_format::check_format_contents(
74            &file_contents,
75            &original_globs,
76            &original_exclude,
77        );
78
79        // Run validation (takes ownership of file contents).
80        let mut result = lintel_validate::validate::run_with_contents(
81            &lib_args,
82            file_contents,
83            None,
84            on_file_checked,
85        )
86        .await?;
87
88        // Merge format errors and I/O errors, then sort.
89        result.errors.extend(format_errors);
90        result.errors.extend(read_errors);
91        sort_errors(&mut result.errors);
92
93        Ok(result)
94    }
95}
96
97/// Run all checks: schema validation and formatting.
98///
99/// Discovers files once and shares the file list between validation and
100/// formatting. Config is loaded once.
101///
102/// Returns `Ok(true)` if any errors were found, `Ok(false)` if clean.
103///
104/// # Errors
105///
106/// Returns an error if schema validation fails to run (e.g. network or I/O issues).
107pub async fn run(args: &mut CheckArgs, reporter: &mut dyn Reporter) -> Result<bool> {
108    let start = Instant::now();
109    let result = check(args, |file| reporter.on_file_checked(file)).await?;
110    let had_errors = result.has_errors();
111    let elapsed = start.elapsed();
112    reporter.report(result, elapsed);
113    Ok(had_errors)
114}
115
116fn sort_errors(errors: &mut [lintel_diagnostics::LintelDiagnostic]) {
117    errors.sort_by(|a, b| {
118        a.path()
119            .cmp(b.path())
120            .then_with(|| a.offset().cmp(&b.offset()))
121    });
122}