run_clang_tidy/cli/
mod.rs

1use 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")] // removed: deny_unknown_fields
15pub struct JsonModel {
16    /// List of paths and/or globs.
17    /// This list may contain paths or shell-style globs to define the files that should be
18    /// filtered. Paths or globs that resolve to folders will be silently ignored. Any path
19    /// contained in this list must be specified relative to the configuration file.
20    pub paths: Vec<String>,
21    /// Optional list of globs used for efficiently pre-filtering paths.
22    /// In contrast to the post-filter, searching will completely skip all paths and its siblings
23    /// for any match with any pattern. E.g., [".git"] will skip all ".git" folders completely.
24    /// By default, i.e., if this field is not present in the configuration, the tool will skip all
25    /// hidden paths and files. Set this entry to an empty list to prevent any kind of
26    /// pre-filtering.
27    pub filter_pre: Option<Vec<String>>,
28    /// Optional list of globs to use for post-filtering.
29    /// This filter will be applied for all paths _after_ they have been resolved. In contrast to
30    /// the pre-filter, siblings of paths will not be filtered without the corresponding glob. E.g.,
31    /// ".git" will not filter any files, only ".git/**" would. Notice that only
32    pub filter_post: Option<Vec<String>>,
33    /// Optional path to a `.clang-tidy` yaml file (can be specified via --tidy). If no such path
34    /// is provided by neither this field nor the command-line option, `clang-tidy` will perform a
35    /// search for the `compile_commands.json` through all parent paths of the file to analyze.
36    pub tidy_file: Option<path::PathBuf>,
37    // TODO: allow this config to be skipped for clang-tidy >= 12.0.0
38    /// Optional path where the `.clang-tidy` file should be copied to while executing.
39    pub tidy_root: Option<path::PathBuf>,
40    /// Optional path to the folder that contains the `compile-commands.json` (can be specified
41    /// via --build-root).
42    pub build_root: Option<path::PathBuf>,
43    /// Optional path to the `clang-tidy` executable or command name
44    pub command: Option<path::PathBuf>,
45    /// Remove text from compile-commands.json
46    // pub remove: Option<Vec<(String, String)>>,
47    // TODO: allow to specify additional options
48    #[serde(skip)]
49    /// Parent directory of the Json file, used to resolve paths specified within
50    pub root: path::PathBuf,
51    #[serde(skip)]
52    /// Lossy Json filename
53    pub name: String,
54}
55
56// goal: have compatible .json configuration files for clang-format and clang-tidy
57// it should be possible to specify all command line options and non-unit relative paths
58// using the command line, such that they can be set using ENV variables
59
60#[derive(Debug)]
61pub struct Data {
62    /// Json input data
63    pub json: JsonModel,
64    /// Command-line override for the tidy file
65    pub tidy_file: Option<path::PathBuf>,
66    // TODO: add tidy_root override
67    /// Command-line override for the build root folder
68    pub build_root: Option<path::PathBuf>,
69    /// Command-line override for the clang-tidy executable
70    pub command: Option<path::PathBuf>,
71    /// Command-line parameter for the number of jobs to use for executing clang-tidy
72    /// If `None` then all available jobs should be used, else the specified number of jobs.
73    pub jobs: Option<u8>,
74    /// Command-line option to suppress warnings issued by clang-tidy.
75    pub ignore_warn: bool,
76    /// Suppress all logging.
77    pub quiet: bool,
78    /// Run with -fix argument.
79    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            // TODO: replace quiet flag with own logger implementation.
229            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}