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
44use 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
1214use anyhow:: { anyhow, Context , Result } ;
1315use git2:: { DiffOptions , Patch } ;
14- // non-std crates
15- use lenient_semver;
1616use regex:: Regex ;
17- use semver:: Version ;
1817use tokio:: task:: JoinSet ;
1918use which:: { which, which_in} ;
2019
2120// project-specific modules/crates
2221use super :: common_fs:: FileObj ;
2322use crate :: {
24- cli:: ClangParams ,
23+ cli:: { ClangParams , RequestedVersion } ,
2524 rest_api:: { RestApiClient , COMMENT_MARKER , USER_OUTREACH } ,
2625} ;
2726pub mod clang_format;
2827use clang_format:: run_clang_format;
2928pub mod clang_tidy;
3029use 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.
165177pub 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) ]
454466mod 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