run_clang_tidy/cli/
mod.rs1use std::{path, process};
2
3mod handlers;
4mod logging;
5pub mod utils;
6
7use clap::{arg, crate_authors, crate_description, crate_name, crate_version, Arg};
8#[allow(unused_imports)]
9use color_eyre::{eyre::eyre, eyre::WrapErr, Help};
10use schemars::{schema_for, JsonSchema};
11use serde::Deserialize;
12
13#[derive(Deserialize, Debug, JsonSchema)]
14#[serde(rename_all = "camelCase")] pub struct JsonModel {
16 pub paths: Vec<String>,
21 pub filter_pre: Option<Vec<String>>,
28 pub filter_post: Option<Vec<String>>,
33 pub tidy_file: Option<path::PathBuf>,
37 pub tidy_root: Option<path::PathBuf>,
40 pub build_root: Option<path::PathBuf>,
43 pub command: Option<path::PathBuf>,
45 #[serde(skip)]
49 pub root: path::PathBuf,
51 #[serde(skip)]
52 pub name: String,
54}
55
56#[derive(Debug)]
61pub struct Data {
62 pub json: JsonModel,
64 pub tidy_file: Option<path::PathBuf>,
66 pub build_root: Option<path::PathBuf>,
69 pub command: Option<path::PathBuf>,
71 pub jobs: Option<u8>,
74 pub ignore_warn: bool,
76 pub quiet: bool,
78 pub fix: bool,
80}
81
82pub struct Builder {
83 pub matches: clap::ArgMatches,
84}
85
86impl Builder {
87 fn app() -> clap::Command {
88 clap::Command::new(crate_name!())
89 .arg_required_else_help(true)
90 .version(crate_version!())
91 .author(crate_authors!())
92 .about(crate_description!())
93 .arg(
94 arg!(<JSON>)
95 .help("Path/configuration as .json")
96 .value_parser(clap::value_parser!(std::path::PathBuf)),
97 )
98 .arg(
99 arg!(-t --tidy ... "Optional path to the .clang-tidy configuration file. \
100 Overrides <JSON> configuration. If no path is provided, \
101 `clang-tidy` will attempt a search for the compile commands \
102 through all parent paths of the file that is being analyzed.")
103 .value_parser(clap::value_parser!(std::path::PathBuf))
104 .required(false)
105 .action(clap::ArgAction::Set),
106 )
107 .arg(
108 clap::Arg::new("build-root")
109 .short('b')
110 .long("build-root")
111 .help(
112 "Optional path to the build root folder which should \
113 contain the compile-commands.json file. Overrides <JSON> \
114 configuration.",
115 )
116 .value_parser(clap::value_parser!(std::path::PathBuf))
117 .action(clap::ArgAction::Set)
118 .required(false),
119 )
120 .arg(
121 arg!(-c --command ... "Optional path to executable or clang-tidy command. \
122 Overrides <JSON> configuration, defaults to `clang-tidy`")
123 .value_parser(clap::value_parser!(std::path::PathBuf))
124 .required(false)
125 .action(clap::ArgAction::Set),
126 )
127 .arg(
128 arg!(-j --jobs ... "Optional parameter to define the number of jobs to use. \
129 If provided without value (e.g., '-j') all available logical \
130 cores are used. Maximum value is 255")
131 .required(false)
132 .num_args(0..=1)
133 .action(clap::ArgAction::Set),
134 )
135 .arg(arg!(-v --verbose ... "Verbosity, use -vv... for verbose output.").global(true))
136 .arg(arg!(--fix "Fix findings, if possible. Executes clang-tidy with the -fix and -fix-errors options."))
137 .arg(
138 arg!(-q --quiet "Suppress all output except for errors; overrides -v")
139 .action(clap::ArgAction::SetTrue),
140 )
141 .arg(
142 Arg::new("suppress-warnings")
143 .long("suppress-warnings")
144 .action(clap::ArgAction::SetTrue)
145 .help("Suppress warnings; overrides -v"),
146 )
147 .subcommand_negates_reqs(true)
148 .subcommand(
149 clap::Command::new("schema")
150 .about("Print the schema used for the <JSON> configuration file"),
151 )
152 }
153
154 pub fn build() -> Builder {
155 let cmd = Builder::app();
156 let builder = Builder {
157 matches: cmd.get_matches(),
158 };
159 logging::setup(&builder.matches);
160 builder
161 }
162
163 pub fn parse(self) -> eyre::Result<Data> {
164 if self.matches.subcommand_matches("schema").is_some() {
165 println!("{}", JsonModel::schema(),);
166 process::exit(0);
167 }
168
169 let json_path = self.path_for_key("JSON", true)?;
170 let json = JsonModel::load(json_path).wrap_err("Invalid parameter for <JSON>")?;
171
172 let tidy_file = match self.matches.contains_id("tidy") {
173 false => None,
174 true => {
175 let tidy_path = self
176 .path_for_key("tidy", true)
177 .wrap_err("Invalid parameter for option --tidy")?;
178 let path = utils::file_with_name_or_ext(tidy_path, ".clang-tidy")
179 .wrap_err("Invalid parameter for option --tidy")?;
180 Some(path)
181 }
182 };
183
184 let command = match self.matches.get_one::<std::path::PathBuf>("command") {
185 None => None,
186 Some(_) => Some(
187 utils::executable_or_exists(self.path_for_key("command", false)?, None)
188 .wrap_err("Invalid parameter for option --command")
189 .suggestion(
190 "Please make sure that '--command' is either a valid absolute path, \
191 a valid path relative to the current working directory \
192 or a known application",
193 )?,
194 ),
195 };
196
197 let build_root = match self.matches.get_one::<std::path::PathBuf>("build-root") {
198 None => None,
199 Some(_) => Some(
200 utils::dir_or_err(self.path_for_key("build-root", false)?)
201 .wrap_err("Invalid parameter for option --build-root")
202 .suggestion(
203 "Please make sure that '--build-root' is either a valid absolute path or \
204 a valid path relative to the current working directory",
205 )?,
206 ),
207 };
208
209 let jobs = {
210 if let Some(val) = self.matches.get_one::<String>("jobs") {
211 let val: u8 = val
212 .parse()
213 .map_err(|_| eyre!("Invalid parameter for option --jobs"))
214 .suggestion("Please provide a number in the range [0 .. 255]")?;
215 Some(val)
216 } else {
217 None
218 }
219 };
220
221 Ok(Data {
222 json,
223 tidy_file,
224 build_root,
225 command,
226 jobs,
227 ignore_warn: self.matches.get_flag("suppress-warnings"),
228 quiet: self.matches.get_flag("quiet"),
230 fix: self.matches.get_flag("fix"),
231 })
232 }
233
234 fn path_for_key(&self, key: &str, check_exists: bool) -> eyre::Result<path::PathBuf> {
235 let path = self
236 .matches
237 .get_one::<std::path::PathBuf>(key)
238 .map(std::path::PathBuf::from)
239 .ok_or(eyre!(format!(
240 "Could not convert parameter '{key}' to path"
241 )))?;
242
243 if check_exists {
244 return utils::path_or_err(path);
245 }
246 Ok(path)
247 }
248}
249
250impl JsonModel {
251 fn schema() -> String {
252 let schema = schema_for!(JsonModel);
253 serde_json::to_string_pretty(&schema).unwrap()
254 }
255
256 fn load(path: impl AsRef<path::Path>) -> eyre::Result<JsonModel> {
257 let json_path = utils::file_with_ext(path.as_ref(), "json", true)?;
258 let json_name = json_path.to_string_lossy();
259
260 let f = std::fs::File::open(path.as_ref())
261 .wrap_err(format!("Failed to open provided JSON file '{json_name}'"))?;
262
263 let mut json: JsonModel = serde_json::from_reader(std::io::BufReader::new(f))
264 .wrap_err(format!("Validation failed for '{json_name}'"))
265 .suggestion(format!(
266 "Please make sure that '{json_name}' is a valid .json file and the contents match the required schema."))?;
267
268 json.root = json_path
269 .canonicalize()
270 .unwrap()
271 .parent()
272 .unwrap()
273 .to_path_buf();
274
275 json.name = json_path.to_string_lossy().into();
276 Ok(json)
277 }
278}