use crate::index::Index;
use crate::intern::{Interner, PathId};
use crate::{Change, Group, HashMap, ItemKind, Kind};
#[derive(Default)]
pub(crate) struct Outcome {
pub(crate) suggestion: Option<(PathId, Change)>,
pub(crate) candidates: Vec<PathId>,
}
pub(crate) fn suggest(
interner: &Interner,
removed: PathId,
head: &Index,
before: &Index,
first_seen: &HashMap<PathId, usize>,
first: usize,
) -> Outcome {
let kind = interner.kind(removed);
let Some((&name, parent)) = interner.segments(removed).split_last() else {
return Outcome::default();
};
let pool: Vec<PathId> = head
.paths
.iter()
.copied()
.filter(|&id| interner.kind(id) == kind && first_seen.get(&id).is_none_or(|&at| at < first))
.collect();
let split = |id: PathId| {
interner
.segments(id)
.split_last()
.expect("an interned path has segments")
};
let moved = |id: PathId| {
let (&other, other_parent) = split(id);
!kind.is_manifest_entry() && other == name && other_parent != parent
};
let near = |id: PathId| {
let (&other, other_parent) = split(id);
other_parent == parent && within_distance(interner.name(name), interner.name(other))
};
let signature = before.signatures.get(&removed);
let same_signature = |id: PathId| {
matches!(kind, ItemKind::Function | ItemKind::Method)
&& split(id).1 == parent
&& signature.is_some_and(|signature| head.signatures.get(&id) == Some(signature))
};
let tables = before.tables.get(&removed).copied().unwrap_or(0);
let same_table = |id: PathId| {
kind == ItemKind::Dependency
&& split(id).1 == parent
&& head
.tables
.get(&id)
.is_some_and(|other| other & tables != 0)
};
let rules: [(Change, &dyn Fn(PathId) -> bool); 4] = [
(Change::Moved, &moved),
(Change::Renamed, &near),
(Change::Replaced, &same_signature),
(Change::Replaced, &same_table),
];
let mut outcome = Outcome::default();
for (change, rule) in rules {
let matched: Vec<PathId> = pool.iter().copied().filter(|&id| rule(id)).collect();
if let [only] = matched[..] {
return Outcome {
suggestion: Some((only, change)),
candidates: Vec::new(),
};
}
if outcome.candidates.is_empty() {
outcome.candidates = matched;
}
}
outcome
}
fn within_distance(name: &str, other: &str) -> bool {
let a: Vec<char> = name.chars().collect();
let b: Vec<char> = other.chars().collect();
let limit = a.len().max(3) / 3;
if a.len().abs_diff(b.len()) > limit {
return false;
}
let mut before: Vec<usize> = Vec::new();
let mut previous: Vec<usize> = (0..=b.len()).collect();
for i in 1..=a.len() {
let mut current = vec![i; b.len() + 1];
for j in 1..=b.len() {
let cost = usize::from(a[i - 1] != b[j - 1]);
current[j] = (previous[j] + 1)
.min(current[j - 1] + 1)
.min(previous[j - 1] + cost);
if i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1] {
current[j] = current[j].min(before[j - 2] + 1);
}
}
before = std::mem::replace(&mut previous, current);
}
previous[b.len()] <= limit
}
fn rewrite(path: &str, suggested: &str, written: &str, kind: ItemKind) -> Option<String> {
let rust = !kind.is_manifest_entry();
let mut words = path.rsplit("::");
let mut count = 0;
for word in written.rsplit("::") {
let word = if rust {
word.strip_prefix("r#").unwrap_or(word)
} else {
word
};
if words.next() != Some(word) {
return None;
}
count += 1;
}
let implied = path.split("::").count() - count;
let unwritten = match implied {
0 => Some(suggested),
_ => path
.match_indices("::")
.nth(implied - 1)
.and_then(|(at, _)| suggested.strip_prefix(&path[..at + 2])),
};
let suffix = match unwritten {
Some(kept) => kept,
None if count == 1 => suggested.rsplit("::").next().unwrap_or(suggested),
None => suggested,
};
if suffix.split("::").eq(written.split("::").map(|word| {
if rust {
word.strip_prefix("r#").unwrap_or(word)
} else {
word
}
})) {
return None;
}
let mut new = String::with_capacity(
suffix.len()
+ if rust {
2 * suffix
.split("::")
.filter(|word| crate::text::KEYWORDS.contains(word))
.count()
} else {
0
},
);
for (at, word) in suffix.split("::").enumerate() {
if at > 0 {
new.push_str("::");
}
if rust && crate::text::KEYWORDS.contains(&word) {
new.push_str("r#");
}
new.push_str(word);
}
Some(new)
}
fn rewritable(kind: ItemKind, context: Kind) -> bool {
let item = !kind.is_manifest_entry() && kind != ItemKind::Serialized;
let dependency = kind == ItemKind::Dependency && !matches!(context, Kind::CodeBlock { .. });
(item || dependency) && context != Kind::Link
}
struct Rewrite {
group: usize,
mention: usize,
new: String,
}
pub(crate) fn replacements(groups: &mut [Group], mut read: impl FnMut(&str) -> Option<String>) {
let mut rewrites = Vec::new();
for (g, group) in groups.iter().enumerate() {
let Some(suggestion) = &group.suggestion else {
continue;
};
for (m, mention) in group.mentions.iter().enumerate() {
if rewritable(group.kind, mention.context.kind)
&& let Some(new) = rewrite(&group.path, &suggestion.path, &mention.text, group.kind)
{
rewrites.push(Rewrite {
group: g,
mention: m,
new,
});
}
}
}
let at = |rewrite: &Rewrite| &groups[rewrite.group].mentions[rewrite.mention];
rewrites.sort_by(|a, b| {
let (a, b) = (at(a), at(b));
(&a.file, a.line, b.column).cmp(&(&b.file, b.line, a.column))
});
let mut text: Option<(String, String)> = None;
let mut filled = Vec::new();
for line in rewrites.chunk_by(|a, b| {
let (a, b) = (at(a), at(b));
(&a.file, a.line) == (&b.file, b.line)
}) {
let first = at(&line[0]);
if text.as_ref().is_none_or(|(name, _)| *name != first.file) {
text = read(&first.file).map(|contents| (first.file.clone(), contents));
}
let Some(original) = text
.as_ref()
.and_then(|(_, contents)| contents.lines().nth(first.line - 1))
else {
continue;
};
let mut rewritten = original.to_owned();
let mut applied = Vec::new();
for rewrite in line {
let mention = at(rewrite);
let Some((start, _)) = original.char_indices().nth(mention.column - 1) else {
continue;
};
let stop = start + mention.text.len();
if original.get(start..stop) != Some(mention.text.as_str()) {
continue;
}
rewritten.replace_range(start..stop, &rewrite.new);
applied.push((rewrite.group, rewrite.mention));
}
filled.push((applied, rewritten));
}
for (applied, rewritten) in filled {
for (g, m) in applied {
groups[g].mentions[m].replacement = Some(rewritten.clone());
}
}
}