Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 36 additions & 11 deletions compiler/rustc_parse_format/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,24 +307,49 @@ impl<'input> Parser<'input> {

let (is_source_literal, end_of_snippet, pre_input_vec) = if let Some(snippet) = snippet {
if let Some(nr_hashes) = style {
// snippet is a raw string, which starts with 'r', a number of hashes, and a quote
// and ends with a quote and the same number of hashes
(true, snippet.len() - nr_hashes - 1, vec![])
// snippet is a raw string

// validate snippet because a proc macro may have
// respanned it to something completely different (fixes #114865)
let prefix_len = nr_hashes + 2; // r + hashes + opening "
let suffix_len = nr_hashes + 1; // closing " + hashes
let snippet_bytes = snippet.as_bytes();
let content_end = snippet.len() - suffix_len;
if snippet.len() >= prefix_len + suffix_len // is sufficiently long
&& snippet_bytes[0] == b'r'
&& snippet_bytes[1..1 + nr_hashes].iter().all(|&c| c == b'#')
&& snippet_bytes[1 + nr_hashes] == b'"'
&& snippet_bytes[content_end] == b'"'
&& snippet_bytes[content_end + 1..].iter().all(|&c| c == b'#')
{
let snippet_without_quotes = &snippet[prefix_len..content_end];
let input_without_newline =
if appended_newline { &input[..input.len() - 1] } else { input };
if snippet_without_quotes == input_without_newline {
(true, snippet.len() - suffix_len, vec![])
} else {
(false, snippet.len(), vec![])
}
} else {
(false, snippet.len(), vec![])
}
} else {
// snippet is not a raw string
if snippet.starts_with('"') {
// snippet looks like an ordinary string literal
// check whether it is the escaped version of input
let without_quotes = &snippet[1..snippet.len() - 1];
let snippet_without_quotes = &snippet[1..snippet.len() - 1];
let (mut ok, mut vec) = (true, vec![]);
let mut chars = input.chars();
rustc_literal_escaper::unescape_str(without_quotes, |range, res| match res {
Ok(ch) if ok && chars.next().is_some_and(|c| ch == c) => {
vec.push((range, ch));
}
_ => {
ok = false;
vec = vec![];
rustc_literal_escaper::unescape_str(snippet_without_quotes, |range, res| {
match res {
Ok(ch) if ok && chars.next().is_some_and(|c| ch == c) => {
vec.push((range, ch));
}
_ => {
ok = false;
vec = vec![];
}
}
});
let end = vec.last().map(|(r, _)| r.end).unwrap_or(0);
Expand Down
48 changes: 48 additions & 0 deletions tests/ui/proc-macro/auxiliary/ice-wrong-span-114865.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#![feature(proc_macro_span)]

extern crate proc_macro;

use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
use std::iter::FromIterator;

/// Builds a `println!(<fmt_str>)` token stream with the given span on the format string literal.
fn make_println(fmt_str: &str, span: Span) -> TokenStream {
let mut lit: Literal = fmt_str.parse().unwrap();
lit.set_span(span);
FromIterator::<TokenTree>::from_iter([
Ident::new("println", Span::mixed_site()).into(),
Punct::new('!', Spacing::Alone).into(),
Group::new(Delimiter::Parenthesis, TokenTree::from(lit).into()).into(),
])
}

/// Expands to `println!(r"{}")` with the span of the first input token.
#[proc_macro]
pub fn foo(input: TokenStream) -> TokenStream {
let span = input.into_iter().next().unwrap().span();
make_println(r#"r"{}""#, span)
}

/// Same as `foo` but with hashes: expands to `println!(r##"{}"##)`.
#[proc_macro]
pub fn foo2(input: TokenStream) -> TokenStream {
let span = input.into_iter().next().unwrap().span();
make_println(r###"r##"{}"##"###, span)
}

/// Expands to `println!(r"{}")` with a span joining two input tokens,
/// creating a span whose source text may not be a valid raw string.
#[proc_macro]
pub fn foo3(input: TokenStream) -> TokenStream {
let mut iter = input.into_iter();
let span = iter.next().unwrap().span().join(iter.next().unwrap().span()).unwrap();
make_println(r#"r"{}""#, span)
}

/// Same as `foo3` but with hashes: expands to `println!(r##"{}"##)`.
#[proc_macro]
pub fn foo4(input: TokenStream) -> TokenStream {
let mut iter = input.into_iter();
let span = iter.next().unwrap().span().join(iter.next().unwrap().span()).unwrap();
make_println(r###"r##"{}"##"###, span)
}
25 changes: 25 additions & 0 deletions tests/ui/proc-macro/ice-wrong-span-114865.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Regression test for ICE https://github.com/rust-lang/rust/issues/114865

// Tests that we do not ICE when a proc macro expands
// to a string formatting macro (like println!) and respans
// this formatting macro's arg to that of its own input which
// happens to be a multi-byte string (see the auxiliary file
// ice-wrong-span-114865.rs).

//@ proc-macro: ice-wrong-span-114865.rs

extern crate ice_wrong_span_114865;

use ice_wrong_span_114865::{foo, foo2, foo3, foo4};

fn main() {
foo!("字"); //~ ERROR 1 positional argument in format string, but no arguments were given
foo!("r字字"); //~ ERROR 1 positional argument in format string, but no arguments were given

foo2!("字"); //~ ERROR 1 positional argument in format string, but no arguments were given
foo2!("r字字"); //~ ERROR 1 positional argument in format string, but no arguments were given

foo3!(r"abc" 字); //~ ERROR 1 positional argument in format string, but no arguments were given

foo4!(r##"abcd"## 字); //~ ERROR 1 positional argument in format string, but no arguments were given
}
38 changes: 38 additions & 0 deletions tests/ui/proc-macro/ice-wrong-span-114865.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
error: 1 positional argument in format string, but no arguments were given
--> $DIR/ice-wrong-span-114865.rs:16:10
|
LL | foo!("字");
| ^^^^

error: 1 positional argument in format string, but no arguments were given
--> $DIR/ice-wrong-span-114865.rs:17:10
|
LL | foo!("r字字");
| ^^^^^^^

error: 1 positional argument in format string, but no arguments were given
--> $DIR/ice-wrong-span-114865.rs:19:11
|
LL | foo2!("字");
| ^^^^

error: 1 positional argument in format string, but no arguments were given
--> $DIR/ice-wrong-span-114865.rs:20:11
|
LL | foo2!("r字字");
| ^^^^^^^

error: 1 positional argument in format string, but no arguments were given
--> $DIR/ice-wrong-span-114865.rs:22:11
|
LL | foo3!(r"abc" 字);
| ^^^^^^^^^

error: 1 positional argument in format string, but no arguments were given
--> $DIR/ice-wrong-span-114865.rs:24:11
|
LL | foo4!(r##"abcd"## 字);
| ^^^^^^^^^^^^^^

error: aborting due to 6 previous errors

Loading