ruff/crates/ruff_macros/src/derive_message_formats.rs
Micha Reiser 8a4158c5f8
Upgrade to Rust 1.86 and bump MSRV to 1.84 (#17171)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

I decided to disable the new
[`needless_continue`](https://rust-lang.github.io/rust-clippy/master/index.html#needless_continue)
rule because I often found the explicit `continue` more readable over an
empty block or having to invert the condition of an other branch.


## Test Plan

`cargo test`

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-03 15:59:44 +00:00

105 lines
3.6 KiB
Rust

use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::token::{Dot, Paren};
use syn::{Block, Expr, ExprLit, ExprMethodCall, ItemFn, Lit, Stmt};
pub(crate) fn derive_message_formats(func: &ItemFn) -> TokenStream {
let mut strings = quote!();
if let Err(err) = parse_block(&func.block, &mut strings) {
return err;
}
quote! {
#func
fn message_formats() -> &'static [&'static str] {
&[#strings]
}
}
}
fn parse_block(block: &Block, strings: &mut TokenStream) -> Result<(), TokenStream> {
let Some(Stmt::Expr(last, _)) = block.stmts.last() else {
panic!("expected last statement in block to be an expression")
};
parse_expr(last, strings)?;
Ok(())
}
fn parse_expr(expr: &Expr, strings: &mut TokenStream) -> Result<(), TokenStream> {
match expr {
Expr::Macro(mac) if mac.mac.path.is_ident("format") => {
let mut tokens = mac.mac.tokens.to_token_stream().into_iter();
let Some(first_token) = tokens.next() else {
return Err(
quote_spanned!(expr.span() => compile_error!("expected `format!` to have an argument")),
);
};
// do not throw an error if the `format!` argument contains a formatting argument
if !first_token.to_string().contains('{') {
// comma and string
if tokens.next().is_none() || tokens.next().is_none() {
return Err(
quote_spanned!(expr.span() => compile_error!("prefer `String::to_string` over `format!` without arguments")),
);
}
}
strings.extend(quote! {#first_token,});
Ok(())
}
Expr::Block(block) => parse_block(&block.block, strings),
Expr::If(expr) => {
parse_block(&expr.then_branch, strings)?;
if let Some((_, then)) = &expr.else_branch {
parse_expr(then, strings)?;
}
Ok(())
}
Expr::MethodCall(method_call) => match method_call {
ExprMethodCall {
method,
receiver,
attrs,
dot_token,
turbofish: None,
paren_token,
args,
} if *method == *"to_string"
&& attrs.is_empty()
&& args.is_empty()
&& *paren_token == Paren::default()
&& *dot_token == Dot::default() =>
{
let Expr::Lit(ExprLit {
lit: Lit::Str(ref literal_string),
..
}) = **receiver
else {
return Err(
quote_spanned!(expr.span() => compile_error!("expected `String::to_string` method on str literal")),
);
};
let str_token = literal_string.token();
strings.extend(quote! {#str_token,});
Ok(())
}
_ => Err(
quote_spanned!(expr.span() => compile_error!("expected `String::to_string` method on str literal")),
),
},
Expr::Match(block) => {
for arm in &block.arms {
parse_expr(&arm.body, strings)?;
}
Ok(())
}
_ => Err(quote_spanned!(
expr.span() =>
compile_error!("expected last expression to be a `format!` macro, a static String or a match block")
)),
}
}