11//! Issue detection for dependency unification
22
33use super :: path_handling:: are_all_identical_workspace_paths;
4- use super :: types:: { DependencyInstance , IssueType , UnificationIssue } ;
4+ use super :: types:: { DependencyInstance , IssueSeverity , IssueType , UnificationIssue } ;
55use crate :: cargo:: WorkspaceMetadata ;
66use crate :: error:: RailResult ;
77use 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+ }
0 commit comments