run_clang_tidy/cmd/
mod.rs

1use std::{io, path, process, str::FromStr};
2
3#[derive(Clone)]
4struct Version {
5    major: u8,
6    minor: u8,
7    patch: u8,
8}
9
10#[derive(Debug)]
11pub enum RunResult {
12    Ok,
13    Err(String),
14    Warn(String),
15}
16
17impl From<&io::Error> for RunResult {
18    fn from(value: &io::Error) -> Self {
19        RunResult::Err(value.to_string())
20    }
21}
22
23impl FromStr for Version {
24    type Err = String;
25
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        let re = regex::Regex::new(r".*version ([\d]+)\.([\d]+)\.([\d]+).*").unwrap();
28        let caps = re.captures(s).ok_or("Failed to match version")?;
29
30        Ok(Version {
31            major: caps[1].parse().map_err(|_| "Invalid major version")?,
32            minor: caps[2].parse().map_err(|_| "Invalid minor version")?,
33            patch: caps[3].parse().map_err(|_| "Invalid patch level")?,
34        })
35    }
36}
37
38pub struct Runner {
39    cmd: path::PathBuf,
40    version: Option<Version>,
41}
42
43impl Runner {
44    pub fn new<P>(path: P) -> Runner
45    where
46        P: AsRef<path::Path>,
47    {
48        let cmd = path::PathBuf::from(path.as_ref());
49        Runner { cmd, version: None }
50    }
51
52    fn eval_status(status: process::ExitStatus) -> Result<(), io::Error> {
53        match status.code() {
54            Some(0) => (),
55            Some(code) => {
56                return Err(io::Error::new(
57                    io::ErrorKind::Other,
58                    format!("Process terminated with code {code}"),
59                ));
60            }
61            None => {
62                return Err(io::Error::new(
63                    io::ErrorKind::Interrupted,
64                    "Process terminated by signal",
65                ))
66            }
67        };
68        Ok(())
69    }
70
71    pub fn get_version(&self) -> Option<String> {
72        self.version
73            .as_ref()
74            .map(|v| format!("{}.{}.{}", v.major, v.minor, v.patch))
75    }
76
77    pub fn get_path(&self) -> path::PathBuf {
78        self.cmd.clone()
79    }
80
81    pub fn validate(&mut self) -> Result<(), io::Error> {
82        let cmd = process::Command::new(self.cmd.as_path())
83            .arg("--version")
84            .output()?;
85
86        if let Err(err) = Runner::eval_status(cmd.status) {
87            log::error!(
88                "Execution failed:\n{}",
89                String::from_utf8_lossy(&cmd.stderr)
90            );
91            return Err(err);
92        }
93
94        // example output of clang-format:
95        // clang-format version 4.0.0 (tags/checker/checker-279)
96        let stdout = String::from_utf8_lossy(&cmd.stdout);
97
98        self.version = Some(stdout.parse::<Version>().map_err(|err| {
99            io::Error::new(
100                io::ErrorKind::Other,
101                format!("Failed to parse --version output {stdout}: {err}"),
102            )
103        })?);
104        Ok(())
105    }
106
107    fn run(mut cmd: process::Command, ignore_warn: bool) -> RunResult {
108        let output = cmd.output();
109        if let Err(err) = &output {
110            return err.into();
111        }
112        let output = output.unwrap();
113
114        let stderr = String::from_utf8_lossy(&output.stderr);
115        let stdout = String::from_utf8_lossy(&output.stdout);
116
117        if let Err(err) = Runner::eval_status(output.status) {
118            if stderr.len() != 0 {
119                return RunResult::Err(format!("{err}\n---\n{stderr}---\n{stdout}"));
120            }
121            return (&err).into();
122        } else if !ignore_warn && !stderr.is_empty() {
123            return RunResult::Warn(format!("warnings encountered\n---\n{stderr}---\n{stdout}"));
124        }
125        RunResult::Ok
126    }
127
128    pub fn run_tidy<P, Q>(&self, file: P, build_root: Q, fix: bool, ignore_warn: bool) -> RunResult
129    where
130        P: AsRef<path::Path>,
131        Q: AsRef<path::Path>,
132    {
133        let mut cmd = process::Command::new(self.cmd.as_path());
134
135        cmd.arg(file.as_ref().as_os_str());
136        // TODO: the --config-file option does not exist for clang-tidy 10.0
137        // if let Some(config_file) = config_file {
138        //     cmd.arg(format!(
139        //         "--config-file={}",
140        //         config_file.as_ref().to_string_lossy()
141        //     ));
142        // }
143        cmd.arg(format!("-p={}", build_root.as_ref().to_string_lossy()));
144        if fix {
145            cmd.arg("-fix").arg("-fix-errors");
146        }
147        // This suppresses printing statistics about ignored warnings:
148        // cmd.arg("-quiet");
149
150        Runner::run(cmd, ignore_warn)
151    }
152
153    pub fn supports_config_file(&self) -> Result<(), io::Error> {
154        if self.version.is_none() {
155            return Err(io::Error::new(
156                io::ErrorKind::Other,
157                "Unknown version, --config-file requires \
158                clang-format version 12.0.0 or higher",
159            ));
160        }
161
162        let version = self.version.as_ref().unwrap();
163        if version.major < 9u8 {
164            return Err(io::Error::new(
165                io::ErrorKind::Other,
166                format!(
167                    "Invalid version {}, --config-file check requires \
168                    clang-format version 12.0.0 or higher",
169                    self.get_version().unwrap()
170                ),
171            ));
172        }
173
174        Ok(())
175    }
176}
177
178impl Clone for Runner {
179    fn clone(&self) -> Runner {
180        Runner {
181            cmd: path::PathBuf::from(self.cmd.as_path()),
182            version: self.version.clone(),
183        }
184    }
185}