use core::num::NonZeroU32;
use rust_llm_tidy_model::parse::ItemKind;
use std::fmt;
pub(crate) struct Change {
pub(crate) line: Option<NonZeroU32>,
pub(crate) kind: ChangeKind,
pub(crate) code: &'static str,
pub(crate) message: Box<str>,
pub(crate) name: Option<Box<str>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ChangeKind {
Item(ItemKind),
Fence,
Link,
Table,
ExternCrate,
}
impl ChangeKind {
pub(crate) fn as_str(self) -> &'static str {
match self {
ChangeKind::Item(kind) => kind.as_str(),
ChangeKind::Fence => "fence",
ChangeKind::Link => "link",
ChangeKind::Table => "table",
ChangeKind::ExternCrate => "extern crate",
}
}
}
impl fmt::Display for Change {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(line) = self.line {
write!(f, "{line}: ")?;
}
match &self.name {
Some(name) => write!(
f,
"success[{}]: {} ({} `{}`)",
self.code,
self.message,
self.kind.as_str(),
name
),
None => write!(
f,
"success[{}]: {} ({})",
self.code,
self.message,
self.kind.as_str()
),
}
}
}
pub(crate) fn fence_changes(anchors: &[rust_llm_tidy_fix::FixAnchor]) -> Vec<Change> {
anchors
.iter()
.map(|a| Change {
line: NonZeroU32::new(a.line),
code: "FIX",
message: format!("flip nested fence at line {}", a.line).into_boxed_str(),
kind: ChangeKind::Fence,
name: None,
})
.collect()
}
pub(crate) fn link_changes(pairs: &[(String, String)]) -> Vec<Change> {
pairs
.iter()
.map(|(before, after)| Change {
line: None,
code: "FIX",
message: format!("`{before}` -> `{after}`").into_boxed_str(),
kind: ChangeKind::Link,
name: None,
})
.collect()
}
pub(crate) fn reorder_changes(
parsed: &rust_llm_tidy_model::parse::ParseResult,
permutation: &rust_llm_tidy_reorder::reorder::Permutation,
) -> Vec<Change> {
let mut change_records = Vec::new();
for mv in rust_llm_tidy_reorder::compute_moves(&parsed.items, permutation) {
let item = &parsed.items[mv.from() - 1];
change_records.push(Change {
line: NonZeroU32::new(item.start_line() as u32),
code: "REORDER",
message: mv.message().into_boxed_str(),
kind: ChangeKind::Item(*mv.kind()),
name: mv.name().map(Box::from),
});
}
for (idx, item) in parsed.items.iter().enumerate() {
let moved = permutation
.member_order(idx)
.map(|order| order.iter().enumerate().any(|(pos, &m)| pos != m))
.unwrap_or(false);
if !moved {
continue;
}
change_records.push(Change {
line: NonZeroU32::new(item.start_line() as u32),
code: "REORDER",
message: format!("rearrange {} members to the profile order", item.kind())
.into_boxed_str(),
kind: ChangeKind::Item(*item.kind()),
name: item.name().map(Box::from),
});
}
change_records
}
pub(crate) fn table_changes() -> Change {
Change {
line: None,
code: "FIX",
message: "tables were aligned".into(),
kind: ChangeKind::Table,
name: None,
}
}
pub(crate) fn vis_changes(source: &str, output: &str) -> Vec<Change> {
if output == source {
return Vec::new();
}
let src_lines: Vec<&str> = source.lines().collect();
let out_lines: Vec<&str> = output.lines().collect();
let count = out_lines.len().max(src_lines.len());
let mut changes = Vec::new();
for i in 0..count {
if src_lines.get(i) == out_lines.get(i) {
continue;
}
let Some((kind, name)) = line_kind_name(out_lines.get(i).copied().unwrap_or("")) else {
continue;
};
let line = (i + 1) as u32;
changes.push(Change {
line: NonZeroU32::new(line),
code: "VIS",
message: format!("narrow visibility of `{name}` at line {line}").into_boxed_str(),
kind,
name: Some(name.into_boxed_str()),
});
}
changes
}
fn line_kind_name(line: &str) -> Option<(ChangeKind, String)> {
let trimmed = line.trim();
let rest = trimmed.split_once(char::is_whitespace)?.1.trim_start();
let toks: Vec<&str> = rest.split_whitespace().collect();
if toks.first() == Some(&"extern") && toks.get(1) == Some(&"crate") {
let name = clean_name(toks.get(2).copied().unwrap_or(""));
return Some((ChangeKind::ExternCrate, name));
}
let mut i = 0;
while i < toks.len() {
let w = toks[i];
if is_modifier(w) {
if w == "extern" && toks.get(i + 1).is_some_and(|t| t.starts_with('"')) {
i += 2;
} else {
i += 1;
}
continue;
}
if w == "const" {
match toks.get(i + 1).copied() {
Some(next) if is_modifier(next) || is_kind(next) => {
i += 1; continue;
}
_ => break, }
}
break;
}
let kind = kind_for(toks.get(i).copied()?)?;
i += 1;
let name_token = if kind == ItemKind::Static && toks.get(i) == Some(&"mut") {
toks.get(i + 1).copied().unwrap_or("")
} else {
toks.get(i).copied().unwrap_or("")
};
let name = clean_name(name_token);
Some((ChangeKind::Item(kind), name))
}
fn clean_name(token: &str) -> String {
token
.split([';', ':', '(', '<', '=', '{'])
.next()
.unwrap_or("")
.to_string()
}
fn is_kind(w: &str) -> bool {
kind_for(w).is_some()
}
fn is_modifier(w: &str) -> bool {
matches!(w, "async" | "unsafe" | "default" | "extern")
}
fn kind_for(w: &str) -> Option<ItemKind> {
match w {
"fn" => Some(ItemKind::Fn),
"struct" => Some(ItemKind::Struct),
"enum" => Some(ItemKind::Enum),
"union" => Some(ItemKind::Union),
"type" => Some(ItemKind::Type),
"const" => Some(ItemKind::Const),
"static" => Some(ItemKind::Static),
"mod" => Some(ItemKind::Mod),
"trait" => Some(ItemKind::Trait),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn change_plaintext_matches_lint_shape() {
let named = Change {
line: NonZeroU32::new(20),
code: "REORDER",
message: "rearrange fn a_main from pos 2 to pos 1 (before b_helper)".into(),
kind: ChangeKind::Item(ItemKind::Fn),
name: Some("a_main".into()),
};
assert_eq!(
named.to_string(),
"20: success[REORDER]: rearrange fn a_main from pos 2 to pos 1 (before b_helper) (fn `a_main`)"
);
}
#[test]
fn change_plaintext_omits_name_when_unnamed() {
let unnamed = Change {
line: NonZeroU32::new(3),
code: "FIX",
message: "flip nested fence at line 3".into(),
kind: ChangeKind::Fence,
name: None,
};
assert_eq!(
unnamed.to_string(),
"3: success[FIX]: flip nested fence at line 3 (fence)"
);
}
#[test]
fn change_plaintext_omits_line_when_zero() {
let table = table_changes();
assert_eq!(
table.to_string(),
"success[FIX]: tables were aligned (table)"
);
assert_eq!(table.line, None);
assert_eq!(table.kind, ChangeKind::Table);
assert_eq!(table.message.as_ref(), "tables were aligned");
}
#[test]
fn fence_changes_maps_one_record_per_anchor() {
let anchors = vec![rust_llm_tidy_fix::FixAnchor {
line: 7,
kind: rust_llm_tidy_fix::FixKind::Fence,
}];
let changes = fence_changes(&anchors);
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].line.unwrap().get(), 7);
assert_eq!(changes[0].code, "FIX");
assert_eq!(changes[0].kind, ChangeKind::Fence);
assert_eq!(changes[0].message.as_ref(), "flip nested fence at line 7");
}
#[test]
fn fence_changes_is_empty_without_anchors() {
assert!(fence_changes(&[]).is_empty());
}
#[test]
fn link_changes_maps_each_pair_to_a_record() {
let pairs = vec".to_string(), "[A]".to_string()),
("[B](v)".to_string(), "[B]".to_string()),
];
let changes = link_changes(&pairs);
assert_eq!(changes.len(), 2, "one record per pair");
assert_eq!(changes[0].line, None, "link records carry no line");
assert_eq!(changes[0].code, "FIX");
assert_eq!(changes[0].kind, ChangeKind::Link);
assert_eq!(changes[0].message.as_ref(), "`[A](u)` -> `[A]`");
assert_eq!(changes[1].message.as_ref(), "`[B](v)` -> `[B]`");
assert_eq!(
changes[1].to_string(),
"success[FIX]: `[B](v)` -> `[B]` (link)"
);
}
#[test]
fn link_changes_is_empty_without_pairs() {
assert!(link_changes(&[]).is_empty());
}
#[test]
fn vis_changes_anchors_each_narrowed_item() {
let source = "pub(crate) mod m {\n pub fn f() {}\n pub struct S;\n}\n";
let output = "pub(crate) mod m {\n pub(crate) fn f() {}\n pub(crate) struct S;\n}\n";
let changes = vis_changes(source, output);
assert_eq!(changes.len(), 2);
assert_eq!(changes[0].line.unwrap().get(), 2);
assert_eq!(
changes[0].message.as_ref(),
"narrow visibility of `f` at line 2"
);
assert_eq!(changes[0].kind, ChangeKind::Item(ItemKind::Fn));
assert_eq!(changes[1].line.unwrap().get(), 3);
assert_eq!(
changes[1].message.as_ref(),
"narrow visibility of `S` at line 3"
);
assert_eq!(changes[1].kind, ChangeKind::Item(ItemKind::Struct));
let tidy = "pub(crate) mod m {\n pub(crate) async fn f() {}\n}\n";
assert!(vis_changes(tidy, tidy).is_empty());
}
#[test]
fn vis_changes_is_empty_when_tidy() {
assert!(vis_changes("same", "same").is_empty());
}
#[test]
fn line_kind_name_handles_const_and_generics() {
let (kind, name) = line_kind_name(" pub(crate) const C: u32 = 0;").unwrap();
assert_eq!(kind, ChangeKind::Item(ItemKind::Const));
assert_eq!(name, "C");
let (kind, name) = line_kind_name(" pub(crate) fn f<T>() {}").unwrap();
assert_eq!(kind, ChangeKind::Item(ItemKind::Fn));
assert_eq!(name, "f");
assert!(line_kind_name(" let x = 1;").is_none());
assert!(line_kind_name(" use crate::m;").is_none());
}
#[test]
fn line_kind_name_skips_modifier_carrying_items() {
for (line, kind, name) in [
(" pub(crate) fn f() {}", "fn", "f"),
(" pub(crate) async fn f() {}", "fn", "f"),
(" pub(crate) unsafe fn g() {}", "fn", "g"),
(" pub(crate) unsafe trait T {}", "trait", "T"),
(" pub(crate) const fn h() {}", "fn", "h"),
(" pub(crate) const unsafe fn j() {}", "fn", "j"),
(" pub(crate) extern \"C\" fn k() {}", "fn", "k"),
(" pub(crate) extern fn l() {}", "fn", "l"),
(" pub(crate) extern crate foo;", "extern crate", "foo"),
(" pub(crate) const C: u32 = 0;", "const", "C"),
(" pub(crate) static X: i32 = 0;", "static", "X"),
(" pub(crate) static mut X: i32 = 0;", "static", "X"),
] {
let (got_kind, got_name) = line_kind_name(line).unwrap();
assert_eq!(
(got_kind.as_str(), got_name.as_str()),
(kind, name),
"for `{line}`"
);
}
}
#[test]
fn vis_changes_reports_modifier_carrying_items() {
let source = "pub(crate) mod m {\n pub async fn f() {}\n pub unsafe fn g() {}\n pub unsafe trait T {}\n pub const fn h() {}\n pub extern \"C\" fn k() {}\n pub static mut X: i32 = 0;\n}\n";
let output = "pub(crate) mod m {\n pub(crate) async fn f() {}\n pub(crate) unsafe fn g() {}\n pub(crate) unsafe trait T {}\n pub(crate) const fn h() {}\n pub(crate) extern \"C\" fn k() {}\n pub(crate) static mut X: i32 = 0;\n}\n";
let changes = vis_changes(source, output);
let expected = [
(2, "fn", "f"),
(3, "fn", "g"),
(4, "trait", "T"),
(5, "fn", "h"),
(6, "fn", "k"),
(7, "static", "X"),
];
assert_eq!(changes.len(), expected.len());
for (c, (line, kind, name)) in changes.iter().zip(expected) {
assert_eq!(c.line.unwrap().get(), line, "line for `{name}`");
assert_eq!(c.kind.as_str(), kind, "kind for `{name}`");
assert_eq!(c.name.as_deref(), Some(name), "name for `{name}`");
}
}
}