fix: don't treat a raw identifier as a raw string prefix CharClasses entered raw string state whenever `r` was followed by `#`, which also matches a raw identifier such as `r#struct`. That marked the first few characters as InString, so replace_names copied `r#s` through verbatim and registered the leftover `truct` as the metavariable name. Undoing the substitution afterwards put the `$` back in the wrong place, so `$r#struct` came out as `r#s$truct` and no longer compiled. Only enter raw string state when the run of `#`s after `r` ends in a quote.
diff --git a/src/comment.rs b/src/comment.rs index 40e42b5..4162b5f 100644 --- a/src/comment.rs +++ b/src/comment.rs
@@ -1347,6 +1347,24 @@ } } +/// Returns `true` if the `r` just consumed opens a raw string literal, i.e. the run of +/// `#`s that follows it ends in a `"`. Peeking a single `#` is not enough to tell a raw +/// string apart from a raw identifier such as `r#struct`. +fn is_raw_string_prefix<T>(iter: &mut MultiPeek<T>) -> bool +where + T: Iterator, + T::Item: RichChar, +{ + while let Some(c) = iter.peek() { + match c.get_char() { + '#' => continue, + '"' => return true, + _ => return false, + } + } + false +} + fn is_raw_string_suffix<T>(iter: &mut MultiPeek<T>, count: u32) -> bool where T: Iterator, @@ -1430,7 +1448,13 @@ CharClassesStatus::LitCharEscape => CharClassesStatus::LitChar, CharClassesStatus::Normal => match chr { 'r' => match self.base.peek().map(RichChar::get_char) { - Some('#') | Some('"') => { + Some('"') => { + char_kind = FullCodeCharKind::InString; + CharClassesStatus::RawStringPrefix(0) + } + // `r#` opens a raw string only if the `#`s end in a `"`; otherwise + // this is a raw identifier like `r#struct` and stays normal code. + Some('#') if is_raw_string_prefix(&mut self.base) => { char_kind = FullCodeCharKind::InString; CharClassesStatus::RawStringPrefix(0) }
diff --git a/tests/target/issue-7056.rs b/tests/target/issue-7056.rs new file mode 100644 index 0000000..daa0538 --- /dev/null +++ b/tests/target/issue-7056.rs
@@ -0,0 +1,25 @@ +// `r#` opens a raw string only when the run of `#`s ends in a `"`. A raw identifier +// such as `r#struct` must stay classified as normal code, or the macro body rewriter +// copies part of it through verbatim and puts the `$` back in the wrong place. + +#![feature(decl_macro)] + +macro_rules! r#struct { + ($r#struct:expr) => { $r#struct } +} + +macro r#decl_macro($r#fn:expr) { + $r#fn +} + +macro_rules! old_macro { + ($a:expr) => { + $a + }; +} + +fn raw_ident_next_to_a_raw_string() { + let r#match = r#"still a raw string"#; + let r#fn = r##"hashes too"##; + let _ = (r#match, r#fn); +}