Skip to content

Commit ec5cb68

Browse files
committed
cargo-rail: pre v1 commit - it's CLOSE
1 parent 6c4325a commit ec5cb68

File tree

21 files changed

+1249
-290
lines changed

21 files changed

+1249
-290
lines changed

src/cargo/manifest.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ impl CargoTransform {
267267
&self,
268268
workspace_toml_path: &std::path::Path,
269269
unified_deps: &[crate::cargo::unify::UnifiedDep],
270+
add_comments: bool,
270271
) -> RailResult<()> {
271272
use toml_edit::{Array, InlineTable, table};
272273

@@ -322,6 +323,19 @@ impl CargoTransform {
322323

323324
// Insert the dependency
324325
workspace_deps.insert(&unified.name, toml_edit::Item::Value(dep_table.into()));
326+
327+
// Add comments if enabled and any exist
328+
if add_comments
329+
&& !unified.comments.is_empty()
330+
&& let Some(item) = workspace_deps.get_mut(&unified.name)
331+
{
332+
let comment_str = unified.comments.join(", ");
333+
item
334+
.as_value_mut()
335+
.unwrap()
336+
.decor_mut()
337+
.set_suffix(format!(" # {}", comment_str));
338+
}
325339
}
326340

327341
// Write back to file
@@ -614,10 +628,11 @@ members = ["crate-a", "crate-b"]
614628
dep_kinds: HashSet::new(),
615629
fragmentation_count: 2,
616630
path: None,
631+
comments: Vec::new(),
617632
}];
618633

619634
// Write workspace dependencies
620-
let result = transformer.write_workspace_dependencies(temp_file.path(), &unified_deps);
635+
let result = transformer.write_workspace_dependencies(temp_file.path(), &unified_deps, true);
621636
assert!(result.is_ok(), "Should successfully write workspace dependencies");
622637

623638
// Read back and verify
@@ -669,10 +684,11 @@ members = ["crate-a"]
669684
dep_kinds: HashSet::new(),
670685
fragmentation_count: 1,
671686
path: None,
687+
comments: Vec::new(),
672688
}];
673689

674690
transformer
675-
.write_workspace_dependencies(temp_file.path(), &unified_deps)
691+
.write_workspace_dependencies(temp_file.path(), &unified_deps, true)
676692
.unwrap();
677693

678694
let content = std::fs::read_to_string(temp_file.path()).unwrap();
@@ -706,10 +722,11 @@ members = ["crate-a"]
706722
dep_kinds: HashSet::new(),
707723
fragmentation_count: 1,
708724
path: None,
725+
comments: Vec::new(),
709726
}];
710727

711728
transformer
712-
.write_workspace_dependencies(temp_file.path(), &unified_deps)
729+
.write_workspace_dependencies(temp_file.path(), &unified_deps, true)
713730
.unwrap();
714731

715732
let content = std::fs::read_to_string(temp_file.path()).unwrap();

src/cargo/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ pub mod validate;
2020

2121
pub use manifest::{CargoTransform, TransformContext};
2222
pub use metadata::WorkspaceMetadata;
23-
pub use unify::{UnifiedDep, UnifyConfig, UnifyStrategy, WorkspaceUnifier};
23+
pub use unify::{IssueSeverity, UnifiedDep, UnifyConfig, UnifyReport, UnifyStrategy, WorkspaceUnifier};
2424
pub use validate::validate_targets;

src/cargo/unify/config.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ pub struct UnifyConfig {
2121
/// Dependencies to force-include in unification
2222
pub include: HashSet<String>,
2323

24-
/// Only unify normal dependencies (exclude dev/build)
25-
pub normal_only: bool,
24+
/// Auto-resolve version conflicts by picking the highest version (default: true)
25+
pub auto_resolve_version_conflicts: bool,
26+
27+
/// Add conflict comments to workspace.dependencies (default: true)
28+
pub add_conflict_comments: bool,
29+
30+
/// Generate a detailed unification report (default: true)
31+
pub generate_report: bool,
2632
}
2733

