ryo_mutations/idiom/
default.rs1use ryo_source::pure::{
33 PureAttrMeta, PureAttribute, PureExpr, PureField, PureFields, PureFile, PureImpl, PureImplItem,
34 PureItem, PureStmt,
35};
36
37use super::detect::{Detect, DetectCategory, DetectLocation, DetectOperation, DetectOpportunity};
38use crate::Mutation;
39
40#[derive(Debug, Clone)]
58pub struct DefaultMutation {
59 pub target_struct: Option<String>,
61 pub use_derive: bool,
63}
64
65impl Default for DefaultMutation {
66 fn default() -> Self {
67 Self {
68 target_struct: None,
69 use_derive: true,
70 }
71 }
72}
73
74impl DefaultMutation {
75 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn for_struct(mut self, name: impl Into<String>) -> Self {
81 self.target_struct = Some(name.into());
82 self
83 }
84
85 pub fn with_derive(mut self, use_derive: bool) -> Self {
87 self.use_derive = use_derive;
88 self
89 }
90
91 fn get_named_fields(fields: &PureFields) -> Option<&Vec<PureField>> {
93 match fields {
94 PureFields::Named(f) => Some(f),
95 _ => None,
96 }
97 }
98
99 fn has_derive_default(attrs: &[PureAttribute]) -> bool {
101 attrs.iter().any(|attr| {
102 attr.path == "derive"
103 && matches!(&attr.meta, PureAttrMeta::List(args) if args.contains("Default"))
104 })
105 }
106
107 fn has_manual_default_impl(file: &PureFile, struct_name: &str) -> bool {
109 file.items.iter().any(|item| {
110 if let PureItem::Impl(imp) = item {
111 imp.self_ty == struct_name && imp.trait_.as_deref() == Some("Default")
112 } else {
113 false
114 }
115 })
116 }
117}
118
119impl Mutation for DefaultMutation {
120 fn describe(&self) -> String {
121 if self.use_derive {
122 "Add #[derive(Default)] to structs".to_string()
123 } else {
124 "Generate impl Default for structs".to_string()
125 }
126 }
127
128 fn mutation_type(&self) -> &'static str {
129 "Default"
130 }
131
132 fn box_clone(&self) -> Box<dyn Mutation> {
133 Box::new(self.clone())
134 }
135}
136
137impl Detect for DefaultMutation {
138 fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity> {
139 let mut opportunities = Vec::new();
140
141 for item in &file.items {
142 if let PureItem::Struct(s) = item {
143 if let Some(ref target) = self.target_struct {
144 if &s.name != target {
145 continue;
146 }
147 }
148
149 if Self::has_derive_default(&s.attrs) {
151 continue;
152 }
153 if Self::has_manual_default_impl(file, &s.name) {
154 continue;
155 }
156
157 if Self::get_named_fields(&s.fields).is_some() {
159 opportunities.push(
160 DetectOpportunity::new(
161 DetectLocation::struct_item(&s.name),
162 format!("Add Default implementation for '{}'", s.name),
163 )
164 .with_operations(vec![DetectOperation::Generate])
165 .with_confidence(0.8),
166 );
167 }
168 }
169 }
170
171 opportunities
172 }
173
174 fn category(&self) -> DetectCategory {
175 DetectCategory::Creational
176 }
177
178 fn detect_name(&self) -> &'static str {
179 "Default"
180 }
181
182 fn detect_description(&self) -> &str {
183 "Add Default implementation to structs"
184 }
185}
186
187#[derive(Debug, Clone, Default)]
208pub struct DeriveDefaultMutation {
209 pub target_type: Option<String>,
211}
212
213impl DeriveDefaultMutation {
214 pub fn new() -> Self {
215 Self::default()
216 }
217
218 pub fn for_type(mut self, type_name: impl Into<String>) -> Self {
220 self.target_type = Some(type_name.into());
221 self
222 }
223
224 fn is_default_impl(imp: &PureImpl) -> bool {
226 imp.trait_.as_deref() == Some("Default")
227 }
228
229 fn is_derivable_default(imp: &PureImpl) -> Option<String> {
231 let default_fn = imp.items.iter().find_map(|item| {
233 if let PureImplItem::Fn(f) = item {
234 if f.name == "default" {
235 return Some(f);
236 }
237 }
238 None
239 })?;
240
241 if default_fn.body.stmts.len() != 1 {
243 return None;
244 }
245
246 let expr = match &default_fn.body.stmts[0] {
247 PureStmt::Expr(e) => e,
248 _ => return None,
249 };
250
251 let fields = match expr {
253 PureExpr::Struct { path, fields } => {
254 if path != "Self" && path != &imp.self_ty {
255 return None;
256 }
257 fields
258 }
259 _ => return None,
260 };
261
262 for (_, value) in fields {
264 if !Self::is_default_value(value) {
265 return None;
266 }
267 }
268
269 Some(imp.self_ty.clone())
270 }
271
272 fn is_default_value(expr: &PureExpr) -> bool {
274 match expr {
275 PureExpr::Lit(lit) => Self::is_zero_literal(lit),
276 PureExpr::Path(p) => p == "None",
277 PureExpr::Call { func, args } => {
278 if args.is_empty() {
279 if let PureExpr::Path(p) = func.as_ref() {
280 return matches!(
281 p.as_str(),
282 "Default::default"
283 | "Vec::new"
284 | "String::new"
285 | "HashMap::new"
286 | "HashSet::new"
287 | "BTreeMap::new"
288 | "BTreeSet::new"
289 | "PathBuf::new"
290 | "OsString::new"
291 );
292 }
293 }
294 false
295 }
296 PureExpr::MethodCall { method, args, .. } => {
297 (method == "default" || method == "new") && args.is_empty()
298 }
299 _ => false,
300 }
301 }
302
303 fn is_zero_literal(lit: &str) -> bool {
305 matches!(
306 lit.trim(),
307 "0" | "0i8"
308 | "0i16"
309 | "0i32"
310 | "0i64"
311 | "0i128"
312 | "0isize"
313 | "0u8"
314 | "0u16"
315 | "0u32"
316 | "0u64"
317 | "0u128"
318 | "0usize"
319 | "0.0"
320 | "0.0f32"
321 | "0.0f64"
322 | "false"
323 | "'\\0'"
324 | "\"\""
325 )
326 }
327}
328
329impl Mutation for DeriveDefaultMutation {
330 fn describe(&self) -> String {
331 "Convert manual Default implementations to #[derive(Default)]".to_string()
332 }
333
334 fn mutation_type(&self) -> &'static str {
335 "DeriveDefault"
336 }
337
338 fn box_clone(&self) -> Box<dyn Mutation> {
339 Box::new(self.clone())
340 }
341}
342
343impl Detect for DeriveDefaultMutation {
344 fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity> {
345 let mut opportunities = Vec::new();
346
347 for item in &file.items {
348 if let PureItem::Impl(imp) = item {
349 if Self::is_default_impl(imp) {
350 if let Some(ref target) = self.target_type {
351 if &imp.self_ty != target {
352 continue;
353 }
354 }
355
356 if Self::is_derivable_default(imp).is_some() {
357 opportunities.push(
358 DetectOpportunity::new(
359 DetectLocation::impl_item(&imp.self_ty),
360 format!(
361 "Convert manual Default impl for '{}' to #[derive(Default)]",
362 imp.self_ty
363 ),
364 )
365 .with_operations(vec![DetectOperation::Refactor])
366 .with_confidence(0.95),
367 );
368 }
369 }
370 }
371 }
372
373 opportunities
374 }
375
376 fn category(&self) -> DetectCategory {
377 DetectCategory::Creational
378 }
379
380 fn detect_name(&self) -> &'static str {
381 "DeriveDefault"
382 }
383
384 fn detect_description(&self) -> &str {
385 "Convert manual Default implementations to #[derive(Default)]"
386 }
387}