1use std::collections::HashSet;
5use std::io::{BufReader, Read, Write};
6use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::thread::{self, JoinHandle};
11
12use annotate_snippets::Group;
13use annotate_snippets::Level;
14use annotate_snippets::Origin;
15use anyhow::{Context as _, Error};
16use cargo_util::ProcessBuilder;
17use serde::{Deserialize, Serialize};
18use tracing::warn;
19
20use crate::core::Edition;
21use crate::util::GlobalContext;
22use crate::util::errors::CargoResult;
23use crate::util::network::LOCALHOST;
24
25const DIAGNOSTICS_SERVER_VAR: &str = "__CARGO_FIX_DIAGNOSTICS_SERVER";
26
27#[derive(Deserialize, Serialize, Hash, Eq, PartialEq, Clone)]
28pub enum Message {
29 Migrating {
30 file: String,
31 from_edition: Edition,
32 to_edition: Edition,
33 },
34 Fixing {
35 file: String,
36 },
37 Fixed {
38 file: String,
39 fixes: u32,
40 },
41 FixFailed {
42 files: Vec<String>,
43 krate: Option<String>,
44 errors: Vec<String>,
45 abnormal_exit: Option<String>,
46 },
47 ReplaceFailed {
48 file: String,
49 message: String,
50 },
51 EditionAlreadyEnabled {
52 message: String,
53 edition: Edition,
54 },
55}
56
57impl Message {
58 pub fn post(&self, gctx: &GlobalContext) -> Result<(), Error> {
59 let addr = gctx
60 .get_env(DIAGNOSTICS_SERVER_VAR)
61 .context("diagnostics collector misconfigured")?;
62 let mut client =
63 TcpStream::connect(&addr).context("failed to connect to parent diagnostics target")?;
64
65 let s = serde_json::to_string(self).context("failed to serialize message")?;
66 client
67 .write_all(s.as_bytes())
68 .context("failed to write message to diagnostics target")?;
69 client
70 .shutdown(Shutdown::Write)
71 .context("failed to shutdown")?;
72
73 client
74 .read_to_end(&mut Vec::new())
75 .context("failed to receive a disconnect")?;
76
77 Ok(())
78 }
79}
80
81pub struct DiagnosticPrinter<'a> {
83 gctx: &'a GlobalContext,
85 workspace_wrapper: &'a Option<PathBuf>,
90 dedupe: HashSet<Message>,
92}
93
94impl<'a> DiagnosticPrinter<'a> {
95 pub fn new(
96 gctx: &'a GlobalContext,
97 workspace_wrapper: &'a Option<PathBuf>,
98 ) -> DiagnosticPrinter<'a> {
99 DiagnosticPrinter {
100 gctx,
101 workspace_wrapper,
102 dedupe: HashSet::new(),
103 }
104 }
105
106 pub fn print(&mut self, msg: &Message) -> CargoResult<()> {
107 match msg {
108 Message::Migrating {
109 file,
110 from_edition,
111 to_edition,
112 } => {
113 if !self.dedupe.insert(msg.clone()) {
114 return Ok(());
115 }
116 self.gctx.shell().status(
117 "Migrating",
118 &format!("{file} from {from_edition} edition to {to_edition}"),
119 )
120 }
121 Message::Fixing { file } => self
122 .gctx
123 .shell()
124 .verbose(|shell| shell.status("Fixing", file)),
125 Message::Fixed { file, fixes } => {
126 let msg = if *fixes == 1 { "fix" } else { "fixes" };
127 let msg = format!("{file} ({fixes} {msg})");
128 self.gctx.shell().status("Fixed", msg)
129 }
130 Message::ReplaceFailed { file, message } => {
131 let issue_link = get_bug_report_url(self.workspace_wrapper);
132
133 let report = &[
134 Level::ERROR
135 .secondary_title("error applying suggestions")
136 .element(Origin::path(file))
137 .element(Level::ERROR.with_name("cause").message(message)),
138 gen_please_report_this_bug_group(issue_link),
139 gen_suggest_broken_code_group(),
140 ];
141 self.gctx.shell().print_report(report, false)?;
142 Ok(())
143 }
144 Message::FixFailed {
145 files,
146 krate,
147 errors,
148 abnormal_exit,
149 } => {
150 let to_crate = if let Some(ref krate) = *krate {
151 format!(" to crate `{krate}`",)
152 } else {
153 "".to_owned()
154 };
155 let issue_link = get_bug_report_url(self.workspace_wrapper);
156
157 let cause_message = if !errors.is_empty() {
158 Some(errors.join("\n").trim().to_owned())
159 } else {
160 None
161 };
162
163 let report = &[
164 Level::ERROR
165 .secondary_title(format!("errors present after applying fixes{to_crate}"))
166 .elements(files.iter().map(|f| Origin::path(f)))
167 .elements(
168 cause_message
169 .into_iter()
170 .map(|err| Level::ERROR.with_name("cause").message(err)),
171 )
172 .elements(abnormal_exit.iter().map(|exit| {
173 Level::ERROR
174 .with_name("cause")
175 .message(format!("rustc exited abnormally: {exit}"))
176 })),
177 gen_please_report_this_bug_group(issue_link),
178 gen_suggest_broken_code_group(),
179 Group::with_title(
180 Level::NOTE.secondary_title("original diagnostics will follow:"),
181 ),
182 ];
183
184 self.gctx.shell().print_report(report, false)?;
185 Ok(())
186 }
187 Message::EditionAlreadyEnabled { message, edition } => {
188 if !self.dedupe.insert(msg.clone()) {
189 return Ok(());
190 }
191 if self.dedupe.insert(Message::EditionAlreadyEnabled {
193 message: "".to_string(), edition: *edition,
195 }) {
196 self.gctx.shell().warn(&format!("\
197{message}
198
199If you are trying to migrate from the previous edition ({prev_edition}), the
200process requires following these steps:
201
2021. Start with `edition = \"{prev_edition}\"` in `Cargo.toml`
2032. Run `cargo fix --edition`
2043. Modify `Cargo.toml` to set `edition = \"{this_edition}\"`
2054. Run `cargo build` or `cargo test` to verify the fixes worked
206
207More details may be found at
208https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html
209",
210 this_edition=edition, prev_edition=edition.previous().unwrap()
211 ))
212 } else {
213 self.gctx.shell().warn(message)
214 }
215 }
216 }
217 }
218}
219
220fn gen_please_report_this_bug_group(url: &str) -> Group<'static> {
221 Group::with_title(Level::HELP.secondary_title(format!(
222 "to report this as a bug, open an issue at {url}, quoting the full output of this command"
223 )))
224}
225
226fn gen_suggest_broken_code_group() -> Group<'static> {
227 Group::with_title(
228 Level::HELP
229 .secondary_title("to possibly apply more fixes, pass in the `--broken-code` flag"),
230 )
231}
232
233fn get_bug_report_url(rustc_workspace_wrapper: &Option<PathBuf>) -> &str {
234 let clippy = std::ffi::OsStr::new("clippy-driver");
235 let issue_link = match rustc_workspace_wrapper.as_ref().and_then(|x| x.file_stem()) {
236 Some(wrapper) if wrapper == clippy => "https://github.com/rust-lang/rust-clippy/issues",
237 _ => "https://github.com/rust-lang/rust/issues",
238 };
239
240 issue_link
241}
242
243#[derive(Debug)]
244pub struct RustfixDiagnosticServer {
245 listener: TcpListener,
246 addr: SocketAddr,
247}
248
249pub struct StartedServer {
250 addr: SocketAddr,
251 done: Arc<AtomicBool>,
252 thread: Option<JoinHandle<()>>,
253}
254
255impl RustfixDiagnosticServer {
256 pub fn new() -> Result<Self, Error> {
257 let listener = TcpListener::bind(&LOCALHOST[..])
258 .context("failed to bind TCP listener to manage locking")?;
259 let addr = listener.local_addr()?;
260
261 Ok(RustfixDiagnosticServer { listener, addr })
262 }
263
264 pub fn configure(&self, process: &mut ProcessBuilder) {
265 process.env(DIAGNOSTICS_SERVER_VAR, self.addr.to_string());
266 }
267
268 pub fn start<F>(self, on_message: F) -> Result<StartedServer, Error>
269 where
270 F: Fn(Message) + Send + 'static,
271 {
272 let addr = self.addr;
273 let done = Arc::new(AtomicBool::new(false));
274 let done2 = done.clone();
275 let thread = thread::spawn(move || {
276 self.run(&on_message, &done2);
277 });
278
279 Ok(StartedServer {
280 addr,
281 thread: Some(thread),
282 done,
283 })
284 }
285
286 fn run(self, on_message: &dyn Fn(Message), done: &AtomicBool) {
287 while let Ok((client, _)) = self.listener.accept() {
288 if done.load(Ordering::SeqCst) {
289 break;
290 }
291 let mut client = BufReader::new(client);
292 let mut s = String::new();
293 if let Err(e) = client.read_to_string(&mut s) {
294 warn!("diagnostic server failed to read: {e}");
295 } else {
296 match serde_json::from_str(&s) {
297 Ok(message) => on_message(message),
298 Err(e) => warn!("invalid diagnostics message: {e}"),
299 }
300 }
301 drop(client);
305 }
306 }
307}
308
309impl Drop for StartedServer {
310 fn drop(&mut self) {
311 self.done.store(true, Ordering::SeqCst);
312 if TcpStream::connect(&self.addr).is_err() {
314 return;
315 }
316 drop(self.thread.take().unwrap().join());
317 }
318}