use ryo_source::pure::{
PureAttrMeta, PureAttribute, PureExpr, PureField, PureFields, PureFile, PureImpl, PureImplItem,
PureItem, PureStmt,
};
use super::detect::{Detect, DetectCategory, DetectLocation, DetectOperation, DetectOpportunity};
use crate::Mutation;
#[derive(Debug, Clone)]
pub struct DefaultMutation {
pub target_struct: Option<String>,
pub use_derive: bool,
}
impl Default for DefaultMutation {
fn default() -> Self {
Self {
target_struct: None,
use_derive: true,
}
}
}
impl DefaultMutation {
pub fn new() -> Self {
Self::default()
}
pub fn for_struct(mut self, name: impl Into<String>) -> Self {
self.target_struct = Some(name.into());
self
}
pub fn with_derive(mut self, use_derive: bool) -> Self {
self.use_derive = use_derive;
self
}
fn get_named_fields(fields: &PureFields) -> Option<&Vec<PureField>> {
match fields {
PureFields::Named(f) => Some(f),
_ => None,
}
}
fn has_derive_default(attrs: &[PureAttribute]) -> bool {
attrs.iter().any(|attr| {
attr.path == "derive"
&& matches!(&attr.meta, PureAttrMeta::List(args) if args.contains("Default"))
})
}
fn has_manual_default_impl(file: &PureFile, struct_name: &str) -> bool {
file.items.iter().any(|item| {
if let PureItem::Impl(imp) = item {
imp.self_ty == struct_name && imp.trait_.as_deref() == Some("Default")
} else {
false
}
})
}
}
impl Mutation for DefaultMutation {
fn describe(&self) -> String {
if self.use_derive {
"Add #[derive(Default)] to structs".to_string()
} else {
"Generate impl Default for structs".to_string()
}
}
fn mutation_type(&self) -> &'static str {
"Default"
}
fn box_clone(&self) -> Box<dyn Mutation> {
Box::new(self.clone())
}
}
impl Detect for DefaultMutation {
fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity> {
let mut opportunities = Vec::new();
for item in &file.items {
if let PureItem::Struct(s) = item {
if let Some(ref target) = self.target_struct {
if &s.name != target {
continue;
}
}
if Self::has_derive_default(&s.attrs) {
continue;
}
if Self::has_manual_default_impl(file, &s.name) {
continue;
}
if Self::get_named_fields(&s.fields).is_some() {
opportunities.push(
DetectOpportunity::new(
DetectLocation::struct_item(&s.name),
format!("Add Default implementation for '{}'", s.name),
)
.with_operations(vec![DetectOperation::Generate])
.with_confidence(0.8),
);
}
}
}
opportunities
}
fn category(&self) -> DetectCategory {
DetectCategory::Creational
}
fn detect_name(&self) -> &'static str {
"Default"
}
fn detect_description(&self) -> &str {
"Add Default implementation to structs"
}
}
#[derive(Debug, Clone, Default)]
pub struct DeriveDefaultMutation {
pub target_type: Option<String>,
}
impl DeriveDefaultMutation {
pub fn new() -> Self {
Self::default()
}
pub fn for_type(mut self, type_name: impl Into<String>) -> Self {
self.target_type = Some(type_name.into());
self
}
fn is_default_impl(imp: &PureImpl) -> bool {
imp.trait_.as_deref() == Some("Default")
}
fn is_derivable_default(imp: &PureImpl) -> Option<String> {
let default_fn = imp.items.iter().find_map(|item| {
if let PureImplItem::Fn(f) = item {
if f.name == "default" {
return Some(f);
}
}
None
})?;
if default_fn.body.stmts.len() != 1 {
return None;
}
let expr = match &default_fn.body.stmts[0] {
PureStmt::Expr(e) => e,
_ => return None,
};
let fields = match expr {
PureExpr::Struct { path, fields } => {
if path != "Self" && path != &imp.self_ty {
return None;
}
fields
}
_ => return None,
};
for (_, value) in fields {
if !Self::is_default_value(value) {
return None;
}
}
Some(imp.self_ty.clone())
}
fn is_default_value(expr: &PureExpr) -> bool {
match expr {
PureExpr::Lit(lit) => Self::is_zero_literal(lit),
PureExpr::Path(p) => p == "None",
PureExpr::Call { func, args } => {
if args.is_empty() {
if let PureExpr::Path(p) = func.as_ref() {
return matches!(
p.as_str(),
"Default::default"
| "Vec::new"
| "String::new"
| "HashMap::new"
| "HashSet::new"
| "BTreeMap::new"
| "BTreeSet::new"
| "PathBuf::new"
| "OsString::new"
);
}
}
false
}
PureExpr::MethodCall { method, args, .. } => {
(method == "default" || method == "new") && args.is_empty()
}
_ => false,
}
}
fn is_zero_literal(lit: &str) -> bool {
matches!(
lit.trim(),
"0" | "0i8"
| "0i16"
| "0i32"
| "0i64"
| "0i128"
| "0isize"
| "0u8"
| "0u16"
| "0u32"
| "0u64"
| "0u128"
| "0usize"
| "0.0"
| "0.0f32"
| "0.0f64"
| "false"
| "'\\0'"
| "\"\""
)
}
}
impl Mutation for DeriveDefaultMutation {
fn describe(&self) -> String {
"Convert manual Default implementations to #[derive(Default)]".to_string()
}
fn mutation_type(&self) -> &'static str {
"DeriveDefault"
}
fn box_clone(&self) -> Box<dyn Mutation> {
Box::new(self.clone())
}
}
impl Detect for DeriveDefaultMutation {
fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity> {
let mut opportunities = Vec::new();
for item in &file.items {
if let PureItem::Impl(imp) = item {
if Self::is_default_impl(imp) {
if let Some(ref target) = self.target_type {
if &imp.self_ty != target {
continue;
}
}
if Self::is_derivable_default(imp).is_some() {
opportunities.push(
DetectOpportunity::new(
DetectLocation::impl_item(&imp.self_ty),
format!(
"Convert manual Default impl for '{}' to #[derive(Default)]",
imp.self_ty
),
)
.with_operations(vec![DetectOperation::Refactor])
.with_confidence(0.95),
);
}
}
}
}
opportunities
}
fn category(&self) -> DetectCategory {
DetectCategory::Creational
}
fn detect_name(&self) -> &'static str {
"DeriveDefault"
}
fn detect_description(&self) -> &str {
"Convert manual Default implementations to #[derive(Default)]"
}
}