1use proc_macro2::Span;
28use serde::Serialize;
29use std::path::{Path, PathBuf};
30use syn::visit::Visit;
31
32pub mod apply;
33pub mod cargo_deps;
34pub mod detector;
35pub mod fleet;
36pub mod pipeline;
37pub mod returns;
38pub use apply::{apply_to_source, ApplyError};
39pub use cargo_deps::{inject_deps, CargoDepsError, DepSource, InjectOutcome};
40pub use detector::{detectors, Detector};
41pub use fleet::{
42 survey_fleet, survey_fleet_apply, survey_fleet_validate, CandidateValidation,
43 CrateSurveyEntry, FleetApplyEntry, FleetApplyOpts, FleetApplyReport, FleetSurveyReport,
44 FleetValidateReport, ValidateOutcome,
45};
46pub use pipeline::{
47 apply_all_to_source, survey_apply_validate, FileOutcome, PipelineError, PipelineOpts,
48 PipelineOutcome,
49};
50pub use returns::{
51 fleet_returns, first_party_frontier_2026_06, Decision, FleetReturnsReport, FleetVerdict,
52 FrontierEstimate, LiftCostModel, PatternEconomics, Readiness,
53};
54
55#[derive(Clone, Debug, Serialize)]
60pub struct RefactorCandidate {
61 pub file: PathBuf,
62 pub line: usize,
64 pub derive_crate: &'static str,
66 pub derive_trait: &'static str,
68 pub pattern: MatchedPattern,
70 pub target_type: String,
72 pub estimated_loc_saved: usize,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
78#[serde(rename_all = "kebab-case")]
79pub enum MatchedPattern {
80 GetterAll,
83 SetterAll,
86 WithBuilder,
89 IsVariant,
92 AsMutAll,
95 OwnedAll,
98 ReplaceAll,
101 TakeAll,
104 ResetAll,
107 SwapAll,
110 VariantCountConst,
113 AllVariantsConst,
116}
117
118impl MatchedPattern {
119 pub fn derive_crate(self) -> &'static str {
125 detector::detectors()
126 .iter()
127 .find(|d| d.pattern() == self)
128 .map(|d| d.derive_crate())
129 .expect("every MatchedPattern variant has a Detector in the registry")
130 }
131 pub fn derive_trait(self) -> &'static str {
134 detector::detectors()
135 .iter()
136 .find(|d| d.pattern() == self)
137 .map(|d| d.derive_trait())
138 .expect("every MatchedPattern variant has a Detector in the registry")
139 }
140}
141
142#[derive(Debug, thiserror::Error)]
143pub enum SurveyError {
144 #[error("io: {0}")]
145 Io(#[from] std::io::Error),
146 #[error("syn parse failed for {path}: {err}")]
147 Parse {
148 path: PathBuf,
149 err: syn::Error,
150 },
151}
152
153pub fn survey_file(path: &Path) -> Result<Vec<RefactorCandidate>, SurveyError> {
155 let src = std::fs::read_to_string(path)?;
156 let file: syn::File = syn::parse_file(&src).map_err(|err| SurveyError::Parse {
157 path: path.to_path_buf(),
158 err,
159 })?;
160 let mut v = SurveyVisitor::new(path.to_path_buf());
161 v.visit_file(&file);
162 Ok(v.candidates)
163}
164
165pub fn survey_tree(root: &Path) -> Result<Vec<RefactorCandidate>, SurveyError> {
168 let mut out = vec![];
169 visit_rs_files(root, &mut |p| {
170 match survey_file(p) {
171 Ok(mut cands) => out.append(&mut cands),
172 Err(SurveyError::Parse { .. }) => {
173 }
176 Err(other) => return Err(other),
177 }
178 Ok(())
179 })?;
180 Ok(out)
181}
182
183fn visit_rs_files(
184 root: &Path,
185 f: &mut dyn FnMut(&Path) -> Result<(), SurveyError>,
186) -> Result<(), SurveyError> {
187 for entry in std::fs::read_dir(root)? {
188 let entry = entry?;
189 let path = entry.path();
190 let name = entry.file_name();
191 let name = name.to_string_lossy();
192 if name.starts_with('.') || name == "target" {
193 continue;
194 }
195 let file_type = entry.file_type()?;
196 if file_type.is_dir() {
197 visit_rs_files(&path, f)?;
198 } else if path.extension().is_some_and(|e| e == "rs") {
199 f(&path)?;
200 }
201 }
202 Ok(())
203}
204
205struct SurveyVisitor {
210 file: PathBuf,
211 candidates: Vec<RefactorCandidate>,
212}
213
214impl SurveyVisitor {
215 fn new(file: PathBuf) -> Self {
216 Self { file, candidates: vec![] }
217 }
218}
219
220impl<'ast> Visit<'ast> for SurveyVisitor {
221 fn visit_item_impl(&mut self, i: &'ast syn::ItemImpl) {
222 if i.trait_.is_some() {
226 return;
227 }
228
229 let Some(target_type) = type_base_ident(&i.self_ty) else {
235 return; };
237 let line = 1;
241
242 let mut counts: std::collections::HashMap<MatchedPattern, usize> =
248 std::collections::HashMap::new();
249 for item in &i.items {
250 match item {
251 syn::ImplItem::Fn(f) => {
252 if let Some(pat) = classify_fn(f) {
253 *counts.entry(pat).or_default() += 1;
254 }
255 }
256 syn::ImplItem::Const(c) => {
257 for d in detector::detectors() {
258 if d.matches_assoc_const(c) {
259 *counts.entry(d.pattern()).or_default() += 1;
260 break; }
262 }
263 }
264 _ => {}
265 }
266 }
267
268 let det_for = |p: MatchedPattern| -> Option<&'static dyn detector::Detector> {
271 detector::detectors().iter().find(|d| d.pattern() == p).copied()
272 };
273 for (pat, count) in counts {
274 let min = det_for(pat).map(|d| d.min_count()).unwrap_or(2);
275 if count >= min {
276 self.candidates.push(RefactorCandidate {
277 file: self.file.clone(),
278 line,
279 derive_crate: pat.derive_crate(),
280 derive_trait: pat.derive_trait(),
281 pattern: pat,
282 target_type: target_type.clone(),
283 estimated_loc_saved: count * 5,
285 });
286 }
287 }
288 }
289}
290
291pub(crate) fn classify_fn(f: &syn::ImplItemFn) -> Option<MatchedPattern> {
304 detector::detectors()
305 .iter()
306 .find(|d| d.matches(f))
307 .map(|d| d.pattern())
308}
309
310pub(crate) fn type_to_string(t: &syn::Type) -> String {
311 use quote::ToTokens;
312 let _ = Span::call_site();
313 let mut buf = proc_macro2::TokenStream::new();
314 t.to_tokens(&mut buf);
315 buf.to_string()
316}
317
318pub(crate) fn type_base_ident(t: &syn::Type) -> Option<String> {
325 let syn::Type::Path(tp) = t else {
326 return None;
327 };
328 tp.path.segments.last().map(|seg| seg.ident.to_string())
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 fn tmp_file(name: &str, body: &str) -> PathBuf {
336 let tmp = std::env::temp_dir().join(format!(
337 "tatara-survey-{}-{}",
338 name,
339 std::process::id()
340 ));
341 std::fs::create_dir_all(&tmp).unwrap();
342 let path = tmp.join("lib.rs");
343 std::fs::write(&path, body).unwrap();
344 path
345 }
346
347 #[test]
348 fn detects_getter_all_pattern() {
349 let path = tmp_file(
350 "getter",
351 r#"
352pub struct Foo { pub a: i32, pub b: String }
353
354impl Foo {
355 pub fn a(&self) -> &i32 { &self.a }
356 pub fn b(&self) -> &String { &self.b }
357}
358"#,
359 );
360 let cands = survey_file(&path).unwrap();
361 assert_eq!(cands.len(), 1);
362 assert_eq!(cands[0].pattern, MatchedPattern::GetterAll);
363 assert_eq!(cands[0].derive_crate, "pleme-getter-derive");
364 assert_eq!(cands[0].target_type, "Foo");
365 }
366
367 #[test]
368 fn detects_setter_all_pattern() {
369 let path = tmp_file(
370 "setter",
371 r#"
372pub struct Foo { pub a: i32, pub b: String }
373
374impl Foo {
375 pub fn set_a(&mut self, v: i32) { self.a = v; }
376 pub fn set_b(&mut self, v: String) { self.b = v; }
377}
378"#,
379 );
380 let cands = survey_file(&path).unwrap();
381 assert!(cands.iter().any(|c| c.pattern == MatchedPattern::SetterAll));
382 }
383
384 #[test]
385 fn detects_with_builder_pattern() {
386 let path = tmp_file(
387 "builder",
388 r#"
389pub struct Foo { pub a: i32, pub b: String }
390
391impl Foo {
392 pub fn with_a(mut self, v: i32) -> Self { self.a = v; self }
393 pub fn with_b(mut self, v: String) -> Self { self.b = v; self }
394}
395"#,
396 );
397 let cands = survey_file(&path).unwrap();
398 assert!(cands.iter().any(|c| c.pattern == MatchedPattern::WithBuilder));
399 }
400
401 #[test]
402 fn detects_isvariant_pattern() {
403 let path = tmp_file(
404 "isvariant",
405 r#"
406pub enum State { A, B(i32), C { x: u8 } }
407
408impl State {
409 pub fn is_a(&self) -> bool { matches!(self, Self::A) }
410 pub fn is_b(&self) -> bool { matches!(self, Self::B(_)) }
411 pub fn is_c(&self) -> bool { matches!(self, Self::C { .. }) }
412}
413"#,
414 );
415 let cands = survey_file(&path).unwrap();
416 assert!(cands.iter().any(|c| c.pattern == MatchedPattern::IsVariant));
417 }
418
419 #[test]
420 fn skips_non_inherent_impls() {
421 let path = tmp_file(
422 "trait_impl",
423 r#"
424pub struct Foo;
425
426impl std::fmt::Display for Foo {
427 fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Ok(()) }
428}
429"#,
430 );
431 let cands = survey_file(&path).unwrap();
432 assert!(cands.is_empty(), "trait impls aren't farm-derive targets");
433 }
434
435 #[test]
436 fn generic_impl_target_type_strips_to_base_ident() {
437 let path = tmp_file(
443 "generic-target",
444 r#"
445pub struct Attested<T> { pub inner: T, pub sig: String }
446
447impl<T> Attested<T> {
448 pub fn inner(&self) -> &T { &self.inner }
449 pub fn sig(&self) -> &String { &self.sig }
450}
451"#,
452 );
453 let cands = survey_file(&path).unwrap();
454 assert!(!cands.is_empty(), "must surface candidates on generic struct");
455 for c in &cands {
456 assert_eq!(
457 c.target_type, "Attested",
458 "target_type must strip generics + module path"
459 );
460 }
461 let src = std::fs::read_to_string(&path).unwrap();
464 let out = apply_to_source(&src, &cands[0]).unwrap();
465 assert!(out.contains("#[derive(GetterAll)]"));
466 assert!(out.contains("pub struct Attested<T>"));
467 }
468
469 #[test]
470 fn skips_singleton_patterns() {
471 let path = tmp_file(
473 "singleton",
474 r#"
475pub struct Foo { pub a: i32 }
476
477impl Foo {
478 pub fn a(&self) -> &i32 { &self.a }
479}
480"#,
481 );
482 let cands = survey_file(&path).unwrap();
483 assert!(cands.is_empty(), "single match doesn't justify a derive");
484 }
485
486 #[test]
487 fn classifies_invalidating_setter_as_non_setter() {
488 let path = tmp_file(
492 "invalidating",
493 r#"
494pub struct R { pub bg: [f32; 4], pub last_seqno: u64 }
495
496impl R {
497 pub fn set_bg(&mut self, v: [f32; 4]) { self.bg = v; self.last_seqno = 0; }
498 pub fn set_fg(&mut self, v: [f32; 4]) { self.fg = v; self.last_seqno = 0; }
499}
500"#,
501 );
502 let cands = survey_file(&path).unwrap();
503 assert!(
506 !cands.iter().any(|c| c.pattern == MatchedPattern::SetterAll),
507 "invalidating-setters must not be confused with plain SetterAll"
508 );
509 }
510
511 #[test]
512 fn estimated_loc_saved_scales_with_field_count() {
513 let path = tmp_file(
514 "many",
515 r#"
516pub struct Foo { pub a: i32, pub b: i32, pub c: i32, pub d: i32 }
517
518impl Foo {
519 pub fn a(&self) -> &i32 { &self.a }
520 pub fn b(&self) -> &i32 { &self.b }
521 pub fn c(&self) -> &i32 { &self.c }
522 pub fn d(&self) -> &i32 { &self.d }
523}
524"#,
525 );
526 let cands = survey_file(&path).unwrap();
527 let getter = cands
528 .iter()
529 .find(|c| c.pattern == MatchedPattern::GetterAll)
530 .unwrap();
531 assert_eq!(getter.estimated_loc_saved, 20);
533 }
534}