Skip to content

Commit a93d316

Browse files
committed
Fix bug in shebang handling
Shebang handling was too agressive in stripping out the first line in cases where it is actually _not_ a shebang, but instead, valid rust (#70528). This is a second attempt at resolving this issue (the first attempt was flawed, for, among other reasons, causing an ICE in certain cases (#71372, #71471). The behavior is now codified by a number of UI tests, but simply: For the first line to be a shebang, the following must all be true: 1. The line must start with `#!` 2. The line must contain a non whitespace character after `#!` 3. The next character in the file, ignoring comments & whitespace must not be `[` I believe this is a strict superset of what we used to allow, so perhaps a crater run is unnecessary, but probably not a terrible idea.
1 parent ee6c0da commit a93d316

13 files changed

+154
-7
lines changed

src/librustc_lexer/src/lib.rs

+19-7
Original file line numberDiff line numberDiff line change
@@ -236,16 +236,28 @@ pub enum Base {
236236
}
237237

238238
/// `rustc` allows files to have a shebang, e.g. "#!/usr/bin/rustrun",
239-
/// but shebang isn't a part of rust syntax, so this function
240-
/// skips the line if it starts with a shebang ("#!").
241-
/// Line won't be skipped if it represents a valid Rust syntax
242-
/// (e.g. "#![deny(missing_docs)]").
239+
/// but shebang isn't a part of rust syntax.
243240
pub fn strip_shebang(input: &str) -> Option<usize> {
244-
debug_assert!(!input.is_empty());
245-
if !input.starts_with("#!") || input.starts_with("#![") {
241+
let first_line = input.lines().next()?;
242+
// A shebang is intentionally loosely defined as `#! [non whitespace]` on the first line.
243+
let could_be_shebang =
244+
first_line.starts_with("#!") && first_line[2..].contains(|c| !is_whitespace(c));
245+
if !could_be_shebang {
246246
return None;
247247
}
248-
Some(input.find('\n').unwrap_or(input.len()))
248+
let non_whitespace_tokens = tokenize(input).map(|tok| tok.kind).filter(|tok|
249+
!matches!(tok, TokenKind::LineComment | TokenKind::BlockComment { .. } | TokenKind::Whitespace)
250+
);
251+
let prefix = [TokenKind::Pound, TokenKind::Not, TokenKind::OpenBracket];
252+
let starts_with_attribute = non_whitespace_tokens.take(3).eq(prefix.iter().copied());
253+
if starts_with_attribute {
254+
// If the file starts with #![ then it's definitely not a shebang -- it couldn't be
255+
// a rust program since a Rust program can't start with `[`
256+
None
257+
} else {
258+
// It's a #!... and there isn't a `[` in sight, must be a shebang
259+
Some(first_line.len())
260+
}
249261
}
250262

251263
/// Parses the first token from the provided input string.

src/librustc_lexer/src/tests.rs

+57
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,61 @@ mod tests {
145145
}),
146146
);
147147
}
148+
149+
#[test]
150+
fn test_valid_shebang() {
151+
// https://github.com/rust-lang/rust/issues/70528
152+
let input = "#!/usr/bin/rustrun\nlet x = 5;";
153+
assert_eq!(strip_shebang(input), Some(18));
154+
}
155+
156+
#[test]
157+
fn test_invalid_shebang_valid_rust_syntax() {
158+
// https://github.com/rust-lang/rust/issues/70528
159+
let input = "#! [bad_attribute]";
160+
assert_eq!(strip_shebang(input), None);
161+
}
162+
163+
#[test]
164+
fn test_shebang_second_line() {
165+
// Because shebangs are interpreted by the kernel, they must be on the first line
166+
let input = "\n#!/bin/bash";
167+
assert_eq!(strip_shebang(input), None);
168+
}
169+
170+
#[test]
171+
fn test_shebang_space() {
172+
let input = "#! /bin/bash";
173+
assert_eq!(strip_shebang(input), Some(input.len()));
174+
}
175+
176+
#[test]
177+
fn test_shebang_empty_shebang() {
178+
let input = "#! \n[attribute(foo)]";
179+
assert_eq!(strip_shebang(input), None);
180+
}
181+
182+
#[test]
183+
fn test_invalid_shebang_comment() {
184+
let input = "#!//bin/ami/a/comment\n[";
185+
assert_eq!(strip_shebang(input), None)
186+
}
187+
188+
#[test]
189+
fn test_invalid_shebang_another_comment() {
190+
let input = "#!/*bin/ami/a/comment*/\n[attribute";
191+
assert_eq!(strip_shebang(input), None)
192+
}
193+
194+
#[test]
195+
fn test_shebang_valid_rust_after() {
196+
let input = "#!/*bin/ami/a/comment*/\npub fn main() {}";
197+
assert_eq!(strip_shebang(input), Some(23))
198+
}
199+
200+
#[test]
201+
fn test_shebang_followed_by_attrib() {
202+
let input = "#!/bin/rust-scripts\n#![allow_unused(true)]";
203+
assert_eq!(strip_shebang(input), Some(19));
204+
}
148205
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
#!B //~ expected `[`, found `B`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
error: expected `[`, found `B`
2+
--> $DIR/issue-71471-ignore-tidy.rs:2:3
3+
|
4+
LL | #!B
5+
| ^ expected `[`
6+
7+
error: aborting due to previous error
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!
2+
[allow(unused_variables)]
3+
// check-pass
4+
5+
fn main() {
6+
let x = 5;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#![allow(unused_variables)]
2+
// check-pass
3+
fn main() {
4+
let x = 5;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env run-cargo-script
2+
3+
// check-pass
4+
#![allow(unused_variables)]
5+
6+
7+
fn main() {
8+
let x = 5;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!//bin/bash
2+
3+
// check-pass
4+
fn main() {
5+
println!("a valid shebang (that is also a rust comment)")
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// something on the first line for tidy
2+
#!/bin/bash //~ expected `[`, found `/`
3+
4+
fn main() {
5+
println!("ok!");
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
error: expected `[`, found `/`
2+
--> $DIR/shebang-must-start-file.rs:2:3
3+
|
4+
LL | #!/bin/bash
5+
| ^ expected `[`
6+
7+
error: aborting due to previous error
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!//bin/bash
2+
3+
4+
// This could not possibly be a shebang & also a valid rust file, since a Rust file
5+
// can't start with `[`
6+
/*
7+
[ (mixing comments to also test that we ignore both types of comments)
8+
9+
*/
10+
11+
[allow(unused_variables)]
12+
13+
// check-pass
14+
fn main() {
15+
let x = 5;
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env run-cargo-script
2+
3+
// check-pass
4+
fn main() {
5+
println!("Hello World!");
6+
}

src/tools/tidy/src/style.rs

+5
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ pub fn check(path: &Path, bad: &mut bool) {
174174

175175
let can_contain =
176176
contents.contains("// ignore-tidy-") || contents.contains("# ignore-tidy-");
177+
// Enable testing ICE's that require specific (untidy)
178+
// file formats easily eg. `issue-1234-ignore-tidy.rs`
179+
if filename.contains("ignore-tidy") {
180+
return;
181+
}
177182
let mut skip_cr = contains_ignore_directive(can_contain, &contents, "cr");
178183
let mut skip_undocumented_unsafe =
179184
contains_ignore_directive(can_contain, &contents, "undocumented-unsafe");

0 commit comments

Comments
 (0)