Skip to content

Commit 54bc19e

Browse files
committed
Auto merge of #9675 - ehuss:diagnostic-dedupe, r=alexcrichton
Deduplicate compiler diagnostics. This adds some logic to deduplicate diagnostics emitted across rustc invocations. There are some situations where different targets can emit the same message, and that has caused confusion particularly for new users. A prominent example is running `cargo test` which will build the library twice concurrently (once as a normal library, and once as a test). As part of this, the "N warnings emitted" message sent by rustc is intercepted, and instead cargo generates its own summary. This is to prevent potentially confusing situations where that message is either deduplicated, or wrong. It also provides a little more context to explain exactly *what* issued the warnings. Example: ```warning: `foo` (lib) generated 1 warning``` This only impacts human-readable output, it does not change JSON behavior. Closes #9104
2 parents 2b4c8d9 + 5606d1b commit 54bc19e

File tree

8 files changed

+322
-86
lines changed

8 files changed

+322
-86
lines changed

src/cargo/core/compiler/custom_build.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub fn prepare(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Job> {
129129
}
130130

131131
fn emit_build_output(
132-
state: &JobState<'_>,
132+
state: &JobState<'_, '_>,
133133
output: &BuildOutput,
134134
out_dir: &Path,
135135
package_id: PackageId,

src/cargo/core/compiler/job.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ pub struct Job {
1212
/// Each proc should send its description before starting.
1313
/// It should send either once or close immediately.
1414
pub struct Work {
15-
inner: Box<dyn FnOnce(&JobState<'_>) -> CargoResult<()> + Send>,
15+
inner: Box<dyn FnOnce(&JobState<'_, '_>) -> CargoResult<()> + Send>,
1616
}
1717

1818
impl Work {
1919
pub fn new<F>(f: F) -> Work
2020
where
21-
F: FnOnce(&JobState<'_>) -> CargoResult<()> + Send + 'static,
21+
F: FnOnce(&JobState<'_, '_>) -> CargoResult<()> + Send + 'static,
2222
{
2323
Work { inner: Box::new(f) }
2424
}
@@ -27,7 +27,7 @@ impl Work {
2727
Work::new(|_| Ok(()))
2828
}
2929

30-
pub fn call(self, tx: &JobState<'_>) -> CargoResult<()> {
30+
pub fn call(self, tx: &JobState<'_, '_>) -> CargoResult<()> {
3131
(self.inner)(tx)
3232
}
3333

@@ -58,7 +58,7 @@ impl Job {
5858

5959
/// Consumes this job by running it, returning the result of the
6060
/// computation.
61-
pub fn run(self, state: &JobState<'_>) -> CargoResult<()> {
61+
pub fn run(self, state: &JobState<'_, '_>) -> CargoResult<()> {
6262
self.work.call(state)
6363
}
6464

src/cargo/core/compiler/job_queue.rs

+127-10
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@
4949
//! The current scheduling algorithm is relatively primitive and could likely be
5050
//! improved.
5151
52-
use std::cell::Cell;
52+
use std::cell::{Cell, RefCell};
5353
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
54+
use std::fmt::Write as _;
5455
use std::io;
5556
use std::marker;
5657
use std::sync::Arc;
@@ -125,6 +126,14 @@ struct DrainState<'cfg> {
125126

126127
queue: DependencyQueue<Unit, Artifact, Job>,
127128
messages: Arc<Queue<Message>>,
129+
/// Diagnostic deduplication support.
130+
diag_dedupe: DiagDedupe<'cfg>,
131+
/// Count of warnings, used to print a summary after the job succeeds.
132+
///
133+
/// First value is the total number of warnings, and the second value is
134+
/// the number that were suppressed because they were duplicates of a
135+
/// previous warning.
136+
warning_count: HashMap<JobId, (usize, usize)>,
128137
active: HashMap<JobId, Unit>,
129138
compiled: HashSet<PackageId>,
130139
documented: HashSet<PackageId>,
@@ -174,7 +183,7 @@ impl std::fmt::Display for JobId {
174183
///
175184
/// The job may execute on either a dedicated thread or the main thread. If the job executes on the
176185
/// main thread, the `output` field must be set to prevent a deadlock.
177-
pub struct JobState<'a> {
186+
pub struct JobState<'a, 'cfg> {
178187
/// Channel back to the main thread to coordinate messages and such.
179188
///
180189
/// When the `output` field is `Some`, care must be taken to avoid calling `push_bounded` on
@@ -191,7 +200,7 @@ pub struct JobState<'a> {
191200
/// interleaved. In the future, it may be wrapped in a `Mutex` instead. In this case
192201
/// interleaving is still prevented as the lock would be held for the whole printing of an
193202
/// output message.
194-
output: Option<&'a Config>,
203+
output: Option<&'a DiagDedupe<'cfg>>,
195204

196205
/// The job id that this state is associated with, used when sending
197206
/// messages back to the main thread.
@@ -207,6 +216,36 @@ pub struct JobState<'a> {
207216
_marker: marker::PhantomData<&'a ()>,
208217
}
209218

219+
/// Handler for deduplicating diagnostics.
220+
struct DiagDedupe<'cfg> {
221+
seen: RefCell<HashSet<u64>>,
222+
config: &'cfg Config,
223+
}
224+
225+
impl<'cfg> DiagDedupe<'cfg> {
226+
fn new(config: &'cfg Config) -> Self {
227+
DiagDedupe {
228+
seen: RefCell::new(HashSet::new()),
229+
config,
230+
}
231+
}
232+
233+
/// Emits a diagnostic message.
234+
///
235+
/// Returns `true` if the message was emitted, or `false` if it was
236+
/// suppressed for being a duplicate.
237+
fn emit_diag(&self, diag: &str) -> CargoResult<bool> {
238+
let h = util::hash_u64(diag);
239+
if !self.seen.borrow_mut().insert(h) {
240+
return Ok(false);
241+
}
242+
let mut shell = self.config.shell();
243+
shell.print_ansi_stderr(diag.as_bytes())?;
244+
shell.err().write_all(b"\n")?;
245+
Ok(true)
246+
}
247+
}
248+
210249
/// Possible artifacts that can be produced by compilations, used as edge values
211250
/// in the dependency graph.
212251
///
@@ -232,6 +271,15 @@ enum Message {
232271
BuildPlanMsg(String, ProcessBuilder, Arc<Vec<OutputFile>>),
233272
Stdout(String),
234273
Stderr(String),
274+
Diagnostic {
275+
id: JobId,
276+
level: String,
277+
diag: String,
278+
},
279+
WarningCount {
280+
id: JobId,
281+
emitted: bool,
282+
},
235283
FixDiagnostic(diagnostic_server::Message),
236284
Token(io::Result<Acquired>),
237285
Finish(JobId, Artifact, CargoResult<()>),
@@ -244,7 +292,7 @@ enum Message {
244292
ReleaseToken(JobId),
245293
}
246294

247-
impl<'a> JobState<'a> {
295+
impl<'a, 'cfg> JobState<'a, 'cfg> {
248296
pub fn running(&self, cmd: &ProcessBuilder) {
249297
self.messages.push(Message::Run(self.id, cmd.to_string()));
250298
}
@@ -260,17 +308,17 @@ impl<'a> JobState<'a> {
260308
}
261309

262310
pub fn stdout(&self, stdout: String) -> CargoResult<()> {
263-
if let Some(config) = self.output {
264-
writeln!(config.shell().out(), "{}", stdout)?;
311+
if let Some(dedupe) = self.output {
312+
writeln!(dedupe.config.shell().out(), "{}", stdout)?;
265313
} else {
266314
self.messages.push_bounded(Message::Stdout(stdout));
267315
}
268316
Ok(())
269317
}
270318

271319
pub fn stderr(&self, stderr: String) -> CargoResult<()> {
272-
if let Some(config) = self.output {
273-
let mut shell = config.shell();
320+
if let Some(dedupe) = self.output {
321+
let mut shell = dedupe.config.shell();
274322
shell.print_ansi_stderr(stderr.as_bytes())?;
275323
shell.err().write_all(b"\n")?;
276324
} else {
@@ -279,6 +327,25 @@ impl<'a> JobState<'a> {
279327
Ok(())
280328
}
281329

330+
pub fn emit_diag(&self, level: String, diag: String) -> CargoResult<()> {
331+
if let Some(dedupe) = self.output {
332+
let emitted = dedupe.emit_diag(&diag)?;
333+
if level == "warning" {
334+
self.messages.push(Message::WarningCount {
335+
id: self.id,
336+
emitted,
337+
});
338+
}
339+
} else {
340+
self.messages.push_bounded(Message::Diagnostic {
341+
id: self.id,
342+
level,
343+
diag,
344+
});
345+
}
346+
Ok(())
347+
}
348+
282349
/// A method used to signal to the coordinator thread that the rmeta file
283350
/// for an rlib has been produced. This is only called for some rmeta
284351
/// builds when required, and can be called at any time before a job ends.
@@ -410,6 +477,8 @@ impl<'cfg> JobQueue<'cfg> {
410477
// typical messages. If you change this, please update the test
411478
// caching_large_output, too.
412479
messages: Arc::new(Queue::new(100)),
480+
diag_dedupe: DiagDedupe::new(cx.bcx.config),
481+
warning_count: HashMap::new(),
413482
active: HashMap::new(),
414483
compiled: HashSet::new(),
415484
documented: HashSet::new(),
@@ -563,6 +632,15 @@ impl<'cfg> DrainState<'cfg> {
563632
shell.print_ansi_stderr(err.as_bytes())?;
564633
shell.err().write_all(b"\n")?;
565634
}
635+
Message::Diagnostic { id, level, diag } => {
636+
let emitted = self.diag_dedupe.emit_diag(&diag)?;
637+
if level == "warning" {
638+
self.bump_warning_count(id, emitted);
639+
}
640+
}
641+
Message::WarningCount { id, emitted } => {
642+
self.bump_warning_count(id, emitted);
643+
}
566644
Message::FixDiagnostic(msg) => {
567645
self.print.print(&msg)?;
568646
}
@@ -586,6 +664,7 @@ impl<'cfg> DrainState<'cfg> {
586664
self.tokens.extend(rustc_tokens);
587665
}
588666
self.to_send_clients.remove(&id);
667+
self.report_warning_count(cx.bcx.config, id);
589668
self.active.remove(&id).unwrap()
590669
}
591670
// ... otherwise if it hasn't finished we leave it
@@ -936,7 +1015,7 @@ impl<'cfg> DrainState<'cfg> {
9361015
let fresh = job.freshness();
9371016
let rmeta_required = cx.rmeta_required(unit);
9381017

939-
let doit = move |state: JobState<'_>| {
1018+
let doit = move |state: JobState<'_, '_>| {
9401019
let mut sender = FinishOnDrop {
9411020
messages: &state.messages,
9421021
id,
@@ -992,7 +1071,7 @@ impl<'cfg> DrainState<'cfg> {
9921071
doit(JobState {
9931072
id,
9941073
messages,
995-
output: Some(cx.bcx.config),
1074+
output: Some(&self.diag_dedupe),
9961075
rmeta_required: Cell::new(rmeta_required),
9971076
_marker: marker::PhantomData,
9981077
});
@@ -1044,6 +1123,44 @@ impl<'cfg> DrainState<'cfg> {
10441123
Ok(())
10451124
}
10461125

1126+
fn bump_warning_count(&mut self, id: JobId, emitted: bool) {
1127+
let cnts = self.warning_count.entry(id).or_default();
1128+
cnts.0 += 1;
1129+
if !emitted {
1130+
cnts.1 += 1;
1131+
}
1132+
}
1133+
1134+
/// Displays a final report of the warnings emitted by a particular job.
1135+
fn report_warning_count(&mut self, config: &Config, id: JobId) {
1136+
let count = match self.warning_count.remove(&id) {
1137+
Some(count) => count,
1138+
None => return,
1139+
};
1140+
let unit = &self.active[&id];
1141+
let mut message = format!("`{}` ({}", unit.pkg.name(), unit.target.description_named());
1142+
if unit.mode.is_rustc_test() && !(unit.target.is_test() || unit.target.is_bench()) {
1143+
message.push_str(" test");
1144+
} else if unit.mode.is_doc_test() {
1145+
message.push_str(" doctest");
1146+
} else if unit.mode.is_doc() {
1147+
message.push_str(" doc");
1148+
}
1149+
message.push_str(") generated ");
1150+
match count.0 {
1151+
1 => message.push_str("1 warning"),
1152+
n => drop(write!(message, "{} warnings", n)),
1153+
};
1154+
match count.1 {
1155+
0 => {}
1156+
1 => message.push_str(" (1 duplicate)"),
1157+
n => drop(write!(message, " ({} duplicates)", n)),
1158+
}
1159+
// Errors are ignored here because it is tricky to handle them
1160+
// correctly, and they aren't important.
1161+
drop(config.shell().warn(message));
1162+
}
1163+
10471164
fn finish(
10481165
&mut self,
10491166
id: JobId,

0 commit comments

Comments
 (0)