Skip to content

Commit 48601a3

Browse files
committed
Auto merge of #13172 - Muscraft:diagnostic-system, r=epage
feat: Add `rustc` style errors for manifest parsing #12235 is tracking user control over warnings. One part of that is making `cargo`'s diagnostic system output messages in the style of `rustc`. To make it so that `cargo` doesn't have to manage a formatter and renderer, I decided to use [`annotate-snippets`](https://crates.io/crates/annotate-snippets), which matches `rustc`'s style well (at one time it was meant to be the formatted for `rustc`). To get this work kicked off, it was suggested to me that we should start with styling manifest parsing errors, and that is what his PR does. What manifest parsing errors look like after this change: ![image](https://github.com/rust-lang/cargo/assets/23045215/0eb388b9-8d72-48ad-84a9-585160995078) --- Note: `cargo` does not currently match `rustc` in color (#12740), `rustc` specifies their colors [here](https://github.com/rust-lang/rust/blob/740cea81d6e34fc03c4495890e29f1c5ecb40b96/compiler/rustc_errors/src/lib.rs#L1755-L1768) and [here](https://github.com/rust-lang/rust/blob/740cea81d6e34fc03c4495890e29f1c5ecb40b96/compiler/rustc_errors/src/emitter.rs#L2689-L2723). I used `annotate-snippets` default colors which match what `rustc` currently uses for colors. I did this as a stopgap while I work to unify the colors used between `rustc` and `cargo`. --- Note: the error messages come directly from `toml` and can be quite long. It would be nice if we could put some of the message below the highlight but this would require changes to `toml`. Example: ``` error: invalid type: integer `3` --> Cargo.toml:7:7 | 7 | bar = 3 | ^ expected a version string like "0.9.8" or a detailed dependency like { version = "0.9.8" } | ```
2 parents 187d4cf + 0d62ae2 commit 48601a3

19 files changed

+389
-344
lines changed

Cargo.lock

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ homepage = "https://github.com/rust-lang/cargo"
1818
repository = "https://github.com/rust-lang/cargo"
1919

2020
[workspace.dependencies]
21+
annotate-snippets = "0.10.1"
2122
anstream = "0.6.5"
2223
anstyle = "1.0.4"
2324
anyhow = "1.0.79"
@@ -139,6 +140,7 @@ name = "cargo"
139140
path = "src/cargo/lib.rs"
140141

141142
[dependencies]
143+
annotate-snippets.workspace = true
142144
anstream.workspace = true
143145
anstyle.workspace = true
144146
anyhow.workspace = true

src/cargo/util/toml/mod.rs

+98-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
use annotate_snippets::{Annotation, AnnotationType, Renderer, Slice, Snippet, SourceAnnotation};
12
use std::collections::{BTreeMap, BTreeSet, HashMap};
23
use std::ffi::OsStr;
34
use std::path::{Path, PathBuf};
45
use std::rc::Rc;
56
use std::str::{self, FromStr};
67

8+
use crate::AlreadyPrintedError;
79
use anyhow::{anyhow, bail, Context as _};
810
use cargo_platform::Platform;
911
use cargo_util::paths;
1012
use cargo_util_schemas::manifest;
1113
use cargo_util_schemas::manifest::RustVersion;
1214
use itertools::Itertools;
1315
use lazycell::LazyCell;
16+
use pathdiff::diff_paths;
1417
use tracing::{debug, trace};
1518
use url::Url;
1619

@@ -43,7 +46,7 @@ pub fn read_manifest(
4346
path: &Path,
4447
source_id: SourceId,
4548
config: &Config,
46-
) -> Result<(EitherManifest, Vec<PathBuf>), ManifestError> {
49+
) -> CargoResult<(EitherManifest, Vec<PathBuf>)> {
4750
trace!(
4851
"read_manifest; path={}; source-id={}",
4952
path.display(),
@@ -56,15 +59,23 @@ pub fn read_manifest(
5659
return Err(ManifestError::new(
5760
anyhow::anyhow!("parsing `{}` requires `-Zscript`", path.display()),
5861
path.into(),
59-
));
62+
))?;
6063
}
6164
contents = embedded::expand_manifest(&contents, path, config)
6265
.map_err(|err| ManifestError::new(err, path.into()))?;
6366
}
6467

65-
read_manifest_from_str(&contents, path, embedded, source_id, config)
66-
.with_context(|| format!("failed to parse manifest at `{}`", path.display()))
67-
.map_err(|err| ManifestError::new(err, path.into()))
68+
read_manifest_from_str(&contents, path, embedded, source_id, config).map_err(|err| {
69+
if err.is::<AlreadyPrintedError>() {
70+
err
71+
} else {
72+
ManifestError::new(
73+
err.context(format!("failed to parse manifest at `{}`", path.display())),
74+
path.into(),
75+
)
76+
.into()
77+
}
78+
})
6879
}
6980