2834
/// Strategy for selecting which dependencies to unify
@@ -39,7 +45,9 @@ impl Default for UnifyConfig {
3945
allow_renamed: false,
4046
exclude: HashSet::new(),
4147
include: HashSet::new(),
42-
normal_only: false,
48+
auto_resolve_version_conflicts: true,
49+
add_conflict_comments: true,
50+
generate_report: true,
4351
}
4452
}
4553
}

src/cargo/unify/issue_detection.rs

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Issue detection for dependency unification
22
33
use super::path_handling::are_all_identical_workspace_paths;
4-
use super::types::{DependencyInstance, IssueType, UnificationIssue};
4+
use super::types::{DependencyInstance, IssueSeverity, IssueType, UnificationIssue};
55
use crate::cargo::WorkspaceMetadata;
66
use crate::error::RailResult;
77
use std::collections::HashMap;
@@ -50,9 +50,10 @@ pub fn detect_multi_version_conflicts(metadata: &WorkspaceMetadata) -> RailResul
5050
if versions.len() > 1 {
5151
issues.push(UnificationIssue {
5252
dep_name: pkg_name.clone(),
53-
issue_type: IssueType::MultipleVersions {
53+
issue_type: IssueType::MultipleResolvedVersions {
5454
versions: versions.clone(),
5555
},
56+
severity: IssueSeverity::Soft, // Can be resolved via workspace.dependencies
5657
affected_members: vec![],
5758
suggestion: format!(
5859
"Multiple versions of '{}' found: {}. Standardize to a single version across the workspace.",
@@ -86,6 +87,7 @@ pub fn detect_issues(
8687
original: dep_name.to_string(),
8788
renames,
8889
},
90+
severity: IssueSeverity::Hard, // Renames block unification
8991
affected_members: instances.iter().map(|i| i.member.clone()).collect(),
9092
suggestion: "Standardize dependency name across all workspace members".to_string(),
9193
});
@@ -97,12 +99,13 @@ pub fn detect_issues(
9799
if versions.len() > 1 {
98100
return Some(UnificationIssue {
99101
dep_name: dep_name.to_string(),
100-
issue_type: IssueType::VersionConflict {
102+
issue_type: IssueType::IncompatibleVersionRequirements {
101103
requirements: instances
102104
.iter()
103105
.map(|i| (i.member.clone(), i.version_req.to_string()))
104106
.collect(),
105107
},
108+
severity: IssueSeverity::Soft, // Can be resolved via workspace.dependencies
106109
affected_members: instances.iter().map(|i| i.member.clone()).collect(),
107110
suggestion: "Standardize version requirement across all workspace members".to_string(),
108111
});
@@ -124,6 +127,7 @@ pub fn detect_issues(
124127
return Some(UnificationIssue {
125128
dep_name: dep_name.to_string(),
126129
issue_type: IssueType::PathDependency { paths },
130+
severity: IssueSeverity::Hard, // Incompatible paths block unification
127131
affected_members: instances.iter().map(|i| i.member.clone()).collect(),
128132
suggestion:
129133
"Convert external path dependencies to version dependencies, or ensure all workspace members use the same path"
@@ -139,6 +143,7 @@ pub fn detect_issues(
139143
issue_type: IssueType::AllTargetSpecific {
140144
targets: targets.clone(),
141145
},
146+
severity: IssueSeverity::Info, // Just informational, not a blocker
142147
affected_members: instances.iter().map(|i| i.member.clone()).collect(),
143148
suggestion: "Target-specific dependencies cannot be unified at workspace level".to_string(),
144149
});
@@ -163,10 +168,138 @@ pub fn detect_issues(
163168
members_with_default: with_default.clone(),
164169
members_without_default: without_default.clone(),
165170
},
171+
severity: IssueSeverity::Soft, // Can be resolved via workspace.dependencies
166172
affected_members: instances.iter().map(|i| i.member.clone()).collect(),
167173
suggestion: "Standardize default-features setting. Consider setting default-features = false and explicitly enabling needed features".to_string(),
168174
});
169175
}
170176

171177
None
172178
}
179+
180+
#[cfg(test)]
181+
mod tests {
182+
use super::*;
183+
use cargo_metadata::{DependencyKind, camino::Utf8PathBuf};
184+
use semver::VersionReq;
185+
186+
fn create_test_metadata() -> WorkspaceMetadata {
187+
let current_dir = std::env::current_dir().unwrap();
188+
WorkspaceMetadata::load(&current_dir).unwrap()
189+
}
190+
191+
fn create_instance(member: &str, version: &str) -> DependencyInstance {
192+
DependencyInstance {
193+
member: member.to_string(),
194+
name: "test-dep".to_string(),
195+
version_req: VersionReq::parse(version).unwrap(),
196+
features: vec![],
197+
default_features: true,
198+
optional: false,
199+
kind: DependencyKind::Normal,
200+
target: None,
201+
rename: None,
202+
path: None,
203+
}
204+
}
205+
206+
#[test]
207+
fn test_detect_renamed_dependency_hard() {
208+
let metadata = create_test_metadata();
209+
let mut instance1 = create_instance("member1", "1.0.0");
210+
instance1.rename = Some("renamed-dep".to_string());
211+
212+
let instance2 = create_instance("member2", "1.0.0");
213+
214+
let instances = vec![instance1, instance2];
215+
216+
let issue = detect_issues("test-dep", &instances, false, &metadata).unwrap();
217+
218+
assert_eq!(issue.severity, IssueSeverity::Hard);
219+
if let IssueType::Renamed { .. } = issue.issue_type {
220+
// OK
221+
} else {
222+
panic!("Expected Renamed issue type");
223+
}
224+
}
225+
226+
#[test]
227+
fn test_detect_version_conflict_soft() {
228+
let metadata = create_test_metadata();
229+
let instance1 = create_instance("member1", "1.0.0");
230+
let instance2 = create_instance("member2", "2.0.0");
231+
232+
let instances = vec![instance1, instance2];
233+
234+
let issue = detect_issues("test-dep", &instances, false, &metadata).unwrap();
235+
236+
assert_eq!(issue.severity, IssueSeverity::Soft);
237+
if let IssueType::IncompatibleVersionRequirements { .. } = issue.issue_type {
238+
// OK
239+
} else {
240+
panic!("Expected IncompatibleVersionRequirements issue type");
241+
}
242+
}
243+
244+
#[test]
245+
fn test_detect_inconsistent_default_features_soft() {
246+
let metadata = create_test_metadata();
247+
let mut instance1 = create_instance("member1", "1.0.0");
248+
instance1.default_features = true;
249+
250+
let mut instance2 = create_instance("member2", "1.0.0");
251+
instance2.default_features = false;
252+
253+
let instances = vec![instance1, instance2];
254+
255+
let issue = detect_issues("test-dep", &instances, false, &metadata).unwrap();
256+
257+
assert_eq!(issue.severity, IssueSeverity::Soft);
258+
if let IssueType::InconsistentDefaultFeatures { .. } = issue.issue_type {
259+
// OK
260+
} else {
261+
panic!("Expected InconsistentDefaultFeatures issue type");
262+
}
263+
}
264+
265+
#[test]
266+
fn test_detect_path_dependency_hard() {
267+
let metadata = create_test_metadata();
268+
let mut instance1 = create_instance("member1", "1.0.0");
269+
instance1.path = Some(Utf8PathBuf::from("/some/external/path"));
270+
271+
let instance2 = create_instance("member2", "1.0.0");
272+
273+
let instances = vec![instance1, instance2];
274+
275+
let issue = detect_issues("test-dep", &instances, false, &metadata).unwrap();
276+
277+
assert_eq!(issue.severity, IssueSeverity::Hard);
278+
if let IssueType::PathDependency { .. } = issue.issue_type {
279+
// OK
280+
} else {
281+
panic!("Expected PathDependency issue type");
282+
}
283+
}
284+
285+
#[test]
286+
fn test_detect_target_specific_info() {
287+
let metadata = create_test_metadata();
288+
let mut instance1 = create_instance("member1", "1.0.0");
289+
instance1.target = Some("cfg(unix)".to_string());
290+
291+
let mut instance2 = create_instance("member2", "1.0.0");
292+
instance2.target = Some("cfg(windows)".to_string());
293+
294+
let instances = vec![instance1, instance2];
295+
296+
let issue = detect_issues("test-dep", &instances, false, &metadata).unwrap();
297+
298+
assert_eq!(issue.severity, IssueSeverity::Info);
299+
if let IssueType::AllTargetSpecific { .. } = issue.issue_type {
300+
// OK
301+
} else {
302+
panic!("Expected AllTargetSpecific issue type");
303+
}
304+
}
305+
}

src/cargo/unify/mod.rs

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ mod config;
4040
mod issue_detection;
4141
mod path_handling;
4242
mod plan;
43+
mod report;
4344
mod resolution;
4445
mod strategy;
4546
mod transitive;
@@ -49,7 +50,8 @@ mod version_merge;
4950

5051
// Re-export public types
5152
pub use config::{UnifyConfig, UnifyStrategy};
52-
pub use types::{MemberEdit, UnificationPlan, UnificationStats, UnifiedDep};
53+
pub use report::UnifyReport;
54+
pub use types::{IssueSeverity, MemberEdit, UnificationPlan, UnificationStats, UnifiedDep};
5355

5456
/// Workspace dependency unifier
5557
///
@@ -124,16 +126,67 @@ impl<'a> WorkspaceUnifier<'a> {
124126
}
125127

126128
// Detect issues (will naturally pass if versions were successfully merged)
129+
// Detect issues (will naturally pass if versions were successfully merged)
130+
let mut resolution_comment = None;
127131
if let Some(issue) =
128132
issue_detection::detect_issues(&dep_name, &instances, self.config.allow_renamed, self.metadata)
129133
{
130-
issues.push(issue);
131-
stats.issue_count += 1;
132-
continue;
134+
let mut resolved = false;
135+
136+
// Attempt auto-resolution if enabled
137+
if self.config.auto_resolve_version_conflicts {
138+
match &issue.issue_type {
139+
types::IssueType::IncompatibleVersionRequirements { .. } => {
140+
// Auto-resolve: Pick the "highest" version requirement
141+
// Heuristic: Sort by string representation and pick the last one
142+
// This is a simple heuristic but effective for standard caret requirements
143+
let mut versions: Vec<_> = instances.iter().map(|i| i.version_req.clone()).collect();
144+
versions.sort_by_key(|a| a.to_string());
145+
146+
if let Some(highest) = versions.last() {
147+
let highest_clone = highest.clone();
148+
// Apply to all instances
149+
for instance in &mut instances {
150+
instance.version_req = highest_clone.clone();
151+
}
152+
resolution_comment = Some(format!("Auto-resolved version conflict to {}", highest));
153+
resolved = true;
154+
}
155+
}
156+
types::IssueType::InconsistentDefaultFeatures { .. } => {
157+
// Auto-resolve: Enable default features (already done by unify_instances logic)
158+
// We just need to suppress the issue
159+
resolution_comment = Some("Auto-resolved inconsistent default-features (enabled)".to_string());
160+
resolved = true;
161+
}
162+
_ => {}
163+
}
164+
}
165+
166+
if !resolved {
167+
// If auto-resolution is disabled, version conflicts should block the apply
168+
let mut issue = issue;
169+
if !self.config.auto_resolve_version_conflicts
170+
&& matches!(
171+
issue.issue_type,
172+
types::IssueType::IncompatibleVersionRequirements { .. }
173+
)
174+
{
175+
issue.severity = types::IssueSeverity::Hard;
176+
}
177+
issues.push(issue);
178+
stats.issue_count += 1;
179+
continue;
180+
}
133181
}
134182

135183
// Compute unified dependency
136-
let unified = unifier::unify_instances(&dep_name, &instances, self.metadata)?;
184+
let mut unified = unifier::unify_instances(&dep_name, &instances, self.metadata)?;
185+
186+
// Add resolution comment if any
187+
if let Some(comment) = resolution_comment {
188+
unified.comments.push(comment);
189+
}
137190

138191
// Track fragmentation savings
139192
if unified.fragmentation_count > 1 {

0 commit comments

Comments
 (0)