1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
//! A small TCP server to handle collection of diagnostics information in a
//! cross-platform way for the `cargo fix` command.

use std::collections::HashSet;
use std::io::{BufReader, Read, Write};
use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};

use anyhow::{Context as _, Error};
use cargo_util::ProcessBuilder;
use serde::{Deserialize, Serialize};
use tracing::warn;

use crate::core::Edition;
use crate::util::errors::CargoResult;
use crate::util::GlobalContext;

const DIAGNOSTICS_SERVER_VAR: &str = "__CARGO_FIX_DIAGNOSTICS_SERVER";

#[derive(Deserialize, Serialize, Hash, Eq, PartialEq, Clone)]
pub enum Message {
    Migrating {
        file: String,
        from_edition: Edition,
        to_edition: Edition,
    },
    Fixing {
        file: String,
    },
    Fixed {
        file: String,
        fixes: u32,
    },
    FixFailed {
        files: Vec<String>,
        krate: Option<String>,
        errors: Vec<String>,
        abnormal_exit: Option<String>,
    },
    ReplaceFailed {
        file: String,
        message: String,
    },
    EditionAlreadyEnabled {
        message: String,
        edition: Edition,
    },
}

impl Message {
    pub fn post(&self, gctx: &GlobalContext) -> Result<(), Error> {
        let addr = gctx
            .get_env(DIAGNOSTICS_SERVER_VAR)
            .context("diagnostics collector misconfigured")?;
        let mut client =
            TcpStream::connect(&addr).context("failed to connect to parent diagnostics target")?;

        let s = serde_json::to_string(self).context("failed to serialize message")?;
        client
            .write_all(s.as_bytes())
            .context("failed to write message to diagnostics target")?;
        client
            .shutdown(Shutdown::Write)
            .context("failed to shutdown")?;

        client
            .read_to_end(&mut Vec::new())
            .context("failed to receive a disconnect")?;

        Ok(())
    }
}

/// A printer that will print diagnostics messages to the shell.
pub struct DiagnosticPrinter<'a> {
    /// The context to get the shell to print to.
    gctx: &'a GlobalContext,
    /// An optional wrapper to be used in addition to `rustc.wrapper` for workspace crates.
    /// This is used to get the correct bug report URL. For instance,
    /// if `clippy-driver` is set as the value for the wrapper,
    /// then the correct bug report URL for `clippy` can be obtained.
    workspace_wrapper: &'a Option<PathBuf>,
    // A set of messages that have already been printed.
    dedupe: HashSet<Message>,
}

impl<'a> DiagnosticPrinter<'a> {
    pub fn new(
        gctx: &'a GlobalContext,
        workspace_wrapper: &'a Option<PathBuf>,
    ) -> DiagnosticPrinter<'a> {
        DiagnosticPrinter {
            gctx,
            workspace_wrapper,
            dedupe: HashSet::new(),
        }
    }

    pub fn print(&mut self, msg: &Message) -> CargoResult<()> {
        match msg {
            Message::Migrating {
                file,
                from_edition,
                to_edition,
            } => {
                if !self.dedupe.insert(msg.clone()) {
                    return Ok(());
                }
                self.gctx.shell().status(
                    "Migrating",
                    &format!("{} from {} edition to {}", file, from_edition, to_edition),
                )
            }
            Message::Fixing { file } => self
                .gctx
                .shell()
                .verbose(|shell| shell.status("Fixing", file)),
            Message::Fixed { file, fixes } => {
                let msg = if *fixes == 1 { "fix" } else { "fixes" };
                let msg = format!("{} ({} {})", file, fixes, msg);
                self.gctx.shell().status("Fixed", msg)
            }
            Message::ReplaceFailed { file, message } => {
                let msg = format!("error applying suggestions to `{}`\n", file);
                self.gctx.shell().warn(&msg)?;
                write!(
                    self.gctx.shell().err(),
                    "The full error message was:\n\n> {}\n\n",
                    message,
                )?;
                let issue_link = get_bug_report_url(self.workspace_wrapper);
                write!(
                    self.gctx.shell().err(),
                    "{}",
                    gen_please_report_this_bug_text(issue_link)
                )?;
                Ok(())
            }
            Message::FixFailed {
                files,
                krate,
                errors,
                abnormal_exit,
            } => {
                if let Some(ref krate) = *krate {
                    self.gctx.shell().warn(&format!(
                        "failed to automatically apply fixes suggested by rustc \
                         to crate `{}`",
                        krate,
                    ))?;
                } else {
                    self.gctx
                        .shell()
                        .warn("failed to automatically apply fixes suggested by rustc")?;
                }
                if !files.is_empty() {
                    writeln!(
                        self.gctx.shell().err(),
                        "\nafter fixes were automatically applied the compiler \
                         reported errors within these files:\n"
                    )?;
                    for file in files {
                        writeln!(self.gctx.shell().err(), "  * {}", file)?;
                    }
                    writeln!(self.gctx.shell().err())?;
                }
                let issue_link = get_bug_report_url(self.workspace_wrapper);
                write!(
                    self.gctx.shell().err(),
                    "{}",
                    gen_please_report_this_bug_text(issue_link)
                )?;
                if !errors.is_empty() {
                    writeln!(
                        self.gctx.shell().err(),
                        "The following errors were reported:"
                    )?;
                    for error in errors {
                        write!(self.gctx.shell().err(), "{}", error)?;
                        if !error.ends_with('\n') {
                            writeln!(self.gctx.shell().err())?;
                        }
                    }
                }
                if let Some(exit) = abnormal_exit {
                    writeln!(self.gctx.shell().err(), "rustc exited abnormally: {}", exit)?;
                }
                writeln!(
                    self.gctx.shell().err(),
                    "Original diagnostics will follow.\n"
                )?;
                Ok(())
            }
            Message::EditionAlreadyEnabled { message, edition } => {
                if !self.dedupe.insert(msg.clone()) {
                    return Ok(());
                }
                // Don't give a really verbose warning if it has already been issued.
                if self.dedupe.insert(Message::EditionAlreadyEnabled {
                    message: "".to_string(), // Dummy, so that this only long-warns once.
                    edition: *edition,
                }) {
                    self.gctx.shell().warn(&format!("\
{}

If you are trying to migrate from the previous edition ({prev_edition}), the
process requires following these steps:

1. Start with `edition = \"{prev_edition}\"` in `Cargo.toml`
2. Run `cargo fix --edition`
3. Modify `Cargo.toml` to set `edition = \"{this_edition}\"`
4. Run `cargo build` or `cargo test` to verify the fixes worked

More details may be found at
https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html
",
                        message, this_edition=edition, prev_edition=edition.previous().unwrap()
                    ))
                } else {
                    self.gctx.shell().warn(message)
                }
            }
        }
    }
}

