#![allow(dead_code)]
use crate::config::{self, TargetModule};
use crate::file_analyzer::{standalone_routing_name, FileAnalyzer, TargetRouting};
use crate::module_generator::{Module, RefVisitor};
use anyhow::Result;
use std::collections::HashSet;
use syn::visit::Visit;
struct Candidate {
name: String,
key: CandidateKey,
refs: HashSet<String>,
}
enum CandidateKey {
Type(String),
Standalone(usize),
}
pub(crate) fn seeded_assign(analyzer: &FileAnalyzer, routing: &mut TargetRouting) {
let rules = analyzer.target_rules();
if rules.is_empty() || routing.modules.is_empty() {
return;
}
let global = analyzer.seeded_assignment_enabled();
let attractors: Vec<usize> = rules
.iter()
.enumerate()
.filter(|(_, rule)| global || rule.pull_dependencies)
.map(|(idx, _)| idx)
.collect();
if attractors.is_empty() {
return;
}
let mut defined: Vec<HashSet<String>> = Vec::new();
let mut referenced: Vec<HashSet<String>> = Vec::new();
for module in &routing.modules {
defined.push(module.get_exported_types().into_iter().collect());
referenced.push(module_path_roots(module));
}
let mut candidates: Vec<Candidate> = Vec::new();
let mut type_names: Vec<&String> = analyzer.types.keys().collect();
type_names.sort();
for name in type_names {
if routing.routed_type_names.contains(name.as_str()) {
continue;
}
let Some(type_info) = analyzer.types.get(name) else {
continue;
};
let mut visitor = RefVisitor::default();
visitor.visit_item(&type_info.item);
for impl_item in &type_info.impls {
visitor.visit_item(impl_item);
}
for trait_impl in &type_info.trait_impls {
visitor.visit_item(&trait_impl.impl_item);
}
candidates.push(Candidate {
name: name.clone(),
key: CandidateKey::Type(name.clone()),
refs: visitor.path_roots,
});
}
for (idx, item) in analyzer.standalone_items.iter().enumerate() {
if routing.routed_standalone_indices.contains(&idx) {
continue;
}
let Some(name) = standalone_routing_name(item) else {
continue;
};
let mut visitor = RefVisitor::default();
visitor.visit_item(item);
candidates.push(Candidate {
name,
key: CandidateKey::Standalone(idx),
refs: visitor.path_roots,
});
}
loop {
let mut wave: Vec<(usize, usize)> = Vec::new(); for (pos, candidate) in candidates.iter().enumerate() {
let mut best: Option<(usize, usize)> = None; for &module_idx in &attractors {
let mut affinity = candidate.refs.intersection(&defined[module_idx]).count();
if referenced[module_idx].contains(&candidate.name) {
affinity += 1;
}
if affinity == 0 {
continue;
}
let better = match best {
None => true,
Some((best_affinity, _)) => affinity > best_affinity,
};
if better {
best = Some((affinity, module_idx));
}
}
if let Some((_, module_idx)) = best {
wave.push((pos, module_idx));
}
}
if wave.is_empty() {
break;
}
for &(pos, module_idx) in wave.iter().rev() {
let candidate = candidates.remove(pos);
defined[module_idx].insert(candidate.name.clone());
referenced[module_idx].extend(candidate.refs.iter().cloned());
match candidate.key {
CandidateKey::Type(name) => {
if let Some(type_info) = analyzer.types.get(&name) {
routing.modules[module_idx].types.push(type_info.clone());
}
routing.routed_type_names.insert(name);
}
CandidateKey::Standalone(idx) => {
if let Some(item) = analyzer.standalone_items.get(idx) {
routing.modules[module_idx]
.standalone_items
.push(item.clone());
routing.modules[module_idx]
.standalone_verbatim
.push(analyzer.standalone_verbatim_for(item));
}
routing.routed_standalone_indices.insert(idx);
}
}
}
}
}
fn module_path_roots(module: &Module) -> HashSet<String> {
let mut visitor = RefVisitor::default();
for type_info in &module.types {
visitor.visit_item(&type_info.item);
for impl_item in &type_info.impls {
visitor.visit_item(impl_item);
}
for trait_impl in &type_info.trait_impls {
visitor.visit_item(&trait_impl.impl_item);
}
}
for item in &module.standalone_items {
visitor.visit_item(item);
}
visitor.path_roots
}
pub fn routable_names(analyzer: &FileAnalyzer) -> HashSet<String> {
let mut names: HashSet<String> = analyzer.types.keys().cloned().collect();
for item in &analyzer.standalone_items {
if let Some(name) = standalone_routing_name(item) {
names.insert(name);
}
}
names
}
pub fn check_unmatched_patterns(available: &HashSet<String>, rules: &[TargetModule]) -> Result<()> {
let mut problems: Vec<String> = Vec::new();
for rule in rules {
for pattern in &rule.items {
if pattern.contains('*') || available.contains(pattern) {
continue;
}
let mut near: Vec<(usize, &String)> = available
.iter()
.map(|name| (levenshtein(pattern, name), name))
.filter(|(distance, _)| *distance <= 3)
.collect();
near.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(b.1)));
let hint = if near.is_empty() {
String::new()
} else {
let suggestions: Vec<&str> =
near.iter().take(3).map(|(_, name)| name.as_str()).collect();
format!(" — did you mean {}?", suggestions.join(", "))
};
problems.push(format!(
"rule `{}`: item `{}` does not name any known item{}",
rule.name, pattern, hint
));
}
}
if problems.is_empty() {
Ok(())
} else {
anyhow::bail!(
"target-modules spec references unknown items:\n {}\n\
(items inside a nested inline module need `parent = \"<mod path>\"` \
on the rule and --split-nested-mods)",
problems.join("\n ")
)
}
}
pub fn explain_named_module(module: &Module, rules: &[TargetModule]) -> Option<Vec<String>> {
let is_rule_module = rules.iter().any(|rule| {
module.name == rule.name
|| module
.name
.strip_prefix(&format!("{}_", rule.name))
.is_some_and(|suffix| {
!suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit())
})
});
if !is_rule_module {
return None;
}
let mut lines = Vec::new();
let mut explain = |name: &str| match config::route_item_detailed(name, rules) {
Some((idx, pattern)) => lines.push(format!("{} (rule {}: {})", name, idx + 1, pattern)),
None => lines.push(format!("{} (seeded)", name)),
};
for type_info in &module.types {
explain(&type_info.name);
}
for item in &module.standalone_items {
if let Some(name) = standalone_routing_name(item) {
explain(&name);
}
}
Some(lines)
}
fn levenshtein(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
let mut current: Vec<usize> = vec![0; b_chars.len() + 1];
for (i, &ca) in a_chars.iter().enumerate() {
current[0] = i + 1;
for (j, &cb) in b_chars.iter().enumerate() {
let substitution = previous[j] + usize::from(ca != cb);
current[j + 1] = substitution.min(previous[j + 1] + 1).min(current[j] + 1);
}
std::mem::swap(&mut previous, &mut current);
}
previous[b_chars.len()]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn levenshtein_basics() {
assert_eq!(levenshtein("", ""), 0);
assert_eq!(levenshtein("abc", "abc"), 0);
assert_eq!(levenshtein("abc", "abd"), 1);
assert_eq!(levenshtein("kitten", "sitting"), 3);
assert_eq!(levenshtein("", "abc"), 3);
}
#[test]
fn unmatched_exact_pattern_is_hard_error_with_suggestion() {
let mut available = HashSet::new();
available.insert("compute_md5".to_string());
available.insert("FsEntry".to_string());
let rules = vec![TargetModule {
name: "hash".to_string(),
items: vec!["compute_md6".to_string()],
..Default::default()
}];
let err = check_unmatched_patterns(&available, &rules)
.expect_err("unknown exact name must be a hard error")
.to_string();
assert!(err.contains("compute_md6"), "missing offender: {err}");
assert!(err.contains("compute_md5"), "missing suggestion: {err}");
}
#[test]
fn unmatched_glob_pattern_is_not_an_error() {
let mut available = HashSet::new();
available.insert("FsEntry".to_string());
let rules = vec![TargetModule {
name: "hash".to_string(),
items: vec!["*hash*".to_string()],
..Default::default()
}];
assert!(check_unmatched_patterns(&available, &rules).is_ok());
}
#[test]
fn matched_exact_pattern_passes() {
let mut available = HashSet::new();
available.insert("FsEntry".to_string());
let rules = vec![TargetModule {
name: "fs".to_string(),
items: vec!["FsEntry".to_string()],
..Default::default()
}];
assert!(check_unmatched_patterns(&available, &rules).is_ok());
}
}