Skip to content

Commit 12cce1f

Browse files
committed
fix: preserve hyphens in qualified import names
Signed-off-by: James Sturtevant <jsturtevant@gmail.com>
1 parent 5911dc1 commit 12cce1f

1 file changed

Lines changed: 51 additions & 17 deletions

File tree

  • src/hyperlight_component_util/src

src/hyperlight_component_util/src/emit.rs

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,36 +64,36 @@ pub fn find_colliding_import_names(imports: &[ExternDecl]) -> HashSet<String> {
6464
/// `wasi:http/types` -> `WasiHttpTypes` / `wasi_http_types`.
6565
pub fn import_member_names(wn: &WitName, collisions: &HashSet<String>) -> (Ident, Ident) {
6666
if collisions.contains(wn.name) {
67-
// Preserve namespace component boundaries so `a:b-c/types` and
68-
// `a-b:c/types` do not both flatten to `a_b_c_types`.
67+
// Preserve namespace component and hyphen boundaries so `a:bc/types`
68+
// and `a:b-c/types` do not both flatten to `a_bc_types`.
6969
let getter_prefix = if wn.namespaces.is_empty() {
7070
wn.name.to_string()
7171
} else {
7272
wn.namespaces
7373
.iter()
74-
.map(|ns| ns.replace('-', ""))
74+
.map(|ns| namespace_component_to_getter_part(ns))
7575
.collect::<Vec<_>>()
7676
.join("_")
7777
};
7878
let mut qualified_getter = format!("{}-{}", getter_prefix, wn.name);
7979

8080
// Use the same boundary-preserving approach for type names.
8181
let type_prefix: String = if wn.namespaces.is_empty() {
82-
component_first_camel(wn.name)
82+
collapsed_kebab_to_camel(wn.name)
8383
} else {
8484
wn.namespaces
8585
.iter()
86-
.map(|ns| component_first_camel(ns))
86+
.map(|ns| namespace_component_to_type_part(ns))
8787
.collect()
8888
};
89-
let mut type_name = format!("{}{}", type_prefix, component_first_camel(wn.name));
89+
let mut type_name = format!("{}{}", type_prefix, collapsed_kebab_to_camel(wn.name));
9090

9191
if !wn._version.is_empty() {
9292
let v = version_to_kebab(&wn._version);
9393
qualified_getter.push_str("-v");
9494
qualified_getter.push_str(&v);
9595
type_name.push('V');
96-
type_name.push_str(&component_first_camel(&v));
96+
type_name.push_str(&collapsed_kebab_to_camel(&v));
9797
}
9898
(
9999
format_ident!("{}", type_name),
@@ -104,12 +104,28 @@ pub fn import_member_names(wn: &WitName, collisions: &HashSet<String>) -> (Ident
104104
}
105105
}
106106

107-
/// Capitalize only the first letter of a kebab component, removing hyphens
108-
/// without capitalizing subsequent sub-words. This preserves namespace-boundary
109-
/// information when concatenating multiple components into a type identifier:
110-
/// "b-c" → "Bc" and "a-b" → "Ab" remain distinguishable when prefixed,
111-
/// whereas full camel-casing gives "BC" and "AB" which collapse on concatenation.
112-
fn component_first_camel(s: &str) -> String {
107+
fn namespace_component_to_getter_part(s: &str) -> String {
108+
s.replace('-', "_dash_")
109+
}
110+
111+
fn namespace_component_to_type_part(s: &str) -> String {
112+
let mut result = String::new();
113+
let mut capitalize_next = true;
114+
for c in s.chars() {
115+
if c == '-' {
116+
result.push_str("Dash");
117+
capitalize_next = true;
118+
} else if capitalize_next {
119+
result.extend(c.to_uppercase());
120+
capitalize_next = false;
121+
} else {
122+
result.push(c);
123+
}
124+
}
125+
result
126+
}
127+
128+
fn collapsed_kebab_to_camel(s: &str) -> String {
113129
let mut result = String::new();
114130
let mut chars = s.chars();
115131
if let Some(first) = chars.next() {
@@ -1139,9 +1155,27 @@ mod tests {
11391155
let (ty1, getter1) = import_member_names(&wn1, &collisions);
11401156
let (ty2, getter2) = import_member_names(&wn2, &collisions);
11411157

1142-
assert_eq!(ty1.to_string(), "ABcTypes");
1143-
assert_eq!(getter1.to_string(), "r#a_bc_types");
1144-
assert_eq!(ty2.to_string(), "AbCTypes");
1145-
assert_eq!(getter2.to_string(), "r#ab_c_types");
1158+
assert_eq!(ty1.to_string(), "ABDashCTypes");
1159+
assert_eq!(getter1.to_string(), "r#a_b_dash_c_types");
1160+
assert_eq!(ty2.to_string(), "ADashBCTypes");
1161+
assert_eq!(getter2.to_string(), "r#a_dash_b_c_types");
1162+
}
1163+
1164+
#[test]
1165+
fn plain_and_hyphenated_namespace_components_produce_distinct_type_names() {
1166+
let collisions = find_colliding_import_names(&[
1167+
instance_decl("a:bc/types"),
1168+
instance_decl("a:b-c/types"),
1169+
]);
1170+
1171+
let wn_plain = split_wit_name("a:bc/types");
1172+
let wn_hyphenated = split_wit_name("a:b-c/types");
1173+
let (ty_plain, getter_plain) = import_member_names(&wn_plain, &collisions);
1174+
let (ty_hyphenated, getter_hyphenated) = import_member_names(&wn_hyphenated, &collisions);
1175+
1176+
assert_eq!(ty_plain.to_string(), "ABcTypes");
1177+
assert_eq!(getter_plain.to_string(), "r#a_bc_types");
1178+
assert_eq!(ty_hyphenated.to_string(), "ABDashCTypes");
1179+
assert_eq!(getter_hyphenated.to_string(), "r#a_b_dash_c_types");
11461180
}
11471181
}

0 commit comments

Comments
 (0)