From 5d379625aa6cf45ee2f9abc6f10fe4c28d3839e5 Mon Sep 17 00:00:00 2001 From: wilfreddenton Date: Sun, 5 Jul 2026 02:29:52 -0400 Subject: [PATCH 1/3] rust: targeted attribute injection via additional_type_attributes/additional_member_attributes --- crates/guest-rust/macro/src/lib.rs | 51 +++++++++ crates/guest-rust/src/lib.rs | 25 +++++ crates/rust/src/interface.rs | 171 +++++++++++++++++++++++++---- crates/rust/src/lib.rs | 103 +++++++++++++++++ crates/rust/tests/codegen.rs | 167 ++++++++++++++++++++++++++++ 5 files changed, 496 insertions(+), 21 deletions(-) diff --git a/crates/guest-rust/macro/src/lib.rs b/crates/guest-rust/macro/src/lib.rs index 78a1197ad..dd29e7812 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -127,6 +127,10 @@ impl Parse for Config { opts.additional_derive_ignore = list.into_iter().map(|i| i.value()).collect() } + Opt::AdditionalTypeAttributes(list) => opts.additional_type_attributes = list, + Opt::AdditionalMemberAttributes(list) => { + opts.additional_member_attributes = list + } Opt::With(with) => opts.with.extend(with), Opt::GenerateAll => { opts.generate_all = true; @@ -312,6 +316,8 @@ mod kw { syn::custom_keyword!(export_prefix); syn::custom_keyword!(additional_derives); syn::custom_keyword!(additional_derives_ignore); + syn::custom_keyword!(additional_type_attributes); + syn::custom_keyword!(additional_member_attributes); syn::custom_keyword!(with); syn::custom_keyword!(generate_all); syn::custom_keyword!(type_section_suffix); @@ -394,6 +400,8 @@ enum Opt { // Parse as paths so we can take the concrete types/macro names rather than raw strings AdditionalDerives(Vec), AdditionalDerivesIgnore(Vec), + AdditionalTypeAttributes(Vec<(String, String)>), + AdditionalMemberAttributes(Vec<(String, String)>), With(HashMap), GenerateAll, TypeSectionSuffix(syn::LitStr), @@ -522,6 +530,26 @@ impl Parse for Opt { syn::bracketed!(contents in input); let list = Punctuated::<_, Token![,]>::parse_terminated(&contents)?; Ok(Opt::AdditionalDerivesIgnore(list.iter().cloned().collect())) + } else if l.peek(kw::additional_type_attributes) { + input.parse::()?; + input.parse::()?; + let contents; + braced!(contents in input); + let fields: Punctuated<_, Token![,]> = + contents.parse_terminated(attr_map_field_parse, Token![,])?; + Ok(Opt::AdditionalTypeAttributes( + fields.into_iter().flatten().collect(), + )) + } else if l.peek(kw::additional_member_attributes) { + input.parse::()?; + input.parse::()?; + let contents; + braced!(contents in input); + let fields: Punctuated<_, Token![,]> = + contents.parse_terminated(attr_map_field_parse, Token![,])?; + Ok(Opt::AdditionalMemberAttributes( + fields.into_iter().flatten().collect(), + )) } else if l.peek(kw::with) { input.parse::()?; input.parse::()?; @@ -601,6 +629,29 @@ impl Parse for Opt { } } +// Parse one `"selector": ["#[attr]", ...]` entry into a (selector, attribute) pair +// per attribute. +fn attr_map_field_parse(input: ParseStream<'_>) -> Result> { + let selector = input.parse::()?; + input.parse::()?; + let contents; + let bracket = syn::bracketed!(contents in input); + let attrs = Punctuated::::parse_terminated(&contents)?; + // An empty list would otherwise flatten away silently and escape the + // unused-selector check in the generator. + if attrs.is_empty() { + return Err(Error::new( + bracket.span.join(), + "attribute list must not be empty", + )); + } + let selector = selector.value(); + Ok(attrs + .into_iter() + .map(|a| (selector.clone(), a.value())) + .collect()) +} + fn with_field_parse(input: ParseStream<'_>) -> Result<(String, WithOption)> { let interface = input.parse::()?.value(); input.parse::()?; diff --git a/crates/guest-rust/src/lib.rs b/crates/guest-rust/src/lib.rs index 34aaea0df..1de6b9166 100644 --- a/crates/guest-rust/src/lib.rs +++ b/crates/guest-rust/src/lib.rs @@ -682,6 +682,31 @@ extern crate std; /// // By default this set is empty. /// additional_derives: [PartialEq, Eq, Hash, Clone], /// +/// // Extra attributes to emit on specific generated types (records, variants, +/// // and enums), rather than on all types like `additional_derives`. A type is +/// // selected by its bare kebab name, its package (`my:pkg`), its owning +/// // interface (`my:pkg/types`), or its fully-qualified name; the qualified +/// // forms written as in `with`, carrying the `@version` when versioned. An +/// // injected `#[derive(...)]` folds into the generated derive (deduped); other +/// // attributes are emitted verbatim, on every form including the borrowed one +/// // under `Borrowing` (so owned-only derives fail there). See the CLI docs for +/// // the full grammar. +/// // +/// // By default this map is empty. +/// additional_type_attributes: { +/// "my-record": [r#"#[derive(serde::Serialize)]"#], // one type, by bare name +/// "my:pkg/types": [r#"#[derive(Clone)]"#], // every type in an interface +/// }, +/// +/// // Like `additional_type_attributes`, but for generated record fields and +/// // enum/variant cases, selected by `.member-name` (any type +/// // selector above) or a bare `member-name`. +/// // +/// // By default this map is empty. +/// additional_member_attributes: { +/// "my-record.my-field": [r#"#[serde(rename = "mf")]"#], +/// }, +/// /// // When generating bindings for interfaces that are not defined in the /// // same package as `world`, this option can be used to either generate /// // those bindings or point to already generated bindings. diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index 148249be0..230792cec 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -6,6 +6,7 @@ use crate::{ }; use anyhow::Result; use heck::*; +use indexmap::IndexSet; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; use std::mem; @@ -2086,6 +2087,119 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) result } + /// The owning interface and its package, when `id` is owned by an interface. + fn type_owner(&self, id: TypeId) -> Option<(InterfaceId, &PackageName)> { + let TypeOwner::Interface(iface_id) = self.resolve.types[id].owner else { + return None; + }; + let pkg_id = self.resolve.interfaces[iface_id].package?; + Some((iface_id, &self.resolve.packages[pkg_id].name)) + } + + /// Selector keys naming the type itself: its bare wit name and its + /// fully-qualified `ns:pkg/iface/type` name. Qualified keys are written exactly + /// as in `with`, so they carry the package `@version` when versioned. + fn type_identity_keys(&self, id: TypeId) -> Vec { + let id = dealias(self.resolve, id); + match self.resolve.types[id].name.as_deref() { + Some(name) => vec![name.to_string(), full_wit_type_name(self.resolve, id)], + None => Vec::new(), + } + } + + /// Every selector key that matches type `id`: its identity keys plus its + /// owning interface and package. + fn type_selector_keys(&self, id: TypeId) -> Vec { + let id = dealias(self.resolve, id); + let mut keys = self.type_identity_keys(id); + if let Some((iface_id, pkg)) = self.type_owner(id) { + keys.push(pkg.to_string()); + if let Some(iface) = self.resolve.id_of(iface_id) { + keys.push(iface); + } + } + keys + } + + /// The attributes from `entries` whose selector satisfies `matches`, deduped in + /// configured order, paired with the distinct selectors that matched. The + /// matched selectors are recorded so `finish` can report selectors that matched + /// nothing, exactly as the `with` option reports unused remappings. + fn matching_attrs( + entries: &[(String, String)], + matches: impl Fn(&str) -> bool, + ) -> (Vec, Vec) { + let mut attrs = IndexSet::new(); + let mut used = IndexSet::new(); + for (sel, attr) in entries.iter().filter(|(sel, _)| matches(sel)) { + attrs.insert(attr.clone()); + used.insert(sel.clone()); + } + (attrs.into_iter().collect(), used.into_iter().collect()) + } + + /// Attributes configured via `additional_type_attributes` for type `id`. + fn additional_type_attrs(&mut self, id: TypeId) -> Vec { + let keys = self.type_selector_keys(id); + let (attrs, used) = + Self::matching_attrs(&self.r#gen.opts.additional_type_attributes, |sel| { + keys.iter().any(|k| k == sel) + }); + self.r#gen.used_type_attr_selectors.extend(used); + attrs + } + + /// Attributes configured via `additional_member_attributes` for `member` (a + /// record field or enum/variant case) of type `id`, matched by `.` + /// for any type selector (bare, qualified, interface, or package) or a bare + /// ``. + fn additional_member_attrs(&mut self, id: TypeId, member: &str) -> Vec { + let type_keys = self.type_selector_keys(id); + let (attrs, used) = + Self::matching_attrs(&self.r#gen.opts.additional_member_attributes, |sel| { + // member names are dot-free, so the last `.` splits `.` + sel == member + || sel + .rsplit_once('.') + .is_some_and(|(ty, m)| m == member && type_keys.iter().any(|tk| tk == ty)) + }); + self.r#gen.used_member_attr_selectors.extend(used); + attrs + } + + /// Split injected attributes into derive paths and everything else. Derive + /// paths are merged into the generated `#[derive(...)]` so they dedup against + /// the built-in and `additional_derives` derives instead of colliding (E0119); + /// other attributes are emitted verbatim. + fn split_injected_derives(attrs: &[String]) -> (Vec, Vec) { + let mut derives = Vec::new(); + let mut others = Vec::new(); + for attr in attrs { + match attr + .trim() + .strip_prefix("#[derive(") + .and_then(|s| s.strip_suffix(")]")) + { + Some(inner) => derives.extend( + inner + .split(',') + .map(str::trim) + .filter(|p| !p.is_empty()) + .map(String::from), + ), + None => others.push(attr.clone()), + } + } + (derives, others) + } + + /// Emit each attribute on its own line, verbatim. + fn push_attrs(&mut self, attrs: &[String]) { + for attr in attrs { + uwriteln!(self.src, "{attr}"); + } + } + fn print_typedef_record(&mut self, id: TypeId, record: &Record, docs: &Docs) { let info = self.info(id); // We use a BTree set to make sure we don't have any duplicates and we have a stable order @@ -2096,6 +2210,14 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .iter() .cloned() .collect(); + // Computed once, then emitted on every ownership mode below. + let (injected_derives, injected_attrs) = + Self::split_injected_derives(&self.additional_type_attrs(id)); + let field_attrs: Vec> = record + .fields + .iter() + .map(|f| self.additional_member_attrs(id, &f.name)) + .collect(); for (name, mode) in self.modes_of(id) { self.rustdoc(docs); let mut derives = BTreeSet::new(); @@ -2113,16 +2235,19 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) } else if info.is_clone() { derives.insert("Clone".to_string()); } + derives.extend(injected_derives.iter().cloned()); if !derives.is_empty() { self.push_str("#[derive("); self.push_str(&derives.into_iter().collect::>().join(", ")); self.push_str(")]\n") } + self.push_attrs(&injected_attrs); self.push_str(&format!("pub struct {name}")); self.print_generics(mode.lifetime); self.push_str(" {\n"); - for field in record.fields.iter() { + for (field, attrs) in record.fields.iter().zip(&field_attrs) { self.rustdoc(&field.docs); + self.push_attrs(attrs); self.push_str("pub "); self.push_str(&to_rust_ident(&field.name)); self.push_str(": "); @@ -2182,10 +2307,12 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) { self.print_rust_enum( id, + // Raw wit case names; `print_rust_enum` upper-camel-cases them at the + // emit site so member selectors still match the wit (kebab) name. variant .cases .iter() - .map(|c| (c.name.to_upper_camel_case(), &c.docs, c.ty.as_ref())), + .map(|c| (c.name.clone(), &c.docs, c.ty.as_ref())), docs, ); } @@ -2207,6 +2334,13 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .iter() .cloned() .collect(); + let (injected_derives, injected_attrs) = + Self::split_injected_derives(&self.additional_type_attrs(id)); + let case_attrs: Vec> = cases + .clone() + .into_iter() + .map(|(case_name, _, _)| self.additional_member_attrs(id, &case_name)) + .collect(); for (name, mode) in self.modes_of(id) { self.rustdoc(docs); let mut derives = BTreeSet::new(); @@ -2223,17 +2357,20 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) } else if info.is_clone() { derives.insert("Clone".to_string()); } + derives.extend(injected_derives.iter().cloned()); if !derives.is_empty() { self.push_str("#[derive("); self.push_str(&derives.into_iter().collect::>().join(", ")); self.push_str(")]\n") } + self.push_attrs(&injected_attrs); self.push_str(&format!("pub enum {name}")); self.print_generics(mode.lifetime); self.push_str(" {\n"); - for (case_name, docs, payload) in cases.clone() { + for ((case_name, docs, payload), attrs) in cases.clone().into_iter().zip(&case_attrs) { self.rustdoc(docs); - self.push_str(&case_name); + self.push_attrs(attrs); + self.push_str(&case_name.to_upper_camel_case()); if let Some(ty) = payload { self.push_str("("); let mode = self.filter_mode(ty, mode); @@ -2250,7 +2387,7 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) cases .clone() .into_iter() - .map(|(name, _docs, ty)| (name, ty)), + .map(|(name, _docs, ty)| (name.to_upper_camel_case(), ty)), ); if info.error { @@ -2343,24 +2480,14 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) } } - fn print_typedef_enum( - &mut self, - id: TypeId, - name: &str, - enum_: &Enum, - docs: &Docs, - attrs: &[String], - case_attr: Box String>, - ) where - Self: Sized, - { + fn print_typedef_enum(&mut self, id: TypeId, name: &str, enum_: &Enum, docs: &Docs) { let info = self.info(id); let name = to_upper_camel_case(name); self.rustdoc(docs); - for attr in attrs { - self.push_str(&format!("{attr}\n")); - } + let (injected_derives, injected_attrs) = + Self::split_injected_derives(&self.additional_type_attrs(id)); + self.push_attrs(&injected_attrs); self.push_str("#[repr("); self.int_repr(enum_.tag()); self.push_str(")]\n"); @@ -2379,13 +2506,15 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .into_iter() .map(|s| s.to_string()), ); + derives.extend(injected_derives); self.push_str("#[derive("); self.push_str(&derives.into_iter().collect::>().join(", ")); self.push_str(")]\n"); self.push_str(&format!("pub enum {name} {{\n")); for case in enum_.cases.iter() { self.rustdoc(&case.docs); - self.push_str(&case_attr(case)); + let case_attrs = self.additional_member_attrs(id, &case.name); + self.push_attrs(&case_attrs); self.push_str(&case.name.to_upper_camel_case()); self.push_str(",\n"); } @@ -2967,7 +3096,7 @@ impl<'a> {camel}Borrow<'a>{{ } fn type_enum(&mut self, id: TypeId, name: &str, enum_: &Enum, docs: &Docs) { - self.print_typedef_enum(id, name, enum_, docs, &[], Box::new(|_| String::new())); + self.print_typedef_enum(id, name, enum_, docs); let name = to_upper_camel_case(name); let mut cases = String::new(); diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index e7f4a36ed..e4325604f 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -42,6 +42,10 @@ pub struct RustWasm { // Track which interfaces and types are generated. Remapped interfaces and types provided via `with` // are required to be used. generated_types: HashSet, + // Selectors from `additional_type_attributes` / `additional_member_attributes` that matched at + // least one generated type or member; used to reject selectors that matched nothing. + used_type_attr_selectors: HashSet, + used_member_attr_selectors: HashSet, world: Option, rt_module: IndexSet, @@ -139,6 +143,19 @@ fn parse_with(s: &str) -> Result<(String, WithOption), String> { Ok((k.to_string(), v)) } +// Split on the first `=` only, so the attribute may itself contain `=`. +#[cfg(feature = "clap")] +fn parse_attribute(s: &str) -> Result<(String, String), String> { + let (sel, attr) = s + .split_once('=') + .ok_or_else(|| format!("expected string of form `=`; got `{s}`"))?; + // an empty attribute would mark the selector used and escape the unused check + if attr.trim().is_empty() { + return Err(format!("attribute must not be empty; got `{s}`")); + } + Ok((sel.to_string(), attr.to_string())) +} + #[derive(Default, Debug, Clone)] #[cfg_attr(feature = "clap", derive(clap::Parser))] #[cfg_attr( @@ -234,6 +251,42 @@ pub struct Opts { #[cfg_attr(feature = "clap", arg(long, value_name = "NAME"))] pub additional_derive_ignore: Vec, + /// Extra attributes to emit on specific generated types (records, variants, + /// and enums) rather than on all types like `additional_derive_attributes`. + /// Each entry is a `(selector, attribute)` pair. The selector matches a type by + /// its bare kebab name (`challenge-data`), its package (`my:pkg`), its owning + /// interface (`my:pkg/types`), or its fully-qualified name + /// (`my:pkg/types/challenge-data`). The qualified forms are written exactly as in + /// the `with` option, so they carry the package `@version` when versioned + /// (`my:pkg/types@1.0.0/challenge-data`); package and interface selectors apply to + /// interface-owned types only (world-owned types are matched by name). An injected + /// `#[derive(...)]` is folded into the type's generated `#[derive(...)]`, so its + /// traits dedup against the built-in and `additional_derive_attributes` derives + /// rather than colliding; any other attribute is emitted verbatim, and multiple + /// matching entries all apply. Attributes land on every generated form of the + /// type, including the borrowed (`<'a>`) form produced under `ownership: + /// Borrowing`; there is no owned-only targeting, so an owned-only derive such as + /// `#[derive(Default)]` or `serde::Deserialize` fails to compile on the borrowed + /// form (as it would via `additional_derive_attributes`). Only records, variants, + /// and enums are covered; flags, resources, type aliases, and the built-in + /// option/result/list types are not. A package or interface selector silently + /// skips them (they do not count toward the selector being used), but a selector + /// that resolves to nothing covered is an error. For a derive on *all* types use + /// `additional_derive_attributes`. In a CLI, this flag can be specified multiple + /// times as `selector=attribute`. + #[cfg_attr(feature = "clap", arg(long, value_name = "SELECTOR=ATTR", value_parser = parse_attribute))] + pub additional_type_attributes: Vec<(String, String)>, + + /// Extra attributes to emit on specific generated fields (of records) and + /// enum/variant cases. Like `additional_type_attributes`, but the selector is + /// `.`, where `` is any type selector above (bare name, + /// package, interface, or fully-qualified, carrying `@version` when versioned), + /// or a bare `member-name` matching that member in any record/variant/enum, all + /// in kebab case. In a CLI, this flag can be specified multiple times as + /// `selector=attribute`. + #[cfg_attr(feature = "clap", arg(long, value_name = "SELECTOR=ATTR", value_parser = parse_attribute))] + pub additional_member_attributes: Vec<(String, String)>, + /// Remapping of wit import interface and type names to Rust module names /// and types. /// @@ -1534,6 +1587,31 @@ impl WorldGenerator for RustWasm { bail!("unused remappings provided via `with`: {unused_keys:?}"); } + // A selector that matched no generated type or member is almost always a + // typo or a stale path, so reject it like an unused `with` remapping. + let mut unused_selectors = self + .opts + .additional_type_attributes + .iter() + .map(|(sel, _)| sel) + .filter(|sel| !self.used_type_attr_selectors.contains(*sel)) + .chain( + self.opts + .additional_member_attributes + .iter() + .map(|(sel, _)| sel) + .filter(|sel| !self.used_member_attr_selectors.contains(*sel)), + ) + .collect::>(); + unused_selectors.sort(); + unused_selectors.dedup(); + if !unused_selectors.is_empty() { + bail!( + "unused selectors provided via `additional_type_attributes` / \ + `additional_member_attributes`: {unused_selectors:?}" + ); + } + // Error about unused async configuration to help catch configuration // errors. self.opts.async_.ensure_all_used()?; @@ -2007,3 +2085,28 @@ fn classify_constructor_return_type( classify(resolve, resource_id, result).expect("invalid constructor") } + +#[cfg(all(test, feature = "clap"))] +mod tests { + use super::parse_attribute; + + #[test] + fn parse_attribute_splits_on_first_equals() { + assert_eq!( + parse_attribute("point=#[derive(Clone)]").unwrap(), + ("point".to_string(), "#[derive(Clone)]".to_string()) + ); + // the attribute may itself contain `=` + assert_eq!( + parse_attribute(r#"point.x=#[serde(rename = "x")]"#).unwrap(), + ( + "point.x".to_string(), + r#"#[serde(rename = "x")]"#.to_string() + ) + ); + assert!(parse_attribute("no-equals-sign").is_err()); + // an empty attribute is rejected rather than silently escaping the check + assert!(parse_attribute("point=").is_err()); + assert!(parse_attribute("point= ").is_err()); + } +} diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index acc068a96..334fedec6 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -301,3 +301,170 @@ mod merge_structurally_equal_types { merge_structurally_equal_types: true }); } + +// Injects attributes onto selected types/fields/cases. Compiling plus the `#[test]` +// proves each attribute lands (on both owned and borrowed forms) and that a package +// selector does not leak into unsupported kinds like flags (see the `perms` note). +mod targeted_attributes { + wit_bindgen::generate!({ + inline: r#" + package test:attrs; + + interface iface { + record point { x: u32, y: u32 } + enum color { red, green, blue } + // `a-b` upper-camels to `AB`; a member selector must still match the raw + // wit name, not the lossy `AB`.to_kebab_case() == `ab`. + variant shape { circle(u32), square(point), a-b } + + // used as both return and param, so under `duplicate_if_necessary` it + // generates an owned `String` form and a borrowed `&str` form + record label { text: string } + + // two records sharing a member name, to exercise bare-member fan-out + record note-a { note: string } + record note-b { note: u32 } + + // an unsupported kind: attribute injection must skip it (boundary test) + flags perms { read, write } + + paint: func(c: color) -> color; + draw: func(s: shape) -> shape; + round: func(p: point) -> point; + make-label: func() -> label; + use-label: func(l: label) -> u32; + first: func() -> note-a; + second: func(x: note-b) -> u32; + set-perms: func(p: perms) -> perms; + } + + world test { + import iface; + } + "#, + generate_all, + ownership: Borrowing { duplicate_if_necessary: true }, + // wit-bindgen already derives Clone/Debug on records+variants and every + // standard trait but Hash on enums; injected derives fold in (see `label`). + additional_type_attributes: { + // cumulative list; the trailing `Hash` also comes from the package + // selector below and folds into one derive rather than colliding. + "point": [ + "#[derive(PartialEq, Eq)]", + "#[derive(PartialOrd, Ord)]", + "#[derive(Hash)]", + ], + "color": [r#"#[doc = "a primary color"]"#], // enum: a non-derive attribute + "shape": ["#[derive(PartialEq)]"], // variant + // `Clone` here overlaps the record's built-in `Clone`: it must fold into + // one derive, not collide (E0119). Must fit owned + borrowed forms. + "label": ["#[derive(Clone, PartialEq, Eq)]"], + // fully-qualified selector against an unversioned package (no `@version`) + "test:attrs/iface/note-a": ["#[derive(PartialEq, Eq)]"], + // Package selector => every record/variant/enum in the package. Doubles as + // the flags boundary probe: `perms` already derives `Hash`, so a leak here + // would be a conflicting-impl error. It compiles => flags are skipped. + "test:attrs": ["#[derive(Hash)]"], + }, + additional_member_attributes: { + "point.x": ["#[allow(dead_code)]"], + "red": [r#"#[doc = "the primary color"]"#], // enum case, bare selector + "shape.circle": [r#"#[doc = "a round shape"]"#], // variant case, qualified + // lossy-kebab variant case: matches only via the raw wit name (regression + // guard: an unmatched selector now trips the unused-selector check). + "shape.a-b": ["#[allow(dead_code)]"], + "label.text": ["#[allow(dead_code)]"], + // bare member selector: fans out to `note` on BOTH note-a and note-b + "note": ["#[allow(dead_code)]"], + }, + }); + + #[test] + fn injected_derives_are_usable() { + use test::attrs::iface::{Color, LabelParam, LabelResult, NoteA, Point, Shape}; + + // record got PartialEq/Eq + PartialOrd/Ord (list) + package Hash. + let a = Point { x: 1, y: 2 }; + assert_eq!(a, a.clone()); + assert!(Point { x: 1, y: 2 } < Point { x: 1, y: 3 }); + let mut set = std::collections::HashSet::new(); + set.insert(a); + assert!(set.contains(&Point { x: 1, y: 2 })); + + // enum got package Hash; PartialEq/Eq are defaults. + let mut colors = std::collections::HashSet::new(); + colors.insert(Color::Red); + assert!(colors.contains(&Color::Red)); + assert!(!colors.contains(&Color::Blue)); + + // variant got PartialEq (injected) + package Hash + default Clone. + assert_eq!(Shape::Circle(3), Shape::Circle(3).clone()); + assert_ne!(Shape::Circle(3), Shape::Circle(4)); + + // BOTH forms of the both-forms record got the injected PartialEq/Eq (+ package + // Hash): the owned `String` form and the borrowed `&str` form. + assert_eq!( + LabelResult { text: "hi".into() }, + LabelResult { text: "hi".into() } + ); + assert_eq!(LabelParam { text: "hi" }, LabelParam { text: "hi" }); + assert_ne!(LabelParam { text: "hi" }, LabelParam { text: "bye" }); + let mut labels = std::collections::HashSet::new(); + labels.insert(LabelResult { text: "hi".into() }); + assert!(labels.contains(&LabelResult { text: "hi".into() })); + + // note-a got PartialEq/Eq via a fully-qualified selector on an unversioned package. + assert_eq!(NoteA { note: "n".into() }, NoteA { note: "n".into() }); + } +} + +// Hierarchical selectors against a *versioned* package: each level (package, +// interface, fully-qualified) injects a different derive, and the qualified forms +// carry the `@version` as in `with`. The asserts prove each level resolves and that +// the type-specific selector does not bleed onto its sibling. +mod hierarchical_selectors { + wit_bindgen::generate!({ + inline: r#" + package test:hier@1.2.3; + interface types { + record alpha { x: u32 } + record beta { y: u32 } + get-alpha: func() -> alpha; + get-beta: func() -> beta; + } + world w { import types; } + "#, + generate_all, + additional_type_attributes: { + "test:hier@1.2.3": ["#[derive(Hash)]"], // package => alpha + beta + "test:hier/types@1.2.3": ["#[derive(PartialEq, Eq)]"], // interface => alpha + beta + "test:hier/types@1.2.3/alpha": ["#[derive(PartialOrd, Ord)]"], // one type => alpha only + }, + additional_member_attributes: { + "test:hier/types@1.2.3/alpha.x": ["#[allow(dead_code)]"], // qualified member selector + // interface-scoped member: `x` on every type in the interface (alpha has it). + // Matches via the interface selector key, same grammar as type selectors. + "test:hier/types@1.2.3.x": [r#"#[doc = "interface-scoped"]"#], + }, + }); + + #[test] + fn hierarchical_and_versioned_selectors_resolve() { + use test::hier::types::{Alpha, Beta}; + + // alpha: Hash (package) + PartialEq/Eq (interface) + PartialOrd/Ord (versioned qualified). + assert_eq!(Alpha { x: 1 }, Alpha { x: 1 }); + assert!(Alpha { x: 1 } < Alpha { x: 2 }); + let mut set = std::collections::HashSet::new(); + set.insert(Alpha { x: 1 }); + assert!(set.contains(&Alpha { x: 1 })); + + // beta: Hash (package) + PartialEq/Eq (interface), but NOT Ord; the qualified + // selector named `alpha` only, proving specificity. + assert_eq!(Beta { y: 7 }, Beta { y: 7 }); + assert_ne!(Beta { y: 7 }, Beta { y: 8 }); + let mut bset = std::collections::HashSet::new(); + bset.insert(Beta { y: 7 }); + assert!(bset.contains(&Beta { y: 7 })); + } +} From 3f4ae8fbdddefd7c8b2b313002654fc6d87d0d4a Mon Sep 17 00:00:00 2001 From: wilfreddenton Date: Tue, 28 Jul 2026 00:38:00 -0400 Subject: [PATCH 2/3] rust: address review feedback on targeted attribute injection --- crates/guest-rust/macro/src/lib.rs | 13 +-- crates/guest-rust/src/lib.rs | 15 ++-- crates/rust/src/interface.rs | 117 ++++++--------------------- crates/rust/src/lib.rs | 122 +++++++++++++++-------------- crates/rust/tests/codegen.rs | 83 ++++++-------------- 5 files changed, 122 insertions(+), 228 deletions(-) diff --git a/crates/guest-rust/macro/src/lib.rs b/crates/guest-rust/macro/src/lib.rs index dd29e7812..603222a77 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -629,16 +629,17 @@ impl Parse for Opt { } } -// Parse one `"selector": ["#[attr]", ...]` entry into a (selector, attribute) pair -// per attribute. +// Parse one `"selector": [#[attr] ...]` entry into a pair per attribute. fn attr_map_field_parse(input: ParseStream<'_>) -> Result> { let selector = input.parse::()?; input.parse::()?; let contents; let bracket = syn::bracketed!(contents in input); - let attrs = Punctuated::::parse_terminated(&contents)?; - // An empty list would otherwise flatten away silently and escape the - // unused-selector check in the generator. + let attrs = contents.call(syn::Attribute::parse_outer)?; + if !contents.is_empty() { + return Err(contents.error("expected an outer attribute, `#[...]`")); + } + // An empty list would flatten away and escape the unused-selector check. if attrs.is_empty() { return Err(Error::new( bracket.span.join(), @@ -648,7 +649,7 @@ fn attr_map_field_parse(input: ParseStream<'_>) -> Result> let selector = selector.value(); Ok(attrs .into_iter() - .map(|a| (selector.clone(), a.value())) + .map(|a| (selector.clone(), a.to_token_stream().to_string())) .collect()) } diff --git a/crates/guest-rust/src/lib.rs b/crates/guest-rust/src/lib.rs index 1de6b9166..5f11fb82d 100644 --- a/crates/guest-rust/src/lib.rs +++ b/crates/guest-rust/src/lib.rs @@ -684,18 +684,13 @@ extern crate std; /// /// // Extra attributes to emit on specific generated types (records, variants, /// // and enums), rather than on all types like `additional_derives`. A type is -/// // selected by its bare kebab name, its package (`my:pkg`), its owning -/// // interface (`my:pkg/types`), or its fully-qualified name; the qualified -/// // forms written as in `with`, carrying the `@version` when versioned. An -/// // injected `#[derive(...)]` folds into the generated derive (deduped); other -/// // attributes are emitted verbatim, on every form including the borrowed one -/// // under `Borrowing` (so owned-only derives fail there). See the CLI docs for -/// // the full grammar. +/// // selected by its bare name, its package, its interface, or its fully +/// // qualified name, the qualified forms written as in `with`. /// // /// // By default this map is empty. /// additional_type_attributes: { -/// "my-record": [r#"#[derive(serde::Serialize)]"#], // one type, by bare name -/// "my:pkg/types": [r#"#[derive(Clone)]"#], // every type in an interface +/// "my-record": [#[derive(serde::Serialize)]], // one type, by bare name +/// "my:pkg/types": [#[derive(Hash)]], // every type in an interface /// }, /// /// // Like `additional_type_attributes`, but for generated record fields and @@ -704,7 +699,7 @@ extern crate std; /// // /// // By default this map is empty. /// additional_member_attributes: { -/// "my-record.my-field": [r#"#[serde(rename = "mf")]"#], +/// "my-record.my-field": [#[serde(rename = "mf")]], /// }, /// /// // When generating bindings for interfaces that are not defined in the diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index 230792cec..e3a663a3a 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -2,7 +2,7 @@ use crate::bindgen::{FunctionBindgen, POINTER_SIZE_EXPRESSION}; use crate::{ ConstructorReturnType, FnSig, Identifier, InterfaceName, Ownership, RuntimeItem, RustFlagsRepr, RustWasm, TypeGeneration, classify_constructor_return_type, full_wit_type_name, int_repr, - to_rust_ident, to_upper_camel_case, wasm_type, + to_rust_ident, to_upper_camel_case, wasm_type, wit_type_selectors, }; use anyhow::Result; use heck::*; @@ -2087,44 +2087,8 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) result } - /// The owning interface and its package, when `id` is owned by an interface. - fn type_owner(&self, id: TypeId) -> Option<(InterfaceId, &PackageName)> { - let TypeOwner::Interface(iface_id) = self.resolve.types[id].owner else { - return None; - }; - let pkg_id = self.resolve.interfaces[iface_id].package?; - Some((iface_id, &self.resolve.packages[pkg_id].name)) - } - - /// Selector keys naming the type itself: its bare wit name and its - /// fully-qualified `ns:pkg/iface/type` name. Qualified keys are written exactly - /// as in `with`, so they carry the package `@version` when versioned. - fn type_identity_keys(&self, id: TypeId) -> Vec { - let id = dealias(self.resolve, id); - match self.resolve.types[id].name.as_deref() { - Some(name) => vec![name.to_string(), full_wit_type_name(self.resolve, id)], - None => Vec::new(), - } - } - - /// Every selector key that matches type `id`: its identity keys plus its - /// owning interface and package. - fn type_selector_keys(&self, id: TypeId) -> Vec { - let id = dealias(self.resolve, id); - let mut keys = self.type_identity_keys(id); - if let Some((iface_id, pkg)) = self.type_owner(id) { - keys.push(pkg.to_string()); - if let Some(iface) = self.resolve.id_of(iface_id) { - keys.push(iface); - } - } - keys - } - - /// The attributes from `entries` whose selector satisfies `matches`, deduped in - /// configured order, paired with the distinct selectors that matched. The - /// matched selectors are recorded so `finish` can report selectors that matched - /// nothing, exactly as the `with` option reports unused remappings. + /// Matching attributes, deduped in configured order, plus the selectors that + /// matched, which `finish` uses to report the ones that did not. fn matching_attrs( entries: &[(String, String)], matches: impl Fn(&str) -> bool, @@ -2138,62 +2102,30 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) (attrs.into_iter().collect(), used.into_iter().collect()) } - /// Attributes configured via `additional_type_attributes` for type `id`. - fn additional_type_attrs(&mut self, id: TypeId) -> Vec { - let keys = self.type_selector_keys(id); + fn additional_type_attrs(&mut self, selectors: &[String]) -> Vec { let (attrs, used) = Self::matching_attrs(&self.r#gen.opts.additional_type_attributes, |sel| { - keys.iter().any(|k| k == sel) + selectors.iter().any(|name| name == sel) }); self.r#gen.used_type_attr_selectors.extend(used); attrs } - /// Attributes configured via `additional_member_attributes` for `member` (a - /// record field or enum/variant case) of type `id`, matched by `.` - /// for any type selector (bare, qualified, interface, or package) or a bare - /// ``. - fn additional_member_attrs(&mut self, id: TypeId, member: &str) -> Vec { - let type_keys = self.type_selector_keys(id); + /// `member` is a record field or an enum/variant case, selected by a bare + /// name or `.`. Member names contain no `.`, so the + /// last one separates the two halves. + fn additional_member_attrs(&mut self, selectors: &[String], member: &str) -> Vec { let (attrs, used) = Self::matching_attrs(&self.r#gen.opts.additional_member_attributes, |sel| { - // member names are dot-free, so the last `.` splits `.` sel == member - || sel - .rsplit_once('.') - .is_some_and(|(ty, m)| m == member && type_keys.iter().any(|tk| tk == ty)) + || sel.rsplit_once('.').is_some_and(|(ty, m)| { + m == member && selectors.iter().any(|name| name == ty) + }) }); self.r#gen.used_member_attr_selectors.extend(used); attrs } - /// Split injected attributes into derive paths and everything else. Derive - /// paths are merged into the generated `#[derive(...)]` so they dedup against - /// the built-in and `additional_derives` derives instead of colliding (E0119); - /// other attributes are emitted verbatim. - fn split_injected_derives(attrs: &[String]) -> (Vec, Vec) { - let mut derives = Vec::new(); - let mut others = Vec::new(); - for attr in attrs { - match attr - .trim() - .strip_prefix("#[derive(") - .and_then(|s| s.strip_suffix(")]")) - { - Some(inner) => derives.extend( - inner - .split(',') - .map(str::trim) - .filter(|p| !p.is_empty()) - .map(String::from), - ), - None => others.push(attr.clone()), - } - } - (derives, others) - } - - /// Emit each attribute on its own line, verbatim. fn push_attrs(&mut self, attrs: &[String]) { for attr in attrs { uwriteln!(self.src, "{attr}"); @@ -2211,12 +2143,12 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .cloned() .collect(); // Computed once, then emitted on every ownership mode below. - let (injected_derives, injected_attrs) = - Self::split_injected_derives(&self.additional_type_attrs(id)); + let selectors = wit_type_selectors(self.resolve, id); + let injected_attrs = self.additional_type_attrs(&selectors); let field_attrs: Vec> = record .fields .iter() - .map(|f| self.additional_member_attrs(id, &f.name)) + .map(|f| self.additional_member_attrs(&selectors, &f.name)) .collect(); for (name, mode) in self.modes_of(id) { self.rustdoc(docs); @@ -2235,7 +2167,6 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) } else if info.is_clone() { derives.insert("Clone".to_string()); } - derives.extend(injected_derives.iter().cloned()); if !derives.is_empty() { self.push_str("#[derive("); self.push_str(&derives.into_iter().collect::>().join(", ")); @@ -2307,8 +2238,6 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) { self.print_rust_enum( id, - // Raw wit case names; `print_rust_enum` upper-camel-cases them at the - // emit site so member selectors still match the wit (kebab) name. variant .cases .iter() @@ -2334,12 +2263,12 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .iter() .cloned() .collect(); - let (injected_derives, injected_attrs) = - Self::split_injected_derives(&self.additional_type_attrs(id)); + let selectors = wit_type_selectors(self.resolve, id); + let injected_attrs = self.additional_type_attrs(&selectors); let case_attrs: Vec> = cases .clone() .into_iter() - .map(|(case_name, _, _)| self.additional_member_attrs(id, &case_name)) + .map(|(case_name, _, _)| self.additional_member_attrs(&selectors, &case_name)) .collect(); for (name, mode) in self.modes_of(id) { self.rustdoc(docs); @@ -2357,7 +2286,6 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) } else if info.is_clone() { derives.insert("Clone".to_string()); } - derives.extend(injected_derives.iter().cloned()); if !derives.is_empty() { self.push_str("#[derive("); self.push_str(&derives.into_iter().collect::>().join(", ")); @@ -2485,9 +2413,8 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) let name = to_upper_camel_case(name); self.rustdoc(docs); - let (injected_derives, injected_attrs) = - Self::split_injected_derives(&self.additional_type_attrs(id)); - self.push_attrs(&injected_attrs); + let selectors = wit_type_selectors(self.resolve, id); + let injected_attrs = self.additional_type_attrs(&selectors); self.push_str("#[repr("); self.int_repr(enum_.tag()); self.push_str(")]\n"); @@ -2506,14 +2433,14 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .into_iter() .map(|s| s.to_string()), ); - derives.extend(injected_derives); self.push_str("#[derive("); self.push_str(&derives.into_iter().collect::>().join(", ")); self.push_str(")]\n"); + self.push_attrs(&injected_attrs); self.push_str(&format!("pub enum {name} {{\n")); for case in enum_.cases.iter() { self.rustdoc(&case.docs); - let case_attrs = self.additional_member_attrs(id, &case.name); + let case_attrs = self.additional_member_attrs(&selectors, &case.name); self.push_attrs(&case_attrs); self.push_str(&case.name.to_upper_camel_case()); self.push_str(",\n"); diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index e4325604f..6ecb1d0b9 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -42,8 +42,7 @@ pub struct RustWasm { // Track which interfaces and types are generated. Remapped interfaces and types provided via `with` // are required to be used. generated_types: HashSet, - // Selectors from `additional_type_attributes` / `additional_member_attributes` that matched at - // least one generated type or member; used to reject selectors that matched nothing. + // Attribute selectors that matched something, so `finish` can reject the rest. used_type_attr_selectors: HashSet, used_member_attr_selectors: HashSet, world: Option, @@ -149,7 +148,7 @@ fn parse_attribute(s: &str) -> Result<(String, String), String> { let (sel, attr) = s .split_once('=') .ok_or_else(|| format!("expected string of form `=`; got `{s}`"))?; - // an empty attribute would mark the selector used and escape the unused check + // An empty attribute would mark the selector used and escape the unused check. if attr.trim().is_empty() { return Err(format!("attribute must not be empty; got `{s}`")); } @@ -251,38 +250,40 @@ pub struct Opts { #[cfg_attr(feature = "clap", arg(long, value_name = "NAME"))] pub additional_derive_ignore: Vec, - /// Extra attributes to emit on specific generated types (records, variants, - /// and enums) rather than on all types like `additional_derive_attributes`. - /// Each entry is a `(selector, attribute)` pair. The selector matches a type by - /// its bare kebab name (`challenge-data`), its package (`my:pkg`), its owning - /// interface (`my:pkg/types`), or its fully-qualified name - /// (`my:pkg/types/challenge-data`). The qualified forms are written exactly as in - /// the `with` option, so they carry the package `@version` when versioned - /// (`my:pkg/types@1.0.0/challenge-data`); package and interface selectors apply to - /// interface-owned types only (world-owned types are matched by name). An injected - /// `#[derive(...)]` is folded into the type's generated `#[derive(...)]`, so its - /// traits dedup against the built-in and `additional_derive_attributes` derives - /// rather than colliding; any other attribute is emitted verbatim, and multiple - /// matching entries all apply. Attributes land on every generated form of the - /// type, including the borrowed (`<'a>`) form produced under `ownership: - /// Borrowing`; there is no owned-only targeting, so an owned-only derive such as - /// `#[derive(Default)]` or `serde::Deserialize` fails to compile on the borrowed - /// form (as it would via `additional_derive_attributes`). Only records, variants, - /// and enums are covered; flags, resources, type aliases, and the built-in - /// option/result/list types are not. A package or interface selector silently - /// skips them (they do not count toward the selector being used), but a selector - /// that resolves to nothing covered is an error. For a derive on *all* types use - /// `additional_derive_attributes`. In a CLI, this flag can be specified multiple - /// times as `selector=attribute`. + /// Extra attributes to emit on specific generated types, rather than on all + /// types like `additional_derive_attributes`. + /// + /// Each entry pairs a selector with an attribute, where a selector names a + /// type by one of: + /// + /// * bare name, `challenge-data` + /// * package, `my:pkg` + /// * interface, `my:pkg/types` + /// * fully qualified, `my:pkg/types/challenge-data` + /// + /// The qualified forms are written as in `with`, so they carry `@version` + /// when versioned. A selector matching nothing is an error, as with `with`. + /// + /// Only records, variants, and enums are covered; broader selectors skip + /// everything else. Attributes are emitted verbatim on every form of the + /// type, including the borrowed form under `ownership: Borrowing`, so an + /// owned-only derive fails to compile there. Identical attributes are + /// emitted once, but overlapping selectors naming one derive twice, say + /// `#[derive(Clone)]` and `#[derive(Clone, Hash)]`, will not compile. + /// + /// In a CLI, this flag can be specified multiple times as + /// `selector=attribute`. #[cfg_attr(feature = "clap", arg(long, value_name = "SELECTOR=ATTR", value_parser = parse_attribute))] pub additional_type_attributes: Vec<(String, String)>, - /// Extra attributes to emit on specific generated fields (of records) and - /// enum/variant cases. Like `additional_type_attributes`, but the selector is - /// `.`, where `` is any type selector above (bare name, - /// package, interface, or fully-qualified, carrying `@version` when versioned), - /// or a bare `member-name` matching that member in any record/variant/enum, all - /// in kebab case. In a CLI, this flag can be specified multiple times as + /// Extra attributes to emit on specific generated record fields and + /// enum/variant cases. + /// + /// As `additional_type_attributes`, except the selector is + /// `.` for any type selector above, or a bare + /// `member-name` matching that member of any type. + /// + /// In a CLI, this flag can be specified multiple times as /// `selector=attribute`. #[cfg_attr(feature = "clap", arg(long, value_name = "SELECTOR=ATTR", value_parser = parse_attribute))] pub additional_member_attributes: Vec<(String, String)>, @@ -1181,6 +1182,18 @@ impl WorldGenerator for RustWasm { self.opts.additional_derive_ignore ); } + for (selector, attr) in self.opts.additional_type_attributes.iter() { + uwriteln!( + self.src_preamble, + "// * additional type attribute {selector:?} = {attr:?}" + ); + } + for (selector, attr) in self.opts.additional_member_attributes.iter() { + uwriteln!( + self.src_preamble, + "// * additional member attribute {selector:?} = {attr:?}" + ); + } for (k, v) in self.opts.with.iter() { uwriteln!(self.src_preamble, "// * with {k:?} = {v}"); } @@ -1587,8 +1600,6 @@ impl WorldGenerator for RustWasm { bail!("unused remappings provided via `with`: {unused_keys:?}"); } - // A selector that matched no generated type or member is almost always a - // typo or a stale path, so reject it like an unused `with` remapping. let mut unused_selectors = self .opts .additional_type_attributes @@ -2026,6 +2037,24 @@ fn full_wit_type_name(resolve: &Resolve, id: TypeId) -> String { } } +/// Every string that names `id` in configuration: its full name, its bare name, +/// and, when interface-owned, its interface and package. `with` matches only the +/// full name, since a remapping names one Rust path, while the attribute options +/// match any of them, since one attribute can apply to many types. +fn wit_type_selectors(resolve: &Resolve, id: TypeId) -> Vec { + let mut names = vec![full_wit_type_name(resolve, id)]; + let type_def = &resolve.types[dealias(resolve, id)]; + names.extend(type_def.name.clone()); + if let TypeOwner::Interface(iface) = type_def.owner { + // `Resolve::id_of` unwraps the package, so only ask once there is one. + if let Some(pkg) = resolve.interfaces[iface].package { + names.extend(resolve.id_of(iface)); + names.push(resolve.packages[pkg].name.to_string()); + } + } + names +} + enum ConstructorReturnType { /// Resource constructor is infallible. E.g.: /// ```wit @@ -2085,28 +2114,3 @@ fn classify_constructor_return_type( classify(resolve, resource_id, result).expect("invalid constructor") } - -#[cfg(all(test, feature = "clap"))] -mod tests { - use super::parse_attribute; - - #[test] - fn parse_attribute_splits_on_first_equals() { - assert_eq!( - parse_attribute("point=#[derive(Clone)]").unwrap(), - ("point".to_string(), "#[derive(Clone)]".to_string()) - ); - // the attribute may itself contain `=` - assert_eq!( - parse_attribute(r#"point.x=#[serde(rename = "x")]"#).unwrap(), - ( - "point.x".to_string(), - r#"#[serde(rename = "x")]"#.to_string() - ) - ); - assert!(parse_attribute("no-equals-sign").is_err()); - // an empty attribute is rejected rather than silently escaping the check - assert!(parse_attribute("point=").is_err()); - assert!(parse_attribute("point= ").is_err()); - } -} diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index 334fedec6..6d5e65fdd 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -302,9 +302,6 @@ mod merge_structurally_equal_types { }); } -// Injects attributes onto selected types/fields/cases. Compiling plus the `#[test]` -// proves each attribute lands (on both owned and borrowed forms) and that a package -// selector does not leak into unsupported kinds like flags (see the `perms` note). mod targeted_attributes { wit_bindgen::generate!({ inline: r#" @@ -313,19 +310,15 @@ mod targeted_attributes { interface iface { record point { x: u32, y: u32 } enum color { red, green, blue } - // `a-b` upper-camels to `AB`; a member selector must still match the raw - // wit name, not the lossy `AB`.to_kebab_case() == `ab`. variant shape { circle(u32), square(point), a-b } - // used as both return and param, so under `duplicate_if_necessary` it - // generates an owned `String` form and a borrowed `&str` form + // used as both return and param, so `duplicate_if_necessary` generates + // an owned `String` form and a borrowed `&str` form record label { text: string } - // two records sharing a member name, to exercise bare-member fan-out record note-a { note: string } record note-b { note: u32 } - // an unsupported kind: attribute injection must skip it (boundary test) flags perms { read, write } paint: func(c: color) -> color; @@ -344,38 +337,25 @@ mod targeted_attributes { "#, generate_all, ownership: Borrowing { duplicate_if_necessary: true }, - // wit-bindgen already derives Clone/Debug on records+variants and every - // standard trait but Hash on enums; injected derives fold in (see `label`). additional_type_attributes: { - // cumulative list; the trailing `Hash` also comes from the package - // selector below and folds into one derive rather than colliding. - "point": [ - "#[derive(PartialEq, Eq)]", - "#[derive(PartialOrd, Ord)]", - "#[derive(Hash)]", - ], - "color": [r#"#[doc = "a primary color"]"#], // enum: a non-derive attribute - "shape": ["#[derive(PartialEq)]"], // variant - // `Clone` here overlaps the record's built-in `Clone`: it must fold into - // one derive, not collide (E0119). Must fit owned + borrowed forms. - "label": ["#[derive(Clone, PartialEq, Eq)]"], - // fully-qualified selector against an unversioned package (no `@version`) - "test:attrs/iface/note-a": ["#[derive(PartialEq, Eq)]"], - // Package selector => every record/variant/enum in the package. Doubles as - // the flags boundary probe: `perms` already derives `Hash`, so a leak here - // would be a conflicting-impl error. It compiles => flags are skipped. - "test:attrs": ["#[derive(Hash)]"], + "point": [#[derive(PartialEq, Eq)] #[derive(PartialOrd, Ord)]], + "color": [#[doc = "a primary color"]], + "shape": [#[derive(PartialEq)]], + "label": [#[derive(PartialEq, Eq)]], + "test:attrs/iface/note-a": [#[derive(PartialEq, Eq)]], + // `perms` already derives `Hash`, so if this package selector leaked + // into flags it would be a conflicting impl. It compiles => skipped. + "test:attrs": [#[derive(Hash)]], }, additional_member_attributes: { - "point.x": ["#[allow(dead_code)]"], - "red": [r#"#[doc = "the primary color"]"#], // enum case, bare selector - "shape.circle": [r#"#[doc = "a round shape"]"#], // variant case, qualified - // lossy-kebab variant case: matches only via the raw wit name (regression - // guard: an unmatched selector now trips the unused-selector check). - "shape.a-b": ["#[allow(dead_code)]"], - "label.text": ["#[allow(dead_code)]"], - // bare member selector: fans out to `note` on BOTH note-a and note-b - "note": ["#[allow(dead_code)]"], + "point.x": [#[allow(dead_code)]], + "red": [#[doc = "the primary color"]], + "shape.circle": [#[doc = "a round shape"]], + // `a-b` upper-camels to `AB`, so this matches only via the raw wit name + "shape.a-b": [#[allow(dead_code)]], + "label.text": [#[allow(dead_code)]], + // bare selector: fans out to `note` on both note-a and note-b + "note": [#[allow(dead_code)]], }, }); @@ -383,7 +363,6 @@ mod targeted_attributes { fn injected_derives_are_usable() { use test::attrs::iface::{Color, LabelParam, LabelResult, NoteA, Point, Shape}; - // record got PartialEq/Eq + PartialOrd/Ord (list) + package Hash. let a = Point { x: 1, y: 2 }; assert_eq!(a, a.clone()); assert!(Point { x: 1, y: 2 } < Point { x: 1, y: 3 }); @@ -391,18 +370,15 @@ mod targeted_attributes { set.insert(a); assert!(set.contains(&Point { x: 1, y: 2 })); - // enum got package Hash; PartialEq/Eq are defaults. let mut colors = std::collections::HashSet::new(); colors.insert(Color::Red); assert!(colors.contains(&Color::Red)); assert!(!colors.contains(&Color::Blue)); - // variant got PartialEq (injected) + package Hash + default Clone. assert_eq!(Shape::Circle(3), Shape::Circle(3).clone()); assert_ne!(Shape::Circle(3), Shape::Circle(4)); - // BOTH forms of the both-forms record got the injected PartialEq/Eq (+ package - // Hash): the owned `String` form and the borrowed `&str` form. + // both the owned `String` form and the borrowed `&str` form got the derives assert_eq!( LabelResult { text: "hi".into() }, LabelResult { text: "hi".into() } @@ -413,15 +389,10 @@ mod targeted_attributes { labels.insert(LabelResult { text: "hi".into() }); assert!(labels.contains(&LabelResult { text: "hi".into() })); - // note-a got PartialEq/Eq via a fully-qualified selector on an unversioned package. assert_eq!(NoteA { note: "n".into() }, NoteA { note: "n".into() }); } } -// Hierarchical selectors against a *versioned* package: each level (package, -// interface, fully-qualified) injects a different derive, and the qualified forms -// carry the `@version` as in `with`. The asserts prove each level resolves and that -// the type-specific selector does not bleed onto its sibling. mod hierarchical_selectors { wit_bindgen::generate!({ inline: r#" @@ -436,15 +407,13 @@ mod hierarchical_selectors { "#, generate_all, additional_type_attributes: { - "test:hier@1.2.3": ["#[derive(Hash)]"], // package => alpha + beta - "test:hier/types@1.2.3": ["#[derive(PartialEq, Eq)]"], // interface => alpha + beta - "test:hier/types@1.2.3/alpha": ["#[derive(PartialOrd, Ord)]"], // one type => alpha only + "test:hier@1.2.3": [#[derive(Hash)]], // package => alpha + beta + "test:hier/types@1.2.3": [#[derive(PartialEq, Eq)]], // interface => alpha + beta + "test:hier/types@1.2.3/alpha": [#[derive(PartialOrd, Ord)]], // one type => alpha only }, additional_member_attributes: { - "test:hier/types@1.2.3/alpha.x": ["#[allow(dead_code)]"], // qualified member selector - // interface-scoped member: `x` on every type in the interface (alpha has it). - // Matches via the interface selector key, same grammar as type selectors. - "test:hier/types@1.2.3.x": [r#"#[doc = "interface-scoped"]"#], + "test:hier/types@1.2.3/alpha.x": [#[allow(dead_code)]], + "test:hier/types@1.2.3.x": [#[doc = "interface-scoped"]], }, }); @@ -452,15 +421,13 @@ mod hierarchical_selectors { fn hierarchical_and_versioned_selectors_resolve() { use test::hier::types::{Alpha, Beta}; - // alpha: Hash (package) + PartialEq/Eq (interface) + PartialOrd/Ord (versioned qualified). assert_eq!(Alpha { x: 1 }, Alpha { x: 1 }); assert!(Alpha { x: 1 } < Alpha { x: 2 }); let mut set = std::collections::HashSet::new(); set.insert(Alpha { x: 1 }); assert!(set.contains(&Alpha { x: 1 })); - // beta: Hash (package) + PartialEq/Eq (interface), but NOT Ord; the qualified - // selector named `alpha` only, proving specificity. + // beta gets the package and interface derives but not `alpha`'s Ord assert_eq!(Beta { y: 7 }, Beta { y: 7 }); assert_ne!(Beta { y: 7 }, Beta { y: 8 }); let mut bset = std::collections::HashSet::new(); From 1ac9e3ced8a015cb4849533b72b4f7bac4c5911e Mon Sep 17 00:00:00 2001 From: wilfreddenton Date: Tue, 28 Jul 2026 16:50:54 -0400 Subject: [PATCH 3/3] rust: require fully-qualified selectors for targeted attributes --- crates/guest-rust/src/lib.rs | 13 +++---- crates/rust/src/interface.rs | 67 +++++++++++++++++------------------- crates/rust/src/lib.rs | 46 +++++-------------------- crates/rust/tests/codegen.rs | 66 +++++++++-------------------------- 4 files changed, 62 insertions(+), 130 deletions(-) diff --git a/crates/guest-rust/src/lib.rs b/crates/guest-rust/src/lib.rs index 5f11fb82d..3efde07ad 100644 --- a/crates/guest-rust/src/lib.rs +++ b/crates/guest-rust/src/lib.rs @@ -683,23 +683,20 @@ extern crate std; /// additional_derives: [PartialEq, Eq, Hash, Clone], /// /// // Extra attributes to emit on specific generated types (records, variants, -/// // and enums), rather than on all types like `additional_derives`. A type is -/// // selected by its bare name, its package, its interface, or its fully -/// // qualified name, the qualified forms written as in `with`. +/// // and enums), rather than on all types like `additional_derives`. A type +/// // is selected by its fully qualified name, written as in `with`. /// // /// // By default this map is empty. /// additional_type_attributes: { -/// "my-record": [#[derive(serde::Serialize)]], // one type, by bare name -/// "my:pkg/types": [#[derive(Hash)]], // every type in an interface +/// "my:pkg/types/my-record": [#[derive(serde::Serialize)]], /// }, /// /// // Like `additional_type_attributes`, but for generated record fields and -/// // enum/variant cases, selected by `.member-name` (any type -/// // selector above) or a bare `member-name`. +/// // enum/variant cases, selected by `.member-name`. /// // /// // By default this map is empty. /// additional_member_attributes: { -/// "my-record.my-field": [#[serde(rename = "mf")]], +/// "my:pkg/types/my-record.my-field": [#[serde(rename = "mf")]], /// }, /// /// // When generating bindings for interfaces that are not defined in the diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index e3a663a3a..e92cc9160 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -2,12 +2,12 @@ use crate::bindgen::{FunctionBindgen, POINTER_SIZE_EXPRESSION}; use crate::{ ConstructorReturnType, FnSig, Identifier, InterfaceName, Ownership, RuntimeItem, RustFlagsRepr, RustWasm, TypeGeneration, classify_constructor_return_type, full_wit_type_name, int_repr, - to_rust_ident, to_upper_camel_case, wasm_type, wit_type_selectors, + to_rust_ident, to_upper_camel_case, wasm_type, }; use anyhow::Result; use heck::*; use indexmap::IndexSet; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::fmt::Write as _; use std::mem; use wit_bindgen_core::abi::{self, AbiVariant, LiftLower}; @@ -2087,43 +2087,38 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) result } - /// Matching attributes, deduped in configured order, plus the selectors that - /// matched, which `finish` uses to report the ones that did not. + /// Matching attributes, deduped in configured order. Selectors that matched + /// are recorded in `used` so `finish` can report the ones that did not. fn matching_attrs( entries: &[(String, String)], + used: &mut HashSet, matches: impl Fn(&str) -> bool, - ) -> (Vec, Vec) { + ) -> Vec { let mut attrs = IndexSet::new(); - let mut used = IndexSet::new(); for (sel, attr) in entries.iter().filter(|(sel, _)| matches(sel)) { attrs.insert(attr.clone()); used.insert(sel.clone()); } - (attrs.into_iter().collect(), used.into_iter().collect()) + attrs.into_iter().collect() } - fn additional_type_attrs(&mut self, selectors: &[String]) -> Vec { - let (attrs, used) = - Self::matching_attrs(&self.r#gen.opts.additional_type_attributes, |sel| { - selectors.iter().any(|name| name == sel) - }); - self.r#gen.used_type_attr_selectors.extend(used); - attrs + fn additional_type_attrs(&mut self, type_name: &str) -> Vec { + Self::matching_attrs( + &self.r#gen.opts.additional_type_attributes, + &mut self.r#gen.used_type_attr_selectors, + |sel| sel == type_name, + ) } - /// `member` is a record field or an enum/variant case, selected by a bare - /// name or `.`. Member names contain no `.`, so the - /// last one separates the two halves. - fn additional_member_attrs(&mut self, selectors: &[String], member: &str) -> Vec { - let (attrs, used) = - Self::matching_attrs(&self.r#gen.opts.additional_member_attributes, |sel| { - sel == member - || sel.rsplit_once('.').is_some_and(|(ty, m)| { - m == member && selectors.iter().any(|name| name == ty) - }) - }); - self.r#gen.used_member_attr_selectors.extend(used); - attrs + /// `member` is a record field or an enum/variant case, selected by + /// `.`. Member names contain no `.`, so the last one + /// separates the two halves. + fn additional_member_attrs(&mut self, type_name: &str, member: &str) -> Vec { + Self::matching_attrs( + &self.r#gen.opts.additional_member_attributes, + &mut self.r#gen.used_member_attr_selectors, + |sel| sel.rsplit_once('.') == Some((type_name, member)), + ) } fn push_attrs(&mut self, attrs: &[String]) { @@ -2143,12 +2138,12 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .cloned() .collect(); // Computed once, then emitted on every ownership mode below. - let selectors = wit_type_selectors(self.resolve, id); - let injected_attrs = self.additional_type_attrs(&selectors); + let type_name = full_wit_type_name(self.resolve, id); + let injected_attrs = self.additional_type_attrs(&type_name); let field_attrs: Vec> = record .fields .iter() - .map(|f| self.additional_member_attrs(&selectors, &f.name)) + .map(|f| self.additional_member_attrs(&type_name, &f.name)) .collect(); for (name, mode) in self.modes_of(id) { self.rustdoc(docs); @@ -2263,12 +2258,12 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) .iter() .cloned() .collect(); - let selectors = wit_type_selectors(self.resolve, id); - let injected_attrs = self.additional_type_attrs(&selectors); + let type_name = full_wit_type_name(self.resolve, id); + let injected_attrs = self.additional_type_attrs(&type_name); let case_attrs: Vec> = cases .clone() .into_iter() - .map(|(case_name, _, _)| self.additional_member_attrs(&selectors, &case_name)) + .map(|(case_name, _, _)| self.additional_member_attrs(&type_name, &case_name)) .collect(); for (name, mode) in self.modes_of(id) { self.rustdoc(docs); @@ -2413,8 +2408,8 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) let name = to_upper_camel_case(name); self.rustdoc(docs); - let selectors = wit_type_selectors(self.resolve, id); - let injected_attrs = self.additional_type_attrs(&selectors); + let type_name = full_wit_type_name(self.resolve, id); + let injected_attrs = self.additional_type_attrs(&type_name); self.push_str("#[repr("); self.int_repr(enum_.tag()); self.push_str(")]\n"); @@ -2440,7 +2435,7 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) self.push_str(&format!("pub enum {name} {{\n")); for case in enum_.cases.iter() { self.rustdoc(&case.docs); - let case_attrs = self.additional_member_attrs(&selectors, &case.name); + let case_attrs = self.additional_member_attrs(&type_name, &case.name); self.push_attrs(&case_attrs); self.push_str(&case.name.to_upper_camel_case()); self.push_str(",\n"); diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index 6ecb1d0b9..dfe8c2623 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -253,23 +253,14 @@ pub struct Opts { /// Extra attributes to emit on specific generated types, rather than on all /// types like `additional_derive_attributes`. /// - /// Each entry pairs a selector with an attribute, where a selector names a - /// type by one of: + /// Each entry pairs a selector with an attribute. A selector is a type's + /// fully qualified name, written as in `with`, so `my:pkg/types/point`, + /// carrying `@version` when the package is versioned. A selector matching + /// nothing is an error, as with `with`. /// - /// * bare name, `challenge-data` - /// * package, `my:pkg` - /// * interface, `my:pkg/types` - /// * fully qualified, `my:pkg/types/challenge-data` - /// - /// The qualified forms are written as in `with`, so they carry `@version` - /// when versioned. A selector matching nothing is an error, as with `with`. - /// - /// Only records, variants, and enums are covered; broader selectors skip - /// everything else. Attributes are emitted verbatim on every form of the - /// type, including the borrowed form under `ownership: Borrowing`, so an - /// owned-only derive fails to compile there. Identical attributes are - /// emitted once, but overlapping selectors naming one derive twice, say - /// `#[derive(Clone)]` and `#[derive(Clone, Hash)]`, will not compile. + /// Only records, variants, and enums are covered. Attributes are emitted + /// verbatim on every form of the type, including the borrowed form under + /// `ownership: Borrowing`, so an owned-only derive fails to compile there. /// /// In a CLI, this flag can be specified multiple times as /// `selector=attribute`. @@ -279,9 +270,8 @@ pub struct Opts { /// Extra attributes to emit on specific generated record fields and /// enum/variant cases. /// - /// As `additional_type_attributes`, except the selector is - /// `.` for any type selector above, or a bare - /// `member-name` matching that member of any type. + /// As `additional_type_attributes`, except the selector is a type's fully + /// qualified name, a `.`, and the member name. /// /// In a CLI, this flag can be specified multiple times as /// `selector=attribute`. @@ -2037,24 +2027,6 @@ fn full_wit_type_name(resolve: &Resolve, id: TypeId) -> String { } } -/// Every string that names `id` in configuration: its full name, its bare name, -/// and, when interface-owned, its interface and package. `with` matches only the -/// full name, since a remapping names one Rust path, while the attribute options -/// match any of them, since one attribute can apply to many types. -fn wit_type_selectors(resolve: &Resolve, id: TypeId) -> Vec { - let mut names = vec![full_wit_type_name(resolve, id)]; - let type_def = &resolve.types[dealias(resolve, id)]; - names.extend(type_def.name.clone()); - if let TypeOwner::Interface(iface) = type_def.owner { - // `Resolve::id_of` unwraps the package, so only ask once there is one. - if let Some(pkg) = resolve.interfaces[iface].package { - names.extend(resolve.id_of(iface)); - names.push(resolve.packages[pkg].name.to_string()); - } - } - names -} - enum ConstructorReturnType { /// Resource constructor is infallible. E.g.: /// ```wit diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index 6d5e65fdd..e8046ceaf 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -316,19 +316,11 @@ mod targeted_attributes { // an owned `String` form and a borrowed `&str` form record label { text: string } - record note-a { note: string } - record note-b { note: u32 } - - flags perms { read, write } - paint: func(c: color) -> color; draw: func(s: shape) -> shape; round: func(p: point) -> point; make-label: func() -> label; use-label: func(l: label) -> u32; - first: func() -> note-a; - second: func(x: note-b) -> u32; - set-perms: func(p: perms) -> perms; } world test { @@ -338,34 +330,28 @@ mod targeted_attributes { generate_all, ownership: Borrowing { duplicate_if_necessary: true }, additional_type_attributes: { - "point": [#[derive(PartialEq, Eq)] #[derive(PartialOrd, Ord)]], - "color": [#[doc = "a primary color"]], - "shape": [#[derive(PartialEq)]], - "label": [#[derive(PartialEq, Eq)]], - "test:attrs/iface/note-a": [#[derive(PartialEq, Eq)]], - // `perms` already derives `Hash`, so if this package selector leaked - // into flags it would be a conflicting impl. It compiles => skipped. - "test:attrs": [#[derive(Hash)]], + // two entries for one selector, both apply + "test:attrs/iface/point": [#[derive(PartialEq, Eq)] #[derive(Hash)]], + "test:attrs/iface/color": [#[doc = "a primary color"] #[derive(Hash)]], + "test:attrs/iface/shape": [#[derive(PartialEq)]], + "test:attrs/iface/label": [#[derive(PartialEq, Eq)]], }, additional_member_attributes: { - "point.x": [#[allow(dead_code)]], - "red": [#[doc = "the primary color"]], - "shape.circle": [#[doc = "a round shape"]], + "test:attrs/iface/point.x": [#[allow(dead_code)]], + "test:attrs/iface/color.red": [#[doc = "the primary color"]], + "test:attrs/iface/shape.circle": [#[doc = "a round shape"]], // `a-b` upper-camels to `AB`, so this matches only via the raw wit name - "shape.a-b": [#[allow(dead_code)]], - "label.text": [#[allow(dead_code)]], - // bare selector: fans out to `note` on both note-a and note-b - "note": [#[allow(dead_code)]], + "test:attrs/iface/shape.a-b": [#[allow(dead_code)]], + "test:attrs/iface/label.text": [#[allow(dead_code)]], }, }); #[test] fn injected_derives_are_usable() { - use test::attrs::iface::{Color, LabelParam, LabelResult, NoteA, Point, Shape}; + use test::attrs::iface::{Color, LabelParam, LabelResult, Point, Shape}; let a = Point { x: 1, y: 2 }; assert_eq!(a, a.clone()); - assert!(Point { x: 1, y: 2 } < Point { x: 1, y: 3 }); let mut set = std::collections::HashSet::new(); set.insert(a); assert!(set.contains(&Point { x: 1, y: 2 })); @@ -385,53 +371,35 @@ mod targeted_attributes { ); assert_eq!(LabelParam { text: "hi" }, LabelParam { text: "hi" }); assert_ne!(LabelParam { text: "hi" }, LabelParam { text: "bye" }); - let mut labels = std::collections::HashSet::new(); - labels.insert(LabelResult { text: "hi".into() }); - assert!(labels.contains(&LabelResult { text: "hi".into() })); - - assert_eq!(NoteA { note: "n".into() }, NoteA { note: "n".into() }); } } -mod hierarchical_selectors { +// Guards where `@version` lands in a qualified selector: after the interface, +// before the type. +mod versioned_selectors { wit_bindgen::generate!({ inline: r#" package test:hier@1.2.3; interface types { record alpha { x: u32 } - record beta { y: u32 } get-alpha: func() -> alpha; - get-beta: func() -> beta; } world w { import types; } "#, generate_all, additional_type_attributes: { - "test:hier@1.2.3": [#[derive(Hash)]], // package => alpha + beta - "test:hier/types@1.2.3": [#[derive(PartialEq, Eq)]], // interface => alpha + beta - "test:hier/types@1.2.3/alpha": [#[derive(PartialOrd, Ord)]], // one type => alpha only + "test:hier/types@1.2.3/alpha": [#[derive(PartialEq, Eq)] #[derive(PartialOrd, Ord)]], }, additional_member_attributes: { "test:hier/types@1.2.3/alpha.x": [#[allow(dead_code)]], - "test:hier/types@1.2.3.x": [#[doc = "interface-scoped"]], }, }); #[test] - fn hierarchical_and_versioned_selectors_resolve() { - use test::hier::types::{Alpha, Beta}; + fn versioned_selectors_resolve() { + use test::hier::types::Alpha; assert_eq!(Alpha { x: 1 }, Alpha { x: 1 }); assert!(Alpha { x: 1 } < Alpha { x: 2 }); - let mut set = std::collections::HashSet::new(); - set.insert(Alpha { x: 1 }); - assert!(set.contains(&Alpha { x: 1 })); - - // beta gets the package and interface derives but not `alpha`'s Ord - assert_eq!(Beta { y: 7 }, Beta { y: 7 }); - assert_ne!(Beta { y: 7 }, Beta { y: 8 }); - let mut bset = std::collections::HashSet::new(); - bset.insert(Beta { y: 7 }); - assert!(bset.contains(&Beta { y: 7 })); } }