3
3
4
4
use std:: {
5
5
env:: { consts:: OS , current_dir} ,
6
+ fs,
6
7
path:: PathBuf ,
7
8
process:: Command ,
8
9
sync:: { Arc , Mutex } ,
@@ -13,8 +14,9 @@ use regex::Regex;
13
14
use serde:: Deserialize ;
14
15
15
16
// project-specific modules/crates
17
+ use super :: MakeSuggestions ;
16
18
use crate :: {
17
- cli:: LinesChangedOnly ,
19
+ cli:: { ClangParams , LinesChangedOnly } ,
18
20
common_fs:: { normalize_path, FileObj } ,
19
21
} ;
20
22
@@ -64,6 +66,9 @@ pub struct TidyNotification {
64
66
/// Sometimes, this code block doesn't exist. Sometimes, it contains suggested
65
67
/// fixes/advice. This information is purely superfluous.
66
68
pub suggestion : Vec < String > ,
69
+
70
+ /// The list of line numbers that had fixes applied via `clang-tidy --fix-error`.
71
+ pub fixed_lines : Vec < u64 > ,
67
72
}
68
73
69
74
impl TidyNotification {
@@ -84,6 +89,27 @@ impl TidyNotification {
84
89
pub struct TidyAdvice {
85
90
/// A list of notifications parsed from clang-tidy stdout.
86
91
pub notes : Vec < TidyNotification > ,
92
+ pub patched : Option < Vec < u8 > > ,
93
+ }
94
+
95
+ impl MakeSuggestions for TidyAdvice {
96
+ fn get_suggestion_help ( & self , start_line : u32 , end_line : u32 ) -> String {
97
+ let mut diagnostics = vec ! [ ] ;
98
+ for note in & self . notes {
99
+ if ( start_line..end_line) . contains ( & note. line ) {
100
+ diagnostics. push ( format ! ( "- {}\n " , note. diagnostic_link( ) ) ) ;
101
+ }
102
+ }
103
+ format ! (
104
+ "### clang-tidy {}\n {}" ,
105
+ if diagnostics. is_empty( ) {
106
+ "suggestion"
107
+ } else {
108
+ "diagnostic(s)"
109
+ } ,
110
+ diagnostics. join( "" )
111
+ )
112
+ }
87
113
}
88
114
89
115
/// Parses clang-tidy stdout.
@@ -95,6 +121,9 @@ fn parse_tidy_output(
95
121
database_json : & Option < Vec < CompilationUnit > > ,
96
122
) -> Option < TidyAdvice > {
97
123
let note_header = Regex :: new ( r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+)\]$" ) . unwrap ( ) ;
124
+ let fixed_note =
125
+ Regex :: new ( r"^.+:(\d+):\d+:\snote: FIX-IT applied suggested code changes$" ) . unwrap ( ) ;
126
+ let mut found_fix = false ;
98
127
let mut notification = None ;
99
128
let mut result = Vec :: new ( ) ;
100
129
let cur_dir = current_dir ( ) . unwrap ( ) ;
@@ -148,11 +177,29 @@ fn parse_tidy_output(
148
177
rationale : String :: from ( & captured[ 5 ] ) . trim ( ) . to_string ( ) ,
149
178
diagnostic : String :: from ( & captured[ 6 ] ) ,
150
179
suggestion : Vec :: new ( ) ,
180
+ fixed_lines : Vec :: new ( ) ,
151
181
} ) ;
152
- } else if let Some ( note) = & mut notification {
153
- // append lines of code that are part of
154
- // the previous line's notification
155
- note. suggestion . push ( line. to_string ( ) ) ;
182
+ // begin capturing subsequent lines as suggestions
183
+ found_fix = false ;
184
+ } else if let Some ( capture) = fixed_note. captures ( line) {
185
+ let fixed_line = capture[ 1 ]
186
+ . parse ( )
187
+ . expect ( "Failed to parse line number's string as integer" ) ;
188
+ if let Some ( note) = & mut notification {
189
+ if !note. fixed_lines . contains ( & fixed_line) {
190
+ note. fixed_lines . push ( fixed_line) ;
191
+ }
192
+ }
193
+ // Suspend capturing subsequent lines as suggestions until
194
+ // a new notification is constructed. If we found a note about applied fixes,
195
+ // then the lines of suggestions for that notification have already been parsed.
196
+ found_fix = true ;
197
+ } else if !found_fix {
198
+ if let Some ( note) = & mut notification {
199
+ // append lines of code that are part of
200
+ // the previous line's notification
201
+ note. suggestion . push ( line. to_string ( ) ) ;
202
+ }
156
203
}
157
204
}
158
205
if let Some ( note) = notification {
@@ -161,7 +208,10 @@ fn parse_tidy_output(
161
208
if result. is_empty ( ) {
162
209
None
163
210
} else {
164
- Some ( TidyAdvice { notes : result } )
211
+ Some ( TidyAdvice {
212
+ notes : result,
213
+ patched : None ,
214
+ } )
165
215
}
166
216
}
167
217
@@ -184,29 +234,25 @@ pub fn tally_tidy_advice(files: &[Arc<Mutex<FileObj>>]) -> u64 {
184
234
185
235
/// Run clang-tidy, then parse and return it's output.
186
236
pub fn run_clang_tidy (
187
- cmd : & mut Command ,
188
237
file : & mut Arc < Mutex < FileObj > > ,
189
- checks : & str ,
190
- lines_changed_only : & LinesChangedOnly ,
191
- database : & Option < PathBuf > ,
192
- extra_args : & Option < Vec < String > > ,
193
- database_json : & Option < Vec < CompilationUnit > > ,
238
+ clang_params : & ClangParams ,
194
239
) -> Vec < ( log:: Level , std:: string:: String ) > {
240
+ let mut cmd = Command :: new ( clang_params. clang_tidy_command . as_ref ( ) . unwrap ( ) ) ;
195
241
let mut logs = vec ! [ ] ;
196
242
let mut file = file. lock ( ) . unwrap ( ) ;
197
- if !checks . is_empty ( ) {
198
- cmd. args ( [ "-checks" , checks ] ) ;
243
+ if !clang_params . tidy_checks . is_empty ( ) {
244
+ cmd. args ( [ "-checks" , & clang_params . tidy_checks ] ) ;
199
245
}
200
- if let Some ( db) = database {
246
+ if let Some ( db) = & clang_params . database {
201
247
cmd. args ( [ "-p" , & db. to_string_lossy ( ) ] ) ;
202
248
}
203
- if let Some ( extras) = extra_args {
249
+ if let Some ( extras) = & clang_params . extra_args {
204
250
for arg in extras {
205
251
cmd. args ( [ "--extra-arg" , format ! ( "\" {}\" " , arg) . as_str ( ) ] ) ;
206
252
}
207
253
}
208
- if * lines_changed_only != LinesChangedOnly :: Off {
209
- let ranges = file. get_ranges ( lines_changed_only) ;
254
+ if clang_params . lines_changed_only != LinesChangedOnly :: Off {
255
+ let ranges = file. get_ranges ( & clang_params . lines_changed_only ) ;
210
256
let filter = format ! (
211
257
"[{{\" name\" :{:?},\" lines\" :{:?}}}]" ,
212
258
& file
@@ -220,6 +266,17 @@ pub fn run_clang_tidy(
220
266
) ;
221
267
cmd. args ( [ "--line-filter" , filter. as_str ( ) ] ) ;
222
268
}
269
+ let mut original_content = None ;
270
+ if clang_params. tidy_review {
271
+ cmd. arg ( "--fix-errors" ) ;
272
+ original_content =
273
+ Some ( fs:: read_to_string ( & file. name ) . expect (
274
+ "Failed to cache file's original content before applying clang-tidy changes." ,
275
+ ) ) ;
276
+ }
277
+ if !clang_params. style . is_empty ( ) {
278
+ cmd. args ( [ "--format-style" , clang_params. style . as_str ( ) ] ) ;
279
+ }
223
280
cmd. arg ( file. name . to_string_lossy ( ) . as_ref ( ) ) ;
224
281
logs. push ( (
225
282
log:: Level :: Info ,
@@ -249,7 +306,20 @@ pub fn run_clang_tidy(
249
306
) ,
250
307
) ) ;
251
308
}
252
- file. tidy_advice = parse_tidy_output ( & output. stdout , database_json) ;
309
+ file. tidy_advice = parse_tidy_output ( & output. stdout , & clang_params. database_json ) ;
310
+ if clang_params. tidy_review {
311
+ let file_name = & file. name . to_owned ( ) ;
312
+ if let Some ( tidy_advice) = & mut file. tidy_advice {
313
+ // cache file changes in a buffer and restore the original contents for further analysis by clang-format
314
+ tidy_advice. patched =
315
+ Some ( fs:: read ( file_name) . expect ( "Failed to read changes from clang-tidy" ) ) ;
316
+ }
317
+ fs:: write (
318
+ file_name,
319
+ original_content. expect ( "original content of file was not cached" ) ,
320
+ )
321
+ . expect ( "failed to restore file's original content." ) ;
322
+ }
253
323
logs
254
324
}
255
325
@@ -258,13 +328,16 @@ mod test {
258
328
use std:: {
259
329
env,
260
330
path:: PathBuf ,
261
- process:: Command ,
262
331
sync:: { Arc , Mutex } ,
263
332
} ;
264
333
265
334
use regex:: Regex ;
266
335
267
- use crate :: { clang_tools:: get_clang_tool_exe, cli:: LinesChangedOnly , common_fs:: FileObj } ;
336
+ use crate :: {
337
+ clang_tools:: get_clang_tool_exe,
338
+ cli:: { ClangParams , LinesChangedOnly } ,
339
+ common_fs:: FileObj ,
340
+ } ;
268
341
269
342
use super :: run_clang_tidy;
270
343
@@ -297,33 +370,41 @@ mod test {
297
370
env:: var ( "CLANG_VERSION" ) . unwrap_or ( "" . to_string ( ) ) . as_str ( ) ,
298
371
)
299
372
. unwrap ( ) ;
300
- let mut cmd = Command :: new ( exe_path) ;
301
373
let file = FileObj :: new ( PathBuf :: from ( "tests/demo/demo.cpp" ) ) ;
302
374
let mut arc_ref = Arc :: new ( Mutex :: new ( file) ) ;
303
375
let extra_args = vec ! [ "-std=c++17" . to_string( ) , "-Wall" . to_string( ) ] ;
304
- run_clang_tidy (
305
- & mut cmd,
306
- & mut arc_ref,
307
- "" , // use .clang-tidy config file
308
- & LinesChangedOnly :: Off , // check all lines
309
- & None , // no database path
310
- & Some ( extra_args) , // <---- the reason for this test
311
- & None , // no deserialized database
312
- ) ;
313
- // since `cmd` was passed as a mutable reference, we can inspect the args that were added
314
- let locked_file = arc_ref. lock ( ) . unwrap ( ) ;
315
- let mut args = cmd
316
- . get_args ( )
317
- . map ( |arg| arg. to_str ( ) . unwrap ( ) )
376
+ let clang_params = ClangParams {
377
+ style : "" . to_string ( ) ,
378
+ tidy_checks : "" . to_string ( ) , // use .clang-tidy config file
379
+ lines_changed_only : LinesChangedOnly :: Off ,
380
+ database : None ,
381
+ extra_args : Some ( extra_args. clone ( ) ) , // <---- the reason for this test
382
+ database_json : None ,
383
+ format_filter : None ,
384
+ tidy_filter : None ,
385
+ tidy_review : false ,
386
+ format_review : false ,
387
+ clang_tidy_command : Some ( exe_path) ,
388
+ clang_format_command : None ,
389
+ } ;
390
+ let logs = run_clang_tidy ( & mut arc_ref, & clang_params)
391
+ . into_iter ( )
392
+ . filter_map ( |( _lvl, msg) | {
393
+ if msg. contains ( "Running " ) {
394
+ Some ( msg)
395
+ } else {
396
+ None
397
+ }
398
+ } )
399
+ . collect :: < Vec < String > > ( ) ;
400
+ let args = & logs
401
+ . first ( )
402
+ . expect ( "expected a log message about invoked clang-tidy command" )
403
+ . split ( ' ' )
318
404
. collect :: < Vec < & str > > ( ) ;
319
- assert_eq ! ( locked_file. name. to_string_lossy( ) , args. pop( ) . unwrap( ) ) ;
320
- assert_eq ! (
321
- vec![ "--extra-arg" , "\" -std=c++17\" " , "--extra-arg" , "\" -Wall\" " ] ,
322
- args
323
- ) ;
324
- assert ! ( !locked_file
325
- . tidy_advice
326
- . as_ref( )
327
- . is_some_and( |advice| advice. notes. is_empty( ) ) ) ;
405
+ for arg in & extra_args {
406
+ let extra_arg = format ! ( "\" {arg}\" " ) ;
407
+ assert ! ( args. contains( & extra_arg. as_str( ) ) ) ;
408
+ }
328
409
}
329
410
}
0 commit comments