44use std:: {
55 fs,
66 ops:: RangeInclusive ,
7- path:: PathBuf ,
87 process:: Command ,
98 sync:: { Arc , Mutex , MutexGuard } ,
109} ;
1110
12- use gix_imara_diff:: { Diff , InternedInput } ;
11+ use gix_imara_diff:: Diff ;
1312use log:: Level ;
1413
1514// project-specific crates/modules
16- use super :: { CACHE_DIR , MakeSuggestions } ;
17- use crate :: { cli:: ClangParams , common_fs:: FileObj , error:: ClangCaptureError } ;
15+ use crate :: {
16+ clang_tools:: make_patch, cli:: ClangParams , common_fs:: FileObj , error:: ClangCaptureError ,
17+ } ;
1818
1919/// A struct to hold clang-format advice for a single file.
2020#[ derive( Debug , Clone , PartialEq , Eq , Default ) ]
2121pub struct FormatAdvice {
2222 /// A list of line ranges that clang-format wants to replace.
2323 pub replacements : Vec < RangeInclusive < u32 > > ,
24-
25- /// A path to a cached file containing the full contents of the file after applying clang-format fixes.
26- pub patched : PathBuf ,
27- }
28-
29- impl MakeSuggestions for FormatAdvice {
30- fn get_suggestion_help ( & self , _start_line : u32 , _end_line : u32 ) -> String {
31- String :: from ( "### clang-format suggestions\n " )
32- }
33-
34- fn get_tool_name ( & self ) -> String {
35- "clang-format" . to_string ( )
36- }
3724}
3825
3926/// Get a string that summarizes the given `--style`
@@ -82,14 +69,7 @@ pub fn run_clang_format(
8269 for range in & ranges {
8370 cmd. arg ( format ! ( "--lines={}:{}" , range. start( ) , range. end( ) ) ) ;
8471 }
85- let cache_path = clang_params. repo_root . join ( CACHE_DIR ) . join ( "patches" ) ;
86- let cache_format_fixes = cache_path. join ( file. name . with_added_extension ( "format" ) ) ;
87- fs:: create_dir_all (
88- cache_format_fixes
89- . parent ( )
90- . ok_or ( ClangCaptureError :: UnknownCacheParentPath ) ?,
91- )
92- . map_err ( ClangCaptureError :: MkDirFailed ) ?;
72+ let cache_path = clang_params. get_cache_path ( ) ;
9373 let file_name = file. name . to_string_lossy ( ) . to_string ( ) ;
9474 cmd. arg ( file. name . to_path_buf ( ) . as_os_str ( ) ) ;
9575 logs. push ( (
@@ -109,12 +89,6 @@ pub fn run_clang_format(
10989 task : format ! ( "get fixes from clang-format {file_name}" ) ,
11090 source : e,
11191 } ) ?;
112- fs:: write ( & cache_format_fixes, & output. stdout ) . map_err ( |e| {
113- ClangCaptureError :: WriteFileFailed {
114- file_name : cache_format_fixes. to_string_lossy ( ) . to_string ( ) ,
115- source : e,
116- }
117- } ) ?;
11892
11993 if !output. stderr . is_empty ( ) || !output. status . success ( ) {
12094 logs. push ( (
@@ -140,9 +114,7 @@ pub fn run_clang_format(
140114 source : e,
141115 }
142116 } ) ?;
143- let input = InternedInput :: new ( original_contents. as_str ( ) , patched_contents. as_str ( ) ) ;
144- let mut diff = Diff :: compute ( gix_imara_diff:: Algorithm :: Histogram , & input) ;
145- diff. postprocess_lines ( & input) ;
117+ let ( diff, _) = make_patch ( & patched_contents, & original_contents) ;
146118 let format_advice = FormatAdvice {
147119 replacements : diff
148120 . hunks ( )
@@ -166,17 +138,184 @@ pub fn run_clang_format(
166138 }
167139 } )
168140 . collect ( ) ,
169- patched : cache_format_fixes,
170141 } ;
142+
143+ // if a clang-tidy patched file exists in cache,
144+ // get the diff between it and the original file,
145+ // then format both clang-tidy fixes and any other changes by clang-format fixes.
146+ if let Some ( patched_path) = & file. patched_path
147+ && patched_path. exists ( )
148+ {
149+ let mut cmd = Command :: new ( cmd_path) ;
150+ cmd. current_dir ( & cache_path) ;
151+ // edit the clang-tody patched file in-place (`-i`)
152+ cmd. args ( [ "--style" , & clang_params. style , "-i" ] ) ;
153+ // if ranges is empty, then we're just formatting the entire file.
154+ if !ranges. is_empty ( ) {
155+ let tidy_patch_contents = fs:: read_to_string ( patched_path) . map_err ( |e| {
156+ ClangCaptureError :: ReadFileFailed {
157+ file_name : patched_path. to_string_lossy ( ) . to_string ( ) ,
158+ source : e,
159+ }
160+ } ) ?;
161+ let ( tidy_diff, _) = make_patch ( & tidy_patch_contents, & original_contents) ;
162+ let joint_ranges = three_way_diff ( & ranges, tidy_diff) ;
163+ for range in & joint_ranges {
164+ cmd. arg ( format ! ( "--lines={}:{}" , range. start( ) , range. end( ) ) . as_str ( ) ) ;
165+ }
166+ }
167+ cmd. arg ( & file_name) ;
168+ let output = cmd
169+ . output ( )
170+ . map_err ( |e| ClangCaptureError :: FailedToRunCommand {
171+ task : format ! ( "apply clang-format to clang-tidy fixes ({file_name})" ) ,
172+ source : e,
173+ } ) ?;
174+ if !output. stderr . is_empty ( ) || !output. status . success ( ) {
175+ logs. push ( (
176+ log:: Level :: Debug ,
177+ format ! (
178+ "clang-format raised the follow errors about clang-tidy fixes:\n {}" ,
179+ String :: from_utf8_lossy( & output. stderr)
180+ ) ,
181+ ) ) ;
182+ }
183+ } else {
184+ // clang-tidy was not run on this file,
185+ // so just use the clang-format fixes as the patched content.
186+ let cache_format_fixes = cache_path. join ( & file. name ) ;
187+ fs:: create_dir_all (
188+ cache_format_fixes
189+ . parent ( )
190+ . ok_or ( ClangCaptureError :: UnknownCacheParentPath ) ?,
191+ )
192+ . map_err ( ClangCaptureError :: MkDirFailed ) ?;
193+ fs:: write ( & cache_format_fixes, & output. stdout ) . map_err ( |e| {
194+ ClangCaptureError :: WriteFileFailed {
195+ file_name : cache_format_fixes. to_string_lossy ( ) . to_string ( ) ,
196+ source : e,
197+ }
198+ } ) ?;
199+ file. patched_path = Some ( cache_format_fixes) ;
200+ }
201+
171202 file. format_advice = Some ( format_advice) ;
172203 Ok ( logs)
173204}
174205
206+ /// Essentially does a three way diff without the original source that generated the given `ranges` (simplified hunks).
207+ ///
208+ /// The returned list of ranges are lines that need formatting in the clang-tidy patched file,
209+ /// provided by the `tidy_diff`. The given `ranges` are the line numbers in the original file
210+ /// that clang-tidy patched.
211+ fn three_way_diff ( ranges : & [ RangeInclusive < u32 > ] , tidy_diff : Diff ) -> Vec < RangeInclusive < u32 > > {
212+ // We're concerned about the formatting cases:
213+ //
214+ // 1. changes that clang-tidy made: `tidy_diff.hunks().after`
215+ // 2. changes in the current CI event's diff (`ranges`)
216+ // that clang-tidy did not touch (`tidy_diff.hunks().before`)
217+ // 3. changes that do not overlap clang-tidy fixes: `ranges` - `tidy_diff.hunks().before`
218+ // 4. changes that overlap with clang-tidy fixes. This one is complex because
219+ // - tidy fixes can prefix an og range
220+ // - tidy fixes can suffix an og range
221+ // - tidy fixes can be contained within an og range
222+ // - multiple tidy fixes can (in order) suffix, be contained within, and prefix an og range
223+ let mut joint_ranges = vec ! [ ] ;
224+ let mut tidy_iter = tidy_diff. hunks ( ) . peekable ( ) ;
225+ let mut line_shift = 0i32 ;
226+
227+ /// Prevent pure removals from causing invalid inclusive ranges.
228+ fn maybe_push_range ( joint_ranges : & mut Vec < RangeInclusive < u32 > > , start : u32 , end : u32 ) {
229+ if start <= end {
230+ joint_ranges. push ( RangeInclusive :: new ( start, end) ) ;
231+ }
232+ }
233+
234+ for og_range in ranges {
235+ let og_start = * og_range. start ( ) ;
236+ let og_end = * og_range. end ( ) ;
237+
238+ // track the start and end of a merged range that gets pushed into joint_ranges.
239+ let mut merged_start = ( og_start as i32 + line_shift) as u32 ;
240+ let mut merged_end = ( og_end as i32 + line_shift) as u32 ;
241+
242+ while let Some ( tidy_hunk) = tidy_iter. peek ( ) {
243+ // alias for readability and prevent some repeated calculations
244+ let before_start = tidy_hunk. before . start ;
245+ let before_end = tidy_hunk. before . end . saturating_sub ( 1 ) ;
246+ let after_start = tidy_hunk. after . start ;
247+ let after_end = tidy_hunk. after . end . saturating_sub ( 1 ) ;
248+ let delta = tidy_hunk. after . len ( ) as i32 - tidy_hunk. before . len ( ) as i32 ;
249+
250+ // The tidy hunk is a pure removal that exactly matches the og range.
251+ if tidy_hunk. is_pure_removal ( ) && before_start == og_start && before_end == og_end {
252+ // Skip the og range and tidy hunk entirely.
253+ // The line shift must still be adjusted for the pure removal though
254+ line_shift += delta;
255+ merged_end = 0 ; // causes invalid inclusive range which does not get pushed.
256+ tidy_iter. next ( ) ; // skip this tidy hunk
257+ break ; // skip og range and iterate to the next one.
258+ }
259+
260+ // tidy hunk is before the og range.
261+ if before_end < og_start {
262+ maybe_push_range ( & mut joint_ranges, after_start, after_end) ;
263+ line_shift += delta;
264+ tidy_iter. next ( ) ;
265+ continue ;
266+ }
267+
268+ // tidy hunk is after the og range.
269+ if before_start > og_end {
270+ // handle the og range before iterating the next tidy hunk
271+ break ;
272+ }
273+
274+ // tidy hunk overlaps with the og range in some way (case 4).
275+ if tidy_hunk. before . contains ( & og_start) {
276+ merged_start = after_start;
277+ }
278+
279+ // commit the line shift now that the tidy hunk start is checked.
280+ line_shift += delta;
281+
282+ // tidy hunk suffixes the og range.
283+ if tidy_hunk. before . contains ( & og_end) {
284+ merged_end = after_end;
285+ tidy_iter. next ( ) ; // this tidy hunk is handled.
286+ break ; // break from loop to push the merged range into joint_ranges.
287+ }
288+
289+ // tidy hunk is contained within the og range.
290+ // so adjust the og range end accordingly and continue iterating tidy hunks
291+ merged_end = ( og_end as i32 + line_shift) as u32 ;
292+ tidy_iter. next ( ) ;
293+ }
294+
295+ maybe_push_range ( & mut joint_ranges, merged_start, merged_end) ;
296+ }
297+
298+ // handle any remaining tidy hunks that are after all og ranges.
299+ for tidy_hunk in tidy_iter {
300+ maybe_push_range (
301+ & mut joint_ranges,
302+ tidy_hunk. after . start ,
303+ tidy_hunk. after . end . saturating_sub ( 1 ) ,
304+ ) ;
305+ }
306+
307+ joint_ranges
308+ }
309+
175310#[ cfg( test) ]
176311mod tests {
177312 #![ allow( clippy:: unwrap_used) ]
178313
179- use super :: summarize_style;
314+ use std:: ops:: RangeInclusive ;
315+
316+ use gix_imara_diff:: { Diff , InternedInput } ;
317+
318+ use super :: { summarize_style, three_way_diff} ;
180319
181320 fn formalize_style ( style : & str , expected : & str ) {
182321 assert_eq ! ( summarize_style( style) , expected) ;
@@ -196,4 +335,42 @@ mod tests {
196335 fn formalize_custom_style ( ) {
197336 formalize_style ( "file" , "Custom" ) ;
198337 }
338+
339+ #[ test]
340+ fn three_way_diff_mixed ( ) {
341+ const OG_SRC : & str =
342+ "line1\n line2\n line3\n line4\n line5\n line6\n line7\n line8\n line9\n line10\n line11" ;
343+ // TIDY_SRC replaces line3->StringA (hunk before=2..3) and
344+ // line8+line9+line10->StringB+StringC (hunk before=7..10), then appends StringE.
345+ // The second hunk's before=7..10 contains og_end=9 but not og_start=6,
346+ // which exercises the "tidy hunk suffixes og range" branch.
347+ const TIDY_SRC : & str =
348+ "line1\n line2\n StringA\n line4\n line5\n line6\n line7\n StringB\n StringC\n line11\n StringE" ;
349+ let input = InternedInput :: new ( OG_SRC , TIDY_SRC ) ;
350+ let mut tidy_diff = Diff :: compute ( gix_imara_diff:: Algorithm :: Histogram , & input) ;
351+ tidy_diff. postprocess_lines ( & input) ;
352+ let ranges = vec ! [ RangeInclusive :: new( 2 , 4 ) , RangeInclusive :: new( 6 , 9 ) ] ;
353+ println ! ( "tidy diff: {tidy_diff:#?}\n compared to og ranges: {ranges:?}" ) ;
354+ let joint_ranges = three_way_diff ( & ranges, tidy_diff) ;
355+ println ! ( "joint ranges: {joint_ranges:#?}" ) ;
356+ assert_eq ! ( joint_ranges, vec![ 2 ..=4 , 6 ..=10 ] ) ;
357+ }
358+
359+ #[ test]
360+ fn three_way_diff_separated ( ) {
361+ const OG_SRC : & str =
362+ "line1\n line2\n line3\n line4\n line5\n line6\n line7\n line8\n line9\n line10\n line11" ;
363+ // TIDY_SRC removes "line3" (index 2) which decrements offsets in ranges[5,8] and removes ranges[2,2].
364+ // TIDY_SRC appends StringE, which handles remaining tidy hunks after done iterating ranges
365+ const TIDY_SRC : & str =
366+ "line1\n line2\n line4\n line5\n line6\n line7\n line8\n line9\n line10\n line11\n StringE" ;
367+ let input = InternedInput :: new ( OG_SRC , TIDY_SRC ) ;
368+ let mut tidy_diff = Diff :: compute ( gix_imara_diff:: Algorithm :: Histogram , & input) ;
369+ tidy_diff. postprocess_lines ( & input) ;
370+ let ranges = vec ! [ 2 ..=2 , 5 ..=8 ] ;
371+ println ! ( "tidy diff: {tidy_diff:#?}\n compared to og ranges: {ranges:?}" ) ;
372+ let joint_ranges = three_way_diff ( & ranges, tidy_diff) ;
373+ println ! ( "joint ranges: {joint_ranges:#?}" ) ;
374+ assert_eq ! ( joint_ranges, vec![ 4 ..=7 , 9 ..=10 ] ) ;
375+ }
199376}
0 commit comments