Skip to content

Commit 0d62ae2

Browse files
committed
feat: Add rustc style errors for manifest parsing
1 parent 187d4cf commit 0d62ae2

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)