Skip to content

Commit cfd7b77

Browse files
sano-sugurusanosuguru
andauthored
csharp: omit version namespace segment when package is unambiguous (#1635)
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 Co-authored-by: sanosuguru <sa.50.00.no.riku@gmail.com>
1 parent 08416e2 commit cfd7b77

3 files changed

Lines changed: 105 additions & 2 deletions

File tree

crates/csharp/src/world_generator.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,8 +1049,18 @@ fn interface_name(
10491049
);
10501050

10511051
if let Some(version) = &name.version {
1052-
let v = version.to_string().replace(['.', '-', '+'], "_");
1053-
ns = format!("{}v{}.", ns, &v);
1052+
// Only include the version segment when the same package name exists at
1053+
// multiple versions in this Resolve; omit it when unambiguous.
1054+
// Mirrors the equivalent guard in the C generator (crates/c/src/lib.rs).
1055+
let pkg_has_multiple_versions = resolve.packages.iter().any(|(_, p)| {
1056+
p.name.namespace == name.namespace
1057+
&& p.name.name == name.name
1058+
&& p.name.version != name.version
1059+
});
1060+
if pkg_has_multiple_versions {
1061+
let v = version.to_string().replace(['.', '-', '+'], "_");
1062+
ns = format!("{}v{}.", ns, &v);
1063+
}
10541064
}
10551065
ns
10561066
}
@@ -1106,3 +1116,86 @@ fn by_resource<'a>(
11061116
}
11071117
by_resource
11081118
}
1119+
1120+
#[cfg(test)]
1121+
mod tests {
1122+
use super::*;
1123+
1124+
/// Build a `Resolve` from inline WIT and return the generated C# namespace
1125+
/// for every imported interface in `world`.
1126+
fn imported_interface_namespaces(wit: &[(&str, &str)], world: &str) -> Vec<String> {
1127+
let mut resolve = Resolve::default();
1128+
for (path, contents) in wit {
1129+
resolve.push_str(path, contents).unwrap();
1130+
}
1131+
let (world_id, _) = resolve
1132+
.worlds
1133+
.iter()
1134+
.find(|(_, w)| w.name == world)
1135+
.unwrap();
1136+
let keys: Vec<WorldKey> = resolve.worlds[world_id]
1137+
.imports
1138+
.keys()
1139+
.filter(|k| matches!(k, WorldKey::Interface(_)))
1140+
.cloned()
1141+
.collect();
1142+
let mut csharp = CSharp::default();
1143+
keys.iter()
1144+
.map(|k| interface_name(&mut csharp, &resolve, k, Direction::Import))
1145+
.collect()
1146+
}
1147+
1148+
#[test]
1149+
fn version_segment_omitted_when_package_is_unambiguous() {
1150+
let names = imported_interface_namespaces(
1151+
&[
1152+
(
1153+
"dep.wit",
1154+
"package my:dep@0.1.0;\ninterface a { x: func(); }",
1155+
),
1156+
(
1157+
"root.wit",
1158+
"package foo:bar;\nworld the-world { import my:dep/a@0.1.0; }",
1159+
),
1160+
],
1161+
"the-world",
1162+
);
1163+
assert!(!names.is_empty());
1164+
for n in &names {
1165+
assert!(n.contains("my.dep"), "expected package segment: {n}");
1166+
assert!(
1167+
!n.contains("v0_1_0"),
1168+
"version segment should be omitted: {n}"
1169+
);
1170+
}
1171+
}
1172+
1173+
#[test]
1174+
fn version_segment_kept_when_multiple_versions_coexist() {
1175+
let names = imported_interface_namespaces(
1176+
&[
1177+
(
1178+
"v1.wit",
1179+
"package my:dep@0.1.0;\ninterface a { x: func(); }",
1180+
),
1181+
(
1182+
"v2.wit",
1183+
"package my:dep@0.2.0;\ninterface a { x: func(); }",
1184+
),
1185+
(
1186+
"root.wit",
1187+
"package foo:bar;\nworld the-world { import my:dep/a@0.1.0; import my:dep/a@0.2.0; }",
1188+
),
1189+
],
1190+
"the-world",
1191+
);
1192+
assert!(
1193+
names.iter().any(|n| n.contains("v0_1_0")),
1194+
"expected a v0_1_0 segment: {names:?}"
1195+
);
1196+
assert!(
1197+
names.iter().any(|n| n.contains("v0_2_0")),
1198+
"expected a v0_2_0 segment: {names:?}"
1199+
);
1200+
}
1201+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package my:dep@0.1.0;
2+
3+
interface a {
4+
x: func();
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package foo:bar;
2+
3+
world the-world {
4+
import my:dep/a@0.1.0;
5+
}

0 commit comments

Comments
 (0)