diff --git a/.gitignore b/.gitignore index 791d974674..4bd7c4df25 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ xcuserdata docs/manual/src/internals/api examples/app/ios/Generated examples/app/ios/IOSApp.xcodeproj/project.xcworkspace/xcshareddata/ +.direnv diff --git a/Cargo.lock b/Cargo.lock index 9a0762e010..126eaf09d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2041,7 +2041,7 @@ dependencies = [ [[package]] name = "uniffi_docs" -version = "0.24.3" +version = "0.25.0" dependencies = [ "anyhow", "indoc", diff --git a/docker/cargo-docker.sh b/docker/cargo-docker.sh index 1ed35f3063..2621e54871 100755 --- a/docker/cargo-docker.sh +++ b/docker/cargo-docker.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Key points for these cmd-line args: # * run a transient image that deletes itself on successful completion diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt index 56f685225e..72924d1bfa 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ErrorTemplate.kt @@ -3,10 +3,12 @@ {%- let canonical_type_name = type_|error_canonical_name %} {% if e.is_flat() %} +{% let struct = e %}{% include "StructureDocsTemplate.kt" %} sealed class {{ type_name }}(message: String): Exception(message){% if contains_object_references %}, Disposable {% endif %} { // Each variant is a nested class // Flat enums carries a string error message, so no special implementation is necessary. {% for variant in e.variants() -%} + {% include "EnumVariantDocsTemplate.kt" %} class {{ variant|error_variant|type_name }}(message: String) : {{ type_name }}(message) {% endfor %} @@ -15,9 +17,11 @@ sealed class {{ type_name }}(message: String): Exception(message){% if contains_ } } {%- else %} +{% let struct = e %}{% include "StructureDocsTemplate.kt" %} sealed class {{ type_name }}: Exception(){% if contains_object_references %}, Disposable {% endif %} { // Each variant is a nested class {% for variant in e.variants() -%} + {% include "EnumVariantDocsTemplate.kt" %} {%- let variant_name = variant|error_variant|type_name %} class {{ variant_name }}( {% for field in variant.fields() -%} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt index 0fd2e85a1e..f5cb4cfb29 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt @@ -3,6 +3,7 @@ {%- let (interface_name, impl_class_name) = obj|object_names %} {%- let methods = obj.methods() %} +{% let struct = obj %}{% include "StructureDocsTemplate.kt" %} {% include "Interface.kt" %} class {{ impl_class_name }}( @@ -33,6 +34,8 @@ class {{ impl_class_name }}( } {% for meth in obj.methods() -%} + {%- let func = meth -%} + {%- include "FunctionDocsTemplate.kt" -%} {%- match meth.throws_type() -%} {%- when Some with (throwable) %} @Throws({{ throwable|error_type_name }}::class) diff --git a/uniffi_bindgen/src/bindings/python/templates/EnumVariantDocsTemplate.py b/uniffi_bindgen/src/bindings/python/templates/EnumVariantDocsTemplate.py index 9e26b5e7e9..52d52bcc6d 100644 --- a/uniffi_bindgen/src/bindings/python/templates/EnumVariantDocsTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/EnumVariantDocsTemplate.py @@ -1,6 +1,6 @@ {% match variant.documentation() -%} -{% when Some with (docs) %}""" + {% when Some with (docs) %}""" {% for line in docs.lines() %} {{ line }} {% endfor %} """ -{%- when None %} -{%- endmatch %} \ No newline at end of file + {%- when None %} +{%- endmatch %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py index 26a1e6452a..1256f04a22 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py @@ -9,15 +9,17 @@ class {{ type_name }}(Exception): _UniffiTemp{{ type_name }} = {{ type_name }} -class {{ type_name }}: # type: ignore +class {{ type_name }}: # type: ignore{% let struct = e %}{% include "StructureDocsTemplate.py" %} {%- for variant in e.variants() -%} {%- let variant_type_name = variant.name()|class_name -%} {%- if e.is_flat() %} class {{ variant_type_name }}(_UniffiTemp{{ type_name }}): + {% include "ErrorVariantDocsTemplate.py" %} def __repr__(self): return "{{ type_name }}.{{ variant_type_name }}({})".format(repr(str(self))) {%- else %} class {{ variant_type_name }}(_UniffiTemp{{ type_name }}): + {% include "ErrorVariantDocsTemplate.py" %} def __init__(self{% for field in variant.fields() %}, {{ field.name()|var_name }}{% endfor %}): {%- if variant.has_fields() %} super().__init__(", ".join([ diff --git a/uniffi_bindgen/src/bindings/python/templates/ErrorVariantDocsTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ErrorVariantDocsTemplate.py new file mode 100644 index 0000000000..aeadbe1431 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/ErrorVariantDocsTemplate.py @@ -0,0 +1,6 @@ +{% match variant.documentation() -%} + {% when Some with (docs) %}""" + {% for line in docs.lines() %} {{ line }} + {% endfor %} """ + {%- when None %} +{%- endmatch %} diff --git a/uniffi_bindgen/src/bindings/python/templates/macros.py b/uniffi_bindgen/src/bindings/python/templates/macros.py index ef3b1bb94d..e5d95daaab 100644 --- a/uniffi_bindgen/src/bindings/python/templates/macros.py +++ b/uniffi_bindgen/src/bindings/python/templates/macros.py @@ -101,6 +101,8 @@ {% if meth.is_async() %} def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}): + {%- let func = meth -%} + {% include "MethodDocsTemplate.py" %} {%- call setup_args_extra_indent(meth) %} return _uniffi_rust_call_async( _UniffiLib.{{ meth.ffi_func().name() }}( @@ -131,6 +133,8 @@ def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}): {%- when Some with (return_type) %} def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> "{{ return_type|type_name }}": + {%- let func = meth -%} + {% include "MethodDocsTemplate.py" %} {%- call setup_args_extra_indent(meth) %} return {{ return_type|lift_fn }}( {% call to_ffi_call_with_prefix("self._pointer", meth) %} @@ -139,6 +143,8 @@ def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> "{{ return_typ {%- when None %} def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}): + {%- let func = meth -%} + {% include "MethodDocsTemplate.py" %} {%- call setup_args_extra_indent(meth) %} {% call to_ffi_call_with_prefix("self._pointer", meth) %} {% endmatch %} diff --git a/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb b/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb index a7e26370c8..285b8988bf 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb @@ -22,13 +22,16 @@ def to_s {%- for e in ci.enum_definitions() %} {% if ci.is_name_used_as_error(e.name()) %} {% if e.is_flat() %} +{% include "EnumDocsTemplate.rb" -%} class {{ e.name()|class_name_rb }} {%- for variant in e.variants() %} + {% include "EnumVariantDocsTemplate.rb" -%} {{ variant.name()|class_name_rb }} = Class.new StandardError {%- endfor %} {% else %} module {{ e.name()|class_name_rb }} {%- for variant in e.variants() %} + {% include "EnumVariantDocsTemplate.rb" -%} class {{ variant.name()|class_name_rb }} < StandardError def initialize({% for field in variant.fields() %}{{ field.name()|var_name_rb }}{% if !loop.last %}, {% endif %}{% endfor %}) {%- for field in variant.fields() %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift index 786091395b..1ea389de22 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -1,13 +1,16 @@ +{% let struct = e %}{% include "StructureDocsTemplate.swift" %} public enum {{ type_name }} { {% if e.is_flat() %} {% for variant in e.variants() %} + {% include "EnumVariantDocsTemplate.swift" %} // Simple error enums only carry a message case {{ variant.name()|class_name }}(message: String) {% endfor %} {%- else %} {% for variant in e.variants() %} + {% include "EnumVariantDocsTemplate.swift" %} case {{ variant.name()|class_name }}{% if variant.fields().len() > 0 %}({% call swift::field_list_decl(variant) %}){% endif -%} {% endfor %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift index 5c4dcb5b0b..087f2e7173 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ObjectTemplate.swift @@ -2,6 +2,7 @@ {%- let (protocol_name, impl_class_name) = obj|object_names %} {%- let methods = obj.methods() %} +{% let struct = obj %}{% include "StructureDocsTemplate.swift" %} {% include "Protocol.swift" %} public class {{ impl_class_name }}: {{ protocol_name }} { @@ -41,6 +42,8 @@ public class {{ impl_class_name }}: {{ protocol_name }} { {% for meth in obj.methods() -%} {%- if meth.is_async() %} + {%- let func = meth -%} + {%- include "FunctionDocsTemplate.swift" %} public func {{ meth.name()|fn_name }}({%- call swift::arg_list_decl(meth) -%}) async {% call swift::throws(meth) %}{% match meth.return_type() %}{% when Some with (return_type) %} -> {{ return_type|type_name }}{% when None %}{% endmatch %} { return {% call swift::try(meth) %} await uniffiRustCallAsync( rustFutureFunc: { diff --git a/uniffi_bindgen/src/library_mode.rs b/uniffi_bindgen/src/library_mode.rs index f170ea5e91..ed69f2cb00 100644 --- a/uniffi_bindgen/src/library_mode.rs +++ b/uniffi_bindgen/src/library_mode.rs @@ -100,7 +100,10 @@ pub fn generate_external_bindings( } } - for source in sources.iter() { + for source in &mut sources { + // Assumes a lib.rs for the source package containing bindings + let path = &source.package.manifest_path.with_file_name("src/lib.rs"); + source.config.update_documentation(&mut source.ci, path)?; binding_generator.write_bindings(&source.ci, &source.config, out_dir)?; } diff --git a/uniffi_docs/src/lib.rs b/uniffi_docs/src/lib.rs index 3cb8e08d63..63defb8f7d 100644 --- a/uniffi_docs/src/lib.rs +++ b/uniffi_docs/src/lib.rs @@ -120,6 +120,24 @@ struct Impl { methods: HashMap, } +#[derive(Debug, PartialEq, Eq)] +struct Trait { + /// The docs on the trait itself + description: String, + /// Methods documentation + methods: HashMap, +} + +impl Into for Trait { + fn into(self) -> Structure { + Structure { + description: self.description, + members: HashMap::default(), + methods: self.methods, + } + } +} + #[derive(Debug, PartialEq, Eq)] pub struct Documentation { pub functions: HashMap, @@ -178,7 +196,9 @@ fn traverse_module_tree>(path: P) -> Result { path.as_ref().with_file_name(format!("{name}/mod.rs")) }; - source_code_buff.push_str(&traverse_module_tree(to_traverse_further)?) + if to_traverse_further.exists() { + source_code_buff.push_str(&traverse_module_tree(to_traverse_further)?) + } } } @@ -193,6 +213,44 @@ pub fn extract_documentation(source_code: &str) -> Result { let mut structures = HashMap::new(); let mut impls = HashMap::new(); + // we build traits up first so we know they're all there to be used when encountering impls later + let mut traits: HashMap = HashMap::new(); + + // first pass to get trait documentation only + for item in file.items.iter() { + match item { + syn::Item::Trait(item) => { + if let Some(description) = extract_doc_comment(&item.attrs) { + let name = item.ident.to_string(); + let methods = item + .items + .iter() + .filter_map(|item| { + if let syn::TraitItem::Method(method) = item { + let name = method.sig.ident.to_string(); + extract_doc_comment(&method.attrs).map(|doc| (name, doc)) + } else { + None + } + }) + .map(|(name, description)| { + (name, Function::from_str(&description).unwrap()) + }) + .collect(); + + traits.insert( + name, + Trait { + description, + methods, + }, + ); + } + } + _ => (), // other item types are ignored, + } + } + for item in file.items.into_iter() { match item { syn::Item::Enum(item) => { @@ -246,28 +304,48 @@ pub fn extract_documentation(source_code: &str) -> Result { } } syn::Item::Impl(item) => { - if item.trait_.is_none() { - if let syn::Type::Path(path) = *item.self_ty { - let name = path.path.segments[0].ident.to_string(); - - let methods = item - .items - .into_iter() - .filter_map(|item| { - if let syn::ImplItem::Method(method) = item { + if let syn::Type::Path(path) = *item.self_ty { + let name = path.path.segments[0].ident.to_string(); + let maybe_trait_name = item.trait_.and_then(|t| match t { + (None, syn::Path { segments, .. }, _) => { + segments.first().map(|segment| segment.ident.to_string()) + } + _ => None, + }); + let methods: HashMap = item + .items + .into_iter() + .filter_map(|inner_item| { + if let syn::ImplItem::Method(method) = inner_item { + // if this is a trait impl, pull the doc from the trait for this method + if let Some(trait_name) = &maybe_trait_name { + let method_name = method.sig.ident.to_string(); + traits + .get(trait_name) + .and_then(|trait_doc| trait_doc.methods.get(&method_name)) + .map(|method_doc| { + (method_name, method_doc.description.clone()) + }) + } else { + // if this isn't a trait impl (or there wasn't a doc for the trait method), get the + // doc directly on the method let name = method.sig.ident.to_string(); extract_doc_comment(&method.attrs).map(|doc| (name, doc)) - } else { - None } - }) - .map(|(name, description)| { - (name, Function::from_str(&description).unwrap()) - }) - .collect(); - - impls.insert(name, Impl { methods }); - } + } else { + None + } + }) + .map(|(name, description)| { + (name, Function::from_str(&description).unwrap()) + }) + .collect(); + impls + .entry(name) + .and_modify(|i: &mut Impl| + // this is safe because impls can't have conflicting method names for the same struct + i.methods.extend(methods.clone())) + .or_insert(Impl { methods }); } } syn::Item::Fn(item) => { @@ -286,6 +364,10 @@ pub fn extract_documentation(source_code: &str) -> Result { } } + for (name, trait_) in traits { + structures.insert(name, trait_.into()); + } + Ok(Documentation { functions, structures, @@ -402,6 +484,12 @@ mod tests { } } + impl Animal for Person { + fn eat(&self, food: String) -> String { + format!("{} ate {food}.", self.get_name()) + } + } + /// Create hello message to a pet. /// /// # Arguments @@ -426,6 +514,12 @@ mod tests { /// A letter 'C'. C, } + + /// Functionality common to animals. + pub trait Animal { + /// Get a message about the Animal eating. + fn eat(&self, food: String) -> String; + } } .to_string(); @@ -469,6 +563,18 @@ mod tests { return_description: None, }, ); + methods.insert( + "eat".to_string(), + Function { + description: indoc! {" + Get a message about the Animal eating. + "} + .trim() + .to_string(), + arguments_descriptions: HashMap::new(), + return_description: None, + }, + ); structures.insert( "Person".to_string(), @@ -506,6 +612,29 @@ mod tests { }, ); + let mut methods = HashMap::new(); + methods.insert( + "eat".to_string(), + Function { + description: indoc! {" + Get a message about the Animal eating. + "} + .trim() + .to_string(), + arguments_descriptions: HashMap::new(), + return_description: None, + }, + ); + + structures.insert( + "Animal".to_string(), + Structure { + description: "Functionality common to animals.".to_string(), + members: HashMap::new(), + methods, + }, + ); + let expected = Documentation { functions, structures,