fn gen_please_report_this_bug_text(url: &str) -> String {
    format!(
        "This likely indicates a bug in either rustc or cargo itself,\n\
     and we would appreciate a bug report! You're likely to see\n\
     a number of compiler warnings after this message which cargo\n\
     attempted to fix but failed. If you could open an issue at\n\
     {}\n\
     quoting the full output of this command we'd be very appreciative!\n\
     Note that you may be able to make some more progress in the near-term\n\
     fixing code with the `--broken-code` flag\n\n\
     ",
        url
    )
}

fn get_bug_report_url(rustc_workspace_wrapper: &Option<PathBuf>) -> &str {
    let clippy = std::ffi::OsStr::new("clippy-driver");
    let issue_link = match rustc_workspace_wrapper.as_ref().and_then(|x| x.file_stem()) {
        Some(wrapper) if wrapper == clippy => "https://github.com/rust-lang/rust-clippy/issues",
        _ => "https://github.com/rust-lang/rust/issues",
    };

    issue_link
}

#[derive(Debug)]
pub struct RustfixDiagnosticServer {
    listener: TcpListener,
    addr: SocketAddr,
}

pub struct StartedServer {
    addr: SocketAddr,
    done: Arc<AtomicBool>,
    thread: Option<JoinHandle<()>>,
}

impl RustfixDiagnosticServer {
    pub fn new() -> Result<Self, Error> {
        let listener = TcpListener::bind("127.0.0.1:0")
            .with_context(|| "failed to bind TCP listener to manage locking")?;
        let addr = listener.local_addr()?;

        Ok(RustfixDiagnosticServer { listener, addr })
    }

    pub fn configure(&self, process: &mut ProcessBuilder) {
        process.env(DIAGNOSTICS_SERVER_VAR, self.addr.to_string());
    }

    pub fn start<F>(self, on_message: F) -> Result<StartedServer, Error>
    where
        F: Fn(Message) + Send + 'static,
    {
        let addr = self.addr;
        let done = Arc::new(AtomicBool::new(false));
        let done2 = done.clone();
        let thread = thread::spawn(move || {
            self.run(&on_message, &done2);
        });

        Ok(StartedServer {
            addr,
            thread: Some(thread),
            done,
        })
    }

    fn run(self, on_message: &dyn Fn(Message), done: &AtomicBool) {
        while let Ok((client, _)) = self.listener.accept() {
            if done.load(Ordering::SeqCst) {
                break;
            }
            let mut client = BufReader::new(client);
            let mut s = String::new();
            if let Err(e) = client.read_to_string(&mut s) {
                warn!("diagnostic server failed to read: {}", e);
            } else {
                match serde_json::from_str(&s) {
                    Ok(message) => on_message(message),
                    Err(e) => warn!("invalid diagnostics message: {}", e),
                }
            }
            // The client should be kept alive until after `on_message` is
            // called to ensure that the client doesn't exit too soon (and
            // Message::Finish getting posted before Message::FixDiagnostic).
            drop(client);
        }
    }
}

impl Drop for StartedServer {
    fn drop(&mut self) {
        self.done.store(true, Ordering::SeqCst);
        // Ignore errors here as this is largely best-effort
        if TcpStream::connect(&self.addr).is_err() {
            return;
        }
        drop(self.thread.take().unwrap().join());
    }
}