Skip to content

Commit 6a0edce

Browse files
committed
style: follow up to #204
- Allow boolean options to be treated as flags if their default value is false. - Some other CLI options can now accept no value (like a flag) including `--verbosity` - Use `std::str::FromStr` trait to parse the given version: * if no value is given, then behave exactly like the `cpp-linter version` subcommand (print version and exit) * if a path is given look for clang-tools in that path. This only ensures the given path exists, not if the clang- tool is present; that is done later if the tool is needed. * expand support for version specifiers using `semver::VersionReq`. This removes the need for `lenient_semver` dependency and allows the user to specify a range of clang versions. For example, `>=14, <16`, `=12.0.1`, or simply `16` (would be treated as `=16`). See [semver::VersionReq docs][ver-req-docs] for more detail. - adjust docs to better describe accepted/possible values. Due to the changes in parsing the user-given version, there is a new enum, `ClangTool` that enforces type-safety about finding the clang tools (when they are needed). * Instead of passing the tool name (as a str), the `ClangTool` enum is used to avoid typos and convey explicit support for only clang-tidy and clang-format. * Getting the clang tool's path and version are now instance methods of the `ClangTool` enum. [ver-req-docs]: https://docs.rs/semver/1.0.27/semver/struct.VersionReq.html
1 parent 106a0e9 commit 6a0edce

File tree

8 files changed

+332
-198
lines changed

8 files changed

+332
-198
lines changed

Cargo.lock

Lines changed: 0 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cpp-linter/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ colored = "3.0.0"
2121
fast-glob = "1.0.0"
2222
futures = "0.3.31"
2323
git2 = "0.20.2"
24-
lenient_semver = "0.4.2"
2524
log = { version = "0.4.28", features = ["std"] }
2625
quick-xml = { version = "0.38.3", features = ["serialize"] }
2726
regex = "1.12.2"

cpp-linter/src/clang_tools/clang_tidy.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use std::{
99
sync::{Arc, Mutex, MutexGuard},
1010
};
1111

12-
use anyhow::{Context, Result};
1312
// non-std crates
13+
use anyhow::{Context, Result};
1414
use regex::Regex;
1515
use serde::Deserialize;
1616

