Skip to content

Commit 914dd85

Browse files
committed
cargo-rail: fixing the last real bug in unify before v1?!
1 parent 65d2cd8 commit 914dd85

File tree

3 files changed

+35
-34
lines changed

3 files changed

+35
-34
lines changed

src/cargo/manifest_analyzer.rs

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@ impl DepKey {
3333
}
3434
}
3535

36-
// Only compare by name, not renamed_from
36+
// Compare by both name AND renamed_from to distinguish:
37+
// - getrandom (direct dependency)
38+
// - old_getrandom (renamed from getrandom, i.e., package = "getrandom")
39+
// These are different dependencies that should NOT have their features merged.
3740
impl PartialEq for DepKey {
3841
fn eq(&self, other: &Self) -> bool {
39-
self.name == other.name
42+
self.name == other.name && self.renamed_from == other.renamed_from
4043
}
4144
}
4245

@@ -45,6 +48,7 @@ impl Eq for DepKey {}
4548
impl std::hash::Hash for DepKey {
4649
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
4750
self.name.hash(state);
51+
self.renamed_from.hash(state);
4852
}
4953
}
5054

@@ -56,7 +60,10 @@ impl PartialOrd for DepKey {
5660

5761
impl Ord for DepKey {
5862
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
59-
self.name.cmp(&other.name)
63+
match self.name.cmp(&other.name) {
64+
std::cmp::Ordering::Equal => self.renamed_from.cmp(&other.renamed_from),
65+
other => other,
66+
}
6067
}
6168
}
6269

@@ -164,31 +171,17 @@ impl ManifestAnalyzer {
164171
}
165172

166173
// Build usage index
167-
// When merging dependencies with same name, preserve renamed_from if any
174+
// DepKey now includes renamed_from in equality, so:
175+
// - `getrandom` (direct) and `old_getrandom` (package = "getrandom") are separate keys
176+
// - This prevents feature confusion between renamed and non-renamed deps
168177
let mut usage_index: HashMap<DepKey, Vec<DepUsage>> = HashMap::new();
169-
let mut renamed_info: HashMap<String, Option<String>> = HashMap::new();
170178

171179
for member in &parsed_members {
172180
for (dep_key, usage) in &member.dependencies {
173-
// Track if any usage of this dep is renamed
174-
if dep_key.renamed_from.is_some() {
175-
renamed_info.insert(dep_key.name.clone(), dep_key.renamed_from.clone());
176-
}
177181
usage_index.entry(dep_key.clone()).or_default().push(usage.clone());
178182
}
179183
}
180184

181-
// Update keys with renamed_from info if any usage was renamed
182-
let usage_index: HashMap<DepKey, Vec<DepUsage>> = usage_index
183-
.into_iter()
184-
.map(|(mut key, usages)| {
185-
if let Some(renamed_from) = renamed_info.get(&key.name) {
186-
key.renamed_from = renamed_from.clone();
187-
}
188-
(key, usages)
189-
})
190-
.collect();
191-
192185
// Pre-compute usage counts for O(1) lookup (20-30% speedup)
193186
let usage_counts: HashMap<DepKey, usize> = usage_index
194187
.iter()

src/cargo/unify_analyzer.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,8 +688,12 @@ impl UnifyAnalyzer {
688688
.cloned()
689689
.collect();
690690

691+
// For renamed dependencies (package = "..."), use the ALIAS name for manifest editing
692+
// e.g., for `old_getrandom = { package = "getrandom", ... }`, use "old_getrandom"
693+
let manifest_dep_name = dep_key.renamed_from.clone().unwrap_or_else(|| dep_key.name.clone());
694+
691695
let edit = MemberEdit::UseWorkspace {
692-
dep_name: dep_key.name.clone(),
696+
dep_name: manifest_dep_name,
693697
dep_kind: usage.kind,
694698
target: usage.target.clone(), // Preserve target for correct section
695699
local_features,

tests/integration/test_unify_comprehensive.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -242,14 +242,16 @@ root = "."
242242
}
243243

244244
// ============================================================================
245-
// SCENARIO 7: Renamed Dependencies (Hard Blocker)
245+
// SCENARIO 7: Renamed Dependencies (Treated Separately)
246246
// ============================================================================
247247

248248
#[test]
249249
fn test_unify_renamed_dependencies_hard_blocker() -> Result<()> {
250250
let workspace = TestWorkspace::new()?;
251251

252252
// Create crates with renamed dependency
253+
// With the bug fix, renamed deps (package = "...") are now properly separated
254+
// from direct deps of the same package. This prevents feature confusion.
253255
workspace.add_crate("crate-a", "0.1.0", &[("serde", r#""1.0""#)])?;
254256

255257
// Manually create crate-b with renamed serde
@@ -276,37 +278,39 @@ serde_crate = { package = "serde", version = "1.0" }
276278

277279
workspace.commit("Add crates with renamed dependency")?;
278280

279-
// Configure rail.toml (allow_renamed = false by default)
281+
// Configure rail.toml (include_renamed = false by default)
280282
std::fs::write(
281283
workspace.path.join("rail.toml"),
282284
r#"[workspace]
283285
root = "."
284286
285287
[unify]
286-
allow_renamed = false
288+
include_renamed = false
287289
"#,
288290
)?;
289291

290-
// Run analyze - should show Hard blocker
292+
// Run analyze - with the fix, renamed deps are now treated separately
293+
// Since each version of serde (direct vs renamed) only has 1 user,
294+
// neither qualifies for unification (needs 2+ users)
291295
let analyze_output = run_cargo_rail(&workspace.path, &["rail", "unify", "--check"])?;
292296
let analyze_stdout = String::from_utf8_lossy(&analyze_output.stdout);
293297

298+
// Should show no unification opportunities since each has only 1 user
294299
assert!(
295-
analyze_stdout.contains("BLOCKING") || analyze_stdout.contains("Renamed"),
296-
"Should show blocking issue for renamed dependency.\nOutput:\n{}",
300+
analyze_stdout.contains("No unification opportunities") || analyze_stdout.contains("No dependencies to unify"),
301+
"Should show no unification opportunities when deps are properly separated.\nOutput:\n{}",
297302
analyze_stdout
298303
);
299304

300-
// Run apply - should FAIL
305+
// Run apply - should succeed (no changes needed)
301306
let apply_output = run_cargo_rail(&workspace.path, &["rail", "unify"])?;
302307
let apply_stdout = String::from_utf8_lossy(&apply_output.stdout);
303-
let apply_stderr = String::from_utf8_lossy(&apply_output.stderr);
304308

309+
// Should indicate no changes
305310
assert!(
306-
!apply_output.status.success(),
307-
"Apply should fail with renamed dependency.\nstdout:\n{}\nstderr:\n{}",
308-
apply_stdout,
309-
apply_stderr
311+
apply_stdout.contains("No unification opportunities") || apply_stdout.contains("No dependencies to unify"),
312+
"Apply should indicate no changes needed.\nstdout:\n{}",
313+
apply_stdout
310314
);
311315

312316
// Check that members were NOT converted to workspace inheritance
@@ -322,7 +326,7 @@ allow_renamed = false
322326
// Should NOT have "serde = { workspace = true }"
323327
assert!(
324328
!crate_a_member.contains("serde = { workspace = true }") && !crate_a_member.contains("serde = {workspace=true}"),
325-
"crate-a should not use workspace = true for serde (apply failed, so no conversion).\ncrate-a Cargo.toml:\n{}",
329+
"crate-a should not use workspace = true for serde.\ncrate-a Cargo.toml:\n{}",
326330
crate_a_member
327331
);
328332

0 commit comments

Comments
 (0)