diff --git a/compiler/rustc_parse_format/src/lib.rs b/compiler/rustc_parse_format/src/lib.rs index 3d8b97b2fde38..acda32fa4ee82 100644 --- a/compiler/rustc_parse_format/src/lib.rs +++ b/compiler/rustc_parse_format/src/lib.rs @@ -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); diff --git a/tests/ui/proc-macro/auxiliary/ice-wrong-span-114865.rs b/tests/ui/proc-macro/auxiliary/ice-wrong-span-114865.rs new file mode 100644 index 0000000000000..45f0b528bb2b4 --- /dev/null +++ b/tests/ui/proc-macro/auxiliary/ice-wrong-span-114865.rs @@ -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!()` 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::::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) +} diff --git a/tests/ui/proc-macro/ice-wrong-span-114865.rs b/tests/ui/proc-macro/ice-wrong-span-114865.rs new file mode 100644 index 0000000000000..1eddc00de55c4 --- /dev/null +++ b/tests/ui/proc-macro/ice-wrong-span-114865.rs @@ -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 +} diff --git a/tests/ui/proc-macro/ice-wrong-span-114865.stderr b/tests/ui/proc-macro/ice-wrong-span-114865.stderr new file mode 100644 index 0000000000000..2324e6d5d60ca --- /dev/null +++ b/tests/ui/proc-macro/ice-wrong-span-114865.stderr @@ -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 +