1use std::io::Write;
2use std::process::ExitCode;
3
4use clap::Parser;
5
6use crate::checkers::{
7 base::{Action, Check},
8 RelativeUrl,
9};
10
11use super::checkers::read_checks_from_path;
12
13#[derive(Copy, Clone)]
14pub enum ExitStatus {
15 Success,
17 Failure,
19 Error,
21}
22
23impl From<ExitStatus> for ExitCode {
24 fn from(status: ExitStatus) -> Self {
25 match status {
26 ExitStatus::Success => ExitCode::from(0),
27 ExitStatus::Failure => ExitCode::from(1),
28 ExitStatus::Error => ExitCode::from(2),
29 }
30 }
31}
32
33#[derive(Parser)]
36#[command(author, version, about, long_about = None)]
37struct Cli {
38 #[arg(short, long, env = "CHECK_CONFIG_PATH", verbatim_doc_comment)]
43 path: Option<String>,
44
45 #[arg(long, default_value = "false")]
47 fix: bool,
48
49 #[arg(short, long, default_value = "false")]
51 list_checkers: bool,
52
53 #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ANY_TAGS")]
55 any_tags: Vec<String>,
56
57 #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ALL_TAGS")]
59 all_tags: Vec<String>,
60
61 #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_SKIP_TAGS")]
63 skip_tags: Vec<String>,
64
65 #[arg(short, long, default_value = "false", env = "CHECK_CONFIG_CREATE_DIRS")]
67 create_missing_directories: bool,
68
69 #[clap(flatten)]
72 verbose: clap_verbosity_flag::Verbosity,
73}
74
75pub(crate) fn filter_checks(
76 checker_tags: &[String],
77 any_tags: &[String],
78 all_tags: &[String],
79 skip_tags: &[String],
80) -> bool {
81 if !any_tags.is_empty() && !any_tags.iter().any(|t| checker_tags.contains(t)) {
83 return false;
84 }
85
86 if !all_tags.is_empty() && !all_tags.iter().all(|t| checker_tags.contains(t)) {
88 return false;
89 }
90
91 if !skip_tags.is_empty() && skip_tags.iter().any(|t| checker_tags.contains(t)) {
93 return false;
94 }
95
96 true
97}
98
99pub(crate) fn parse_path_str_to_uri(path: &str) -> Option<url::Url> {
100 if path.starts_with("/") {
101 super::uri::parse_uri(format!("file://{path}").as_str(), None).ok()
102 } else {
103 let cwd = super::uri::parse_uri(
104 &format!(
105 "file://{}/",
106 std::env::current_dir().unwrap().as_path().to_str().unwrap()
107 ),
108 None,
109 )
110 .unwrap();
111 super::uri::parse_uri(path, Some(&cwd)).ok()
112 }
113}
114pub fn cli() -> ExitCode {
115 let cli = Cli::parse();
116 env_logger::Builder::new()
117 .filter_level(cli.verbose.log_level_filter())
118 .format(|buf, record| writeln!(buf, "{}", record.args()))
119 .init();
120
121 log::info!("Starting check-config");
122
123 let mut checks = match cli.path {
124 Some(path_str) => match parse_path_str_to_uri(path_str.as_str()) {
125 Some(uri) => {
126 log::info!("Using checkers from {}", &uri.short_url_str());
127 read_checks_from_path(&uri, vec![])
128 }
129 None => {
130 log::error!(
131 "Unable to load checkers. Path ({path_str}) specified is not a valid path.",
132 );
133 return ExitCode::from(ExitStatus::Error);
134 }
135 },
136 None => {
137 log::warn!("⚠️ No path specified. Trying check-config.toml");
138 let uri = parse_path_str_to_uri("check-config.toml").expect("valid path");
139 match std::path::Path::new(uri.path()).exists() {
140 true => {
141 log::info!("Using checkers from {}", &uri);
142 read_checks_from_path(&uri, vec![])
143 }
144 false => {
145 log::warn!("check-config.toml does not exists.");
146 log::warn!("Trying pyproject.toml.");
147 let uri = parse_path_str_to_uri("pyproject.toml").expect("valid path");
148 match std::path::Path::new(uri.path()).exists() {
149 true => {
150 log::info!("Using checkers from {}", &uri);
151 read_checks_from_path(&uri, vec!["tool", "check-config"])
152 }
153 false => {
154 log::error!("⚠️ No path specified and default paths are not found, so we ran out of options to load the config");
155 return ExitCode::from(ExitStatus::Error);
156 }
157 }
158 }
159 }
160 }
161 };
162
163 log::info!("Fix: {}", &cli.fix);
164
165 if cli.list_checkers {
166 log::error!("List of checks (type, location of definition, file to check, tags)");
167 checks.iter().for_each(|check| {
168 let enabled = filter_checks(
169 &check.generic_check().tags,
170 &cli.any_tags,
171 &cli.all_tags,
172 &cli.skip_tags,
173 );
174
175 log::error!(
176 "{} {} - {} - {} - {:?}",
177 if enabled { "⬜" } else { " ✖️" },
178 check.generic_check().file_with_checks.short_url_str(),
179 check
180 .generic_check()
181 .file_to_check
182 .as_os_str()
183 .to_string_lossy(),
184 check.check_type(),
185 check.generic_check().tags
186 )
187 });
188 return ExitCode::from(ExitStatus::Success);
189 }
190
191 checks.retain(|check| {
197 filter_checks(
198 &check.generic_check().tags,
199 &cli.any_tags,
200 &cli.all_tags,
201 &cli.skip_tags,
202 )
203 });
204
205 dbg!(checks.len());
206
207 let (action_count, success_count) =
208 run_checks(&checks, cli.fix, cli.create_missing_directories);
209
210 log::warn!("{success_count} checks successful.");
211 if action_count > 0 {
212 if action_count == 1 {
214 log::error!("🪛 There is 1 violation to fix.",);
215 } else {
216 log::error!("🪛 There are {action_count} violations to fix.",);
217 }
218 ExitCode::from(ExitStatus::Failure)
219 } else {
220 log::error!("🥇 No violations found.");
222 ExitCode::from(ExitStatus::Success)
223 }
224}
225
226pub(crate) fn run_checks(
227 checks: &Vec<Box<dyn Check>>,
228 fix: bool,
229 create_missing_directories: bool,
230) -> (i32, i32) {
231 let mut action_count = 0;
232 let mut success_count = 0;
233
234 for check in checks {
235 let result = if fix {
236 check.fix(create_missing_directories)
237 } else {
238 check.check()
239 };
240 match result {
241 Err(_) => {
242 log::error!("⚠ There was an error fixing files.");
243 return (0, 0);
244 }
245 Ok(Action::None) => success_count += 1,
246 Ok(_action) => {
247 action_count += 1;
248 }
249 };
250 }
251
252 (action_count, success_count)
253}