33
44use std:: {
55 fs,
6+ ops:: RangeInclusive ,
7+ path:: PathBuf ,
68 process:: Command ,
79 sync:: { Arc , Mutex , MutexGuard } ,
810} ;
911
12+ use gix_imara_diff:: { Diff , InternedInput } ;
1013use log:: Level ;
11- use serde:: Deserialize ;
1214
1315// project-specific crates/modules
1416use super :: MakeSuggestions ;
15- use crate :: {
16- cli:: ClangParams ,
17- common_fs:: { FileObj , get_line_count_from_offset} ,
18- error:: ClangCaptureError ,
19- } ;
17+ use crate :: { cli:: ClangParams , common_fs:: FileObj , error:: ClangCaptureError } ;
2018
21- #[ derive( Debug , Clone , Deserialize , PartialEq , Eq , Default ) ]
19+ #[ derive( Debug , Clone , PartialEq , Eq , Default ) ]
2220pub struct FormatAdvice {
2321 /// A list of [`Replacement`]s that clang-tidy wants to make.
24- #[ serde( rename( deserialize = "replacement" ) , default ) ]
25- pub replacements : Vec < Replacement > ,
22+ pub replacements : Vec < RangeInclusive < u32 > > ,
2623
27- pub patched : Option < Vec < u8 > > ,
24+ pub patched : PathBuf ,
2825}
2926
3027impl MakeSuggestions for FormatAdvice {
@@ -37,21 +34,6 @@ impl MakeSuggestions for FormatAdvice {
3734 }
3835}
3936
40- /// A single replacement that clang-format wants to make.
41- #[ derive( Debug , PartialEq , Eq , Default , Clone , Copy , Deserialize ) ]
42- pub struct Replacement {
43- /// The byte offset where the replacement will start.
44- #[ serde( rename = "@offset" ) ]
45- pub offset : u32 ,
46-
47- /// The line number described by the [`Replacement::offset`].
48- ///
49- /// This value is not provided by the XML output, but we calculate it after
50- /// deserialization.
51- #[ serde( default ) ]
52- pub line : u32 ,
53- }
54-
5537/// Get a string that summarizes the given `--style`
5638pub fn summarize_style ( style : & str ) -> String {
5739 let mut char_iter = style. chars ( ) ;
@@ -97,49 +79,40 @@ pub fn run_clang_format(
9779 for range in & ranges {
9880 cmd. arg ( format ! ( "--lines={}:{}" , range. start( ) , range. end( ) ) ) ;
9981 }
82+ let cache_path = clang_params. project_cache_dir . join ( "patches" ) ;
83+ let cache_format_fixes = cache_path. join ( file. name . with_added_extension ( "format" ) ) ;
84+ fs:: create_dir_all (
85+ cache_format_fixes
86+ . parent ( )
87+ . ok_or ( ClangCaptureError :: UnknownCacheParentPath ) ?,
88+ )
89+ . map_err ( ClangCaptureError :: MkDirFailed ) ?;
10090 let file_name = file. name . to_string_lossy ( ) . to_string ( ) ;
10191 cmd. arg ( file. name . to_path_buf ( ) . as_os_str ( ) ) ;
102- let patched = if !clang_params. format_review {
103- None
104- } else {
105- logs. push ( (
106- Level :: Info ,
107- format ! (
108- "Getting format fixes with \" {} {}\" " ,
109- cmd. get_program( ) . to_string_lossy( ) ,
110- cmd. get_args( )
111- . map( |a| a. to_string_lossy( ) )
112- . collect:: <Vec <_>>( )
113- . join( " " )
114- ) ,
115- ) ) ;
116- Some (
117- cmd. output ( )
118- . map_err ( |e| ClangCaptureError :: FailedToRunCommand {
119- task : format ! ( "get fixes from clang-format {file_name}" ) ,
120- source : e,
121- } ) ?
122- . stdout ,
123- )
124- } ;
125- cmd. arg ( "--output-replacements-xml" ) ;
12692 logs. push ( (
127- log :: Level :: Info ,
93+ Level :: Info ,
12894 format ! (
129- "Running \" {} {}\" " ,
95+ "Getting format fixes with \" {} {}\" " ,
13096 cmd. get_program( ) . to_string_lossy( ) ,
13197 cmd. get_args( )
132- . map( |x| x . to_string_lossy( ) )
98+ . map( |a| a . to_string_lossy( ) )
13399 . collect:: <Vec <_>>( )
134100 . join( " " )
135101 ) ,
136102 ) ) ;
137103 let output = cmd
138104 . output ( )
139105 . map_err ( |e| ClangCaptureError :: FailedToRunCommand {
140- task : format ! ( "Failed to get replacements from clang-format: {file_name}" ) ,
106+ task : format ! ( "get fixes from clang-format {file_name}" ) ,
141107 source : e,
142108 } ) ?;
109+ fs:: write ( & cache_format_fixes, & output. stdout ) . map_err ( |e| {
110+ ClangCaptureError :: WriteFileFailed {
111+ file_name : cache_format_fixes. to_string_lossy ( ) . to_string ( ) ,
112+ source : e,
113+ }
114+ } ) ?;
115+
143116 if !output. stderr . is_empty ( ) || !output. status . success ( ) {
144117 logs. push ( (
145118 log:: Level :: Debug ,
@@ -149,46 +122,47 @@ pub fn run_clang_format(
149122 ) ,
150123 ) ) ;
151124 }
152- let mut format_advice = if !output. stdout . is_empty ( ) {
153- let xml =
154- String :: from_utf8 ( output. stdout ) . map_err ( |e| ClangCaptureError :: NonUtf8Output {
155- task : format ! ( "XML output from clang-format (for {file_name})" ) ,
156- source : e,
157- } ) ?;
158- quick_xml:: de:: from_str :: < FormatAdvice > ( & xml) . map_err ( |e| {
159- ClangCaptureError :: XmlParsingFailed {
160- file_name : file_name. clone ( ) ,
161- source : e,
162- }
163- } ) ?
164- } else {
165- FormatAdvice :: default ( )
166- } ;
167- format_advice. patched = patched;
168- if !format_advice. replacements . is_empty ( ) {
169- let original_contents =
170- fs:: read ( & file. name ) . map_err ( |e| ClangCaptureError :: ReadFileFailed {
171- file_name : file_name. clone ( ) ,
172- source : e,
173- } ) ?;
174- // get line and column numbers from format_advice.offset
175- let mut filtered_replacements = Vec :: new ( ) ;
176- for replacement in & mut format_advice. replacements {
177- let line_number = get_line_count_from_offset ( & original_contents, replacement. offset ) ;
178- replacement. line = line_number;
179- for range in & ranges {
180- if range. contains ( & line_number) {
181- filtered_replacements. push ( * replacement) ;
182- break ;
183- }
184- }
185- if ranges. is_empty ( ) {
186- // lines_changed_only is disabled
187- filtered_replacements. push ( * replacement) ;
188- }
125+
126+ // use a diff between patched and original contents to get format results
127+ let original_contents =
128+ fs:: read_to_string ( & file. name ) . map_err ( |e| ClangCaptureError :: ReadFileFailed {
129+ file_name : file_name. clone ( ) ,
130+ source : e,
131+ } ) ?;
132+ let patched_contents = String :: from_utf8 ( output. stdout . to_vec ( ) ) . map_err ( |e| {
133+ ClangCaptureError :: NonUtf8Output {
134+ task : "clang-format" . to_string ( ) ,
135+ source : e,
189136 }
190- format_advice. replacements = filtered_replacements;
191- }
137+ } ) ?;
138+ let input = InternedInput :: new ( original_contents. as_str ( ) , patched_contents. as_str ( ) ) ;
139+ let mut diff = Diff :: compute ( gix_imara_diff:: Algorithm :: Histogram , & input) ;
140+ diff. postprocess_lines ( & input) ;
141+ let format_advice = FormatAdvice {
142+ replacements : diff
143+ . hunks ( )
144+ . filter_map ( |hunk| {
145+ let replacement = if hunk. is_pure_insertion ( ) {
146+ RangeInclusive :: new ( hunk. after . start , hunk. after . end . saturating_sub ( 1 ) )
147+ } else {
148+ RangeInclusive :: new ( hunk. before . start , hunk. before . end . saturating_sub ( 1 ) )
149+ } ;
150+ if ranges. is_empty ( ) {
151+ Some ( replacement)
152+ } else {
153+ // only include replacements that fall within the specified line ranges
154+ if ranges. iter ( ) . any ( |range| {
155+ range. contains ( replacement. start ( ) ) && range. contains ( replacement. end ( ) )
156+ } ) {
157+ Some ( replacement)
158+ } else {
159+ None
160+ }
161+ }
162+ } )
163+ . collect ( ) ,
164+ patched : cache_format_fixes,
165+ } ;
192166 file. format_advice = Some ( format_advice) ;
193167 Ok ( logs)
194168}
@@ -197,56 +171,7 @@ pub fn run_clang_format(
197171mod tests {
198172 #![ allow( clippy:: unwrap_used) ]
199173
200- use super :: { FormatAdvice , Replacement , summarize_style} ;
201-
202- #[ test]
203- fn parse_blank_xml ( ) {
204- let xml = String :: new ( ) ;
205- let result = quick_xml:: de:: from_str :: < FormatAdvice > ( & xml) ;
206- assert ! ( result. is_err( ) ) ;
207- }
208-
209- #[ test]
210- fn parse_xml_no_replacements ( ) {
211- let xml_raw = r#"<?xml version='1.0'?>
212- <replacements xml:space='preserve' incomplete_format='false'>
213- </replacements>"#
214- . as_bytes ( )
215- . to_vec ( ) ;
216- let expected = FormatAdvice :: default ( ) ;
217- let xml = String :: from_utf8 ( xml_raw) . unwrap ( ) ;
218- let document = quick_xml:: de:: from_str :: < FormatAdvice > ( & xml) . unwrap ( ) ;
219- assert_eq ! ( expected, document) ;
220- }
221-
222- #[ test]
223- fn parse_xml ( ) {
224- let xml_raw = r#"<?xml version='1.0'?>
225- <replacements xml:space='preserve' incomplete_format='false'>
226- <replacement offset='113' length='5'> </replacement>
227- <replacement offset='147' length='0'> </replacement>
228- <replacement offset='161' length='0'></replacement>
229- <replacement offset='165' length='19'> </replacement>
230- </replacements>"#
231- . as_bytes ( )
232- . to_vec ( ) ;
233-
234- let expected = FormatAdvice {
235- replacements : [ 113 , 147 , 161 , 165 ]
236- . iter ( )
237- . map ( |offset| Replacement {
238- offset : * offset,
239- ..Default :: default ( )
240- } )
241- . collect ( ) ,
242- patched : None ,
243- } ;
244-
245- let xml = String :: from_utf8 ( xml_raw) . unwrap ( ) ;
246-
247- let document = quick_xml:: de:: from_str :: < FormatAdvice > ( & xml) . unwrap ( ) ;
248- assert_eq ! ( expected, document) ;
249- }
174+ use super :: summarize_style;
250175
251176 fn formalize_style ( style : & str , expected : & str ) {
252177 assert_eq ! ( summarize_style( style) , expected) ;
0 commit comments