use syn::spanned::Spanned;
use super::{is_self_field_access, self_type_of, BoilerplateFind};
use crate::config::sections::BoilerplateConfig;
const MIN_BUILDER_METHOD_COUNT: usize = 3;
pub(super) fn check_builder_boilerplate(
parsed: &[(String, String, syn::File)],
config: &BoilerplateConfig,
) -> Vec<BoilerplateFind> {
super::scan_items(parsed, config, "BP-004", |item, file| {
builder_find(item, file, config)
})
}
fn builder_find(
item: &syn::Item,
file: &str,
config: &BoilerplateConfig,
) -> Option<BoilerplateFind> {
let syn::Item::Impl(imp) = item else {
return None;
};
if imp.trait_.is_some() {
return None;
}
let count = imp.items.iter().filter(|i| is_builder_method(i)).count();
if count < MIN_BUILDER_METHOD_COUNT {
return None;
}
let suggest = if config.suggest_crates {
"Consider using typed_builder or derive_builder"
} else {
"Consider using a builder derive macro to reduce repetition"
};
Some(BoilerplateFind {
pattern_id: "BP-004".to_string(),
file: file.to_string(),
line: imp.self_ty.span().start().line,
struct_name: self_type_of(imp),
description: format!(
"{count} builder-style methods with repetitive set-and-return pattern"
),
suggestion: suggest.to_string(),
suppressed: false,
})
}
fn is_builder_method(item: &syn::ImplItem) -> bool {
let syn::ImplItem::Fn(m) = item else {
return false;
};
method_returns_self(&m.sig.output)
&& m.block.stmts.len() == 2
&& is_field_assign(&m.block.stmts[0])
&& is_self_expr(&m.block.stmts[1])
}
fn method_returns_self(output: &syn::ReturnType) -> bool {
matches!(output, syn::ReturnType::Type(_, ty)
if matches!(&**ty, syn::Type::Path(tp)
if tp.path.segments.last().is_some_and(|s| s.ident == "Self")))
}
fn is_field_assign(stmt: &syn::Stmt) -> bool {
matches!(stmt, syn::Stmt::Expr(syn::Expr::Assign(a), Some(_)) if is_self_field_access(&a.left))
}
fn is_self_expr(stmt: &syn::Stmt) -> bool {
matches!(stmt, syn::Stmt::Expr(syn::Expr::Path(p), None)
if p.path.segments.last().is_some_and(|s| s.ident == "self"))
}