use std::collections::BTreeSet;
use syn::spanned::Spanned;
use super::{self_type_of, trait_name_of, BoilerplateFind};
use crate::config::sections::BoilerplateConfig;
enum WriteOp {
Idiom(&'static str),
Neutral,
}
const SUGGEST_WITH_CRATES: &str = "Derive it (derive_more::Display), declare the \
project's house idiom in [boilerplate] accepted_display_idioms, or generate \
the impls with one local macro_rules! — details: rustqual --explain BP-002";
const SUGGEST_NEUTRAL: &str = "Derive it with a derive macro, declare the \
project's house idiom in [boilerplate] accepted_display_idioms, or generate \
the impls with one local macro_rules! — details: rustqual --explain BP-002";
pub(super) fn check_trivial_display(
parsed: &[(String, String, syn::File)],
config: &BoilerplateConfig,
) -> Vec<BoilerplateFind> {
pattern_guard!("BP-002", config);
let suggest = if config.suggest_crates {
SUGGEST_WITH_CRATES
} else {
SUGGEST_NEUTRAL
};
let accepted = &config.accepted_display_idioms;
parsed
.iter()
.flat_map(|(file, _, syntax)| {
syntax.items.iter().filter_map({
let file = file.clone();
move |item| {
let syn::Item::Impl(imp) = item else {
return None;
};
if trait_name_of(imp)? != "Display" {
return None;
}
let idioms = trivial_display_idioms(single_fmt_method(imp)?)?;
if idioms.iter().all(|i| accepted.iter().any(|a| a == i)) {
return None;
}
Some(build_find(imp, &idioms, &file, suggest))
}
})
})
.collect()
}
fn build_find(
imp: &syn::ItemImpl,
idioms: &BTreeSet<&'static str>,
file: &str,
suggest: &str,
) -> BoilerplateFind {
let used: Vec<&str> = idioms.iter().copied().collect();
BoilerplateFind {
pattern_id: "BP-002".to_string(),
file: file.to_string(),
line: imp.self_ty.span().start().line,
struct_name: self_type_of(imp),
description: format!(
"Trivial Display implementation (branch-free, {} only)",
used.join("/")
),
suggestion: suggest.to_string(),
suppressed: false,
}
}
fn single_fmt_method(imp: &syn::ItemImpl) -> Option<&syn::ImplItemFn> {
let mut fns = imp.items.iter().filter_map(|i| match i {
syn::ImplItem::Fn(m) => Some(m),
_ => None,
});
let first = fns.next()?;
if fns.next().is_some() || first.sig.ident != "fmt" {
return None;
}
Some(first)
}
fn trivial_display_idioms(method: &syn::ImplItemFn) -> Option<BTreeSet<&'static str>> {
formatter_ident(method).and_then(|fmt| collect_idioms(&method.block, &fmt))
}
fn formatter_ident(method: &syn::ImplItemFn) -> Option<String> {
let syn::FnArg::Typed(pat_type) = method.sig.inputs.iter().nth(1)? else {
return None;
};
let syn::Pat::Ident(pi) = &*pat_type.pat else {
return None;
};
Some(pi.ident.to_string())
}
fn collect_idioms(block: &syn::Block, fmt_ident: &str) -> Option<BTreeSet<&'static str>> {
let ops: Option<Vec<WriteOp>> = block
.stmts
.iter()
.map(|s| classify_stmt(s, fmt_ident))
.collect();
let idioms: BTreeSet<&'static str> = ops?
.into_iter()
.filter_map(|op| match op {
WriteOp::Idiom(i) => Some(i),
WriteOp::Neutral => None,
})
.collect();
(!idioms.is_empty()).then_some(idioms)
}
fn classify_stmt(stmt: &syn::Stmt, fmt_ident: &str) -> Option<WriteOp> {
match stmt {
syn::Stmt::Expr(e, _) => classify_expr(unwrap_try(e), fmt_ident),
syn::Stmt::Macro(m) => classify_macro(&m.mac, fmt_ident),
_ => None,
}
}
fn unwrap_try(expr: &syn::Expr) -> &syn::Expr {
let mut e = expr;
while let syn::Expr::Try(t) = e {
e = &t.expr;
}
e
}
fn classify_expr(expr: &syn::Expr, fmt_ident: &str) -> Option<WriteOp> {
match expr {
syn::Expr::Macro(m) => classify_macro(&m.mac, fmt_ident),
syn::Expr::MethodCall(mc) => classify_method_call(mc, fmt_ident),
syn::Expr::Call(c) => classify_call(c, fmt_ident),
_ => None,
}
}
fn classify_macro(mac: &syn::Macro, fmt_ident: &str) -> Option<WriteOp> {
let first_arg_is_fmt = || {
crate::adapters::shared::macro_tokens::recover_exprs(&mac.tokens)
.first()
.is_some_and(|e| is_path_ident(e, fmt_ident))
};
let last = mac.path.segments.last()?;
if (last.ident == "write" || last.ident == "writeln") && first_arg_is_fmt() {
Some(WriteOp::Idiom("write_macro"))
} else {
None
}
}
fn classify_method_call(mc: &syn::ExprMethodCall, fmt_ident: &str) -> Option<WriteOp> {
let is_fmt = |e: &syn::Expr| is_path_ident(e, fmt_ident);
let method = mc.method.to_string();
if method == "write_str" && is_fmt(&mc.receiver) {
return Some(WriteOp::Idiom("write_str"));
}
if method == "write_char" && is_fmt(&mc.receiver) {
return Some(WriteOp::Idiom("write_char"));
}
if method == "fmt" && mc.args.len() == 1 && is_fmt(&mc.args[0]) {
return Some(WriteOp::Idiom("delegation"));
}
None
}
fn classify_call(call: &syn::ExprCall, fmt_ident: &str) -> Option<WriteOp> {
let is_fmt = |e: &syn::Expr| is_path_ident(e, fmt_ident);
let syn::Expr::Path(p) = &*call.func else {
return None;
};
let segs: Vec<&syn::Ident> = p.path.segments.iter().map(|s| &s.ident).collect();
let unit_arg = matches!(call.args.first(), Some(syn::Expr::Tuple(t)) if t.elems.is_empty());
if segs.last().is_some_and(|s| *s == "Ok") && call.args.len() == 1 && unit_arg {
return Some(WriteOp::Neutral);
}
let display_fmt =
segs.len() >= 2 && *segs[segs.len() - 2] == "Display" && *segs[segs.len() - 1] == "fmt";
if display_fmt && call.args.len() == 2 && is_fmt(&call.args[1]) {
return Some(WriteOp::Idiom("delegation"));
}
None
}
fn is_path_ident(expr: &syn::Expr, name: &str) -> bool {
let inner = if let syn::Expr::Reference(r) = expr {
&*r.expr
} else {
expr
};
matches!(inner, syn::Expr::Path(p) if p.path.is_ident(name))
}