Skip to main content

reinhardt_admin_cli/migrate_v2/rules/
component_props.rs

1//! Rule §6.2: migrate `#[derive(Default)] struct *Props` to
2//! `#[derive(bon::Builder)]` with `#[builder(default)]` on optional fields.
3//!
4//! Heuristics (matches the spec §6.2 "Mechanical" strategy):
5//!
6//! - Target: structs whose ident ends in `Props` and carry
7//!   `#[derive(Default)]`.
8//! - For each field whose type is `Option<...>`, attach
9//!   `#[builder(default)]` — these are always optional.
10//! - The first non-`Option` field stays required (no `#[builder(default)]`)
11//!   so `Card { item: x }` keeps working out of the box. Every other
12//!   non-`Option` field gets `#[builder(default)]`. The developer can
13//!   promote any of those back to required by deleting the attribute
14//!   during review.
15
16use syn::visit_mut::{self, VisitMut};
17
18use crate::migrate_v2::rewriter::FileRewriter;
19
20/// `component_props` rule entry.
21pub struct Rule;
22
23impl FileRewriter for Rule {
24	fn name(&self) -> &'static str {
25		"component_props"
26	}
27
28	fn rewrite(&self, mut file: syn::File) -> syn::File {
29		StructVisitor.visit_file_mut(&mut file);
30		file
31	}
32}
33
34struct StructVisitor;
35
36impl VisitMut for StructVisitor {
37	fn visit_item_struct_mut(&mut self, s: &mut syn::ItemStruct) {
38		let is_props = s.ident.to_string().ends_with("Props");
39		if !is_props {
40			visit_mut::visit_item_struct_mut(self, s);
41			return;
42		}
43		if !has_derive_default(&s.attrs) {
44			visit_mut::visit_item_struct_mut(self, s);
45			return;
46		}
47
48		replace_derive_default_with_bon_builder(&mut s.attrs);
49
50		if let syn::Fields::Named(fields) = &mut s.fields {
51			let mut seen_first_non_option = false;
52			for field in fields.named.iter_mut() {
53				let is_option = is_option_type(&field.ty);
54				let should_default = if is_option {
55					true
56				} else if !seen_first_non_option {
57					seen_first_non_option = true;
58					false
59				} else {
60					true
61				};
62				if should_default {
63					let has_builder_default = field.attrs.iter().any(|a| {
64						a.path().is_ident("builder")
65							&& matches!(&a.meta, syn::Meta::List(l) if l.tokens.to_string().contains("default"))
66					});
67					if !has_builder_default {
68						field.attrs.push(syn::parse_quote!(#[builder(default)]));
69					}
70				}
71			}
72		}
73
74		visit_mut::visit_item_struct_mut(self, s);
75	}
76}
77
78fn has_derive_default(attrs: &[syn::Attribute]) -> bool {
79	attrs.iter().any(|a| {
80		a.path().is_ident("derive") && {
81			let mut found = false;
82			let _ = a.parse_nested_meta(|m| {
83				if m.path.is_ident("Default") {
84					found = true;
85				}
86				Ok(())
87			});
88			found
89		}
90	})
91}
92
93fn replace_derive_default_with_bon_builder(attrs: &mut Vec<syn::Attribute>) {
94	// Collect all derives from all #[derive(...)] attributes first,
95	// so multiple derive attributes (e.g. #[derive(Clone)] #[derive(Default)])
96	// are merged into a single combined attribute.
97	let mut derives: Vec<syn::Path> = Vec::new();
98	for a in attrs.iter() {
99		if a.path().is_ident("derive") {
100			let _ = a.parse_nested_meta(|m| {
101				if !m.path.is_ident("Default") {
102					derives.push(m.path.clone());
103				}
104				Ok(())
105			});
106		}
107	}
108	if !derives.iter().any(|p| {
109		p.segments.len() == 2 && p.segments[0].ident == "bon" && p.segments[1].ident == "Builder"
110	}) {
111		derives.push(syn::parse_quote!(bon::Builder));
112	}
113
114	let mut new_attrs: Vec<syn::Attribute> = attrs
115		.iter()
116		.filter(|a| !a.path().is_ident("derive"))
117		.cloned()
118		.collect();
119
120	let insert_pos = attrs
121		.iter()
122		.position(|a| a.path().is_ident("derive"))
123		.unwrap_or(0)
124		.min(new_attrs.len());
125	new_attrs.insert(insert_pos, syn::parse_quote!(#[derive(#(#derives),*)]));
126
127	*attrs = new_attrs;
128}
129
130fn is_option_type(ty: &syn::Type) -> bool {
131	if let syn::Type::Path(p) = ty
132		&& let Some(seg) = p.path.segments.last()
133	{
134		return seg.ident == "Option";
135	}
136	false
137}