Skip to main content

lintel_validate/
lib.rs

1#![doc = include_str!("../README.md")]
2#![allow(unused_assignments)] // thiserror/miette derive macros trigger false positives
3
4extern crate alloc;
5
6use std::time::Instant;
7
8use anyhow::Result;
9use bpaf::{Bpaf, ShellComp};
10use lintel_diagnostics::reporter::{CheckResult, Reporter};
11
12use lintel_cli_common::CliCacheOptions;
13
14// -----------------------------------------------------------------------
15// Core validation modules
16// -----------------------------------------------------------------------
17
18pub mod catalog;
19pub mod parsers;
20pub mod registry;
21pub(crate) mod suggest;
22pub mod validate;
23
24// -----------------------------------------------------------------------
25// ValidateArgs — shared CLI struct
26// -----------------------------------------------------------------------
27
28#[derive(Debug, Clone, Bpaf)]
29pub struct ValidateArgs {
30    #[bpaf(long("exclude"), argument("PATTERN"))]
31    pub exclude: Vec<String>,
32
33    #[bpaf(external(lintel_cli_common::cli_cache_options))]
34    pub cache: CliCacheOptions,
35
36    #[bpaf(positional("PATH"), complete_shell(ShellComp::File { mask: None }))]
37    pub globs: Vec<String>,
38}
39
40impl From<&ValidateArgs> for validate::ValidateArgs {
41    fn from(args: &ValidateArgs) -> Self {
42        // When a single directory is passed as an arg, use it as the config
43        // search directory so that `lintel.toml` inside that directory is found.
44        let config_dir = args
45            .globs
46            .iter()
47            .find(|g| std::path::Path::new(g).is_dir())
48            .map(std::path::PathBuf::from);
49
50        validate::ValidateArgs {
51            globs: args.globs.clone(),
52            exclude: args.exclude.clone(),
53            cache_dir: args.cache.cache_dir.clone(),
54            force_schema_fetch: args.cache.force_schema_fetch || args.cache.force,
55            force_validation: args.cache.force_validation || args.cache.force,
56            no_catalog: args.cache.no_catalog,
57            config_dir,
58            schema_cache_ttl: args.cache.schema_cache_ttl,
59        }
60    }
61}
62
63// -----------------------------------------------------------------------
64// Helpers
65// -----------------------------------------------------------------------
66
67/// Load `lintel.toml` and merge its excludes into the args.
68///
69/// Config excludes are prepended so they have the same priority as CLI excludes.
70/// When a directory arg is passed (e.g. `lintel check some/dir`), we search
71/// for `lintel.toml` starting from that directory rather than cwd.
72pub fn merge_config(args: &mut ValidateArgs) {
73    let search_dir = args
74        .globs
75        .iter()
76        .find(|g| std::path::Path::new(g).is_dir())
77        .map(std::path::PathBuf::from);
78
79    let cfg_result = match &search_dir {
80        Some(dir) => lintel_config::find_and_load(dir).map(Option::unwrap_or_default),
81        None => lintel_config::load(),
82    };
83
84    match cfg_result {
85        Ok(cfg) => {
86            // Config excludes first, then CLI excludes.
87            let cli_excludes = core::mem::take(&mut args.exclude);
88            args.exclude = cfg.exclude;
89            args.exclude.extend(cli_excludes);
90        }
91        Err(e) => {
92            eprintln!("warning: failed to load lintel.toml: {e}");
93        }
94    }
95}
96
97// -----------------------------------------------------------------------
98// Run function — shared between check/ci/validate commands
99// -----------------------------------------------------------------------
100
101/// Run validation and report results via the given reporter.
102///
103/// Returns `true` if there were validation errors, `false` if clean.
104///
105/// # Errors
106///
107/// Returns an error if file collection or schema validation encounters an I/O error.
108pub async fn run(args: &mut ValidateArgs, reporter: &mut dyn Reporter) -> Result<bool> {
109    merge_config(args);
110
111    let lib_args = validate::ValidateArgs::from(&*args);
112    let start = Instant::now();
113    let result: CheckResult = validate::run_with(&lib_args, None, |file| {
114        reporter.on_file_checked(file);
115    })
116    .await?;
117    let had_errors = result.has_errors();
118    let elapsed = start.elapsed();
119
120    reporter.report(result, elapsed);
121
122    Ok(had_errors)
123}