[PATCH 4/5] rust: macros: use `syn` for `fmt!` string parsing
Gary Guo <[email protected]> Tue, 28 Jul 2026 15:52:11 +0100
| Newsgroups | org.kernel.vger.rust-for-linux,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
Use `syn` to parse `fmt!` string as it handles string escapes properly and is able to parse when the literal is wrapped inside a invisible delimiter group, which can be added when `fmt!` invocation comes from declarative macro. Signed-off-by: Gary Guo <[email protected]> --- rust/macros/fmt.rs | 92 +++++++++++++++++++++++++++++------------------------- rust/macros/lib.rs | 4 ++- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/rust/macros/fmt.rs b/rust/macros/fmt.rs index ce6c7249305a..0b9b2d328650 100644 --- a/rust/macros/fmt.rs +++ b/rust/macros/fmt.rs @@ -2,54 +2,60 @@ use std::collections::BTreeSet; -use proc_macro2::{Ident, TokenStream, TokenTree}; -use quote::quote_spanned; +use proc_macro2::{ + Ident, + Span, + TokenStream, + TokenTree, // +}; +use quote::{ + quote_spanned, + ToTokens, // +}; +use syn::{ + Error, + LitStr, + Result, // +}; /// Please see [`crate::fmt`] for documentation. -pub(crate) fn fmt(input: TokenStream) -> TokenStream { +pub(crate) fn fmt(input: TokenStream) -> Result<TokenStream> { let mut input = input.into_iter(); - let first_opt = input.next(); - let first_owned_str; - let mut names = BTreeSet::new(); - let first_span = { - let Some((mut first_str, first_span)) = (match first_opt.as_ref() { - Some(TokenTree::Literal(first_lit)) => { - first_owned_str = first_lit.to_string(); - Some(first_owned_str.as_str()).and_then(|first| { - let first = first.strip_prefix('"')?; - let first = first.strip_suffix('"')?; - Some((first, first_lit.span())) - }) - } - _ => None, - }) else { - return first_opt.into_iter().chain(input).collect(); - }; + let Some(fmt_tt) = input.next() else { + return Err(Error::new( + Span::call_site(), + "requires at least a format string argument", + )); + }; - // Parse `identifier`s from the format string. - // - // See https://doc.rust-lang.org/std/fmt/index.html#syntax. - while let Some((_, rest)) = first_str.split_once('{') { - first_str = rest; - if let Some(rest) = first_str.strip_prefix('{') { - first_str = rest; - continue; - } - if let Some((name, rest)) = first_str.split_once('}') { - first_str = rest; - let name = name.split_once(':').map_or(name, |(name, _)| name); - if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) { - names.insert(name); - } + let fmt: LitStr = syn::parse2(fmt_tt.into())?; + let fmt_str = fmt.value(); + let fmt_span = fmt.span(); + + // Parse `identifier`s from the format string. + // + // See https://doc.rust-lang.org/std/fmt/index.html#syntax. + let mut names = BTreeSet::new(); + let mut fmt_str_rest = fmt_str.as_str(); + while let Some((_, rest)) = fmt_str_rest.split_once('{') { + fmt_str_rest = rest; + if let Some(rest) = fmt_str_rest.strip_prefix('{') { + fmt_str_rest = rest; + continue; + } + if let Some((name, rest)) = fmt_str_rest.split_once('}') { + fmt_str_rest = rest; + let name = name.split_once(':').map_or(name, |(name, _)| name); + if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) { + names.insert(name); } } - first_span - }; + } - let adapter = quote_spanned!(first_span => ::kernel::fmt::Adapter); + let adapter = quote_spanned!(fmt_span => ::kernel::fmt::Adapter); - let mut args = TokenStream::from_iter(first_opt); + let mut args = fmt.to_token_stream(); { let mut flush = |args: &mut TokenStream, current: &mut TokenStream| { let current = std::mem::take(current); @@ -69,7 +75,7 @@ pub(crate) fn fmt(input: TokenStream) -> TokenStream { } (None, acc) })(); - args.extend(quote_spanned!(first_span => #lhs #adapter(&(#rhs)))); + args.extend(quote_spanned!(fmt_span => #lhs #adapter(&(#rhs)))); } }; @@ -88,9 +94,9 @@ pub(crate) fn fmt(input: TokenStream) -> TokenStream { } for name in names { - let name = Ident::new(name, first_span); - args.extend(quote_spanned!(first_span => , #name = #adapter(&#name))); + let name = Ident::new(name, fmt_span); + args.extend(quote_spanned!(fmt_span => , #name = #adapter(&#name))); } - quote_spanned!(first_span => ::core::format_args!(#args)) + Ok(quote_spanned!(fmt_span => ::core::format_args!(#args))) } diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs index 0514fc7c0a55..81f6a6ed696b 100644 --- a/rust/macros/lib.rs +++ b/rust/macros/lib.rs @@ -259,7 +259,9 @@ pub fn export(attr: TokenStream, input: TokenStream) -> TokenStream { /// [`pr_info!`]: ../kernel/macro.pr_info.html #[proc_macro] pub fn fmt(input: TokenStream) -> TokenStream { - fmt::fmt(input.into()).into() + fmt::fmt(input.into()) + .unwrap_or_else(|e| e.into_compile_error()) + .into() } /// Concatenate two identifiers. -- 2.54.0