use crate::config::sections::BoilerplateConfig;
#[derive(Debug, Clone)]
pub struct BoilerplateFind {
pub pattern_id: String,
pub file: String,
pub line: usize,
pub struct_name: Option<String>,
pub description: String,
pub suggestion: String,
pub suppressed: bool,
}
macro_rules! pattern_guard {
($id:expr, $config:expr) => {
if !$config.patterns.is_empty() && $config.patterns.iter().all(|p| p != $id) {
return vec![];
}
};
}
pub(crate) fn trait_name_of(imp: &syn::ItemImpl) -> Option<String> {
imp.trait_
.as_ref()
.and_then(|(_, path, _)| path.segments.last().map(|s| s.ident.to_string()))
}
pub(crate) fn self_type_of(imp: &syn::ItemImpl) -> Option<String> {
if let syn::Type::Path(tp) = &*imp.self_ty {
tp.path.segments.last().map(|s| s.ident.to_string())
} else {
None
}
}
pub(crate) fn single_return_expr(block: &syn::Block) -> Option<&syn::Expr> {
if block.stmts.len() == 1 {
if let syn::Stmt::Expr(expr, None) = &block.stmts[0] {
return Some(expr);
}
}
None
}
pub(crate) fn is_self_field_access(expr: &syn::Expr) -> bool {
if let syn::Expr::Field(f) = expr {
if let syn::Expr::Path(p) = &*f.base {
return p.path.segments.last().is_some_and(|s| s.ident == "self");
}
}
false
}
pub(crate) fn is_default_value_expr(expr: &syn::Expr) -> bool {
match expr {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Int(i) => i.base10_parse::<i64>().ok() == Some(0),
syn::Lit::Float(f) => f.base10_parse::<f64>().ok() == Some(0.0),
syn::Lit::Bool(b) => !b.value,
syn::Lit::Str(s) => s.value().is_empty(),
_ => false,
},
syn::Expr::Path(p) => p.path.segments.last().is_some_and(|s| s.ident == "None"),
syn::Expr::Call(call) => {
if let syn::Expr::Path(p) = &*call.func {
let segs: Vec<_> = p
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
matches!(
segs.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.as_slice(),
["Default", "default"]
| ["String", "new"]
| ["Vec", "new"]
| ["HashMap", "new"]
| ["HashSet", "new"]
| ["BTreeMap", "new"]
| ["BTreeSet", "new"]
)
} else {
false
}
}
syn::Expr::Macro(m) => {
let name = m
.mac
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
name == "vec" && m.mac.tokens.is_empty()
}
_ => false,
}
}
fn is_simple_enum_pattern(pat: &syn::Pat) -> bool {
match pat {
syn::Pat::Path(_) => true,
syn::Pat::TupleStruct(ts) => ts.elems.iter().all(|p| matches!(p, syn::Pat::Wild(_))),
syn::Pat::Struct(ps) => ps.fields.is_empty(),
_ => false,
}
}
pub(crate) fn is_repetitive_enum_mapping(arms: &[syn::Arm]) -> bool {
arms.iter().all(|arm| {
if arm.guard.is_some() {
return false;
}
is_simple_enum_pattern(&arm.pat)
&& matches!(&*arm.body, syn::Expr::Path(_) | syn::Expr::Call(_))
})
}
pub(crate) fn count_field_clones(expr: &syn::Expr) -> usize {
if let syn::Expr::Struct(s) = expr {
s.fields
.iter()
.filter(|f| {
matches!(&f.expr, syn::Expr::MethodCall(mc) if mc.method == "clone" && mc.args.is_empty())
})
.count()
} else {
0
}
}
mod builder;
mod clone_conversion;
mod error_enum;
mod format_repetition;
mod getter_setter;
mod manual_default;
mod repetitive_match;
mod struct_update;
mod trivial_display;
mod trivial_from;
pub fn detect_boilerplate(
parsed: &[(String, String, syn::File)],
config: &BoilerplateConfig,
) -> Vec<BoilerplateFind> {
let mut findings = trivial_from::check_trivial_from(parsed, config);
findings.extend(trivial_display::check_trivial_display(parsed, config));
findings.extend(getter_setter::check_manual_getter_setter(parsed, config));
findings.extend(builder::check_builder_boilerplate(parsed, config));
findings.extend(manual_default::check_manual_default(parsed, config));
findings.extend(repetitive_match::check_repetitive_match(parsed, config));
findings.extend(error_enum::check_error_enum_boilerplate(parsed, config));
findings.extend(clone_conversion::check_clone_heavy_conversion(
parsed, config,
));
findings.extend(struct_update::check_repetitive_struct_update(
parsed, config,
));
findings.extend(format_repetition::check_format_repetition(parsed, config));
findings
}
#[cfg(test)]
mod tests;