use crate::types::{Symbol, SymbolKind};
use std::collections::HashMap;
pub(super) struct RelocationMatch<'a> {
pub old: &'a Symbol,
pub new: &'a Symbol,
pub relocation_type: RelocationType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum RelocationType {
MovedToDeprecated,
PromotedFromDeprecated,
PromotedFromNext,
MovedToNext,
Relocated,
}
pub(super) fn detect_relocations<'a>(
removed: &[&'a Symbol],
added: &[&'a Symbol],
) -> (Vec<RelocationMatch<'a>>, Vec<usize>, Vec<usize>) {
if removed.is_empty() || added.is_empty() {
return (Vec::new(), Vec::new(), Vec::new());
}
let mut added_by_canonical: HashMap<(String, SymbolKind), Vec<(usize, &'a Symbol)>> =
HashMap::new();
for (ai, sym) in added.iter().enumerate() {
let canonical = canonical_path(&sym.qualified_name);
added_by_canonical
.entry((canonical, sym.kind))
.or_default()
.push((ai, sym));
}
let mut matches = Vec::new();
let mut skip_removed = Vec::new();
let mut skip_added = Vec::new();
for (ri, rsym) in removed.iter().enumerate() {
let canonical = canonical_path(&rsym.qualified_name);
let key = (canonical, rsym.kind);
if let Some(added_syms) = added_by_canonical.get_mut(&key) {
let best_idx = added_syms
.iter()
.position(|(_, asym)| asym.name == rsym.name)
.or({
if !added_syms.is_empty() {
Some(0)
} else {
None
}
});
if let Some(idx) = best_idx {
let (ai, asym) = added_syms.remove(idx);
let relocation_type =
classify_relocation(&rsym.qualified_name, &asym.qualified_name);
matches.push(RelocationMatch {
old: rsym,
new: asym,
relocation_type,
});
skip_removed.push(ri);
skip_added.push(ai);
}
}
}
(matches, skip_removed, skip_added)
}
fn canonical_path(qualified_name: &str) -> String {
qualified_name
.replace("/deprecated/", "/")
.replace("/next/", "/")
}
fn classify_relocation(old_qname: &str, new_qname: &str) -> RelocationType {
let old_deprecated = old_qname.contains("/deprecated/");
let new_deprecated = new_qname.contains("/deprecated/");
let old_next = old_qname.contains("/next/");
let new_next = new_qname.contains("/next/");
match (old_deprecated, new_deprecated, old_next, new_next) {
(false, true, _, _) => RelocationType::MovedToDeprecated,
(true, false, _, _) => RelocationType::PromotedFromDeprecated,
(_, _, true, false) => RelocationType::PromotedFromNext,
(_, _, false, true) => RelocationType::MovedToNext,
_ => RelocationType::Relocated,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_strips_deprecated() {
assert_eq!(
canonical_path("pkg/dist/esm/deprecated/components/Chip/Chip.Chip"),
"pkg/dist/esm/components/Chip/Chip.Chip"
);
}
#[test]
fn canonical_strips_next() {
assert_eq!(
canonical_path("pkg/dist/esm/next/components/Modal/Modal.Modal"),
"pkg/dist/esm/components/Modal/Modal.Modal"
);
}
#[test]
fn canonical_preserves_normal_path() {
let path = "pkg/dist/esm/components/Button/Button.Button";
assert_eq!(canonical_path(path), path);
}
#[test]
fn classify_moved_to_deprecated() {
assert_eq!(
classify_relocation(
"pkg/dist/esm/components/Chip/Chip.Chip",
"pkg/dist/esm/deprecated/components/Chip/Chip.Chip"
),
RelocationType::MovedToDeprecated
);
}
#[test]
fn classify_promoted_from_deprecated() {
assert_eq!(
classify_relocation(
"pkg/dist/esm/deprecated/components/Modal/Modal.Modal",
"pkg/dist/esm/components/Modal/Modal.Modal"
),
RelocationType::PromotedFromDeprecated
);
}
#[test]
fn classify_relocated() {
assert_eq!(
classify_relocation(
"pkg/dist/esm/components/Chip/Chip.Chip",
"pkg/dist/esm/components/Label/Chip.Chip"
),
RelocationType::Relocated
);
}
#[test]
fn classify_promoted_from_next() {
assert_eq!(
classify_relocation(
"pkg/dist/esm/next/components/Modal/ModalBody.ModalBody",
"pkg/dist/esm/components/Modal/ModalBody.ModalBody"
),
RelocationType::PromotedFromNext
);
}
#[test]
fn classify_moved_to_next() {
assert_eq!(
classify_relocation(
"pkg/dist/esm/components/Foo/Foo.Foo",
"pkg/dist/esm/next/components/Foo/Foo.Foo"
),
RelocationType::MovedToNext
);
}
}