@@ -344,16 +344,14 @@ pub fn run_clang_tidy(
344344
#[cfg(test)]
345345
mod test {
346346
use std::{
347-
env,
348-
path::PathBuf,
349-
sync::{Arc, Mutex},
347+
env, path::PathBuf, str::FromStr, sync::{Arc, Mutex}
350348
};
351349

352350
use regex::Regex;
353351

354352
use crate::{
355-
clang_tools::get_clang_tool_exe,
356-
cli::{ClangParams, LinesChangedOnly},
353+
clang_tools::ClangTool,
354+
cli::{ClangParams, LinesChangedOnly, RequestedVersion},
357355
common_fs::FileObj,
358356
};
359357

@@ -421,11 +419,14 @@ mod test {
421419

422420
#[test]
423421
fn use_extra_args() {
424-
let exe_path = get_clang_tool_exe(
425-
"clang-tidy",
426-
env::var("CLANG_VERSION").unwrap_or("".to_string()).as_str(),
427-
)
428-
.unwrap();
422+
let exe_path = ClangTool::ClangTidy
423+
.get_exe_path(
424+
&RequestedVersion::from_str(
425+
env::var("CLANG_VERSION").unwrap_or("".to_string()).as_str(),
426+
)
427+
.unwrap(),
428+
)
429+
.unwrap();
429430
let file = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
430431
let arc_ref = Arc::new(Mutex::new(file));
431432
let extra_args = vec!["-std=c++17".to_string(), "-Wall".to_string()];

cpp-linter/src/clang_tools/mod.rs

Lines changed: 94 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,106 @@
1-
//! This crate holds the functionality related to running clang-format and/or
1+
//! This module holds the functionality related to running clang-format and/or
22
//! clang-tidy.
33
44
use std::{
55
env::current_dir,
6+
fmt::{self, Display},
67
fs,
78
path::{Path, PathBuf},
89
process::Command,
910
sync::{Arc, Mutex},
1011
};
1112

13+
// non-std crates
1214
use anyhow::{anyhow, Context, Result};
1315
use git2::{DiffOptions, Patch};
14-
// non-std crates
15-
use lenient_semver;
1616
use regex::Regex;
17-
use semver::Version;
1817
use tokio::task::JoinSet;
1918
use which::{which, which_in};
2019

2120
// project-specific modules/crates
2221
use super::common_fs::FileObj;
2322
use crate::{
24-
cli::ClangParams,
23+
cli::{ClangParams, RequestedVersion},
2524
rest_api::{RestApiClient, COMMENT_MARKER, USER_OUTREACH},
2625
};
2726
pub mod clang_format;
2827
use clang_format::run_clang_format;
2928
pub mod clang_tidy;
3029
use clang_tidy::{run_clang_tidy, CompilationUnit};
3130

32-
/// Fetch the path to a clang tool by `name` (ie `"clang-tidy"` or `"clang-format"`) and
33-
/// `version`.
34-
///
35-
/// The specified `version` can be either
36-
///
37-
/// - a full or partial semantic version specification
38-
/// - a path to a directory containing the executable binary `name`d
39-
///
40-
/// If the executable is not found using the specified `version`, then the tool is
41-
/// sought only by it's `name`.
42-
///
43-
/// The only reason this function would return an error is if the specified tool is not
44-
/// installed or present on the system (nor in the `$PATH` environment variable).
45-
pub fn get_clang_tool_exe(name: &str, version: &str) -> Result<PathBuf> {
46-
if version.is_empty() {
47-
// The default CLI value is an empty string.
48-
// Thus, we should use whatever is installed and added to $PATH.
49-
if let Ok(cmd) = which(name) {
50-
return Ok(cmd);
51-
} else {
52-
return Err(anyhow!("Could not find clang tool by name"));
53-
}
31+
#[derive(Debug)]
32+
pub enum ClangTool {
33+
ClangTidy,
34+
ClangFormat,
35+
}
36+
37+
impl Display for ClangTool {
38+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39+
write!(f, "{}", self.as_str())
5440
}
55-
if let Ok(semver) = lenient_semver::parse_into::<Version>(version) {
56-
// `version` specified has at least a major version number
57-
if let Ok(cmd) = which(format!("{}-{}", name, semver.major)) {
58-
Ok(cmd)
59-
} else if let Ok(cmd) = which(name) {
60-
// USERS SHOULD MAKE SURE THE PROPER VERSION IS INSTALLED BEFORE USING CPP-LINTER!!!
61-
// This block essentially ignores the version specified as a fail-safe.
62-
//
63-
// On Windows, the version's major number is typically not appended to the name of
64-
// the executable (or symlink for executable), so this is useful in that scenario.
65-
// On Unix systems, this block is not likely reached. Typically, installing clang
66-
// will produce a symlink to the executable with the major version appended to the
67-
// name.
68-
Ok(cmd)
69-
} else {
70-
Err(anyhow!("Could not find clang tool by name and version"))
41+
}
42+
43+
impl ClangTool {
44+
/// Get the string representation of the clang tool's name.
45+
pub const fn as_str(&self) -> &'static str {
46+
match self {
47+
ClangTool::ClangTidy => "clang-tidy",
48+
ClangTool::ClangFormat => "clang-format",
7149
}
72-
} else {
73-
// `version` specified is not a semantic version; treat as path/to/bin
74-
if let Ok(exe_path) = which_in(name, Some(version), current_dir().unwrap()) {
75-
Ok(exe_path)
76-
} else {
77-
Err(anyhow!("Could not find clang tool by path"))
50+
}
51+
52+
/// Fetch the path to an executable clang tool for the specified `version`.
53+
///
54+
/// If the executable is not found using the specified `version`, then the tool is
55+
/// sought only by it's name ([`Self::as_str()`]).
56+
///
57+
/// The only reason this function would return an error is if the specified tool is not
58+
/// installed or present on the system (nor in the `PATH` environment variable).
59+
pub fn get_exe_path(&self, version: &RequestedVersion) -> Result<PathBuf> {
60+
let name = self.as_str();
61+
match version {
62+
RequestedVersion::Path(path_buf) => {
63+
which_in(name, Some(path_buf), current_dir().unwrap())
64+
.map_err(|_| anyhow!("Could not find {name} by path"))
65+
}
66+
// Thus, we should use whatever is installed and added to $PATH.
67+
RequestedVersion::SystemDefault | RequestedVersion::NoValue => {
68+
which(name).map_err(|_| anyhow!("Could not find clang tool by name"))
69+
}
70+
RequestedVersion::Requirement(req) => {
71+
// `version` specified has at least a major version number.
72+
for req_ver in &req.comparators {
73+
let major = req_ver.major;
74+
if let Ok(cmd) = which(format!("{name}-{major}")) {
75+
return Ok(cmd);
76+
}
77+
}
78+
// failed to find a binary where the major version number is suffixed to the tool name.
79+
80+
// USERS SHOULD MAKE SURE THE PROPER VERSION IS INSTALLED BEFORE USING CPP-LINTER!!!
81+
// This line essentially ignores the version specified as a fail-safe.
82+
//
83+
// On Windows, the version's major number is typically not appended to the name of
84+
// the executable (or symlink for executable), so this is useful in that scenario.
85+
// On Unix systems, this line is not likely reached. Typically, installing clang
86+
// will produce a symlink to the executable with the major version appended to the
87+
// name.
88+
which(name).map_err(|_| anyhow!("Could not find {name} by version"))
89+
}
7890
}
7991
}
92+
93+
/// Run `clang-tool --version`, then extract and return the version number.
94+
fn capture_version(clang_tool: &PathBuf) -> Result<String> {
95+
let output = Command::new(clang_tool).arg("--version").output()?;
96+
let stdout = String::from_utf8_lossy(&output.stdout);
97+
let version_pattern = Regex::new(r"(?i)version[^\d]*([\d.]+)").unwrap();
98+
let captures = version_pattern.captures(&stdout).ok_or(anyhow!(
99+
"Failed to find version number in `{} --version` output",
100+
clang_tool.to_string_lossy()
101+
))?;
102+
Ok(captures.get(1).unwrap().as_str().to_string())
103+
}
80104
}
81105

82106
/// This creates a task to run clang-tidy and clang-format on a single file.
@@ -146,34 +170,22 @@ pub struct ClangVersions {
146170
pub tidy_version: Option<String>,
147171
}
148172

149-
/// Run `clang-tool --version`, then extract and return the version number.
150-
fn capture_clang_version(clang_tool: &PathBuf) -> Result<String> {
151-
let output = Command::new(clang_tool).arg("--version").output()?;
152-
let stdout = String::from_utf8_lossy(&output.stdout);
153-
let version_pattern = Regex::new(r"(?i)version\s*([\d.]+)").unwrap();
154-
let captures = version_pattern.captures(&stdout).ok_or(anyhow!(
155-
"Failed to find version number in `{} --version` output",
156-
clang_tool.to_string_lossy()
157-
))?;
158-
Ok(captures.get(1).unwrap().as_str().to_string())
159-
}
160-
161173
/// Runs clang-tidy and/or clang-format and returns the parsed output from each.
162174
///
163175
/// If `tidy_checks` is `"-*"` then clang-tidy is not executed.
164176
/// If `style` is a blank string (`""`), then clang-format is not executed.
165177
pub async fn capture_clang_tools_output(
166178
files: &mut Vec<Arc<Mutex<FileObj>>>,
167-
version: &str,
179+
version: &RequestedVersion,
168180
clang_params: &mut ClangParams,
169181
rest_api_client: &impl RestApiClient,
170182
) -> Result<ClangVersions> {
171183
let mut clang_versions = ClangVersions::default();
172184
// find the executable paths for clang-tidy and/or clang-format and show version
173185
// info as debugging output.
174186
if clang_params.tidy_checks != "-*" {
175-
let exe_path = get_clang_tool_exe("clang-tidy", version)?;
176-
let version_found = capture_clang_version(&exe_path)?;
187+
let exe_path = ClangTool::ClangTidy.get_exe_path(version)?;
188+
let version_found = ClangTool::capture_version(&exe_path)?;
177189
log::debug!(
178190
"{} --version: v{version_found}",
179191
&exe_path.to_string_lossy()
@@ -182,8 +194,8 @@ pub async fn capture_clang_tools_output(
182194
clang_params.clang_tidy_command = Some(exe_path);
183195
}
184196
if !clang_params.style.is_empty() {
185-
let exe_path = get_clang_tool_exe("clang-format", version)?;
186-
let version_found = capture_clang_version(&exe_path)?;
197+
let exe_path = ClangTool::ClangFormat.get_exe_path(version)?;
198+
let version_found = ClangTool::capture_version(&exe_path)?;
187199
log::debug!(
188200
"{} --version: v{version_found}",
189201
&exe_path.to_string_lossy()
@@ -452,45 +464,49 @@ pub trait MakeSuggestions {
452464

453465
#[cfg(test)]
454466
mod tests {
455-
use std::env;
467+
use std::{env, path::PathBuf, str::FromStr};
468+
469+
use which::which;
456470

457-
use super::get_clang_tool_exe;
471+
use super::ClangTool;
472+
use crate::cli::RequestedVersion;
458473

459-
const TOOL_NAME: &str = "clang-format";
474+
const CLANG_FORMAT: ClangTool = ClangTool::ClangFormat;
460475

461476
#[test]
462477
fn get_exe_by_version() {
463478
let clang_version = env::var("CLANG_VERSION").unwrap_or("16".to_string());
464-
let tool_exe = get_clang_tool_exe(TOOL_NAME, clang_version.as_str());
479+
let req_version = RequestedVersion::from_str(&clang_version).unwrap();
480+
let tool_exe = CLANG_FORMAT.get_exe_path(&req_version);
465481
println!("tool_exe: {:?}", tool_exe);
466482
assert!(tool_exe.is_ok_and(|val| val
467483
.file_name()
468484
.unwrap()
469485
.to_string_lossy()
470486
.to_string()
471-
.contains(TOOL_NAME)));
487+
.contains(CLANG_FORMAT.as_str())));
472488
}
473489

474490
#[test]
475491
fn get_exe_by_default() {
476-
let tool_exe = get_clang_tool_exe(TOOL_NAME, "");
492+
let tool_exe = CLANG_FORMAT.get_exe_path(&RequestedVersion::from_str("").unwrap());
477493
println!("tool_exe: {:?}", tool_exe);
478494
assert!(tool_exe.is_ok_and(|val| val
479495
.file_name()
480496
.unwrap()
481497
.to_string_lossy()
482498
.to_string()
483-
.contains(TOOL_NAME)));
499+
.contains(CLANG_FORMAT.as_str())));
484500
}
485501

486-
use which::which;
487-
488502
#[test]
489503
fn get_exe_by_path() {
504+
static TOOL_NAME: &'static str = CLANG_FORMAT.as_str();
490505
let clang_version = which(TOOL_NAME).unwrap();
491506
let bin_path = clang_version.parent().unwrap().to_str().unwrap();
492507
println!("binary exe path: {bin_path}");
493-
let tool_exe = get_clang_tool_exe(TOOL_NAME, bin_path);
508+
let tool_exe =
509+
CLANG_FORMAT.get_exe_path(&RequestedVersion::from_str(bin_path).unwrap());
494510
println!("tool_exe: {:?}", tool_exe);
495511
assert!(tool_exe.is_ok_and(|val| val
496512
.file_name()
@@ -502,14 +518,8 @@ mod tests {
502518

503519
#[test]
504520
fn get_exe_by_invalid_path() {
505-
let tool_exe = get_clang_tool_exe(TOOL_NAME, "non-existent-path");
506-
assert!(tool_exe.is_err());
507-
}
508-
509-
#[test]
510-
fn get_exe_by_invalid_name() {
511-
let clang_version = env::var("CLANG_VERSION").unwrap_or("16".to_string());
512-
let tool_exe = get_clang_tool_exe("not-a-clang-tool", &clang_version);
521+
let tool_exe =
522+
CLANG_FORMAT.get_exe_path(&RequestedVersion::Path(PathBuf::from("non-existent-path")));
513523
assert!(tool_exe.is_err());
514524
}
515525
}

0 commit comments

Comments
 (0)