1use crate::engine::{CompiledRule, RuleMatch};
25use crate::error::RulesError;
26use crate::model::Rule;
27
28fn site_rule(language: &str) -> CompiledRule {
30 let toml = format!(
31 "id = \"destructure-fold\"\nlanguage = \"{language}\"\n[rule]\npattern = \"let $N:identifier = $X?.$K:identifier ?? $D\"\n"
32 );
33 let rule = Rule::from_toml_str(&toml).expect("internal fold rule parses");
34 CompiledRule::compile(&rule).expect("internal fold rule compiles")
35}
36
37struct Site {
39 binding: String,
40 key: String,
41 default: String,
42 source: String,
43 start_byte: usize,
44 end_byte: usize,
45 start_row: usize,
46 end_row: usize,
47}
48
49impl Site {
50 fn from_match(m: &RuleMatch) -> Option<Self> {
51 Some(Self {
52 binding: m.bindings.get("N")?.text.clone(),
53 key: m.bindings.get("K")?.text.clone(),
54 default: m.bindings.get("D")?.text.clone(),
55 source: m.bindings.get("X")?.text.clone(),
56 start_byte: m.span.start_byte,
57 end_byte: m.span.end_byte,
58 start_row: m.span.start_row,
59 end_row: m.span.end_row,
60 })
61 }
62
63 fn field(&self) -> String {
64 if self.binding == self.key {
65 format!("{} = {}", self.key, self.default)
66 } else {
67 format!("{}: {} = {}", self.key, self.binding, self.default)
68 }
69 }
70}
71
72pub fn fold_destructure_defaults(source: &str, language: &str) -> Result<String, RulesError> {
77 let rule = site_rule(language);
78 let matches = rule.run(source)?;
79
80 let mut sites: Vec<Site> = matches.iter().filter_map(Site::from_match).collect();
81 sites.sort_by_key(|s| s.start_byte);
82
83 let mut groups: Vec<Vec<Site>> = Vec::new();
87 for site in sites {
88 match groups.last_mut() {
89 Some(group)
90 if group.last().is_some_and(|prev| {
91 prev.source == site.source && site.start_row == prev.end_row + 1
92 }) =>
93 {
94 group.push(site);
95 }
96 _ => groups.push(vec![site]),
97 }
98 }
99
100 let mut edits: Vec<(usize, usize, String)> = groups
103 .into_iter()
104 .filter(|group| group.len() >= 2)
105 .filter(|group| has_unique_keys(group))
106 .map(|group| {
107 let fields = group.iter().map(Site::field).collect::<Vec<_>>().join(", ");
108 let replacement = format!("let {{ {fields} }} = {} ?? {{}}", group[0].source);
109 (
110 group[0].start_byte,
111 group[group.len() - 1].end_byte,
112 replacement,
113 )
114 })
115 .collect();
116 edits.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
117
118 let mut out = source.to_string();
119 for (start, end, replacement) in edits {
120 out.replace_range(start..end, &replacement);
121 }
122 Ok(out)
123}
124
125fn has_unique_keys(group: &[Site]) -> bool {
126 let mut seen = std::collections::BTreeSet::new();
127 group.iter().all(|site| seen.insert(&site.key))
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn fold(src: &str) -> String {
135 fold_destructure_defaults(src, "harn").unwrap()
136 }
137
138 #[test]
139 fn folds_a_consecutive_run() {
140 let src =
141 "fn f() {\n let timeout = cfg?.timeout ?? 30\n let retries = cfg?.retries ?? 3\n}\n";
142 let out = fold(src);
143 assert_eq!(
144 out,
145 "fn f() {\n let { timeout = 30, retries = 3 } = cfg ?? {}\n}\n"
146 );
147 }
148
149 #[test]
150 fn leaves_a_single_site_untouched() {
151 let src = "fn f() {\n let timeout = cfg?.timeout ?? 30\n}\n";
153 assert_eq!(fold(src), src);
154 }
155
156 #[test]
157 fn does_not_merge_across_different_sources() {
158 let src = "fn f() {\n let a = x?.a ?? 1\n let b = y?.b ?? 2\n}\n";
159 assert_eq!(fold(src), src);
161 }
162
163 #[test]
164 fn does_not_merge_across_a_blank_line() {
165 let src = "fn f() {\n let a = x?.a ?? 1\n\n let b = x?.b ?? 2\n}\n";
166 assert_eq!(fold(src), src);
167 }
168
169 #[test]
170 fn folds_three_and_preserves_surrounding_code() {
171 let src = "fn f() {\n before()\n let a = s?.a ?? 1\n let b = s?.b ?? 2\n let c = s?.c ?? 3\n after()\n}\n";
172 let out = fold(src);
173 assert_eq!(
174 out,
175 "fn f() {\n before()\n let { a = 1, b = 2, c = 3 } = s ?? {}\n after()\n}\n"
176 );
177 }
178
179 #[test]
180 fn folds_aliased_sites() {
181 let src = "fn f() {\n let t = cfg?.timeout ?? 30\n let retries = cfg?.retries ?? 3\n let label = cfg?.name ?? \"anon\"\n}\n";
182 let out = fold(src);
183 assert_eq!(
184 out,
185 "fn f() {\n let { timeout: t = 30, retries = 3, name: label = \"anon\" } = cfg ?? {}\n}\n"
186 );
187 }
188
189 #[test]
190 fn folds_after_a_wrapped_previous_site() {
191 let src = "fn f() {\n let path = parse({name: \"x\"}, argv).ok?.path\n ?? \"\"\n let verbose = parse({name: \"x\"}, argv).ok?.verbose ?? false\n}\n";
192 let out = fold(src);
193 assert_eq!(
194 out,
195 "fn f() {\n let { path = \"\", verbose = false } = parse({name: \"x\"}, argv).ok ?? {}\n}\n"
196 );
197 }
198
199 #[test]
200 fn leaves_duplicate_property_keys_untouched() {
201 let src = "fn f() {\n let first = cfg?.value ?? 1\n let second = cfg?.value ?? 2\n}\n";
202 assert_eq!(fold(src), src);
203 }
204}