7081
/// See also `bin/cargo/commands/run.rs`s `is_manifest_command`
@@ -94,11 +105,61 @@ fn read_manifest_from_str(
94105

95106
let mut unused = BTreeSet::new();
96107
let deserializer = toml::de::Deserializer::new(contents);
97-
let manifest: manifest::TomlManifest = serde_ignored::deserialize(deserializer, |path| {
108+
let manifest: manifest::TomlManifest = match serde_ignored::deserialize(deserializer, |path| {
98109
let mut key = String::new();
99110
stringify(&mut key, &path);
100111
unused.insert(key);
101-
})?;
112+
}) {
113+
Ok(manifest) => manifest,
114+
Err(e) => {
115+
let Some(span) = e.span() else {
116+
return Err(e.into());
117+
};
118+
119+
let (line_num, column) = translate_position(&contents, span.start);
120+
let source_start = contents[0..span.start]
121+
.rfind('\n')
122+
.map(|s| s + 1)
123+
.unwrap_or(0);
124+
let source_end = contents[span.end - 1..]
125+
.find('\n')
126+
.map(|s| s + span.end)
127+
.unwrap_or(contents.len());
128+
let source = &contents[source_start..source_end];
129+
// Make sure we don't try to highlight past the end of the line,
130+
// but also make sure we are highlighting at least one character
131+
let highlight_end = (column + contents[span].chars().count())
132+
.min(source.len())
133+
.max(column + 1);
134+
// Get the path to the manifest, relative to the cwd
135+
let manifest_path = diff_paths(manifest_file, config.cwd())
136+
.unwrap_or_else(|| manifest_file.to_path_buf())
137+
.display()
138+
.to_string();
139+
let snippet = Snippet {
140+
title: Some(Annotation {
141+
id: None,
142+
label: Some(e.message()),
143+
annotation_type: AnnotationType::Error,
144+
}),
145+
footer: vec![],
146+
slices: vec![Slice {
147+
source: &source,
148+
line_start: line_num + 1,
149+
origin: Some(manifest_path.as_str()),
150+
annotations: vec![SourceAnnotation {
151+
range: (column, highlight_end),
152+
label: "",
153+
annotation_type: AnnotationType::Error,
154+
}],
155+
fold: false,
156+
}],
157+
};
158+
let renderer = Renderer::styled();
159+
writeln!(config.shell().err(), "{}", renderer.render(snippet))?;
160+
return Err(AlreadyPrintedError::new(e.into()).into());
161+
}
162+
};
102163
let add_unused = |warnings: &mut Warnings| {
103164
for key in unused {
104165
warnings.add_warning(format!("unused manifest key: {}", key));
@@ -2113,3 +2174,33 @@ impl ResolveToPath for ConfigRelativePath {
21132174
self.resolve_path(c)
21142175
}
21152176
}
2177+
2178+
fn translate_position(input: &str, index: usize) -> (usize, usize) {
2179+
if input.is_empty() {
2180+
return (0, index);
2181+
}
2182+
2183+
let safe_index = index.min(input.len() - 1);
2184+
let column_offset = index - safe_index;
2185+
2186+
let nl = input[0..safe_index]
2187+
.as_bytes()
2188+
.iter()
2189+
.rev()
2190+
.enumerate()
2191+
.find(|(_, b)| **b == b'\n')
2192+
.map(|(nl, _)| safe_index - nl - 1);
2193+
let line_start = match nl {
2194+
Some(nl) => nl + 1,
2195+
None => 0,
2196+
};
2197+
let line = input[0..line_start]
2198+
.as_bytes()
2199+
.iter()
2200+
.filter(|c| **c == b'\n')
2201+
.count();
2202+
let column = input[line_start..=safe_index].chars().count() - 1;
2203+
let column = column + column_offset;
2204+
2205+
(line, column)
2206+
}

tests/testsuite/alt_registry.rs

+15-16
Original file line numberDiff line numberDiff line change
@@ -712,16 +712,17 @@ fn bad_registry_name() {
712712
.with_status(101)
713713
.with_stderr(
714714
"\
715-
[ERROR] failed to parse manifest at `[CWD]/Cargo.toml`
716-
717-
Caused by:
718-
TOML parse error at line 7, column 17
719-
|
720-
7 | [dependencies.bar]
721-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
722-
invalid character ` ` in registry name: `bad name`, [..]
715+
[ERROR] invalid character ` ` in registry name: `bad name`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters)
723716
724717
718+
--> Cargo.toml:7:17
719+
|
720+
7 | [dependencies.bar]
721+
| _________________^
722+
8 | | version = \"0.0.1\"
723+
9 | | registry = \"bad name\"
724+
| |_____________________________________^
725+
|
725726
",
726727
)
727728
.run();
@@ -1622,16 +1623,14 @@ fn empty_dependency_registry() {
16221623
.with_status(101)
16231624
.with_stderr(
16241625
"\
1625-
[ERROR] failed to parse manifest at `[CWD]/Cargo.toml`
1626-
1627-
Caused by:
1628-
TOML parse error at line 7, column 23
1629-
|
1630-
7 | bar = { version = \"0.1.0\", registry = \"\" }
1631-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1632-
registry name cannot be empty
1626+
[ERROR] registry name cannot be empty
16331627
16341628
1629+
--> Cargo.toml:7:23
1630+
|
1631+
7 | bar = { version = \"0.1.0\", registry = \"\" }
1632+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1633+
|
16351634
",
16361635
)
16371636
.run();

tests/testsuite/bad_config.rs

+39-51
Original file line numberDiff line numberDiff line change
@@ -449,15 +449,13 @@ fn malformed_override() {
449449
.with_status(101)
450450
.with_stderr(
451451
"\
452-
[ERROR] failed to parse manifest at `[..]`
453-
454-
Caused by:
455-
TOML parse error at line 8, column 27
456-
|
457-
8 | native = {
458-
| ^
459-
invalid inline table
460-
expected `}`
452+
[ERROR] invalid inline table
453+
expected `}`
454+
--> Cargo.toml:8:27
455+
|
456+
8 | native = {
457+
| ^
458+
|
461459
",
462460
)
463461
.run();
@@ -1282,14 +1280,12 @@ fn bad_dependency() {
12821280
.with_status(101)
12831281
.with_stderr(
12841282
"\
1285-
error: failed to parse manifest at `[..]`
1286-
1287-
Caused by:
1288-
TOML parse error at line 8, column 23
1289-
|
1290-
8 | bar = 3
1291-
| ^
1292-
invalid type: integer `3`, expected a version string like [..]
1283+
[ERROR] invalid type: integer `3`, expected a version string like [..]
1284+
--> Cargo.toml:8:23
1285+
|
1286+
8 | bar = 3
1287+
| ^
1288+
|
12931289
",
12941290
)
12951291
.run();
@@ -1317,14 +1313,12 @@ fn bad_debuginfo() {
13171313
.with_status(101)
13181314
.with_stderr(
13191315
"\
1320-
error: failed to parse manifest [..]
1321-
1322-
Caused by:
1323-
TOML parse error at line 8, column 25
1324-
|
1325-
8 | debug = 'a'
1326-
| ^^^
1327-
invalid value: string \"a\", expected a boolean, 0, 1, 2, \"line-tables-only\", or \"line-directives-only\"
1316+
[ERROR] invalid value: string \"a\", expected a boolean, 0, 1, 2, \"line-tables-only\", or \"line-directives-only\"
1317+
--> Cargo.toml:8:25
1318+
|
1319+
8 | debug = 'a'
1320+
| ^^^
1321+
|
13281322
",
13291323
)
13301324
.run();
@@ -1352,14 +1346,12 @@ fn bad_debuginfo2() {
13521346
.with_status(101)
13531347
.with_stderr(
13541348
"\
1355-
error: failed to parse manifest at `[..]`
1356-
1357-
Caused by:
1358-
TOML parse error at line 8, column 25
1359-
|
1360-
8 | debug = 3.6
1361-
| ^^^
1362-
invalid type: floating point `3.6`, expected a boolean, 0, 1, 2, \"line-tables-only\", or \"line-directives-only\"
1349+
[ERROR] invalid type: floating point `3.6`, expected a boolean, 0, 1, 2, \"line-tables-only\", or \"line-directives-only\"
1350+
--> Cargo.toml:8:25
1351+
|
1352+
8 | debug = 3.6
1353+
| ^^^
1354+
|
13631355
",
13641356
)
13651357
.run();
@@ -1385,14 +1377,12 @@ fn bad_opt_level() {
13851377
.with_status(101)
13861378
.with_stderr(
13871379
"\
1388-
error: failed to parse manifest at `[..]`
1389-
1390-
Caused by:
1391-
TOML parse error at line 6, column 25
1392-
|
1393-
6 | build = 3
1394-
| ^
1395-
invalid type: integer `3`, expected a boolean or string
1380+
[ERROR] invalid type: integer `3`, expected a boolean or string
1381+
--> Cargo.toml:6:25
1382+
|
1383+
6 | build = 3
1384+
| ^
1385+
|
13961386
",
13971387
)
13981388
.run();
@@ -1685,16 +1675,14 @@ fn bad_trim_paths() {
16851675
p.cargo("check -Ztrim-paths")
16861676
.masquerade_as_nightly_cargo(&["trim-paths"])
16871677
.with_status(101)
1688-
.with_stderr(
1689-
r#"error: failed to parse manifest at `[..]`
1690-
1691-
Caused by:
1692-
TOML parse error at line 7, column 30
1693-
|
1694-
7 | trim-paths = "split-debuginfo"
1695-
| ^^^^^^^^^^^^^^^^^
1696-
expected a boolean, "none", "diagnostics", "macro", "object", "all", or an array with these options
1697-
"#,
1678+
.with_stderr("\
1679+
[ERROR] expected a boolean, \"none\", \"diagnostics\", \"macro\", \"object\", \"all\", or an array with these options
1680+
--> Cargo.toml:7:30
1681+
|
1682+
7 | trim-paths = \"split-debuginfo\"
1683+
| ^^^^^^^^^^^^^^^^^
1684+
|
1685+
",
16981686
)
16991687
.run();
17001688
}

0 commit comments

Comments
 (0)