1use std::collections::{BTreeMap, HashMap};
52
53use crate::ir::{
54 CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef,
55 Module, Param, StructDef, StructField, TypeRef,
56};
57use crate::ty::Prim;
58use proc_macro2::Span;
59use syn::spanned::Spanned;
60
61#[derive(Clone, Default)]
72pub struct SourceMap {
73 spans: BTreeMap<Vec<String>, Vec<Span>>,
74 cfgs: BTreeMap<Vec<String>, Vec<syn::Attribute>>,
75}
76
77impl std::fmt::Debug for SourceMap {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("SourceMap")
80 .field("paths", &self.spans.keys().collect::<Vec<_>>())
81 .finish_non_exhaustive()
82 }
83}
84
85impl SourceMap {
86 pub const TYPE: &'static str = "#type";
89 pub const RETURN: &'static str = "#return";
92
93 fn record(&mut self, path: Vec<String>, span: Span) {
94 self.spans.entry(path).or_default().push(span);
95 }
96
97 fn record_cfg(&mut self, path: Vec<String>, attrs: &[syn::Attribute]) {
98 let cfg: Vec<syn::Attribute> = attrs.iter().filter(|a| is_cfg(a)).cloned().collect();
99 if !cfg.is_empty() {
100 self.cfgs.entry(path).or_default().extend(cfg);
101 }
102 }
103
104 #[must_use]
107 pub fn spans(&self, path: &[String]) -> &[Span] {
108 self.spans.get(path).map_or(&[], Vec::as_slice)
109 }
110
111 #[must_use]
115 pub fn cfg(&self, path: &[String]) -> &[syn::Attribute] {
116 self.cfgs.get(path).map_or(&[], Vec::as_slice)
117 }
118}
119
120fn is_cfg(attr: &syn::Attribute) -> bool {
122 attr.path().is_ident("cfg")
123}
124
125fn no_member_cfg(attrs: &[syn::Attribute], what: &str, instead: &str) -> syn::Result<()> {
127 match attrs.iter().find(|a| is_cfg(a)) {
128 Some(attr) => Err(syn::Error::new(
129 attr.span(),
130 format!(
131 "weaveffi: `#[cfg]` on {what} isn't supported, because the generated bindings \
132 can't follow it; {instead}"
133 ),
134 )),
135 None => Ok(()),
136 }
137}
138
139type Aliases = HashMap<String, syn::Type>;
142
143fn collect_aliases(item_mod: &syn::ItemMod, out: &mut Aliases) {
144 let Some((_, items)) = &item_mod.content else {
145 return;
146 };
147 for item in items {
148 match item {
149 syn::Item::Type(t) if t.generics.params.is_empty() => {
150 out.insert(t.ident.to_string(), (*t.ty).clone());
151 }
152 syn::Item::Mod(m) if has_marker(&m.attrs, "module") => collect_aliases(m, out),
153 _ => {}
154 }
155 }
156}
157
158struct Extractor<'a> {
161 aliases: &'a Aliases,
162 map: SourceMap,
163}
164
165fn is_marker(attr: &syn::Attribute, name: &str) -> bool {
171 attr.path().segments.last().is_some_and(|s| s.ident == name)
172}
173
174pub fn has_marker(attrs: &[syn::Attribute], name: &str) -> bool {
176 attrs.iter().any(|a| is_marker(a, name))
177}
178
179fn has_repr_i32(attrs: &[syn::Attribute]) -> bool {
180 attrs.iter().any(|a| {
181 a.path().is_ident("repr") && a.parse_args::<syn::Ident>().is_ok_and(|id| id == "i32")
182 })
183}
184
185fn parse_deprecated(attrs: &[syn::Attribute]) -> (Option<String>, Option<String>) {
187 let Some(attr) = attrs.iter().find(|a| a.path().is_ident("deprecated")) else {
188 return (None, None);
189 };
190 let mut since = None;
191 let mut note = None;
192 if matches!(attr.meta, syn::Meta::Path(_)) {
193 return (None, Some("deprecated".to_string()));
194 }
195 let _ = attr.parse_nested_meta(|meta| {
196 let Some(ident) = meta.path.get_ident() else {
197 return Ok(());
198 };
199 let value = meta.value()?;
200 let lit: syn::LitStr = value.parse()?;
201 match ident.to_string().as_str() {
202 "since" => since = Some(lit.value()),
203 "note" => note = Some(lit.value()),
204 _ => {}
205 }
206 Ok(())
207 });
208 if note.is_none() && since.is_none() {
209 note = Some("deprecated".to_string());
210 }
211 (since, note)
212}
213
214fn extract_doc(attrs: &[syn::Attribute]) -> Option<String> {
219 let lines: Vec<String> = attrs
220 .iter()
221 .filter_map(|attr| {
222 let syn::Meta::NameValue(nv) = &attr.meta else {
223 return None;
224 };
225 if !nv.path.is_ident("doc") {
226 return None;
227 }
228 let syn::Expr::Lit(syn::ExprLit {
229 lit: syn::Lit::Str(s),
230 ..
231 }) = &nv.value
232 else {
233 return None;
234 };
235 let val = s.value();
236 Some(match val.strip_prefix(' ') {
237 Some(stripped) => stripped.to_string(),
238 None => val,
239 })
240 })
241 .collect();
242 if lines.is_empty() {
243 return None;
244 }
245 let mut fenced = false;
246 let lines: Vec<String> = lines
247 .into_iter()
248 .map(|line| {
249 if line.trim_start().starts_with("```") {
250 fenced = !fenced;
251 line
252 } else if fenced {
253 line
254 } else {
255 unwrap_doc_links(&line)
256 }
257 })
258 .collect();
259 Some(lines.join("\n"))
260}
261
262fn unwrap_doc_links(line: &str) -> String {
267 let mut out = String::with_capacity(line.len());
268 let mut rest = line;
269 while let Some(start) = rest.find("[`") {
270 let after = &rest[start + 2..];
271 let Some(end) = after.find('`') else {
272 break;
273 };
274 let code = &after[..end];
275 let tail = &after[end + 1..];
276 let link = tail.starts_with(']')
277 && !code.is_empty()
278 && !matches!(tail[1..].chars().next(), Some('(' | '[' | ':'));
279 if link {
280 out.push_str(&rest[..start]);
281 out.push('`');
282 out.push_str(code);
283 out.push('`');
284 rest = &tail[1..];
285 } else {
286 out.push_str(&rest[..start + 2]);
287 rest = after;
288 }
289 }
290 out.push_str(rest);
291 out
292}
293
294fn is_ident(ty: &syn::Type, name: &str) -> bool {
295 matches!(ty, syn::Type::Path(p) if p.path.is_ident(name))
296}
297
298fn single_generic_arg(seg: &syn::PathSegment) -> syn::Result<&syn::Type> {
299 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
300 return Err(syn::Error::new(seg.span(), "expected generic arguments"));
301 };
302 if args.args.len() != 1 {
303 return Err(syn::Error::new(
304 seg.span(),
305 "expected exactly 1 generic argument",
306 ));
307 }
308 let syn::GenericArgument::Type(ty) = &args.args[0] else {
309 return Err(syn::Error::new(seg.span(), "expected a type argument"));
310 };
311 Ok(ty)
312}
313
314fn two_generic_args(seg: &syn::PathSegment) -> syn::Result<(&syn::Type, &syn::Type)> {
315 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
316 return Err(syn::Error::new(seg.span(), "expected generic arguments"));
317 };
318 if args.args.len() != 2 {
319 return Err(syn::Error::new(
320 seg.span(),
321 "expected exactly 2 generic arguments",
322 ));
323 }
324 let syn::GenericArgument::Type(k) = &args.args[0] else {
325 return Err(syn::Error::new(seg.span(), "expected a key type argument"));
326 };
327 let syn::GenericArgument::Type(v) = &args.args[1] else {
328 return Err(syn::Error::new(
329 seg.span(),
330 "expected a value type argument",
331 ));
332 };
333 Ok((k, v))
334}
335
336fn type_path_ident(ty: &syn::Type) -> Option<String> {
337 let syn::Type::Path(p) = ty else { return None };
338 p.path.segments.last().map(|s| s.ident.to_string())
339}
340
341fn trait_object_name(ty: &syn::Type) -> Option<String> {
344 let syn::Type::TraitObject(obj) = ty else {
345 return None;
346 };
347 let mut names = obj.bounds.iter().filter_map(|b| match b {
348 syn::TypeParamBound::Trait(t) => {
349 let name = t.path.segments.last()?.ident.to_string();
350 (!matches!(name.as_str(), "Send" | "Sync" | "Unpin")).then_some(name)
351 }
352 _ => None,
353 });
354 let first = names.next()?;
355 names.next().is_none().then_some(first)
356}
357
358pub fn peel_arc(ty: &syn::Type) -> &syn::Type {
364 if let syn::Type::Path(p) = ty {
365 if let Some(seg) = p.path.segments.last() {
366 if seg.ident == "Arc" {
367 if let Ok(inner) = single_generic_arg(seg) {
368 return inner;
369 }
370 }
371 }
372 }
373 ty
374}
375
376fn is_cancel_token(ty: &syn::Type) -> bool {
384 type_path_ident(ty).as_deref() == Some("CancelToken")
385}
386
387fn type_ref_from_syn(ty: &syn::Type, aliases: &Aliases) -> syn::Result<TypeRef> {
409 type_ref_at(ty, aliases, 0)
410}
411
412fn type_ref_at(ty: &syn::Type, aliases: &Aliases, depth: usize) -> syn::Result<TypeRef> {
413 let recurse = |t: &syn::Type| type_ref_at(t, aliases, depth);
414 match ty {
415 syn::Type::Reference(r) => {
416 if let syn::Type::Path(p) = r.elem.as_ref() {
417 if p.path.is_ident("str") {
418 return Ok(TypeRef::Prim(Prim::String));
419 }
420 }
421 if let syn::Type::Slice(slice) = r.elem.as_ref() {
422 if is_ident(&slice.elem, "u8") {
423 return Ok(TypeRef::Prim(Prim::Bytes));
424 }
425 return Ok(TypeRef::List(Box::new(recurse(&slice.elem)?)));
426 }
427 recurse(&r.elem)
428 }
429 syn::Type::Ptr(_) => Err(syn::Error::new(
430 ty.span(),
431 "weaveffi: raw pointers cannot cross the FFI boundary; declare the pointee as a \
432 #[weaveffi::interface] and pass it as `&T` or `Arc<T>`",
433 )),
434 syn::Type::TraitObject(_) => {
435 let name = trait_object_name(ty).ok_or_else(|| {
436 syn::Error::new(
437 ty.span(),
438 "weaveffi: a trait object must name exactly one #[weaveffi::callback_interface] \
439 trait (plus optional `Send`/`Sync` bounds)",
440 )
441 })?;
442 Ok(TypeRef::Named(name))
443 }
444 syn::Type::Path(type_path) => {
445 let seg = type_path
446 .path
447 .segments
448 .last()
449 .ok_or_else(|| syn::Error::new(ty.span(), "empty type path"))?;
450 let ident = seg.ident.to_string();
451 if let Some(p) =
454 Prim::from_name(&ident).filter(|p| !matches!(p, Prim::String | Prim::Bytes))
455 {
456 return Ok(TypeRef::Prim(p));
457 }
458 match ident.as_str() {
459 "String" => Ok(TypeRef::Prim(Prim::String)),
460 "Arc" => recurse(single_generic_arg(seg)?),
464 "Box" | "Rc" => Err(syn::Error::new(
465 ty.span(),
466 format!(
467 "weaveffi: `{ident}` cannot cross the FFI boundary; objects and callback \
468 interfaces are shared, so spell them as `Arc<T>` / `Arc<dyn Trait>`"
469 ),
470 )),
471 "Vec" => {
472 let inner = single_generic_arg(seg)?;
473 if is_ident(inner, "u8") {
474 return Ok(TypeRef::Prim(Prim::Bytes));
475 }
476 Ok(TypeRef::List(Box::new(recurse(inner)?)))
477 }
478 "Option" => {
479 let inner = single_generic_arg(seg)?;
480 Ok(TypeRef::Optional(Box::new(recurse(inner)?)))
481 }
482 "Iter" => {
485 let inner = single_generic_arg(seg)?;
486 Ok(TypeRef::Iterator(Box::new(recurse(inner)?)))
487 }
488 "HashMap" | "BTreeMap" => {
489 let (k, v) = two_generic_args(seg)?;
490 Ok(TypeRef::Map(Box::new(recurse(k)?), Box::new(recurse(v)?)))
491 }
492 other => match aliases.get(other) {
493 Some(target) if matches!(seg.arguments, syn::PathArguments::None) => {
494 if depth > 16 {
495 return Err(syn::Error::new(
496 ty.span(),
497 format!("weaveffi: type alias `{other}` refers to itself"),
498 ));
499 }
500 type_ref_at(target, aliases, depth + 1)
501 }
502 _ => Ok(TypeRef::Named(other.to_string())),
503 },
504 }
505 }
506 _ => Err(syn::Error::new(ty.span(), "unsupported type syntax")),
507 }
508}
509
510pub fn peel_result(ty: &syn::Type) -> &syn::Type {
514 if let syn::Type::Path(p) = ty {
515 if let Some(seg) = p.path.segments.last() {
516 if seg.ident == "Result" {
517 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
518 if let Some(syn::GenericArgument::Type(ok)) = args.args.first() {
519 return ok;
520 }
521 }
522 }
523 }
524 }
525 ty
526}
527
528pub fn output_is_result(output: &syn::ReturnType) -> bool {
531 let syn::ReturnType::Type(_, ty) = output else {
532 return false;
533 };
534 matches!(ty.as_ref(), syn::Type::Path(p)
535 if p.path.segments.last().is_some_and(|s| s.ident == "Result"))
536}
537
538fn is_unit(ty: &syn::Type) -> bool {
539 matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty())
540}
541
542fn parse_discriminant(expr: &syn::Expr) -> syn::Result<i32> {
543 match expr {
544 syn::Expr::Lit(lit) => {
545 let syn::Lit::Int(int_lit) = &lit.lit else {
546 return Err(syn::Error::new(
547 expr.span(),
548 "expected an integer literal discriminant",
549 ));
550 };
551 int_lit.base10_parse::<i32>()
552 }
553 syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
554 Ok(-parse_discriminant(&unary.expr)?)
555 }
556 _ => Err(syn::Error::new(
557 expr.span(),
558 "unsupported discriminant expression",
559 )),
560 }
561}
562
563fn at(path: &[String], names: &[&str]) -> Vec<String> {
565 let mut out = path.to_vec();
566 out.extend(names.iter().map(|n| (*n).to_string()));
567 out
568}
569
570pub fn returns_foreign_result(output: &syn::ReturnType) -> bool {
573 let syn::ReturnType::Type(_, ty) = output else {
574 return false;
575 };
576 let syn::Type::Path(p) = ty.as_ref() else {
577 return false;
578 };
579 let Some(seg) = p.path.segments.last() else {
580 return false;
581 };
582 if seg.ident != "Result" {
583 return false;
584 }
585 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
586 return false;
587 };
588 match args.args.iter().nth(1) {
589 Some(syn::GenericArgument::Type(err)) => {
590 type_path_ident(err).as_deref() == Some("ForeignError")
591 }
592 _ => false,
593 }
594}
595
596pub fn receiver_is_supported(recv: &syn::Receiver) -> bool {
603 if recv.mutability.is_some() {
604 return false;
605 }
606 if recv.reference.is_some() {
607 return true;
608 }
609 recv.colon_token.is_some() && type_path_ident(&recv.ty).as_deref() == Some("Arc")
610}
611
612fn returns_self(output: &syn::ReturnType, iface: &str) -> bool {
616 let syn::ReturnType::Type(_, ty) = output else {
617 return false;
618 };
619 match type_path_ident(peel_arc(peel_result(ty))) {
620 Some(name) => name == "Self" || name == iface,
621 None => false,
622 }
623}
624
625impl Extractor<'_> {
626 fn ty(&self, ty: &syn::Type) -> syn::Result<TypeRef> {
627 type_ref_from_syn(ty, self.aliases)
628 }
629
630 fn return_type(&self, output: &syn::ReturnType) -> syn::Result<Option<TypeRef>> {
633 match output {
634 syn::ReturnType::Default => Ok(None),
635 syn::ReturnType::Type(_, ty) => {
636 let inner = peel_result(ty);
637 if is_unit(inner) {
638 Ok(None)
639 } else {
640 Ok(Some(self.ty(inner)?))
641 }
642 }
643 }
644 }
645
646 fn params(
647 &mut self,
648 path: &[String],
649 inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
650 ) -> syn::Result<Vec<Param>> {
651 let mut out = Vec::new();
652 for pt in inputs.iter().filter_map(|arg| match arg {
653 syn::FnArg::Typed(pt) => Some(pt),
654 syn::FnArg::Receiver(_) => None,
655 }) {
656 if is_cancel_token(&pt.ty) {
660 continue;
661 }
662 let name = match pt.pat.as_ref() {
663 syn::Pat::Ident(id) => id.ident.to_string(),
664 _ => return Err(syn::Error::new(pt.span(), "unsupported parameter pattern")),
665 };
666 if matches!(pt.ty.as_ref(), syn::Type::Reference(r) if r.mutability.is_some()) {
667 return Err(syn::Error::new(
668 pt.ty.span(),
669 "weaveffi: `&mut` parameters cannot cross the FFI boundary; take the value \
670 by `&T` or by value and return the updated result",
671 ));
672 }
673 self.map.record(at(path, &[&name]), pt.pat.span());
674 self.map
675 .record(at(path, &[&name, SourceMap::TYPE]), pt.ty.span());
676 out.push(Param {
677 ty: self.ty(&pt.ty)?,
678 name,
679 doc: extract_doc(&pt.attrs),
680 });
681 }
682 Ok(out)
683 }
684
685 fn function(
690 &mut self,
691 path: &[String],
692 sig: &syn::Signature,
693 attrs: &[syn::Attribute],
694 returns: Option<TypeRef>,
695 ) -> syn::Result<Function> {
696 let ret_span = match &sig.output {
697 syn::ReturnType::Type(_, ty) => ty.span(),
698 syn::ReturnType::Default => sig.ident.span(),
699 };
700 self.map.record(at(path, &[SourceMap::RETURN]), ret_span);
701 Ok(Function {
702 name: sig.ident.to_string(),
703 params: self.params(path, &sig.inputs)?,
704 returns,
705 doc: extract_doc(attrs),
706 throws: output_is_result(&sig.output),
707 r#async: sig.asyncness.is_some(),
708 cancellable: has_marker(attrs, "cancellable"),
709 deprecated: parse_deprecated(attrs).1,
710 })
711 }
712
713 fn member(
718 &mut self,
719 path: &[String],
720 item: &syn::ImplItemFn,
721 iface: &str,
722 is_ctor: bool,
723 ) -> syn::Result<Function> {
724 let returns = if is_ctor {
725 None
726 } else {
727 match self.return_type(&item.sig.output)? {
728 Some(TypeRef::Named(name)) if name == "Self" => {
731 Some(TypeRef::Named(iface.to_string()))
732 }
733 other => other,
734 }
735 };
736 let member = at(path, &[&item.sig.ident.to_string()]);
737 self.map.record(member.clone(), item.sig.ident.span());
738 self.function(&member, &item.sig, &item.attrs, returns)
739 }
740
741 fn members(
756 &mut self,
757 path: &[String],
758 item_impl: &syn::ItemImpl,
759 iface: &mut InterfaceDef,
760 ) -> syn::Result<()> {
761 for impl_item in &item_impl.items {
762 let syn::ImplItem::Fn(f) = impl_item else {
763 continue;
764 };
765 if !matches!(f.vis, syn::Visibility::Public(_)) {
766 continue;
767 }
768 no_member_cfg(
769 &f.attrs,
770 "an interface member",
771 "move the member into its own `impl` block and put the `#[cfg]` on that block",
772 )?;
773 let member = at(path, &[&f.sig.ident.to_string()]);
774 self.map.record_cfg(member, &item_impl.attrs);
775 match f.sig.receiver() {
776 Some(recv) => {
777 if !receiver_is_supported(recv) {
778 return Err(syn::Error::new(
779 recv.span(),
780 "weaveffi: interface methods must take `&self` or `self: Arc<Self>`; \
781 use interior mutability (Mutex, RwLock, atomics) for mutable state, \
782 because the object is shared across the FFI boundary",
783 ));
784 }
785 let m = self.member(path, f, &iface.name, false)?;
786 iface.methods.push(m);
787 }
788 None if f.sig.asyncness.is_none() && returns_self(&f.sig.output, &iface.name) => {
789 let m = self.member(path, f, &iface.name, true)?;
790 iface.constructors.push(m);
791 }
792 None => {
793 let m = self.member(path, f, &iface.name, false)?;
794 iface.statics.push(m);
795 }
796 }
797 }
798 Ok(())
799 }
800
801 fn callback_interface(
808 &mut self,
809 path: &[String],
810 item: &syn::ItemTrait,
811 ) -> syn::Result<CallbackInterfaceDef> {
812 let mut methods = Vec::new();
813 for trait_item in &item.items {
814 let syn::TraitItem::Fn(f) = trait_item else {
815 continue;
816 };
817 no_member_cfg(
818 &f.attrs,
819 "a callback interface method",
820 "put the `#[cfg]` on the whole trait",
821 )?;
822 match f.sig.receiver() {
823 Some(recv) if recv.reference.is_some() && recv.mutability.is_none() => {}
824 Some(recv) => {
825 return Err(syn::Error::new(
826 recv.span(),
827 "weaveffi: callback interface methods must take `&self`",
828 ));
829 }
830 None => {
831 return Err(syn::Error::new(
832 f.sig.span(),
833 "weaveffi: callback interface methods must take `&self` (associated \
834 functions can't be implemented by the consumer)",
835 ));
836 }
837 }
838 if !returns_foreign_result(&f.sig.output) {
839 let span = match &f.sig.output {
840 syn::ReturnType::Type(_, ty) => ty.span(),
841 syn::ReturnType::Default => f.sig.ident.span(),
842 };
843 return Err(syn::Error::new(
844 span,
845 "weaveffi: a callback interface method must return \
846 `Result<T, weaveffi::ForeignError>` (or `Result<(), weaveffi::ForeignError>` \
847 for no value), because the consumer's implementation can fail and the \
848 `Err` is how its failure reaches you",
849 ));
850 }
851 let returns = self.return_type(&f.sig.output)?;
852 let method_path = at(path, &[&f.sig.ident.to_string()]);
853 self.map.record(method_path.clone(), f.sig.ident.span());
854 let mut method = self.function(&method_path, &f.sig, &f.attrs, returns)?;
855 method.throws = has_marker(&f.attrs, "throws");
856 methods.push(method);
857 }
858 Ok(CallbackInterfaceDef {
859 name: item.ident.to_string(),
860 doc: extract_doc(&item.attrs),
861 deprecated: parse_deprecated(&item.attrs).1,
862 methods,
863 })
864 }
865
866 fn fields(
869 &mut self,
870 path: &[String],
871 fields: &syn::FieldsNamed,
872 what: &str,
873 ) -> syn::Result<Vec<StructField>> {
874 let mut out = Vec::new();
875 for f in &fields.named {
876 no_member_cfg(&f.attrs, what, "put the `#[cfg]` on the whole type")?;
877 let name = f
878 .ident
879 .as_ref()
880 .ok_or_else(|| syn::Error::new(f.span(), "unnamed field"))?
881 .to_string();
882 self.map.record(at(path, &[&name]), f.span());
883 self.map
884 .record(at(path, &[&name, SourceMap::TYPE]), f.ty.span());
885 out.push(StructField {
886 ty: self.ty(&f.ty)?,
887 name,
888 doc: extract_doc(&f.attrs),
889 });
890 }
891 Ok(out)
892 }
893
894 fn record(&mut self, path: &[String], item: &syn::ItemStruct) -> syn::Result<StructDef> {
895 let syn::Fields::Named(named) = &item.fields else {
896 return Err(syn::Error::new(
897 item.span(),
898 "only named fields are supported for #[weaveffi::record]",
899 ));
900 };
901 Ok(StructDef {
902 name: item.ident.to_string(),
903 doc: extract_doc(&item.attrs),
904 deprecated: parse_deprecated(&item.attrs).1,
905 fields: self.fields(path, named, "a record field")?,
906 })
907 }
908
909 fn enumeration(&mut self, path: &[String], item: &syn::ItemEnum) -> syn::Result<EnumDef> {
910 let name = item.ident.to_string();
911
912 let is_rich = item
919 .variants
920 .iter()
921 .any(|v| !matches!(v.fields, syn::Fields::Unit));
922
923 if !is_rich && !has_repr_i32(&item.attrs) {
924 return Err(syn::Error::new(
925 item.span(),
926 format!(
927 "enum `{}` must have #[repr(i32)] to be a #[weaveffi::enumeration]",
928 item.ident
929 ),
930 ));
931 }
932
933 let mut next_value: i32 = 0;
934 let mut variants = Vec::new();
935 for v in &item.variants {
936 no_member_cfg(
937 &v.attrs,
938 "an enum variant",
939 "put the `#[cfg]` on the whole enum",
940 )?;
941 let value = match v.discriminant.as_ref() {
942 Some((_, expr)) => parse_discriminant(expr)?,
943 None if is_rich => next_value,
944 None => {
945 return Err(syn::Error::new(
946 v.span(),
947 format!(
948 "enum `{name}` variant `{}` must have an explicit discriminant",
949 v.ident
950 ),
951 ))
952 }
953 };
954 next_value = value.wrapping_add(1);
955 let variant = v.ident.to_string();
956 let vpath = at(path, &[&variant]);
957 self.map.record(vpath.clone(), v.ident.span());
958 let fields = match &v.fields {
959 syn::Fields::Unit => vec![],
960 syn::Fields::Named(named) => self.fields(&vpath, named, "an enum variant field")?,
961 syn::Fields::Unnamed(_) => {
962 return Err(syn::Error::new(
963 v.span(),
964 format!(
965 "enum `{name}` variant `{variant}`: tuple-style variants are not \
966 supported; use named fields"
967 ),
968 ))
969 }
970 };
971 variants.push(EnumVariant {
972 name: variant,
973 value,
974 doc: extract_doc(&v.attrs),
975 fields,
976 });
977 }
978 Ok(EnumDef {
979 name,
980 doc: extract_doc(&item.attrs),
981 deprecated: parse_deprecated(&item.attrs).1,
982 variants,
983 })
984 }
985
986 fn error_domain(&mut self, path: &[String], item: &syn::ItemEnum) -> syn::Result<ErrorDomain> {
1000 let mut codes = Vec::new();
1001 for v in &item.variants {
1002 no_member_cfg(
1003 &v.attrs,
1004 "an error code",
1005 "put the `#[cfg]` on the whole error enum",
1006 )?;
1007 let code = v.ident.to_string();
1008 let cpath = at(path, &[&code]);
1009 self.map.record(cpath.clone(), v.ident.span());
1010 let fields = match &v.fields {
1011 syn::Fields::Unit => vec![],
1012 syn::Fields::Named(named) => self.fields(&cpath, named, "an error code field")?,
1013 syn::Fields::Unnamed(_) => {
1014 return Err(syn::Error::new(
1015 v.span(),
1016 "weaveffi: #[weaveffi::error] payload variants must use named \
1017 fields (tuple-style variants are not supported)",
1018 ));
1019 }
1020 };
1021 let Some((_, expr)) = v.discriminant.as_ref() else {
1022 return Err(syn::Error::new(
1023 v.span(),
1024 format!(
1025 "weaveffi: error variant `{code}` must have an explicit discriminant \
1026 (its stable ABI error code)"
1027 ),
1028 ));
1029 };
1030 let doc = extract_doc(&v.attrs);
1031 let message = doc
1032 .as_deref()
1033 .and_then(|d| d.lines().next())
1034 .unwrap_or(&code)
1035 .to_string();
1036 codes.push(ErrorCode {
1037 code: parse_discriminant(expr)?,
1038 name: code,
1039 message,
1040 doc,
1041 fields,
1042 });
1043 }
1044 Ok(ErrorDomain {
1045 name: item.ident.to_string(),
1046 codes,
1047 })
1048 }
1049
1050 fn module(&mut self, parent: &[String], item_mod: &syn::ItemMod) -> syn::Result<Module> {
1052 let name = item_mod.ident.to_string();
1053 let path = at(parent, &[&name]);
1054 self.map.record(path.clone(), item_mod.ident.span());
1055 self.map.record_cfg(path.clone(), &item_mod.attrs);
1056 let mut module = Module {
1057 name,
1058 doc: extract_doc(&item_mod.attrs),
1059 functions: Vec::new(),
1060 interfaces: Vec::new(),
1061 callback_interfaces: Vec::new(),
1062 structs: Vec::new(),
1063 enums: Vec::new(),
1064 errors: None,
1065 modules: Vec::new(),
1066 };
1067 let Some((_, items)) = &item_mod.content else {
1068 return Err(syn::Error::new(
1069 item_mod.span(),
1070 "weaveffi: a #[weaveffi::module] must have an inline body (`mod name { ... }`); \
1071 the macro can't read a module from another file",
1072 ));
1073 };
1074 let declare = |map: &mut SourceMap, decl: &str, span: Span, attrs: &[syn::Attribute]| {
1076 let decl = at(&path, &[decl]);
1077 map.record(decl.clone(), span);
1078 map.record_cfg(decl, attrs);
1079 };
1080 for item in items {
1081 match item {
1082 syn::Item::Trait(t) if has_marker(&t.attrs, "callback_interface") => {
1083 let decl = t.ident.to_string();
1084 declare(&mut self.map, &decl, t.ident.span(), &t.attrs);
1085 let cb = self.callback_interface(&at(&path, &[&decl]), t)?;
1086 module.callback_interfaces.push(cb);
1087 }
1088 syn::Item::Fn(f) if has_marker(&f.attrs, "export") => {
1089 let decl = f.sig.ident.to_string();
1090 declare(&mut self.map, &decl, f.sig.ident.span(), &f.attrs);
1091 let returns = self.return_type(&f.sig.output)?;
1092 let func = self.function(&at(&path, &[&decl]), &f.sig, &f.attrs, returns)?;
1093 module.functions.push(func);
1094 }
1095 syn::Item::Struct(s) if has_marker(&s.attrs, "interface") => {
1096 let decl = s.ident.to_string();
1097 declare(&mut self.map, &decl, s.ident.span(), &s.attrs);
1098 module.interfaces.push(InterfaceDef {
1099 name: decl,
1100 doc: extract_doc(&s.attrs),
1101 deprecated: parse_deprecated(&s.attrs).1,
1102 constructors: vec![],
1103 methods: vec![],
1104 statics: vec![],
1105 });
1106 }
1107 syn::Item::Struct(s) if has_marker(&s.attrs, "record") => {
1108 let decl = s.ident.to_string();
1109 declare(&mut self.map, &decl, s.ident.span(), &s.attrs);
1110 let record = self.record(&at(&path, &[&decl]), s)?;
1111 module.structs.push(record);
1112 }
1113 syn::Item::Enum(e) if has_marker(&e.attrs, "error") => {
1114 if module.errors.is_some() {
1115 return Err(syn::Error::new(
1116 e.span(),
1117 "weaveffi: a module may declare at most one #[weaveffi::error] \
1118 domain",
1119 ));
1120 }
1121 let decl = e.ident.to_string();
1122 declare(&mut self.map, &decl, e.ident.span(), &e.attrs);
1123 module.errors = Some(self.error_domain(&at(&path, &[&decl]), e)?);
1124 }
1125 syn::Item::Enum(e) if has_marker(&e.attrs, "enumeration") => {
1126 let decl = e.ident.to_string();
1127 declare(&mut self.map, &decl, e.ident.span(), &e.attrs);
1128 let def = self.enumeration(&at(&path, &[&decl]), e)?;
1129 module.enums.push(def);
1130 }
1131 syn::Item::Mod(m) if has_marker(&m.attrs, "module") => {
1132 let child = self.module(&path, m)?;
1133 module.modules.push(child);
1134 }
1135 _ => {}
1136 }
1137 }
1138
1139 for item in items {
1143 let syn::Item::Impl(item_impl) = item else {
1144 continue;
1145 };
1146 if item_impl.trait_.is_some() {
1147 continue;
1148 }
1149 let Some(self_name) = type_path_ident(&item_impl.self_ty) else {
1150 continue;
1151 };
1152 if let Some(i) = module.interfaces.iter().position(|i| i.name == self_name) {
1153 let mut iface = std::mem::replace(
1154 &mut module.interfaces[i],
1155 InterfaceDef {
1156 name: String::new(),
1157 doc: None,
1158 deprecated: None,
1159 constructors: vec![],
1160 methods: vec![],
1161 statics: vec![],
1162 },
1163 );
1164 let result = self.members(&at(&path, &[&self_name]), item_impl, &mut iface);
1165 module.interfaces[i] = iface;
1166 result?;
1167 }
1168 }
1169 Ok(module)
1170 }
1171}
1172
1173pub fn extract_module(item_mod: &syn::ItemMod) -> syn::Result<(Module, SourceMap)> {
1189 let mut aliases = Aliases::new();
1190 collect_aliases(item_mod, &mut aliases);
1191 let mut extractor = Extractor {
1192 aliases: &aliases,
1193 map: SourceMap::default(),
1194 };
1195 let module = extractor.module(&[], item_mod)?;
1196 Ok((module, extractor.map))
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201 use super::*;
1202 use crate::ir::{Api, CURRENT_SCHEMA_VERSION};
1203
1204 fn api_from_src(src: &str) -> syn::Result<Api> {
1206 let file = syn::parse_file(src)?;
1207 let mut modules = Vec::new();
1208 for item in &file.items {
1209 if let syn::Item::Mod(item_mod) = item {
1210 if has_marker(&item_mod.attrs, "module") {
1211 modules.push(extract_module(item_mod)?.0);
1212 }
1213 }
1214 }
1215 Ok(Api {
1216 version: CURRENT_SCHEMA_VERSION.to_string(),
1217 modules,
1218 })
1219 }
1220
1221 fn one_module(src: &str) -> Module {
1222 let api = api_from_src(src).unwrap();
1223 assert_eq!(api.modules.len(), 1, "expected exactly one module");
1224 api.modules.into_iter().next().unwrap()
1225 }
1226
1227 #[test]
1228 fn extracts_exported_function() {
1229 let m = one_module(
1230 r#"
1231 #[weaveffi::module]
1232 mod math {
1233 #[weaveffi::export]
1234 pub fn add(a: i32, b: i32) -> i32 { a + b }
1235 }
1236 "#,
1237 );
1238 assert_eq!(m.name, "math");
1239 assert_eq!(m.functions.len(), 1);
1240 let f = &m.functions[0];
1241 assert_eq!(f.name, "add");
1242 assert_eq!(f.params[0].ty, TypeRef::Prim(Prim::I32));
1243 assert_eq!(f.returns, Some(TypeRef::Prim(Prim::I32)));
1244 assert!(!f.r#async);
1245 }
1246
1247 #[test]
1248 fn unmarked_module_is_ignored() {
1249 let api = api_from_src(
1250 r#"
1251 mod plain {
1252 #[weaveffi::export]
1253 pub fn add(a: i32) -> i32 { a }
1254 }
1255 "#,
1256 )
1257 .unwrap();
1258 assert!(api.modules.is_empty());
1259 }
1260
1261 #[test]
1262 fn result_return_peels_to_ok_type() {
1263 let m = one_module(
1264 r#"
1265 #[weaveffi::module]
1266 mod m {
1267 #[weaveffi::export]
1268 pub fn get(id: u64) -> Result<Contact, MyError> { todo!() }
1269 }
1270 "#,
1271 );
1272 assert_eq!(
1273 m.functions[0].returns,
1274 Some(TypeRef::Named("Contact".into()))
1275 );
1276 assert_eq!(m.functions[0].params[0].ty, TypeRef::Prim(Prim::U64));
1277 }
1278
1279 #[test]
1280 fn result_unit_return_is_none() {
1281 let m = one_module(
1282 r#"
1283 #[weaveffi::module]
1284 mod m {
1285 #[weaveffi::export]
1286 pub fn run() -> Result<(), MyError> { Ok(()) }
1287 }
1288 "#,
1289 );
1290 assert_eq!(m.functions[0].returns, None);
1291 }
1292
1293 #[test]
1294 fn async_fn_is_async() {
1295 let m = one_module(
1296 r#"
1297 #[weaveffi::module]
1298 mod m {
1299 #[weaveffi::export]
1300 pub async fn fetch(url: String) -> String { url }
1301 }
1302 "#,
1303 );
1304 assert!(m.functions[0].r#async);
1305 }
1306
1307 #[test]
1308 fn record_and_enumeration() {
1309 let m = one_module(
1310 r#"
1311 #[weaveffi::module]
1312 mod contacts {
1313 #[weaveffi::enumeration]
1314 #[repr(i32)]
1315 pub enum ContactType { Personal = 0, Work = 1 }
1316
1317 #[weaveffi::record]
1318 pub struct Contact {
1319 pub id: i64,
1320 pub email: Option<String>,
1321 pub kind: ContactType,
1322 }
1323 }
1324 "#,
1325 );
1326 assert_eq!(m.enums.len(), 1);
1327 assert_eq!(m.enums[0].variants.len(), 2);
1328 assert_eq!(m.structs.len(), 1);
1329 let s = &m.structs[0];
1330 assert_eq!(
1331 s.fields[1].ty,
1332 TypeRef::Optional(Box::new(TypeRef::Prim(Prim::String)))
1333 );
1334 assert_eq!(s.fields[2].ty, TypeRef::Named("ContactType".into()));
1335 }
1336
1337 #[test]
1338 fn enumeration_requires_repr_i32() {
1339 let err = api_from_src(
1340 r#"
1341 #[weaveffi::module]
1342 mod m {
1343 #[weaveffi::enumeration]
1344 pub enum Bad { A = 0 }
1345 }
1346 "#,
1347 )
1348 .unwrap_err();
1349 assert!(err.to_string().contains("repr(i32)"));
1350 }
1351
1352 #[test]
1353 fn references_and_arcs_name_the_pointee() {
1354 let m = one_module(
1355 r#"
1356 #[weaveffi::module]
1357 mod m {
1358 #[weaveffi::export]
1359 pub fn open(name: &str, data: &[u8], tags: &[String]) -> Arc<Store> { todo!() }
1360 #[weaveffi::export]
1361 pub fn peek(store: &Store, other: Option<Arc<Store>>) -> Option<Arc<Store>> { None }
1362 }
1363 "#,
1364 );
1365 let open = &m.functions[0];
1366 assert_eq!(open.params[0].ty, TypeRef::Prim(Prim::String));
1367 assert_eq!(open.params[1].ty, TypeRef::Prim(Prim::Bytes));
1368 assert_eq!(
1369 open.params[2].ty,
1370 TypeRef::List(Box::new(TypeRef::Prim(Prim::String)))
1371 );
1372 assert_eq!(open.returns, Some(TypeRef::Named("Store".into())));
1373 let peek = &m.functions[1];
1374 assert_eq!(peek.params[0].ty, TypeRef::Named("Store".into()));
1375 let opt_store = TypeRef::Optional(Box::new(TypeRef::Named("Store".into())));
1376 assert_eq!(peek.params[1].ty, opt_store);
1377 assert_eq!(peek.returns, Some(opt_store));
1378 }
1379
1380 #[test]
1381 fn raw_pointers_and_boxes_are_rejected() {
1382 let err = api_from_src(
1383 r#"
1384 #[weaveffi::module]
1385 mod m {
1386 #[weaveffi::export]
1387 pub fn close(token: *mut Token) {}
1388 }
1389 "#,
1390 )
1391 .unwrap_err();
1392 assert!(err.to_string().contains("raw pointers"));
1393 let err = api_from_src(
1394 r#"
1395 #[weaveffi::module]
1396 mod m {
1397 #[weaveffi::export]
1398 pub fn listen(l: Box<dyn Listener>) {}
1399 }
1400 "#,
1401 )
1402 .unwrap_err();
1403 assert!(err.to_string().contains("Arc<dyn Trait>"));
1404 let err = api_from_src(
1405 r#"
1406 #[weaveffi::module]
1407 mod m {
1408 #[weaveffi::export]
1409 pub fn bump(counter: &mut i32) {}
1410 }
1411 "#,
1412 )
1413 .unwrap_err();
1414 assert!(err.to_string().contains("&mut"));
1415 }
1416
1417 #[test]
1418 fn cancel_token_param_is_skipped() {
1419 let m = one_module(
1420 r#"
1421 #[weaveffi::module]
1422 mod m {
1423 #[weaveffi::export]
1424 #[weaveffi::cancellable]
1425 pub async fn compact(store: Arc<Store>, cancel: weaveffi::CancelToken) -> i64 { 0 }
1426 }
1427 "#,
1428 );
1429 let f = &m.functions[0];
1430 assert!(f.r#async);
1431 assert!(f.cancellable);
1432 assert_eq!(f.params.len(), 1);
1434 assert_eq!(f.params[0].name, "store");
1435 assert_eq!(f.params[0].ty, TypeRef::Named("Store".into()));
1436 }
1437
1438 #[test]
1439 fn interface_members_and_receivers() {
1440 let m = one_module(
1441 r#"
1442 #[weaveffi::module]
1443 mod m {
1444 #[weaveffi::interface]
1445 pub struct Store;
1446 impl Store {
1447 pub fn open(path: String) -> Result<Arc<Self>, StoreError> { todo!() }
1448 pub fn new() -> Self { Store }
1449 pub fn len(&self) -> u64 { 0 }
1450 pub fn share(self: Arc<Self>) -> Arc<Self> { self }
1451 pub fn default_path() -> String { String::new() }
1452 fn private(&self) {}
1453 }
1454 }
1455 "#,
1456 );
1457 let s = &m.interfaces[0];
1458 assert_eq!(s.constructors.len(), 2);
1459 assert!(s.constructors[0].throws);
1460 assert_eq!(s.constructors[0].returns, None);
1461 assert_eq!(s.methods.len(), 2);
1462 assert_eq!(s.methods[1].returns, Some(TypeRef::Named("Store".into())));
1463 assert_eq!(s.statics.len(), 1);
1464 }
1465
1466 #[test]
1467 fn doc_links_unwrap_to_inline_code() {
1468 assert_eq!(
1469 unwrap_doc_links("Fails with [`KvError::KeyNotFound`] for [`Store`]."),
1470 "Fails with `KvError::KeyNotFound` for `Store`."
1471 );
1472 assert_eq!(
1473 unwrap_doc_links("See [`docs`](https://x.dev) and [`a`][b]; [`c`]: d"),
1474 "See [`docs`](https://x.dev) and [`a`][b]; [`c`]: d"
1475 );
1476 assert_eq!(
1477 unwrap_doc_links("A `[string]`, [`]`, [x]."),
1478 "A `[string]`, [`]`, [x]."
1479 );
1480 assert_eq!(unwrap_doc_links("open [`"), "open [`");
1481
1482 let m = one_module(
1483 r#"
1484 /// Uses [`Store`].
1485 #[weaveffi::module]
1486 mod m {
1487 /// Returns [`Store::size`] items.
1488 ///
1489 /// ```
1490 /// let v = [`x`];
1491 /// ```
1492 #[weaveffi::export]
1493 pub fn count() -> i32 { 0 }
1494 }
1495 "#,
1496 );
1497 assert_eq!(
1498 m.functions[0].doc.as_deref(),
1499 Some("Returns `Store::size` items.\n\n```\nlet v = [`x`];\n```")
1500 );
1501 }
1502
1503 #[test]
1504 fn async_associated_functions_returning_the_interface_are_statics() {
1505 let m = one_module(
1506 r#"
1507 #[weaveffi::module]
1508 mod m {
1509 #[weaveffi::interface]
1510 pub struct Store;
1511 impl Store {
1512 pub async fn open_async(path: String) -> Result<Arc<Self>, StoreError> { todo!() }
1513 pub async fn fresh() -> Self { Store }
1514 pub async fn named() -> Store { Store }
1515 }
1516 }
1517 "#,
1518 );
1519 let s = &m.interfaces[0];
1520 assert!(s.constructors.is_empty());
1521 assert_eq!(s.statics.len(), 3);
1522 for f in &s.statics {
1523 assert!(f.r#async, "{} is async", f.name);
1524 assert_eq!(
1525 f.returns,
1526 Some(TypeRef::Named("Store".into())),
1527 "{}",
1528 f.name
1529 );
1530 }
1531 assert!(s.statics[0].throws);
1532 }
1533
1534 #[test]
1535 fn mut_self_receiver_is_rejected() {
1536 let err = api_from_src(
1537 r#"
1538 #[weaveffi::module]
1539 mod m {
1540 #[weaveffi::interface]
1541 pub struct Store;
1542 impl Store {
1543 pub fn bump(&mut self) {}
1544 }
1545 }
1546 "#,
1547 )
1548 .unwrap_err();
1549 assert!(err.to_string().contains("&self"));
1550 }
1551
1552 #[test]
1553 fn callback_interface_trait() {
1554 let m = one_module(
1555 r#"
1556 #[weaveffi::module]
1557 mod m {
1558 /// Receives messages.
1559 #[weaveffi::callback_interface]
1560 pub trait Listener: Send + Sync {
1561 fn on_message(&self, text: String, meta: Option<Meta>) -> Result<(), ForeignError>;
1562 fn level(&self) -> Result<i32, weaveffi::ForeignError>;
1563 #[weaveffi::throws]
1564 fn label(&self) -> Result<String, ForeignError>;
1565 }
1566 #[weaveffi::export]
1567 pub fn subscribe(listener: Arc<dyn Listener>) {}
1568 #[weaveffi::export]
1569 pub fn subscribe_bounded(listener: Option<Arc<dyn Listener + Send + Sync>>) {}
1570 }
1571 "#,
1572 );
1573 assert_eq!(m.callback_interfaces.len(), 1);
1574 let cb = &m.callback_interfaces[0];
1575 assert_eq!(cb.name, "Listener");
1576 assert_eq!(cb.doc.as_deref(), Some("Receives messages."));
1577 let methods = &cb.methods;
1578 assert_eq!(methods[0].params.len(), 2);
1579 assert_eq!(methods[0].returns, None);
1580 assert!(!methods[0].throws);
1581 assert_eq!(methods[1].returns, Some(TypeRef::Prim(Prim::I32)));
1582 assert!(!methods[1].throws);
1583 assert_eq!(methods[2].returns, Some(TypeRef::Prim(Prim::String)));
1584 assert!(methods[2].throws);
1585 assert_eq!(
1586 m.functions[0].params[0].ty,
1587 TypeRef::Named("Listener".into())
1588 );
1589 assert_eq!(
1590 m.functions[1].params[0].ty,
1591 TypeRef::Optional(Box::new(TypeRef::Named("Listener".into())))
1592 );
1593 }
1594
1595 #[test]
1596 fn callback_methods_must_return_foreign_results() {
1597 for ret in ["", "-> i32", "-> Result<i32, String>"] {
1598 let src = format!(
1599 "#[weaveffi::module] mod m {{ #[weaveffi::callback_interface] \
1600 pub trait L {{ fn f(&self) {ret}; }} }}"
1601 );
1602 let err = api_from_src(&src).unwrap_err();
1603 assert!(err.to_string().contains("ForeignError"), "{ret}: {err}");
1604 }
1605 }
1606
1607 #[test]
1608 fn aliases_are_substituted() {
1609 let m = one_module(
1610 r#"
1611 #[weaveffi::module]
1612 mod m {
1613 pub type Id = u64;
1614 pub type Ids = Vec<Id>;
1615 #[weaveffi::export]
1616 pub fn get(id: Id, all: Ids) -> Option<Id> { None }
1617 #[weaveffi::module]
1618 mod inner {
1619 #[weaveffi::export]
1620 pub fn deep(id: super::Id) {}
1621 }
1622 }
1623 "#,
1624 );
1625 let f = &m.functions[0];
1626 assert_eq!(f.params[0].ty, TypeRef::Prim(Prim::U64));
1627 assert_eq!(
1628 f.params[1].ty,
1629 TypeRef::List(Box::new(TypeRef::Prim(Prim::U64)))
1630 );
1631 assert_eq!(
1632 f.returns,
1633 Some(TypeRef::Optional(Box::new(TypeRef::Prim(Prim::U64))))
1634 );
1635 assert_eq!(
1636 m.modules[0].functions[0].params[0].ty,
1637 TypeRef::Prim(Prim::U64)
1638 );
1639 }
1640
1641 #[test]
1642 fn member_cfgs_and_out_of_line_modules_are_rejected() {
1643 for (src, needle) in [
1644 (
1645 "#[weaveffi::module] mod m { #[weaveffi::record] pub struct R { #[cfg(unix)] pub x: i32 } }",
1646 "record field",
1647 ),
1648 (
1649 "#[weaveffi::module] mod m { #[weaveffi::interface] pub struct S; impl S { #[cfg(unix)] pub fn f(&self) {} } }",
1650 "interface member",
1651 ),
1652 (
1653 "#[weaveffi::module] mod m { #[weaveffi::enumeration] #[repr(i32)] pub enum E { #[cfg(unix)] A = 0 } }",
1654 "enum variant",
1655 ),
1656 (
1657 "#[weaveffi::module] mod m { #[weaveffi::module] mod inner; }",
1658 "inline body",
1659 ),
1660 ] {
1661 let err = api_from_src(src).unwrap_err().to_string();
1662 assert!(err.contains(needle), "{src}: {err}");
1663 }
1664 }
1665
1666 #[test]
1667 fn the_source_map_records_spans_and_item_cfgs() {
1668 let file: syn::File = syn::parse_str(
1669 r#"
1670 #[weaveffi::module]
1671 mod m {
1672 #[cfg(unix)]
1673 #[weaveffi::export]
1674 pub fn f(x: usize) {}
1675 #[weaveffi::interface]
1676 pub struct S;
1677 #[cfg(feature = "s")]
1678 impl S {
1679 pub fn get(&self) -> i32 { 0 }
1680 }
1681 }
1682 "#,
1683 )
1684 .unwrap();
1685 let syn::Item::Mod(item) = &file.items[0] else {
1686 panic!("a module")
1687 };
1688 let (_, map) = extract_module(item).unwrap();
1689 let path = |p: &[&str]| p.iter().map(ToString::to_string).collect::<Vec<_>>();
1690 assert_eq!(map.spans(&path(&["m", "f"])).len(), 1);
1691 assert_eq!(map.spans(&path(&["m", "f", "x", SourceMap::TYPE])).len(), 1);
1692 assert_eq!(map.cfg(&path(&["m", "f"])).len(), 1);
1693 assert_eq!(map.cfg(&path(&["m", "S", "get"])).len(), 1);
1694 assert!(map.cfg(&path(&["m", "S"])).is_empty());
1695 }
1696
1697 #[test]
1698 fn callback_interface_methods_need_self() {
1699 let err = api_from_src(
1700 r#"
1701 #[weaveffi::module]
1702 mod m {
1703 #[weaveffi::callback_interface]
1704 pub trait Listener {
1705 fn make() -> i32;
1706 }
1707 }
1708 "#,
1709 )
1710 .unwrap_err();
1711 assert!(err.to_string().contains("&self"));
1712 }
1713
1714 #[test]
1715 fn nested_modules() {
1716 let m = one_module(
1717 r#"
1718 #[weaveffi::module]
1719 mod outer {
1720 #[weaveffi::export]
1721 pub fn top() -> i32 { 0 }
1722 #[weaveffi::module]
1723 mod inner {
1724 #[weaveffi::export]
1725 pub fn deep(x: bool) -> bool { x }
1726 }
1727 }
1728 "#,
1729 );
1730 assert_eq!(m.modules.len(), 1);
1731 assert_eq!(m.modules[0].name, "inner");
1732 assert_eq!(m.modules[0].functions[0].name, "deep");
1733 }
1734}