use syn::visit::Visit;
use crate::config::StructuralConfig;
use crate::findings::Dimension;
use super::{StructuralWarning, StructuralWarningKind};
pub(crate) fn detect_nms(
warnings: &mut Vec<StructuralWarning>,
parsed: &[(String, String, syn::File)],
config: &StructuralConfig,
) {
if !config.check_nms {
return;
}
super::visit_inherent_methods(parsed, |method, path| {
check_method(method, path, warnings);
});
}
fn check_method(method: &syn::ImplItemFn, path: &str, warnings: &mut Vec<StructuralWarning>) {
let is_mut_self = method
.sig
.inputs
.first()
.and_then(|arg| match arg {
syn::FnArg::Receiver(r) => Some(r.mutability.is_some() && r.reference.is_some()),
_ => None,
})
.unwrap_or(false);
if !is_mut_self {
return;
}
if method.block.stmts.is_empty() {
return;
}
let mut checker = MutationChecker {
has_mutation: false,
has_self_ref: false,
};
checker.visit_block(&method.block);
if checker.has_self_ref && !checker.has_mutation {
let line = method.sig.ident.span().start().line;
warnings.push(StructuralWarning {
file: path.to_string(),
line,
name: method.sig.ident.to_string(),
kind: StructuralWarningKind::NeedlessMutSelf,
dimension: Dimension::Srp,
suppressed: false,
});
}
}
#[derive(Default)]
struct MutationChecker {
has_mutation: bool,
has_self_ref: bool,
}
impl<'ast> Visit<'ast> for MutationChecker {
fn visit_expr(&mut self, expr: &'ast syn::Expr) {
if is_self_ref(expr) {
self.has_self_ref = true;
}
match expr {
syn::Expr::Assign(a) if is_self_target(&a.left) => {
self.has_mutation = true;
}
syn::Expr::Binary(b) if is_compound_assign(&b.op) && is_self_target(&b.left) => {
self.has_mutation = true;
}
syn::Expr::MethodCall(mc)
if is_self_field(&mc.receiver)
|| is_self_path(&mc.receiver)
|| is_self_indexed_field(&mc.receiver) =>
{
self.has_mutation = true;
}
syn::Expr::Reference(r) if r.mutability.is_some() && is_self_target(&r.expr) => {
self.has_mutation = true;
}
_ => {}
}
if !self.has_mutation {
syn::visit::visit_expr(self, expr);
}
}
}
fn is_self_target(expr: &syn::Expr) -> bool {
match expr {
syn::Expr::Field(f) => matches!(&*f.base, syn::Expr::Path(p) if p.path.is_ident("self")),
syn::Expr::Index(idx) => {
matches!(&*idx.expr, syn::Expr::Field(f) if matches!(&*f.base, syn::Expr::Path(p) if p.path.is_ident("self")))
}
_ => false,
}
}
fn is_compound_assign(op: &syn::BinOp) -> bool {
matches!(
op,
syn::BinOp::AddAssign(_)
| syn::BinOp::SubAssign(_)
| syn::BinOp::MulAssign(_)
| syn::BinOp::DivAssign(_)
| syn::BinOp::RemAssign(_)
| syn::BinOp::BitAndAssign(_)
| syn::BinOp::BitOrAssign(_)
| syn::BinOp::BitXorAssign(_)
| syn::BinOp::ShlAssign(_)
| syn::BinOp::ShrAssign(_)
)
}
fn is_self_field(expr: &syn::Expr) -> bool {
match expr {
syn::Expr::Field(f) => matches!(&*f.base, syn::Expr::Path(p) if p.path.is_ident("self")),
_ => false,
}
}
fn is_self_indexed_field(expr: &syn::Expr) -> bool {
matches!(expr, syn::Expr::Index(idx) if is_self_field(&idx.expr))
}
fn is_self_path(expr: &syn::Expr) -> bool {
matches!(expr, syn::Expr::Path(p) if p.path.is_ident("self"))
}
fn is_self_ref(expr: &syn::Expr) -> bool {
match expr {
syn::Expr::Path(p) => p
.path
.segments
.first()
.map(|s| s.ident == "self")
.unwrap_or(false),
syn::Expr::Field(f) => matches!(&*f.base, syn::Expr::Path(p) if p.path.is_ident("self")),
_ => false,
}
}