From 23e4c85c8f57e70b4b9a58e43cced9e29bac779d Mon Sep 17 00:00:00 2001 From: sanosuguru Date: Sat, 13 Jun 2026 22:03:16 +0900 Subject: [PATCH] csharp: omit version namespace segment when package is unambiguous MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the `pkg_has_multiple_versions` guard from the C generator (crates/c/src/lib.rs, `interface_identifier`) to the C# backend. Previously the C# generator unconditionally appended a version segment to the namespace of every versioned package (e.g. `my.dep.v0_1_0`). Now the segment is emitted only when the same package namespace+name appears at more than one version in the Resolve — i.e. when omitting it would cause a name collision. The guard logic is identical to the C backend's. The mangling format intentionally differs: C# emits `v{major}_{minor}_{patch}.` while C uses a trailing-underscore snake-case segment. Tests: - Unit tests on `interface_name` assert the namespace directly: the segment is dropped for a single-version package and kept when two versions coexist (verified to fail if the guard is reverted). - A `tests/codegen/single-version-package` fixture (one versioned import, single version) builds the drop path across all backends, alongside the existing `multiversion` fixture (two versions). Closes #1078 --- crates/csharp/src/world_generator.rs | 97 ++++++++++++++++++- .../wit/deps/dep/root.wit | 5 + .../single-version-package/wit/root.wit | 5 + 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 tests/codegen/single-version-package/wit/deps/dep/root.wit create mode 100644 tests/codegen/single-version-package/wit/root.wit diff --git a/crates/csharp/src/world_generator.rs b/crates/csharp/src/world_generator.rs index 685c69076..fb7d2450b 100644 --- a/crates/csharp/src/world_generator.rs +++ b/crates/csharp/src/world_generator.rs @@ -1049,8 +1049,18 @@ fn interface_name( ); if let Some(version) = &name.version { - let v = version.to_string().replace(['.', '-', '+'], "_"); - ns = format!("{}v{}.", ns, &v); + // Only include the version segment when the same package name exists at + // multiple versions in this Resolve; omit it when unambiguous. + // Mirrors the equivalent guard in the C generator (crates/c/src/lib.rs). + let pkg_has_multiple_versions = resolve.packages.iter().any(|(_, p)| { + p.name.namespace == name.namespace + && p.name.name == name.name + && p.name.version != name.version + }); + if pkg_has_multiple_versions { + let v = version.to_string().replace(['.', '-', '+'], "_"); + ns = format!("{}v{}.", ns, &v); + } } ns } @@ -1106,3 +1116,86 @@ fn by_resource<'a>( } by_resource } + +#[cfg(test)] +mod tests { + use super::*; + + /// Build a `Resolve` from inline WIT and return the generated C# namespace + /// for every imported interface in `world`. + fn imported_interface_namespaces(wit: &[(&str, &str)], world: &str) -> Vec { + let mut resolve = Resolve::default(); + for (path, contents) in wit { + resolve.push_str(path, contents).unwrap(); + } + let (world_id, _) = resolve + .worlds + .iter() + .find(|(_, w)| w.name == world) + .unwrap(); + let keys: Vec = resolve.worlds[world_id] + .imports + .keys() + .filter(|k| matches!(k, WorldKey::Interface(_))) + .cloned() + .collect(); + let mut csharp = CSharp::default(); + keys.iter() + .map(|k| interface_name(&mut csharp, &resolve, k, Direction::Import)) + .collect() + } + + #[test] + fn version_segment_omitted_when_package_is_unambiguous() { + let names = imported_interface_namespaces( + &[ + ( + "dep.wit", + "package my:dep@0.1.0;\ninterface a { x: func(); }", + ), + ( + "root.wit", + "package foo:bar;\nworld the-world { import my:dep/a@0.1.0; }", + ), + ], + "the-world", + ); + assert!(!names.is_empty()); + for n in &names { + assert!(n.contains("my.dep"), "expected package segment: {n}"); + assert!( + !n.contains("v0_1_0"), + "version segment should be omitted: {n}" + ); + } + } + + #[test] + fn version_segment_kept_when_multiple_versions_coexist() { + let names = imported_interface_namespaces( + &[ + ( + "v1.wit", + "package my:dep@0.1.0;\ninterface a { x: func(); }", + ), + ( + "v2.wit", + "package my:dep@0.2.0;\ninterface a { x: func(); }", + ), + ( + "root.wit", + "package foo:bar;\nworld the-world { import my:dep/a@0.1.0; import my:dep/a@0.2.0; }", + ), + ], + "the-world", + ); + assert!( + names.iter().any(|n| n.contains("v0_1_0")), + "expected a v0_1_0 segment: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("v0_2_0")), + "expected a v0_2_0 segment: {names:?}" + ); + } +} diff --git a/tests/codegen/single-version-package/wit/deps/dep/root.wit b/tests/codegen/single-version-package/wit/deps/dep/root.wit new file mode 100644 index 000000000..cdda0a38b --- /dev/null +++ b/tests/codegen/single-version-package/wit/deps/dep/root.wit @@ -0,0 +1,5 @@ +package my:dep@0.1.0; + +interface a { + x: func(); +} diff --git a/tests/codegen/single-version-package/wit/root.wit b/tests/codegen/single-version-package/wit/root.wit new file mode 100644 index 000000000..82df7fd45 --- /dev/null +++ b/tests/codegen/single-version-package/wit/root.wit @@ -0,0 +1,5 @@ +package foo:bar; + +world the-world { + import my:dep/a@0.1.0; +}