Skip to content

Commit 493e8b7

Browse files
committed
Deduplicate compiler diagnostics.
1 parent cc80b40 commit 493e8b7

File tree

8 files changed

+281
-77
lines changed

8 files changed

+281
-77
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,

src/cargo/core/compiler/mod.rs

+16-11
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ fn rustc(cx: &mut Context<'_, '_>, unit: &Unit, exec: &Arc<dyn Executor>) -> Car
347347
};
348348
format!("could not compile `{}`{}{}", name, errors, warnings)
349349
})?;
350+
// Exec should never return with success *and* generate an error.
351+
debug_assert_eq!(output_options.errors_seen, 0);
350352
}
351353

352354
if rustc_dep_info_loc.exists() {
@@ -1199,7 +1201,7 @@ impl OutputOptions {
11991201
}
12001202

12011203
fn on_stdout_line(
1202-
state: &JobState<'_>,
1204+
state: &JobState<'_, '_>,
12031205
line: &str,
12041206
_package_id: PackageId,
12051207
_target: &Target,
@@ -1209,7 +1211,7 @@ fn on_stdout_line(
12091211
}
12101212

12111213
fn on_stderr_line(
1212-
state: &JobState<'_>,
1214+
state: &JobState<'_, '_>,
12131215
line: &str,
12141216
package_id: PackageId,
12151217
manifest_path: &std::path::Path,
@@ -1231,7 +1233,7 @@ fn on_stderr_line(
12311233

12321234
/// Returns true if the line should be cached.
12331235
fn on_stderr_line_inner(
1234-
state: &JobState<'_>,
1236+
state: &JobState<'_, '_>,
12351237
line: &str,
12361238
package_id: PackageId,
12371239
manifest_path: &std::path::Path,
@@ -1296,27 +1298,30 @@ fn on_stderr_line_inner(
12961298
message: String,
12971299
level: String,
12981300
}
1299-
if let Ok(mut error) = serde_json::from_str::<CompilerMessage>(compiler_message.get()) {
1300-
if error.level == "error" && error.message.starts_with("aborting due to") {
1301+
if let Ok(mut msg) = serde_json::from_str::<CompilerMessage>(compiler_message.get()) {
1302+
if msg.message.starts_with("aborting due to")
1303+
|| msg.message.ends_with("warning emitted")
1304+
|| msg.message.ends_with("warnings emitted")
1305+
{
13011306
// Skip this line; we'll print our own summary at the end.
13021307
return Ok(true);
13031308
}
13041309
// state.stderr will add a newline
1305-
if error.rendered.ends_with('\n') {
1306-
error.rendered.pop();
1310+
if msg.rendered.ends_with('\n') {
1311+
msg.rendered.pop();
13071312
}
13081313
let rendered = if options.color {
1309-
error.rendered
1314+
msg.rendered
13101315
} else {
13111316
// Strip only fails if the the Writer fails, which is Cursor
13121317
// on a Vec, which should never fail.
1313-
strip_ansi_escapes::strip(&error.rendered)
1318+
strip_ansi_escapes::strip(&msg.rendered)
13141319
.map(|v| String::from_utf8(v).expect("utf8"))
13151320
.expect("strip should never fail")
13161321
};
13171322
if options.show_warnings {
1318-
count_diagnostic(&error.level, options);
1319-
state.stderr(rendered)?;
1323+
count_diagnostic(&msg.level, options);
1324+
state.emit_diag(msg.level, rendered)?;
13201325
}
13211326
return Ok(true);
13221327
}

src/cargo/core/manifest.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,7 @@ impl Target {
925925
TargetKind::ExampleLib(..) | TargetKind::ExampleBin => {
926926
format!("example \"{}\"", self.name())
927927
}
928-
TargetKind::CustomBuild => "custom-build".to_string(),
928+
TargetKind::CustomBuild => "build script".to_string(),
929929
}
930930
}
931931
}

0 commit comments

Comments
 (0)