lisensor/
runner.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2025-2026 Pistonite
3
4use std::collections::BTreeMap;
5use std::path::PathBuf;
6use std::sync::Arc;
7
8use crate::{Config, format};
9
10/// Issues found
11#[derive(Debug, Default, Clone, PartialEq)]
12pub struct Failure {
13    pub errors: Vec<String>,
14}
15
16impl std::fmt::Display for Failure {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        for e in &self.errors {
19            e.fmt(f)?;
20            writeln!(f)?;
21        }
22        Ok(())
23    }
24}
25
26/// Run the tool for the given config.
27///
28/// - `Ok(Ok(())` means successful.
29/// - `Ok(Err(failure))` means the run was successful, but issues are found.
30/// - `Err(e)` means the run itself was not successful.
31pub async fn run(config: Config, fix: bool) -> cu::Result<Result<(), Failure>> {
32    let bar = cu::progress_unbounded_lowp(if fix {
33        "fixing files"
34    } else {
35        "processing files"
36    });
37
38    let mut path_map = BTreeMap::new();
39    let mut handles = Vec::new();
40    let mut no_match_glob = Vec::new();
41    let mut glob_errors = Vec::new();
42
43    // avoid opening too many files. max open 1024 files
44    let pool = cu::co::pool(1024);
45    for (glob, holder, license) in config.into_iter() {
46        let result = run_glob(
47            &glob,
48            holder,
49            license,
50            fix,
51            &pool,
52            &mut handles,
53            &mut path_map,
54        );
55        match result {
56            Ok(matched) => {
57                if !matched {
58                    no_match_glob.push(glob);
59                }
60            }
61            Err(e) => {
62                glob_errors.push((glob, e));
63            }
64        }
65    }
66    // put handles into a set to be auto aborted
67    // with error handling below
68    let total = handles.len();
69    bar.set_total(total);
70    let mut set = cu::co::set(handles);
71
72    // handle glob errors first
73    if !glob_errors.is_empty() {
74        for (glob, error) in &glob_errors {
75            cu::error!("while globbing '{glob}': {error}");
76        }
77        cu::error!(
78            "got {} errors while searching for files, see above",
79            glob_errors.len()
80        );
81        cu::bail!("error while searching for files");
82    }
83
84    let mut count = 0;
85    let mut errors = vec![];
86    while let Some(result) = set.next().await {
87        // join error
88        let (path, result) = result?;
89        // handle check error
90        if let Err(e) = result {
91            errors.push(e)
92        }
93        count += 1;
94        cu::progress!(&bar, count, "{}", path.display());
95    }
96
97    if !errors.is_empty() {
98        let failed = errors.len();
99        cu::error!("checked {total} files, found {failed} issue(s).");
100        cu::hint!("run with --fix to fix them automatically.");
101
102        let errors = errors.into_iter().map(|x| x.to_string()).collect();
103
104        return Ok(Err(Failure { errors }));
105    }
106
107    cu::info!("license check successful for {total} files.");
108    Ok(Ok(()))
109}
110
111fn run_glob(
112    glob: &str,
113    holder: Arc<String>,
114    license: Arc<String>,
115    fix: bool,
116    pool: &cu::co::Pool,
117    handles: &mut Vec<cu::co::Handle<(PathBuf, cu::Result<()>)>>,
118    path_map: &mut BTreeMap<PathBuf, (Arc<String>, Arc<String>)>,
119) -> cu::Result<bool> {
120    let mut matched = false;
121    for path in cu::fs::glob(glob)? {
122        let path = path?;
123        if !path.is_file() {
124            continue;
125        }
126        matched = true;
127        let holder = Arc::clone(&holder);
128        let license = Arc::clone(&license);
129
130        // in fix mode, run additional check for if there are conflicts
131        // in the config. Otherwise, the fix result is arbitrary
132        let handle = if fix {
133            use std::collections::btree_map::Entry;
134            match path_map.entry(path.clone()) {
135                Entry::Occupied(e) => {
136                    let (existing_h, existing_l) = e.get();
137                    if (existing_h, existing_l) != (&holder, &license) {
138                        cu::error!(
139                            "file '{}' matched by multiple globs of conflicting config!",
140                            e.key().display()
141                        );
142                        cu::error!(
143                            "- in one config, it has holder '{holder}' and license '{license}'"
144                        );
145                        cu::error!(
146                            "- in another, it has holder '{existing_h}' and license '{existing_l}'"
147                        );
148                        cu::bail!(
149                            "conflicting config found for '{}', while globbing '{glob}'",
150                            e.key().display()
151                        );
152                    }
153                    // since the file is already checked by previous job,
154                    // we can just skip it
155                    continue;
156                }
157                Entry::Vacant(e) => e.insert((Arc::clone(&holder), Arc::clone(&license))),
158            };
159            pool.spawn(async move {
160                let check_result = format::check_file(&path, &holder, &license);
161                let Err(e) = check_result else {
162                    return (path, Ok(()));
163                };
164                cu::trace!("'{}': {e}", path.display());
165                cu::debug!("fixing '{}'", path.display());
166                let Err(e) = format::fix_file(&path, &holder, &license) else {
167                    return (path, Ok(()));
168                };
169                cu::error!("failed to fix '{}': {e}", path.display());
170                (path, Err(e))
171            })
172        } else {
173            pool.spawn(async move {
174                let Err(e) = format::check_file(&path, &holder, &license) else {
175                    return (path, Ok(()));
176                };
177                cu::warn!("'{}': {e}", path.display());
178                (path, Err(e))
179            })
180        };
181
182        handles.push(handle);
183    }
184
185    Ok(matched)
186}