Skip to content

Commit 0e07ceb

Browse files
authored
fix(manifest): Report script manifest errors for the right line number (#15927)
### What does this PR try to resolve? Currently, if you have a manifest parse error for a cargo script, the line number will be relative to the start of the frontmatter and not the source file. This changes it so we get the line number relative to the start of the source file. ### How to test and review this PR? ### Notes To do this, I added span tracking to our frontmatter parser. To make this easier, I used some helpers from winnow which is an existing transitive dependency. This span tracking will also come into use when I change the frontmatter parser to start reporting spans in errors so we get annotated snippets of source in the error messages to users.
2 parents a9e120f + 0603e4e commit 0e07ceb

File tree

7 files changed

+166
-73
lines changed

7 files changed

+166
-73
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ url = "2.5.4"
118118
varisat = "0.2.2"
119119
walkdir = "2.5.0"
120120
windows-sys = "0.60"
121+
winnow = "0.7.13"
121122

122123
[workspace.lints.rust]
123124
rust_2018_idioms = "warn" # TODO: could this be removed?
@@ -220,6 +221,7 @@ unicode-width.workspace = true
220221
unicode-xid.workspace = true
221222
url.workspace = true
222223
walkdir.workspace = true
224+
winnow.workspace = true
223225

224226
[target.'cfg(target_has_atomic = "64")'.dependencies]
225227
tracing-chrome.workspace = true

src/cargo/util/frontmatter.rs

Lines changed: 96 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,66 @@
11
use crate::CargoResult;
22

3+
type Span = std::ops::Range<usize>;
4+
35
#[derive(Debug)]
46
pub struct ScriptSource<'s> {
5-
shebang: Option<&'s str>,
6-
info: Option<&'s str>,
7-
frontmatter: Option<&'s str>,
8-
content: &'s str,
7+
/// The full file
8+
raw: &'s str,
9+
/// The `#!/usr/bin/env cargo` line, if present
10+
shebang: Option<Span>,
11+
/// The code fence opener (`---`)
12+
open: Option<Span>,
13+
/// Trailing text after `ScriptSource::open` that identifies the meaning of
14+
/// `ScriptSource::frontmatter`
15+
info: Option<Span>,
16+
/// The lines between `ScriptSource::open` and `ScriptSource::close`
17+
frontmatter: Option<Span>,
18+
/// The code fence closer (`---`)
19+
close: Option<Span>,
20+
/// All content after the frontmatter and shebang
21+
content: Span,
922
}
1023

