Skip to content

Commit e3f6477

Browse files
authored
fix(manifest): Show error source to users (#15939)
### What does this PR try to resolve? Ooch our way towards rustc's quality of error reporting to make up for the fact that users won't see most frontmatter rustc error messages. ### How to test and review this PR? Several parts of this are not ideal yet - Frontmatter close should show the open and show the EOF, instead of pointing at the open - Trailing content on close will usually have a newline - Multiple frontmatters should show the original frontmatter - Some content, like supported infostrings, should go in a help - Ideally we try to recover on no closing and instead point out a mismatched open/close But this is still an improvement over nothing!
2 parents 24bb93c + 90b68c8 commit e3f6477

18 files changed

+179
-31
lines changed

src/cargo/util/frontmatter.rs

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use crate::CargoResult;
2-
31
type Span = std::ops::Range<usize>;
42

53
#[derive(Debug)]
@@ -22,7 +20,7 @@ pub struct ScriptSource<'s> {
2220
}
2321

2422
impl<'s> ScriptSource<'s> {
25-
pub fn parse(raw: &'s str) -> CargoResult<Self> {
23+
pub fn parse(raw: &'s str) -> Result<Self, FrontmatterError> {
2624
use winnow::stream::FindSlice as _;
2725
use winnow::stream::Location as _;
2826
use winnow::stream::Offset as _;
@@ -61,24 +59,30 @@ impl<'s> ScriptSource<'s> {
6159
.char_indices()
6260
.find_map(|(i, c)| (c != FENCE_CHAR).then_some(i))
6361
.unwrap_or_else(|| input.eof_offset());
62+
let open_start = input.current_token_start();
63+
let fence_pattern = input.next_slice(fence_length);
64+
let open_end = input.current_token_start();
6465
match fence_length {
6566
0 => {
6667
return Ok(source);
6768
}
6869
1 | 2 => {
6970
// either not a frontmatter or invalid frontmatter opening
70-
anyhow::bail!(
71-
"found {fence_length} `{FENCE_CHAR}` in rust frontmatter, expected at least 3"
72-
)
71+
return Err(FrontmatterError::new(
72+
format!(
73+
"found {fence_length} `{FENCE_CHAR}` in rust frontmatter, expected at least 3"
74+
),
75+
open_start..open_end,
76+
));
7377
}
7478
_ => {}
7579
}
76-
let open_start = input.current_token_start();
77-
let fence_pattern = input.next_slice(fence_length);
78-
let open_end = input.current_token_start();
7980
source.open = Some(open_start..open_end);
8081
let Some(info_nl) = input.find_slice("\n") else {
81-
anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
82+
return Err(FrontmatterError::new(
83+
format!("no closing `{fence_pattern}` found for frontmatter"),
84+
open_start..open_end,
85+
));
8286
};
8387
let info = input.next_slice(info_nl.start);
8488
let info = info.trim_matches(is_whitespace);
@@ -91,7 +95,10 @@ impl<'s> ScriptSource<'s> {
9195
// Ends with a line that starts with a matching number of `-` only followed by whitespace
9296
let nl_fence_pattern = format!("\n{fence_pattern}");
9397
let Some(frontmatter_nl) = input.find_slice(nl_fence_pattern.as_str()) else {
94-
anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
98+
return Err(FrontmatterError::new(
99+
format!("no closing `{fence_pattern}` found for frontmatter"),
100+
open_start..open_end,
101+
));
95102
};
96103
let frontmatter_start = input.current_token_start() + 1; // skip nl from infostring
97104
let _ = input.next_slice(frontmatter_nl.start + 1);
@@ -111,14 +118,29 @@ impl<'s> ScriptSource<'s> {
111118
let after_closing_fence = after_closing_fence.trim_matches(is_whitespace);
112119
if !after_closing_fence.is_empty() {
113120
// extra characters beyond the original fence pattern, even if they are extra `-`
114-
anyhow::bail!("trailing characters found after frontmatter close");
121+
return Err(FrontmatterError::new(
122+
format!("trailing characters found after frontmatter close"),
123+
close_end..content_start,
124+
));
115125
}
116126

117127
source.content = content_start..content_end;
118128

119-
let repeat = Self::parse(source.content())?;
120-
if repeat.frontmatter.is_some() {
121-
anyhow::bail!("only one frontmatter is supported");
129+
if let Some(nl_end) = strip_ws_lines(input.as_ref()) {
130+
let _ = input.next_slice(nl_end);
131+
}
132+
let fence_length = input
133+
.as_ref()
134+
.char_indices()
135+
.find_map(|(i, c)| (c != FENCE_CHAR).then_some(i))
136+
.unwrap_or_else(|| input.eof_offset());
137+
if 0 < fence_length {
138+
let fence_start = input.current_token_start();
139+
let fence_end = fence_start + fence_length;
140+
return Err(FrontmatterError::new(
141+
format!("only one frontmatter is supported"),
142+
fence_start..fence_end,
143+
));
122144
}
123145

124146
Ok(source)
@@ -232,6 +254,37 @@ fn is_whitespace(c: char) -> bool {
232254
)
233255
}
234256

257+
#[derive(Debug)]
258+
pub struct FrontmatterError {
259+
message: String,
260+
span: Span,
261+
}
262+
263+
impl FrontmatterError {
264+
pub fn new(message: impl Into<String>, span: Span) -> Self {
265+
Self {
266+
message: message.into(),
267+
span,
268+
}
269+
}
270+
271+
pub fn message(&self) -> &str {
272+
self.message.as_str()
273+
}
274+
275+
pub fn span(&self) -> Span {
276+
self.span.clone()
277+
}
278+
}
279+
280+
impl std::fmt::Display for FrontmatterError {
281+
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282+
self.message.fmt(fmt)
283+
}
284+
}
285+
286+
impl std::error::Error for FrontmatterError {}
287+
235288
#[cfg(test)]
236289
mod test {
237290
use snapbox::assert_data_eq;

src/cargo/util/toml/embedded.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
use cargo_util_schemas::manifest::PackageName;
22

3-
use crate::CargoResult;
3+
use crate::util::frontmatter::FrontmatterError;
44
use crate::util::frontmatter::ScriptSource;
55
use crate::util::restricted_names;
66

7-
pub(super) fn expand_manifest(content: &str) -> CargoResult<String> {
7+
pub(super) fn expand_manifest(content: &str) -> Result<String, FrontmatterError> {
88
let source = ScriptSource::parse(content)?;
99
if let Some(span) = source.frontmatter_span() {
1010
match source.info() {
1111
Some("cargo") | None => {}
1212
Some(other) => {
1313
if let Some(remainder) = other.strip_prefix("cargo,") {
14-
anyhow::bail!(
15-
"cargo does not support frontmatter infostring attributes like `{remainder}` at this time"
16-
)
14+
return Err(FrontmatterError::new(
15+
format!(
16+
"cargo does not support frontmatter infostring attributes like `{remainder}` at this time"
17+
),
18+
source.info_span().unwrap(),
19+
));
1720
} else {
18-
anyhow::bail!(
19-
"frontmatter infostring `{other}` is unsupported by cargo; specify `cargo` for embedding a manifest"
20-
)
21+
return Err(FrontmatterError::new(
22+
format!(
23+
"frontmatter infostring `{other}` is unsupported by cargo; specify `cargo` for embedding a manifest"
24+
),
25+
source.info_span().unwrap(),
26+
));
2127
}
2228
}
2329
}

src/cargo/util/toml/mod.rs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,11 @@ pub fn read_manifest(
6767
let mut errors = Default::default();
6868

6969
let is_embedded = is_embedded(path);
70-
let contents = read_toml_string(path, is_embedded, gctx)
71-
.map_err(|err| ManifestError::new(err, path.into()))?;
72-
let document =
73-
parse_document(&contents).map_err(|e| emit_diagnostic(e.into(), &contents, path, gctx))?;
70+
let contents = read_toml_string(path, is_embedded, gctx)?;
71+
let document = parse_document(&contents)
72+
.map_err(|e| emit_toml_diagnostic(e.into(), &contents, path, gctx))?;
7473
let original_toml = deserialize_toml(&document)
75-
.map_err(|e| emit_diagnostic(e.into(), &contents, path, gctx))?;
74+
.map_err(|e| emit_toml_diagnostic(e.into(), &contents, path, gctx))?;
7675

7776
let mut manifest = (|| {
7877
let empty = Vec::new();
@@ -152,12 +151,13 @@ pub fn read_manifest(
152151

153152
#[tracing::instrument(skip_all)]
154153
fn read_toml_string(path: &Path, is_embedded: bool, gctx: &GlobalContext) -> CargoResult<String> {
155-
let mut contents = paths::read(path)?;
154+
let mut contents = paths::read(path).map_err(|err| ManifestError::new(err, path.into()))?;
156155
if is_embedded {
157156
if !gctx.cli_unstable().script {
158157
anyhow::bail!("parsing `{}` requires `-Zscript`", path.display());
159158
}
160-
contents = embedded::expand_manifest(&contents)?;
159+
contents = embedded::expand_manifest(&contents)
160+
.map_err(|e| emit_frontmatter_diagnostic(e, &contents, path, gctx))?;
161161
}
162162
Ok(contents)
163163
}
@@ -2777,7 +2777,32 @@ fn lints_to_rustflags(lints: &manifest::TomlLints) -> CargoResult<Vec<String>> {
27772777
Ok(rustflags)
27782778
}
27792779

2780-
fn emit_diagnostic(
2780+
fn emit_frontmatter_diagnostic(
2781+
e: crate::util::frontmatter::FrontmatterError,
2782+
contents: &str,
2783+
manifest_file: &Path,
2784+
gctx: &GlobalContext,
2785+
) -> anyhow::Error {
2786+
let span = e.span();
2787+
2788+
// Get the path to the manifest, relative to the cwd
2789+
let manifest_path = diff_paths(manifest_file, gctx.cwd())
2790+
.unwrap_or_else(|| manifest_file.to_path_buf())
2791+
.display()
2792+
.to_string();
2793+
let group = Group::with_title(Level::ERROR.primary_title(e.message())).element(
2794+
Snippet::source(contents)
2795+
.path(manifest_path)
2796+
.annotation(AnnotationKind::Primary.span(span)),
2797+
);
2798+
2799+
if let Err(err) = gctx.shell().print_report(&[group], true) {
2800+
return err.into();
2801+
}
2802+
return AlreadyPrintedError::new(e.into()).into();
2803+
}
2804+
2805+
fn emit_toml_diagnostic(
27812806
e: toml::de::Error,
27822807
contents: &str,
27832808
manifest_file: &Path,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
[ERROR] frontmatter infostring `.toml` is unsupported by cargo; specify `cargo` for embedding a manifest
2+
--> script:1:4
3+
|
4+
1 | ---.toml
5+
| ^^^^^
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
[ERROR] frontmatter infostring `Cargo.toml` is unsupported by cargo; specify `cargo` for embedding a manifest
2+
--> script:1:4
3+
|
4+
1 | ---Cargo.toml
5+
| ^^^^^^^^^^
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
[ERROR] trailing characters found after frontmatter close
2+
--> script:2:4
3+
|
4+
2 | ---cargo
5+
| ____^
6+
3 | | //~^ ERROR: extra characters after frontmatter close are not allowed
7+
| |_^
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
[ERROR] no closing `---` found for frontmatter
2+
--> script:1:1
3+
|
4+
1 | ---cargo
5+
| ^^^
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
[ERROR] frontmatter infostring `-toml` is unsupported by cargo; specify `cargo` for embedding a manifest
2+
--> script:1:5
3+
|
4+
1 | --- -toml
5+
| ^^^^^
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
[ERROR] frontmatter infostring `Cargo-toml` is unsupported by cargo; specify `cargo` for embedding a manifest
2+
--> script:1:5
3+
|
4+
1 | --- Cargo-toml
5+
| ^^^^^^^^^^
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
[ERROR] cargo does not support frontmatter infostring attributes like `clippy` at this time
2+
--> script:1:4
3+
|
4+
1 | ---cargo,clippy
5+
| ^^^^^^^^^^^^

0 commit comments

Comments
 (0)