@@ -9,7 +9,7 @@ use std::str;
9
9
use failure:: { Error , ResultExt } ;
10
10
use git2;
11
11
use rustfix:: diagnostics:: Diagnostic ;
12
- use rustfix;
12
+ use rustfix:: { self , CodeFix } ;
13
13
use serde_json;
14
14
15
15
use core:: Workspace ;
@@ -144,15 +144,18 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
144
144
// If we didn't actually make any changes then we can immediately exec the
145
145
// new rustc, and otherwise we capture the output to hide it in the scenario
146
146
// that we have to back it all out.
147
- if !fixes. original_files . is_empty ( ) {
147
+ if !fixes. files . is_empty ( ) {
148
148
let mut cmd = Command :: new ( & rustc) ;
149
149
args. apply ( & mut cmd) ;
150
150
cmd. arg ( "--error-format=json" ) ;
151
151
let output = cmd. output ( ) . context ( "failed to spawn rustc" ) ?;
152
152
153
153
if output. status . success ( ) {
154
- for message in fixes. messages . drain ( ..) {
155
- message. post ( ) ?;
154
+ for ( path, file) in fixes. files . iter ( ) {
155
+ Message :: Fixing {
156
+ file : path. clone ( ) ,
157
+ fixes : file. fixes_applied ,
158
+ } . post ( ) ?;
156
159
}
157
160
}
158
161
@@ -167,9 +170,9 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
167
170
// user's code with our changes. Back out everything and fall through
168
171
// below to recompile again.
169
172
if !output. status . success ( ) {
170
- for ( k , v ) in fixes. original_files {
171
- fs:: write ( & k , v )
172
- . with_context ( |_| format ! ( "failed to write file `{}`" , k ) ) ?;
173
+ for ( path , file ) in fixes. files . iter ( ) {
174
+ fs:: write ( path , & file . original_code )
175
+ . with_context ( |_| format ! ( "failed to write file `{}`" , path ) ) ?;
173
176
}
174
177
log_failed_fix ( & output. stderr ) ?;
175
178
}
@@ -182,8 +185,13 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
182
185
183
186
#[ derive( Default ) ]
184
187
struct FixedCrate {
185
- messages : Vec < Message > ,
186
- original_files : HashMap < String , String > ,
188
+ files : HashMap < String , FixedFile > ,
189
+ }
190
+
191
+ struct FixedFile {
192
+ errors_applying_fixes : Vec < String > ,
193
+ fixes_applied : u32 ,
194
+ original_code : String ,
187
195
}
188
196
189
197
fn rustfix_crate ( lock_addr : & str , rustc : & Path , filename : & Path , args : & FixArgs )
@@ -192,11 +200,6 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
192
200
args. verify_not_preparing_for_enabled_edition ( ) ?;
193
201
args. warn_if_preparing_probably_inert ( ) ?;
194
202
195
- // If not empty, filter by these lints
196
- //
197
- // TODO: Implement a way to specify this
198
- let only = HashSet :: new ( ) ;
199
-
200
203
// First up we want to make sure that each crate is only checked by one
201
204
// process at a time. If two invocations concurrently check a crate then
202
205
// it's likely to corrupt it.
@@ -205,7 +208,96 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
205
208
// argument that looks like a Rust file.
206
209
let _lock = LockServerClient :: lock ( & lock_addr. parse ( ) ?, filename) ?;
207
210
208
- let mut cmd = Command :: new ( & rustc) ;
211
+ // Next up this is a bit suspicious, but we *iteratively* execute rustc and
212
+ // collect suggestions to feed to rustfix. Once we hit our limit of times to
213
+ // execute rustc or we appear to be reaching a fixed point we stop running
214
+ // rustc.
215
+ //
216
+ // This is currently done to handle code like:
217
+ //
218
+ // ::foo::<::Bar>();
219
+ //
220
+ // where there are two fixes to happen here: `crate::foo::<crate::Bar>()`.
221
+ // The spans for these two suggestions are overlapping and its difficult in
222
+ // the compiler to *not* have overlapping spans here. As a result, a naive
223
+ // implementation would feed the two compiler suggestions for the above fix
224
+ // into `rustfix`, but one would be rejected because it overlaps with the
225
+ // other.
226
+ //
227
+ // In this case though, both suggestions are valid and can be automatically
228
+ // applied! To handle this case we execute rustc multiple times, collecting
229
+ // fixes each time we do so. Along the way we discard any suggestions that
230
+ // failed to apply, assuming that they can be fixed the next time we run
231
+ // rustc.
232
+ //
233
+ // Naturally we want a few protections in place here though to avoid looping
234
+ // forever or otherwise losing data. To that end we have a few termination
235
+ // conditions:
236
+ //
237
+ // * Do this whole process a fixed number of times. In theory we probably
238
+ // need an infinite number of times to apply fixes, but we're not gonna
239
+ // sit around waiting for that.
240
+ // * If it looks like a fix genuinely can't be applied we need to bail out.
241
+ // Detect this when a fix fails to get applied *and* no suggestions
242
+ // successfully applied to the same file. In that case looks like we
243
+ // definitely can't make progress, so bail out.
244
+ let mut fixes = FixedCrate :: default ( ) ;
245
+ let mut last_fix_counts = HashMap :: new ( ) ;
246
+ let iterations = env:: var ( "CARGO_FIX_MAX_RETRIES" )
247
+ . ok ( )
248
+ . and_then ( |n| n. parse ( ) . ok ( ) )
249
+ . unwrap_or ( 4 ) ;
250
+ for _ in 0 ..iterations {
251
+ last_fix_counts. clear ( ) ;
252
+ for ( path, file) in fixes. files . iter_mut ( ) {
253
+ last_fix_counts. insert ( path. clone ( ) , file. fixes_applied ) ;
254
+ file. errors_applying_fixes . clear ( ) ; // we'll generate new errors below
255
+ }
256
+ rustfix_and_fix ( & mut fixes, rustc, filename, args) ?;
257
+ let mut progress_yet_to_be_made = false ;
258
+ for ( path, file) in fixes. files . iter_mut ( ) {
259
+ if file. errors_applying_fixes . len ( ) == 0 {
260
+ continue
261
+ }
262
+ // If anything was successfully fixed *and* there's at least one
263
+ // error, then assume the error was spurious and we'll try again on
264
+ // the next iteration.
265
+ if file. fixes_applied != * last_fix_counts. get ( path) . unwrap_or ( & 0 ) {
266
+ progress_yet_to_be_made = true ;
267
+ }
268
+ }
269
+ if !progress_yet_to_be_made {
270
+ break
271
+ }
272
+ }
273
+
274
+ // Any errors still remaining at this point need to be reported as probably
275
+ // bugs in Cargo and/or rustfix.
276
+ for ( path, file) in fixes. files . iter_mut ( ) {
277
+ for error in file. errors_applying_fixes . drain ( ..) {
278
+ Message :: ReplaceFailed {
279
+ file : path. clone ( ) ,
280
+ message : error,
281
+ } . post ( ) ?;
282
+ }
283
+ }
284
+
285
+ Ok ( fixes)
286
+ }
287
+
288
+ /// Execute `rustc` to apply one round of suggestions to the crate in question.
289
+ ///
290
+ /// This will fill in the `fixes` map with original code, suggestions applied,
291
+ /// and any errors encountered while fixing files.
292
+ fn rustfix_and_fix ( fixes : & mut FixedCrate , rustc : & Path , filename : & Path , args : & FixArgs )
293
+ -> Result < ( ) , Error >
294
+ {
295
+ // If not empty, filter by these lints
296
+ //
297
+ // TODO: Implement a way to specify this
298
+ let only = HashSet :: new ( ) ;
299
+
300
+ let mut cmd = Command :: new ( rustc) ;
209
301
cmd. arg ( "--error-format=json" ) ;
210
302
args. apply ( & mut cmd) ;
211
303
let output = cmd. output ( )
@@ -280,8 +372,6 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
280
372
filename. display( ) ,
281
373
) ;
282
374
283
- let mut original_files = HashMap :: with_capacity ( file_map. len ( ) ) ;
284
- let mut messages = Vec :: new ( ) ;
285
375
for ( file, suggestions) in file_map {
286
376
// Attempt to read the source code for this file. If this fails then
287
377
// that'd be pretty surprising, so log a message and otherwise keep
@@ -296,29 +386,35 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
296
386
let num_suggestions = suggestions. len ( ) ;
297
387
debug ! ( "applying {} fixes to {}" , num_suggestions, file) ;
298
388
299
- messages. push ( Message :: fixing ( & file, num_suggestions) ) ;
300
-
301
- match rustfix:: apply_suggestions ( & code, & suggestions) {
302
- Err ( e) => {
303
- Message :: ReplaceFailed {
304
- file,
305
- message : e. to_string ( ) ,
306
- } . post ( ) ?;
307
- // TODO: Add flag to decide if we want to continue or bail out
308
- continue ;
309
- }
310
- Ok ( new_code) => {
311
- fs:: write ( & file, new_code)
312
- . with_context ( |_| format ! ( "failed to write file `{}`" , file) ) ?;
313
- original_files. insert ( file, code) ;
389
+ // If this file doesn't already exist then we just read the original
390
+ // code, so save it. If the file already exists then the original code
391
+ // doesn't need to be updated as we've just read an interim state with
392
+ // some fixes but perhaps not all.
393
+ let fixed_file = fixes. files . entry ( file. clone ( ) )
394
+ . or_insert_with ( || {
395
+ FixedFile {
396
+ errors_applying_fixes : Vec :: new ( ) ,
397
+ fixes_applied : 0 ,
398
+ original_code : code. clone ( ) ,
399
+ }
400
+ } ) ;
401
+ let mut fixed = CodeFix :: new ( & code) ;
402
+
403
+ // As mentioned above in `rustfix_crate`, we don't immediately warn
404
+ // about suggestions that fail to apply here, and instead we save them
405
+ // off for later processing.
406
+ for suggestion in suggestions. iter ( ) . rev ( ) {
407
+ match fixed. apply ( suggestion) {
408
+ Ok ( ( ) ) => fixed_file. fixes_applied += 1 ,
409
+ Err ( e) => fixed_file. errors_applying_fixes . push ( e. to_string ( ) ) ,
314
410
}
315
411
}
412
+ let new_code = fixed. finish ( ) ?;
413
+ fs:: write ( & file, new_code)
414
+ . with_context ( |_| format ! ( "failed to write file `{}`" , file) ) ?;
316
415
}
317
416
318
- Ok ( FixedCrate {
319
- messages,
320
- original_files,
321
- } )
417
+ Ok ( ( ) )
322
418
}
323
419
324
420
fn exit_with ( status : ExitStatus ) -> ! {
0 commit comments