Skip to content

Commit d46c4e1

Browse files
committed
diagnostics: finalize robust shadow-to-user path mapping for compiler errors
* Normalize paths between integration runner's * Map cargo's absolute shadow output paths reliably by replacing temporary workspaces * Inject effectively through Charon driver * Remove diagnostic prints and fallback to deterministic string-replacement for verification assertions * Fix mapping of outputs to use robust non-spanning fallbacks * Take responsibility for all terminal UI gherrit-pr-id: Gd1a7f9c4436f266d37e83ebc2310f5a99a73d24d
1 parent 76a916e commit d46c4e1

File tree

251 files changed

+20171
-17
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

251 files changed

+20171
-17
lines changed

tools/Cargo.lock

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/hermes/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ syn = { version = "2.0.114", features = ["full", "visit", "extra-traits", "parsi
2323
quote = "1.0"
2424
thiserror = "2.0.18"
2525
walkdir = "2.5.0"
26+
indicatif = { version = "0.18.3", features = ["improved_unicode"] }
27+
console = "0.16.2"
2628

2729
[dev-dependencies]
2830
syn = { version = "2.0.114", features = ["printing", "full", "visit", "extra-traits", "parsing"] }
@@ -35,6 +37,8 @@ datatest-stable = "0.3.3"
3537
serde = { version = "1.0", features = ["derive"] }
3638
toml = "0.8"
3739
which = "6.0"
40+
regex.workspace = true
41+
strip-ansi-escapes = "0.2.1"
3842

3943
[[test]]
4044
name = "integration"

tools/hermes/src/charon.rs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
use std::process::Command;
1+
use std::{
2+
io::{BufRead, BufReader},
3+
process::Command,
4+
};
25

36
use anyhow::{bail, Context as _, Result};
7+
use cargo_metadata::{diagnostic::DiagnosticLevel, Message};
48

59
use crate::{
610
resolve::{Args, HermesTargetKind, Roots},
@@ -39,6 +43,9 @@ pub fn run_charon(args: &Args, roots: &Roots, packages: &[HermesArtifact]) -> Re
3943
// Separator for the underlying cargo command
4044
cmd.arg("--");
4145

46+
// Ensure cargo emits json msgs which charon-driver natively generates
47+
cmd.arg("--message-format=json");
48+
4249
cmd.arg("--manifest-path").arg(&artifact.shadow_manifest_path);
4350

4451
match artifact.target_kind {
@@ -77,9 +84,100 @@ pub fn run_charon(args: &Args, roots: &Roots, packages: &[HermesArtifact]) -> Re
7784

7885
log::debug!("Command: {:?}", cmd);
7986

80-
let status = cmd.status().context("Failed to execute charon")?;
87+
cmd.stdout(std::process::Stdio::piped());
88+
cmd.stderr(std::process::Stdio::piped());
89+
let mut child = cmd.spawn().context("Failed to spawn charon")?;
90+
91+
let mut output_error = false;
92+
93+
let safety_buffer = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
94+
let safety_buffer_clone = std::sync::Arc::clone(&safety_buffer);
95+
if let Some(stderr) = child.stderr.take() {
96+
std::thread::spawn(move || {
97+
use std::io::{BufRead, BufReader};
98+
let reader = BufReader::new(stderr);
99+
for line in reader.lines() {
100+
if let Ok(line) = line {
101+
if let Ok(mut buf) = safety_buffer_clone.lock() {
102+
buf.push(line);
103+
}
104+
}
105+
}
106+
});
107+
}
81108

82-
if !status.success() {
109+
let pb = indicatif::ProgressBar::new_spinner();
110+
pb.set_style(
111+
indicatif::ProgressStyle::default_spinner().template("{spinner:.green} {msg}").unwrap(),
112+
);
113+
pb.enable_steady_tick(std::time::Duration::from_millis(100));
114+
pb.set_message("Compiling...");
115+
116+
if let Some(stdout) = child.stdout.take() {
117+
let reader = BufReader::new(stdout);
118+
119+
// TODO: We shouldn't hard-code assumptions about the integration
120+
// testing environment.
121+
//
122+
// When resolving mapped diagnostic paths, the original workspace is
123+
// either test_case_root/source or workspace root.
124+
let user_root = if roots.workspace.join("source").exists() {
125+
roots.workspace.join("source")
126+
} else {
127+
roots.workspace.clone()
128+
};
129+
let mut mapper = crate::diagnostics::DiagnosticMapper::new(
130+
artifact.shadow_manifest_path.parent().unwrap().to_path_buf(),
131+
user_root,
132+
);
133+
134+
for line in reader.lines() {
135+
if let Ok(line) = line {
136+
if let Ok(msg) = serde_json::from_str::<cargo_metadata::Message>(&line) {
137+
match msg {
138+
Message::CompilerArtifact(a) => {
139+
pb.set_message(format!("Compiling {}", a.target.name));
140+
}
141+
Message::CompilerMessage(msg) => {
142+
pb.suspend(|| {
143+
mapper.render_miette(&msg.message, |s| eprintln!("{}", s));
144+
});
145+
if matches!(
146+
msg.message.level,
147+
DiagnosticLevel::Error | DiagnosticLevel::Ice
148+
) {
149+
output_error = true;
150+
}
151+
}
152+
Message::TextLine(t) => {
153+
if let Ok(mut buf) = safety_buffer.lock() {
154+
buf.push(t);
155+
}
156+
}
157+
_ => {}
158+
}
159+
} else {
160+
if let Ok(mut buf) = safety_buffer.lock() {
161+
buf.push(line);
162+
}
163+
}
164+
}
165+
}
166+
}
167+
168+
pb.finish_and_clear();
169+
170+
let status = child.wait().context("Failed to wait for charon")?;
171+
172+
if output_error {
173+
bail!("Diagnostic error in charon");
174+
} else if !status.success() {
175+
// "Silent Death" dump
176+
if let Ok(buf) = safety_buffer.lock() {
177+
for line in buf.iter() {
178+
eprintln!("{}", line);
179+
}
180+
}
83181
bail!("Charon failed with status: {}", status);
84182
}
85183
}

tools/hermes/src/diagnostics.rs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use std::{
2+
collections::HashMap,
3+
fs,
4+
path::{Path, PathBuf},
5+
};
6+
7+
use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel, DiagnosticSpan};
8+
use miette::{NamedSource, Report, SourceOffset};
9+
use thiserror::Error;
10+
11+
pub struct DiagnosticMapper {
12+
shadow_root: PathBuf,
13+
user_root: PathBuf,
14+
user_root_canonical: PathBuf,
15+
source_cache: HashMap<PathBuf, String>,
16+
}
17+
18+
#[derive(Error, Debug)]
19+
#[error("{message}")]
20+
struct MappedError {
21+
message: String,
22+
src: NamedSource<String>,
23+
labels: Vec<miette::LabeledSpan>,
24+
help: Option<String>,
25+
related: Vec<MappedError>,
26+
}
27+
28+
impl miette::Diagnostic for MappedError {
29+
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
30+
Some(&self.src)
31+
}
32+
33+
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
34+
if self.labels.is_empty() {
35+
None
36+
} else {
37+
Some(Box::new(self.labels.iter().cloned()))
38+
}
39+
}
40+
41+
fn help(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
42+
self.help.as_ref().map(|h| Box::new(h.clone()) as Box<dyn std::fmt::Display>)
43+
}
44+
45+
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
46+
if self.related.is_empty() {
47+
None
48+
} else {
49+
let iter = self.related.iter().map(|e| e as &dyn miette::Diagnostic);
50+
Some(Box::new(iter))
51+
}
52+
}
53+
}
54+
55+
impl DiagnosticMapper {
56+
pub fn new(shadow_root: PathBuf, user_root: PathBuf) -> Self {
57+
let user_root_canonical =
58+
fs::canonicalize(&user_root).unwrap_or_else(|_| user_root.clone());
59+
Self { shadow_root, user_root, user_root_canonical, source_cache: HashMap::new() }
60+
}
61+
62+
pub fn map_path(&self, path: &Path) -> Option<PathBuf> {
63+
let mut p = path.to_path_buf();
64+
if p.is_relative() {
65+
p = self.user_root.join(p);
66+
}
67+
68+
// Strategy A: Starts with shadow_root
69+
if let Ok(suffix) = p.strip_prefix(&self.shadow_root) {
70+
return Some(self.user_root.join(suffix));
71+
}
72+
73+
// Strategy B: Starts with user_root or user_root_canonical
74+
if p.starts_with(&self.user_root) || p.starts_with(&self.user_root_canonical) {
75+
return Some(p);
76+
}
77+
78+
None
79+
}
80+
81+
fn get_source(&mut self, path: &Path) -> Option<String> {
82+
if let Some(src) = self.source_cache.get(path) {
83+
return Some(src.clone());
84+
}
85+
if let Ok(src) = fs::read_to_string(path) {
86+
self.source_cache.insert(path.to_path_buf(), src.clone());
87+
Some(src)
88+
} else {
89+
None
90+
}
91+
}
92+
93+
pub fn render_miette<F>(&mut self, diag: &Diagnostic, mut printer: F)
94+
where
95+
F: FnMut(String),
96+
{
97+
let mut mapped_paths_and_spans: HashMap<PathBuf, Vec<&DiagnosticSpan>> = HashMap::new();
98+
99+
// 1) Group spans by mapped path
100+
for s in &diag.spans {
101+
let p = PathBuf::from(&s.file_name);
102+
if let Some(mapped_path) = self.map_path(&p) {
103+
mapped_paths_and_spans.entry(mapped_path).or_default().push(s);
104+
}
105+
}
106+
107+
// Check children for help messages
108+
let mut help_msg = None;
109+
for child in &diag.children {
110+
if child.level == DiagnosticLevel::Help {
111+
help_msg = Some(child.message.clone());
112+
}
113+
}
114+
115+
if !mapped_paths_and_spans.is_empty() {
116+
// Find the path that contains the primary span, or just take the first one
117+
let primary_path = diag
118+
.spans
119+
.iter()
120+
.find(|s| s.is_primary)
121+
.and_then(|s| self.map_path(&PathBuf::from(&s.file_name)))
122+
.or_else(|| mapped_paths_and_spans.keys().next().cloned());
123+
124+
if let Some(main_path) = primary_path {
125+
let mut all_errors = Vec::new();
126+
127+
// Sort the paths to have the primary path first
128+
let mut paths: Vec<PathBuf> = mapped_paths_and_spans.keys().cloned().collect();
129+
paths.sort_by_key(|p| p != &main_path);
130+
131+
for p in paths {
132+
if let Some(src) = self.get_source(&p) {
133+
let mut labels = Vec::new();
134+
for s in mapped_paths_and_spans.get(&p).unwrap() {
135+
let label_text = s.label.clone().unwrap_or_default();
136+
let start = s.byte_start as usize;
137+
let len = (s.byte_end - s.byte_start) as usize;
138+
if start <= src.len() && start + len <= src.len() {
139+
let offset = SourceOffset::from(start);
140+
labels.push(miette::LabeledSpan::new(
141+
Some(label_text),
142+
offset.offset(),
143+
len,
144+
));
145+
}
146+
}
147+
148+
let err = MappedError {
149+
message: if p == main_path {
150+
diag.message.clone()
151+
} else {
152+
format!("related to: {}", p.display())
153+
},
154+
src: NamedSource::new(p.to_string_lossy(), src),
155+
labels,
156+
help: if p == main_path { help_msg.clone() } else { None },
157+
related: Vec::new(),
158+
};
159+
all_errors.push(err);
160+
}
161+
}
162+
163+
if !all_errors.is_empty() {
164+
let mut main_err = all_errors.remove(0);
165+
main_err.related = all_errors;
166+
printer(format!("{:?}", Report::new(main_err)));
167+
return;
168+
}
169+
}
170+
}
171+
172+
// If we get here, no span was successfully mapped
173+
let prefix = match diag.level {
174+
DiagnosticLevel::Error | DiagnosticLevel::Ice => "[External Error]",
175+
DiagnosticLevel::Warning => "[External Warning]",
176+
_ => "[External Info]",
177+
};
178+
printer(format!("{} {}", prefix, diag.message));
179+
}
180+
}

0 commit comments

Comments
 (0)