Merge pull request #2542 from topecongiro/macro-2.0

Handle macro arguments which exceeds max width
This commit is contained in:
Nick Cameron 2018-03-19 15:13:47 +13:00 committed by GitHub
commit 5516223900
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 573 additions and 121 deletions

View File

@ -463,6 +463,435 @@ fn replace_names(input: &str) -> Option<(String, HashMap<String, String>)> {
Some((result, substs))
}
#[derive(Debug, Clone)]
enum MacroArgKind {
/// e.g. `$x: expr`.
MetaVariable(ast::Ident, String),
/// e.g. `$($foo: expr),*`
Repeat(
/// `()`, `[]` or `{}`.
DelimToken,
/// Inner arguments inside delimiters.
Vec<ParsedMacroArg>,
/// Something after the closing delimiter and the repeat token, if available.
Option<Box<ParsedMacroArg>>,
/// The repeat token. This could be one of `*`, `+` or `?`.
Token,
),
/// e.g. `[derive(Debug)]`
Delimited(DelimToken, Vec<ParsedMacroArg>),
/// A possible separator. e.g. `,` or `;`.
Separator(String, String),
/// Other random stuff that does not fit to other kinds.
/// e.g. `== foo` in `($x: expr == foo)`.
Other(String, String),
}
fn delim_token_to_str(
context: &RewriteContext,
delim_token: &DelimToken,
shape: Shape,
use_multiple_lines: bool,
) -> (String, String) {
let (lhs, rhs) = match *delim_token {
DelimToken::Paren => ("(", ")"),
DelimToken::Bracket => ("[", "]"),
DelimToken::Brace => ("{ ", " }"),
DelimToken::NoDelim => ("", ""),
};
if use_multiple_lines {
let indent_str = shape.indent.to_string_with_newline(context.config);
let nested_indent_str = shape
.indent
.block_indent(context.config)
.to_string_with_newline(context.config);
(
format!("{}{}", lhs, nested_indent_str),
format!("{}{}", indent_str, rhs),
)
} else {
(lhs.to_owned(), rhs.to_owned())
}
}
impl MacroArgKind {
fn starts_with_brace(&self) -> bool {
match *self {
MacroArgKind::Repeat(DelimToken::Brace, _, _, _)
| MacroArgKind::Delimited(DelimToken::Brace, _) => true,
_ => false,
}
}
fn starts_with_dollar(&self) -> bool {
match *self {
MacroArgKind::Repeat(..) | MacroArgKind::MetaVariable(..) => true,
_ => false,
}
}
fn ends_with_space(&self) -> bool {
match *self {
MacroArgKind::Separator(..) => true,
_ => false,
}
}
fn has_meta_var(&self) -> bool {
match *self {
MacroArgKind::MetaVariable(..) => true,
MacroArgKind::Repeat(_, ref args, _, _) => args.iter().any(|a| a.kind.has_meta_var()),
_ => false,
}
}
fn rewrite(
&self,
context: &RewriteContext,
shape: Shape,
use_multiple_lines: bool,
) -> Option<String> {
let rewrite_delimited_inner = |delim_tok, args| -> Option<(String, String, String)> {
let (lhs, rhs) = delim_token_to_str(context, delim_tok, shape, false);
let inner = wrap_macro_args(context, args, shape)?;
if lhs.len() + inner.len() + rhs.len() <= shape.width {
return Some((lhs, inner, rhs));
}
let (lhs, rhs) = delim_token_to_str(context, delim_tok, shape, true);
let nested_shape = shape
.block_indent(context.config.tab_spaces())
.with_max_width(context.config);
let inner = wrap_macro_args(context, args, nested_shape)?;
Some((lhs, inner, rhs))
};
match *self {
MacroArgKind::MetaVariable(ty, ref name) => {
Some(format!("${}: {}", name, ty.name.as_str()))
}
MacroArgKind::Repeat(ref delim_tok, ref args, ref another, ref tok) => {
let (lhs, inner, rhs) = rewrite_delimited_inner(delim_tok, args)?;
let another = another
.as_ref()
.and_then(|a| a.rewrite(context, shape, use_multiple_lines))
.unwrap_or("".to_owned());
let repeat_tok = pprust::token_to_string(tok);
Some(format!("${}{}{}{}{}", lhs, inner, rhs, another, repeat_tok))
}
MacroArgKind::Delimited(ref delim_tok, ref args) => {
rewrite_delimited_inner(delim_tok, args)
.map(|(lhs, inner, rhs)| format!("{}{}{}", lhs, inner, rhs))
}
MacroArgKind::Separator(ref sep, ref prefix) => Some(format!("{}{} ", prefix, sep)),
MacroArgKind::Other(ref inner, ref prefix) => Some(format!("{}{}", prefix, inner)),
}
}
}
#[derive(Debug, Clone)]
struct ParsedMacroArg {
kind: MacroArgKind,
span: Span,
}
impl ParsedMacroArg {
pub fn rewrite(
&self,
context: &RewriteContext,
shape: Shape,
use_multiple_lines: bool,
) -> Option<String> {
self.kind.rewrite(context, shape, use_multiple_lines)
}
}
/// Parses macro arguments on macro def.
struct MacroArgParser {
/// Holds either a name of the next metavariable, a separator or a junk.
buf: String,
/// The start position on the current buffer.
lo: BytePos,
/// The first token of the current buffer.
start_tok: Token,
/// Set to true if we are parsing a metavariable or a repeat.
is_meta_var: bool,
/// The position of the last token.
hi: BytePos,
/// The last token parsed.
last_tok: Token,
/// Holds the parsed arguments.
result: Vec<ParsedMacroArg>,
}
fn last_tok(tt: &TokenTree) -> Token {
match *tt {
TokenTree::Token(_, ref t) => t.clone(),
TokenTree::Delimited(_, ref d) => d.close_token(),
}
}
impl MacroArgParser {
pub fn new() -> MacroArgParser {
MacroArgParser {
lo: BytePos(0),
hi: BytePos(0),
buf: String::new(),
is_meta_var: false,
last_tok: Token::Eof,
start_tok: Token::Eof,
result: vec![],
}
}
fn set_last_tok(&mut self, tok: &TokenTree) {
self.hi = tok.span().hi();
self.last_tok = last_tok(tok);
}
fn add_separator(&mut self) {
let prefix = if self.need_space_prefix() {
" ".to_owned()
} else {
"".to_owned()
};
self.result.push(ParsedMacroArg {
kind: MacroArgKind::Separator(self.buf.clone(), prefix),
span: mk_sp(self.lo, self.hi),
});
self.buf.clear();
}
fn add_other(&mut self) {
let prefix = if self.need_space_prefix() {
" ".to_owned()
} else {
"".to_owned()
};
self.result.push(ParsedMacroArg {
kind: MacroArgKind::Other(self.buf.clone(), prefix),
span: mk_sp(self.lo, self.hi),
});
self.buf.clear();
}
fn add_meta_variable(&mut self, iter: &mut Cursor) {
match iter.next() {
Some(TokenTree::Token(sp, Token::Ident(ref ident))) => {
self.result.push(ParsedMacroArg {
kind: MacroArgKind::MetaVariable(ident.clone(), self.buf.clone()),
span: mk_sp(self.lo, sp.hi()),
});
self.buf.clear();
self.is_meta_var = false;
}
_ => unreachable!(),
}
}
fn add_delimited(&mut self, inner: Vec<ParsedMacroArg>, delim: DelimToken, span: Span) {
self.result.push(ParsedMacroArg {
kind: MacroArgKind::Delimited(delim, inner),
span,
});
}
// $($foo: expr),?
fn add_repeat(
&mut self,
inner: Vec<ParsedMacroArg>,
delim: DelimToken,
iter: &mut Cursor,
span: Span,
) {
let mut buffer = String::new();
let mut first = false;
let mut lo = span.lo();
let mut hi = span.hi();
// Parse '*', '+' or '?.
while let Some(ref tok) = iter.next() {
self.set_last_tok(tok);
if first {
first = false;
lo = tok.span().lo();
}
match tok {
TokenTree::Token(_, Token::BinOp(BinOpToken::Plus))
| TokenTree::Token(_, Token::Question)
| TokenTree::Token(_, Token::BinOp(BinOpToken::Star)) => {
break;
}
TokenTree::Token(sp, ref t) => {
buffer.push_str(&pprust::token_to_string(t));
hi = sp.hi();
}
_ => unreachable!(),
}
}
// There could be some random stuff between ')' and '*', '+' or '?'.
let another = if buffer.trim().is_empty() {
None
} else {
Some(Box::new(ParsedMacroArg {
kind: MacroArgKind::Other(buffer, "".to_owned()),
span: mk_sp(lo, hi),
}))
};
self.result.push(ParsedMacroArg {
kind: MacroArgKind::Repeat(delim, inner, another, self.last_tok.clone()),
span: mk_sp(self.lo, self.hi),
});
}
fn update_buffer(&mut self, lo: BytePos, t: &Token) {
if self.buf.is_empty() {
self.lo = lo;
self.start_tok = t.clone();
} else {
let needs_space = match next_space(&self.last_tok) {
SpaceState::Ident => ident_like(t),
SpaceState::Punctuation => !ident_like(t),
SpaceState::Always => true,
SpaceState::Never => false,
};
if force_space_before(t) || needs_space {
self.buf.push(' ');
}
}
self.buf.push_str(&pprust::token_to_string(t));
}
fn need_space_prefix(&self) -> bool {
if self.result.is_empty() {
return false;
}
let last_arg = self.result.last().unwrap();
if let MacroArgKind::MetaVariable(..) = last_arg.kind {
if ident_like(&self.start_tok) {
return true;
}
if self.start_tok == Token::Colon {
return true;
}
}
if force_space_before(&self.start_tok) {
return true;
}
false
}
/// Returns a collection of parsed macro def's arguments.
pub fn parse(mut self, tokens: ThinTokenStream) -> Vec<ParsedMacroArg> {
let mut iter = (tokens.into(): TokenStream).trees();
while let Some(ref tok) = iter.next() {
match tok {
TokenTree::Token(sp, Token::Dollar) => {
// We always want to add a separator before meta variables.
if !self.buf.is_empty() {
self.add_separator();
}
// Start keeping the name of this metavariable in the buffer.
self.is_meta_var = true;
self.lo = sp.lo();
self.start_tok = Token::Dollar;
}
TokenTree::Token(_, Token::Colon) if self.is_meta_var => {
self.add_meta_variable(&mut iter);
}
TokenTree::Token(sp, ref t) => self.update_buffer(sp.lo(), t),
TokenTree::Delimited(sp, delimited) => {
if !self.buf.is_empty() {
if next_space(&self.last_tok) == SpaceState::Always {
self.add_separator();
} else {
self.add_other();
}
}
// Parse the stuff inside delimiters.
let mut parser = MacroArgParser::new();
parser.lo = sp.lo();
let delimited_arg = parser.parse(delimited.tts.clone());
if self.is_meta_var {
self.add_repeat(delimited_arg, delimited.delim, &mut iter, *sp);
} else {
self.add_delimited(delimited_arg, delimited.delim, *sp);
}
}
}
self.set_last_tok(tok);
}
// We are left with some stuff in the buffer. Since there is nothing
// left to separate, add this as `Other`.
if !self.buf.is_empty() {
self.add_other();
}
self.result
}
}
fn wrap_macro_args(
context: &RewriteContext,
args: &[ParsedMacroArg],
shape: Shape,
) -> Option<String> {
wrap_macro_args_inner(context, args, shape, false)
.or_else(|| wrap_macro_args_inner(context, args, shape, true))
}
fn wrap_macro_args_inner(
context: &RewriteContext,
args: &[ParsedMacroArg],
shape: Shape,
use_multiple_lines: bool,
) -> Option<String> {
let mut result = String::with_capacity(128);
let mut iter = args.iter().peekable();
let indent_str = shape.indent.to_string_with_newline(context.config);
while let Some(ref arg) = iter.next() {
result.push_str(&arg.rewrite(context, shape, use_multiple_lines)?);
if use_multiple_lines
&& (arg.kind.ends_with_space() || iter.peek().map_or(false, |a| a.kind.has_meta_var()))
{
if arg.kind.ends_with_space() {
result.pop();
}
result.push_str(&indent_str);
} else if let Some(ref next_arg) = iter.peek() {
let space_before_dollar =
!arg.kind.ends_with_space() && next_arg.kind.starts_with_dollar();
let space_before_brace = next_arg.kind.starts_with_brace();
if space_before_dollar || space_before_brace {
result.push(' ');
}
}
}
if !use_multiple_lines && result.len() >= shape.width {
None
} else {
Some(result)
}
}
// This is a bit sketchy. The token rules probably need tweaking, but it works
// for some common cases. I hope the basic logic is sufficient. Note that the
// meaning of some tokens is a bit different here from usual Rust, e.g., `*`
@ -470,66 +899,17 @@ fn replace_names(input: &str) -> Option<(String, HashMap<String, String>)> {
//
// We always try and format on one line.
// FIXME: Use multi-line when every thing does not fit on one line.
fn format_macro_args(toks: ThinTokenStream, shape: Shape) -> Option<String> {
let mut result = String::with_capacity(128);
let mut insert_space = SpaceState::Never;
for tok in (toks.into(): TokenStream).trees() {
match tok {
TokenTree::Token(_, t) => {
if !result.is_empty() && force_space_before(&t) {
insert_space = SpaceState::Always;
}
if force_no_space_before(&t) {
insert_space = SpaceState::Never;
}
match (insert_space, ident_like(&t)) {
(SpaceState::Always, _)
| (SpaceState::Punctuation, false)
| (SpaceState::Ident, true) => {
result.push(' ');
}
_ => {}
}
result.push_str(&pprust::token_to_string(&t));
insert_space = next_space(&t);
}
TokenTree::Delimited(_, d) => {
if let SpaceState::Always = insert_space {
result.push(' ');
}
let formatted = format_macro_args(d.tts, shape)?;
match d.delim {
DelimToken::Paren => {
result.push_str(&format!("({})", formatted));
insert_space = SpaceState::Always;
}
DelimToken::Bracket => {
result.push_str(&format!("[{}]", formatted));
insert_space = SpaceState::Always;
}
DelimToken::Brace => {
result.push_str(&format!(" {{ {} }}", formatted));
insert_space = SpaceState::Always;
}
DelimToken::NoDelim => {
result.push_str(&format!("{}", formatted));
insert_space = SpaceState::Always;
}
}
}
}
}
if result.len() <= shape.width {
Some(result)
} else {
None
}
fn format_macro_args(
context: &RewriteContext,
toks: ThinTokenStream,
shape: Shape,
) -> Option<String> {
let parsed_args = MacroArgParser::new().parse(toks);
wrap_macro_args(context, &parsed_args, shape)
}
// We should insert a space if the next token is a:
#[derive(Copy, Clone)]
#[derive(Copy, Clone, PartialEq)]
enum SpaceState {
Never,
Punctuation,
@ -538,6 +918,8 @@ enum SpaceState {
}
fn force_space_before(tok: &Token) -> bool {
debug!("tok: force_space_before {:?}", tok);
match *tok {
Token::Eq
| Token::Lt
@ -555,20 +937,13 @@ fn force_space_before(tok: &Token) -> bool {
| Token::RArrow
| Token::LArrow
| Token::FatArrow
| Token::BinOp(_)
| Token::Pound
| Token::Dollar => true,
Token::BinOp(bot) => bot != BinOpToken::Star,
_ => false,
}
}
fn force_no_space_before(tok: &Token) -> bool {
match *tok {
Token::Semi | Token::Comma | Token::Dot => true,
Token::BinOp(bot) => bot == BinOpToken::Star,
_ => false,
}
}
fn ident_like(tok: &Token) -> bool {
match *tok {
Token::Ident(_) | Token::Literal(..) | Token::Lifetime(_) => true,
@ -577,8 +952,11 @@ fn ident_like(tok: &Token) -> bool {
}
fn next_space(tok: &Token) -> SpaceState {
debug!("next_space: {:?}", tok);
match *tok {
Token::Not
| Token::BinOp(BinOpToken::And)
| Token::Tilde
| Token::At
| Token::Comma
@ -588,8 +966,7 @@ fn next_space(tok: &Token) -> SpaceState {
| Token::DotDotEq
| Token::DotEq
| Token::Question
| Token::Underscore
| Token::BinOp(_) => SpaceState::Punctuation,
| Token::Underscore => SpaceState::Punctuation,
Token::ModSep
| Token::Pound
@ -804,7 +1181,7 @@ impl MacroBranch {
}
// 5 = " => {"
let mut result = format_macro_args(self.args.clone(), shape.sub_width(5)?)?;
let mut result = format_macro_args(context, self.args.clone(), shape.sub_width(5)?)?;
if multi_branch_style {
result += " =>";
@ -957,50 +1334,3 @@ fn format_lazy_static(context: &RewriteContext, shape: Shape, ts: &TokenStream)
Some(result)
}
#[cfg(test)]
mod test {
use super::*;
use syntax;
use syntax::parse::{parse_stream_from_source_str, ParseSess};
use syntax::codemap::{FileName, FilePathMapping};
fn format_macro_args_str(s: &str) -> String {
let mut result = String::new();
syntax::with_globals(|| {
let input = parse_stream_from_source_str(
FileName::Custom("stdin".to_owned()),
s.to_owned(),
&ParseSess::new(FilePathMapping::empty()),
None,
);
let shape = Shape {
width: 100,
indent: Indent::empty(),
offset: 0,
};
result = format_macro_args(input.into(), shape).unwrap();
});
result
}
#[test]
fn test_format_macro_args() {
assert_eq!(format_macro_args_str(""), "".to_owned());
assert_eq!(format_macro_args_str("$ x : ident"), "$x: ident".to_owned());
assert_eq!(
format_macro_args_str("$ m1 : ident , $ m2 : ident , $ x : ident"),
"$m1: ident, $m2: ident, $x: ident".to_owned()
);
assert_eq!(
format_macro_args_str("$($beginning:ident),*;$middle:ident;$($end:ident),*"),
"$($beginning: ident),*; $middle: ident; $($end: ident),*".to_owned()
);
assert_eq!(
format_macro_args_str(
"$ name : ident ( $ ( $ dol : tt $ var : ident ) * ) $ ( $ body : tt ) *"
),
"$name: ident($($dol: tt $var: ident)*) $($body: tt)*".to_owned()
);
}
}

View File

@ -1,5 +1,44 @@
// rustfmt-error_on_line_overflow: false
macro_rules! m {
() => ();
( $ x : ident ) => ();
( $ m1 : ident , $ m2 : ident , $ x : ident ) => ();
( $($beginning:ident),*;$middle:ident;$($end:ident),* ) => ();
( $($beginning: ident),*; $middle: ident; $($end: ident),*; $($beginning: ident),*; $middle: ident; $($end: ident),* ) => {};
( $ name : ident ( $ ( $ dol : tt $ var : ident ) * ) $ ( $ body : tt ) * ) => ();
( $( $ i : ident : $ ty : ty , $def : expr , $stb : expr , $ ( $ dstring : tt ) , + ) ; + $ ( ; ) *
$( $ i : ident : $ ty : ty , $def : expr , $stb : expr , $ ( $ dstring : tt ) , + ) ; + $ ( ; ) *
) => {};
( $foo: tt foo [$ attr : meta] $name: ident ) => {};
( $foo: tt [$ attr: meta] $name: ident ) => {};
( $foo: tt &'a [$attr : meta] $name: ident ) => {};
( $foo: tt foo # [ $attr : meta] $name: ident ) => {};
( $foo: tt # [ $attr : meta] $name: ident) => {};
( $foo: tt &'a # [ $attr : meta] $name: ident ) => {};
( $ x : tt foo bar foo bar foo bar $ y : tt => x*y*z $ z : tt , $ ( $a: tt ) , * ) => {};
}
macro_rules! impl_a_method {
($n:ident ( $a:ident : $ta:ty ) -> $ret:ty { $body:expr }) => {
fn $n($a:$ta) -> $ret { $body }
macro_rules! $n { ($va:expr) => { $n($va) } }
};
($n:ident ( $a:ident : $ta:ty, $b:ident : $tb:ty ) -> $ret:ty { $body:expr }) => {
fn $n($a:$ta, $b:$tb) -> $ret { $body }
macro_rules! $n { ($va:expr, $vb:expr) => { $n($va, $vb) } }
};
($n:ident ( $a:ident : $ta:ty, $b:ident : $tb:ty, $c:ident : $tc:ty ) -> $ret:ty { $body:expr }) => {
fn $n($a:$ta, $b:$tb, $c:$tc) -> $ret { $body }
macro_rules! $n { ($va:expr, $vb:expr, $vc:expr) => { $n($va, $vb, $vc) } }
};
($n:ident ( $a:ident : $ta:ty, $b:ident : $tb:ty, $c:ident : $tc:ty, $d:ident : $td:ty ) -> $ret:ty { $body:expr }) => {
fn $n($a:$ta, $b:$tb, $c:$tc, $d:$td) -> $ret { $body }
macro_rules! $n { ($va:expr, $vb:expr, $vc:expr, $vd:expr) => { $n($va, $vb, $vc, $vd) } }
};
}
macro_rules! m {
// a
($expr :expr, $( $func : ident ) * ) => {
@ -88,12 +127,7 @@ macro_rules! m {
// #2439
macro_rules! m {
(
$line0_xxxxxxxxxxxxxxxxx: expr,
$line1_xxxxxxxxxxxxxxxxx: expr,
$line2_xxxxxxxxxxxxxxxxx: expr,
$line3_xxxxxxxxxxxxxxxxx: expr,
) => {};
($line0_xxxxxxxxxxxxxxxxx: expr, $line1_xxxxxxxxxxxxxxxxx: expr, $line2_xxxxxxxxxxxxxxxxx: expr, $line3_xxxxxxxxxxxxxxxxx: expr,) => {};
}
// #2466
@ -113,6 +147,12 @@ macro foo($type_name: ident, $docs: expr) {
pub struct $type_name;
}
// #2534
macro_rules! foo {
($a:ident : $b:ty) => {};
($a:ident $b:ident $c:ident) => {};
}
// #2538
macro_rules! add_message_to_notes {
($msg:expr) => {{

View File

@ -1,5 +1,81 @@
// rustfmt-error_on_line_overflow: false
macro_rules! m {
() => {};
($x: ident) => {};
($m1: ident, $m2: ident, $x: ident) => {};
($($beginning: ident),*; $middle: ident; $($end: ident),*) => {};
(
$($beginning: ident),*;
$middle: ident;
$($end: ident),*;
$($beginning: ident),*;
$middle: ident;
$($end: ident),*
) => {};
($name: ident($($dol: tt $var: ident)*) $($body: tt)*) => {};
(
$($i: ident : $ty: ty, $def: expr, $stb: expr, $($dstring: tt),+);+ $(;)*
$($i: ident : $ty: ty, $def: expr, $stb: expr, $($dstring: tt),+);+ $(;)*
) => {};
($foo: tt foo[$attr: meta] $name: ident) => {};
($foo: tt[$attr: meta] $name: ident) => {};
($foo: tt &'a[$attr: meta] $name: ident) => {};
($foo: tt foo #[$attr: meta] $name: ident) => {};
($foo: tt #[$attr: meta] $name: ident) => {};
($foo: tt &'a #[$attr: meta] $name: ident) => {};
($x: tt foo bar foo bar foo bar $y: tt => x * y * z $z: tt, $($a: tt),*) => {};
}
macro_rules! impl_a_method {
($n: ident($a: ident : $ta: ty) -> $ret: ty { $body: expr }) => {
fn $n($a: $ta) -> $ret {
$body
}
macro_rules! $n {
($va: expr) => {
$n($va)
};
}
};
($n: ident($a: ident : $ta: ty, $b: ident : $tb: ty) -> $ret: ty { $body: expr }) => {
fn $n($a: $ta, $b: $tb) -> $ret {
$body
}
macro_rules! $n {
($va: expr,$vb: expr) => {
$n($va, $vb)
};
}
};
(
$n: ident($a: ident : $ta: ty, $b: ident : $tb: ty, $c: ident : $tc: ty) ->
$ret: ty { $body: expr }
) => {
fn $n($a: $ta, $b: $tb, $c: $tc) -> $ret {
$body
}
macro_rules! $n {
($va: expr,$vb: expr,$vc: expr) => {
$n($va, $vb, $vc)
};
}
};
(
$n: ident($a: ident : $ta: ty, $b: ident : $tb: ty, $c: ident : $tc: ty, $d: ident : $td: ty) ->
$ret: ty { $body: expr }
) => {
fn $n($a: $ta, $b: $tb, $c: $tc, $d: $td) -> $ret {
$body
}
macro_rules! $n {
($va: expr,$vb: expr,$vc: expr,$vd: expr) => {
$n($va, $vb, $vc, $vd)
};
}
};
}
macro_rules! m {
// a
($expr: expr, $($func: ident)*) => {{
@ -104,6 +180,12 @@ macro foo($type_name: ident, $docs: expr) {
pub struct $type_name;
}
// #2534
macro_rules! foo {
($a: ident : $b: ty) => {};
($a: ident $b: ident $c: ident) => {};
}
// #2538
macro_rules! add_message_to_notes {
($msg: expr) => {{