1124
impl<'s> ScriptSource<'s> {
12-
pub fn parse(input: &'s str) -> CargoResult<Self> {
25+
pub fn parse(raw: &'s str) -> CargoResult<Self> {
26+
use winnow::stream::FindSlice as _;
27+
use winnow::stream::Location as _;
28+
use winnow::stream::Offset as _;
29+
use winnow::stream::Stream as _;
30+
31+
let content_end = raw.len();
1332
let mut source = Self {
33+
raw,
1434
shebang: None,
35+
open: None,
1536
info: None,
1637
frontmatter: None,
17-
content: input,
38+
close: None,
39+
content: 0..content_end,
1840
};
1941

20-
if let Some(shebang_end) = strip_shebang(source.content) {
21-
let (shebang, content) = source.content.split_at(shebang_end);
22-
source.shebang = Some(shebang);
23-
source.content = content;
24-
}
42+
let mut input = winnow::stream::LocatingSlice::new(raw);
2543

26-
let mut rest = source.content;
44+
if let Some(shebang_end) = strip_shebang(input.as_ref()) {
45+
let shebang_start = input.current_token_start();
46+
let _ = input.next_slice(shebang_end);
47+
let shebang_end = input.current_token_start();
48+
source.shebang = Some(shebang_start..shebang_end);
49+
source.content = shebang_end..content_end;
50+
}
2751

2852
// Whitespace may precede a frontmatter but must end with a newline
29-
if let Some(nl_end) = strip_ws_lines(rest) {
30-
rest = &rest[nl_end..];
53+
if let Some(nl_end) = strip_ws_lines(input.as_ref()) {
54+
let _ = input.next_slice(nl_end);
3155
}
3256

3357
// Opens with a line that starts with 3 or more `-` followed by an optional identifier
3458
const FENCE_CHAR: char = '-';
35-
let fence_length = rest
59+
let fence_length = input
60+
.as_ref()
3661
.char_indices()
3762
.find_map(|(i, c)| (c != FENCE_CHAR).then_some(i))
38-
.unwrap_or(rest.len());
63+
.unwrap_or_else(|| input.eof_offset());
3964
match fence_length {
4065
0 => {
4166
return Ok(source);
@@ -48,39 +73,50 @@ impl<'s> ScriptSource<'s> {
4873
}
4974
_ => {}
5075
}
51-
let (fence_pattern, rest) = rest.split_at(fence_length);
52-
let Some(info_end_index) = rest.find('\n') else {
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();
79+
source.open = Some(open_start..open_end);
80+
let Some(info_nl) = input.find_slice("\n") else {
5381
anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
5482
};
55-
let (info, rest) = rest.split_at(info_end_index);
83+
let info = input.next_slice(info_nl.start);
5684
let info = info.trim_matches(is_whitespace);
5785
if !info.is_empty() {
58-
source.info = Some(info);
86+
let info_start = info.offset_from(&raw);
87+
let info_end = info_start + info.len();
88+
source.info = Some(info_start..info_end);
5989
}
6090

6191
// Ends with a line that starts with a matching number of `-` only followed by whitespace
6292
let nl_fence_pattern = format!("\n{fence_pattern}");
63-
let Some(frontmatter_nl) = rest.find(&nl_fence_pattern) else {
93+
let Some(frontmatter_nl) = input.find_slice(nl_fence_pattern.as_str()) else {
6494
anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
6595
};
66-
let frontmatter = &rest[..frontmatter_nl + 1];
67-
let frontmatter = frontmatter
68-
.strip_prefix('\n')
69-
.expect("earlier `found` + `split_at` left us here");
70-
source.frontmatter = Some(frontmatter);
71-
let rest = &rest[frontmatter_nl + nl_fence_pattern.len()..];
72-
73-
let (after_closing_fence, rest) = rest.split_once("\n").unwrap_or((rest, ""));
96+
let frontmatter_start = input.current_token_start() + 1; // skip nl from infostring
97+
let _ = input.next_slice(frontmatter_nl.start + 1);
98+
let frontmatter_end = input.current_token_start();
99+
source.frontmatter = Some(frontmatter_start..frontmatter_end);
100+
let close_start = input.current_token_start();
101+
let _ = input.next_slice(fence_length);
102+
let close_end = input.current_token_start();
103+
source.close = Some(close_start..close_end);
104+
105+
let nl = input.find_slice("\n");
106+
let after_closing_fence = input.next_slice(
107+
nl.map(|span| span.end)
108+
.unwrap_or_else(|| input.eof_offset()),
109+
);
110+
let content_start = input.current_token_start();
74111
let after_closing_fence = after_closing_fence.trim_matches(is_whitespace);
75112
if !after_closing_fence.is_empty() {
76113
// extra characters beyond the original fence pattern, even if they are extra `-`
77114
anyhow::bail!("trailing characters found after frontmatter close");
78115
}
79116

80-
let frontmatter_len = input.len() - rest.len();
81-
source.content = &input[frontmatter_len..];
117+
source.content = content_start..content_end;
82118

83-
let repeat = Self::parse(source.content)?;
119+
let repeat = Self::parse(source.content())?;
84120
if repeat.frontmatter.is_some() {
85121
anyhow::bail!("only one frontmatter is supported");
86122
}
@@ -89,19 +125,43 @@ impl<'s> ScriptSource<'s> {
89125
}
90126

91127
pub fn shebang(&self) -> Option<&'s str> {
92-
self.shebang
128+
self.shebang.clone().map(|span| &self.raw[span])
129+
}
130+
131+
pub fn shebang_span(&self) -> Option<Span> {
132+
self.shebang.clone()
133+
}
134+
135+
pub fn open_span(&self) -> Option<Span> {
136+
self.open.clone()
93137
}
94138

95139
pub fn info(&self) -> Option<&'s str> {
96-
self.info
140+
self.info.clone().map(|span| &self.raw[span])
141+
}
142+
143+
pub fn info_span(&self) -> Option<Span> {
144+
self.info.clone()
97145
}
98146

99147
pub fn frontmatter(&self) -> Option<&'s str> {
100-
self.frontmatter
148+
self.frontmatter.clone().map(|span| &self.raw[span])
149+
}
150+
151+
pub fn frontmatter_span(&self) -> Option<Span> {
152+
self.frontmatter.clone()
153+
}
154+
155+
pub fn close_span(&self) -> Option<Span> {
156+
self.close.clone()
101157
}
102158

103159
pub fn content(&self) -> &'s str {
104-
self.content
160+
&self.raw[self.content.clone()]
161+
}
162+
163+
pub fn content_span(&self) -> Span {
164+
self.content.clone()
105165
}
106166
}
107167

src/cargo/util/toml/embedded.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::util::restricted_names;
66

77
pub(super) fn expand_manifest(content: &str) -> CargoResult<String> {
88
let source = ScriptSource::parse(content)?;
9-
if let Some(frontmatter) = source.frontmatter() {
9+
if let Some(span) = source.frontmatter_span() {
1010
match source.info() {
1111
Some("cargo") | None => {}
1212
Some(other) => {
@@ -22,10 +22,21 @@ pub(super) fn expand_manifest(content: &str) -> CargoResult<String> {
2222
}
2323
}
2424

25-
Ok(frontmatter.to_owned())
25+
// Include from file start to frontmatter end when we parse the TOML to get line numbers
26+
// correct and so if a TOML error says "entire file", it shows the existing content, rather
27+
// than blank lines.
28+
//
29+
// HACK: Since frontmatter open isn't valid TOML, we insert a comment
30+
let mut frontmatter = content[0..span.end].to_owned();
31+
let open_span = source.open_span().unwrap();
32+
frontmatter.insert(open_span.start, '#');
33+
Ok(frontmatter)
2634
} else {
27-
let frontmatter = "";
28-
Ok(frontmatter.to_owned())
35+
// Consider the shebang to be part of the frontmatter
36+
// so if a TOML error says "entire file", it shows the existing content, rather
37+
// than blank lines.
38+
let span = source.shebang_span().unwrap_or(0..0);
39+
Ok(content[span].to_owned())
2940
}
3041
}
3142

@@ -88,11 +99,12 @@ time="0.1.25"
8899
fn main() {}
89100
"#
90101
),
91-
str![[r#"
102+
str![[r##"
103+
#---cargo
92104
[dependencies]
93105
time="0.1.25"
94106
95-
"#]]
107+
"##]]
96108
);
97109
}
98110
}

src/cargo/util/toml_mut/manifest.rs

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,11 @@ impl LocalManifest {
277277
let mut embedded = None;
278278
if is_embedded(path) {
279279
let source = ScriptSource::parse(&data)?;
280-
if let Some(frontmatter) = source.frontmatter() {
281-
embedded = Some(Embedded::exists(&data, frontmatter));
282-
data = frontmatter.to_owned();
283-
} else if let Some(shebang) = source.shebang() {
284-
embedded = Some(Embedded::after(&data, shebang));
280+
if let Some(frontmatter) = source.frontmatter_span() {
281+
embedded = Some(Embedded::exists(frontmatter));
282+
data = source.frontmatter().unwrap().to_owned();
283+
} else if let Some(shebang) = source.shebang_span() {
284+
embedded = Some(Embedded::after(shebang));
285285
data = String::new();
286286
} else {
287287
embedded = Some(Embedded::start());
@@ -592,33 +592,15 @@ impl Embedded {
592592
Self::Implicit(0)
593593
}
594594

595-
fn after(input: &str, after: &str) -> Self {
596-
let span = substr_span(input, after);
597-
let end = span.end;
598-
Self::Implicit(end)
595+
fn after(after: std::ops::Range<usize>) -> Self {
596+
Self::Implicit(after.end)
599597
}
600598

601-
fn exists(input: &str, exists: &str) -> Self {
602-
let span = substr_span(input, exists);
603-
Self::Explicit(span)
599+
fn exists(exists: std::ops::Range<usize>) -> Self {
600+
Self::Explicit(exists)
604601
}
605602
}
606603

607-
fn substr_span(haystack: &str, needle: &str) -> std::ops::Range<usize> {
608-
let haystack_start_ptr = haystack.as_ptr();
609-
let haystack_end_ptr = haystack[haystack.len()..haystack.len()].as_ptr();
610-
611-
let needle_start_ptr = needle.as_ptr();
612-
let needle_end_ptr = needle[needle.len()..needle.len()].as_ptr();
613-
614-
assert!(needle_end_ptr < haystack_end_ptr);
615-
assert!(haystack_start_ptr <= needle_start_ptr);
616-
let start = needle_start_ptr as usize - haystack_start_ptr as usize;
617-
let end = start + needle.len();
618-
619-
start..end
620-
}
621-
622604
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
623605
enum DependencyStatus {
624606
None,

tests/testsuite/script/cargo.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,42 @@ fn requires_z_flag() {
182182
.run();
183183
}
184184

185+
#[cargo_test(nightly, reason = "-Zscript is unstable")]
186+
fn manifest_parse_error() {
187+
// Exagerate the newlines to make it more obvious if the error's line number is off
188+
let script = r#"#!/usr/bin/env cargo
189+
190+
191+
192+
193+
194+
---
195+
[dependencies]
196+
bar = 3
197+
---
198+
199+
fn main() {
200+
println!("Hello world!");
201+
}"#;
202+
let p = cargo_test_support::project()
203+
.file("script.rs", script)
204+
.build();
205+
206+
p.cargo("-Zscript -v script.rs")
207+
.masquerade_as_nightly_cargo(&["script"])
208+
.with_status(101)
209+
.with_stdout_data(str![""])
210+
.with_stderr_data(str![[r#"
211+
[ERROR] invalid type: integer `3`, expected a version string like "0.9.8" or a detailed dependency like { version = "0.9.8" }
212+
--> script.rs:9:7
213+
|
214+
9 | bar = 3
215+
| ^
216+
217+
"#]])
218+
.run();
219+
}
220+
185221
#[cargo_test(nightly, reason = "-Zscript is unstable")]
186222
fn clean_output_with_edition() {
187223
let script = r#"#!/usr/bin/env cargo
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[ERROR] invalid multi-line basic string, expected `/`, characters
2-
--> script:8:2
3-
|
4-
8 | 4␌+
5-
| ^
2+
--> script:10:2
3+
|
4+
10 | 4␌+
5+
| ^

0 commit comments

Comments
 (0)