From 28c4513985fe31d6dfcb8fd6a7d5b48eff6f44f2 Mon Sep 17 00:00:00 2001 From: Yarvin Date: Fri, 26 Jul 2024 15:50:50 +0200 Subject: [PATCH] Docs comments do not show up if only some of the class members/methods are documented - Extend docs test case to feature some undocumented members of godot instances - Change map&unwrap in docs.rs to filter_map (allow to document only subset of all the properties of a given class) - Generate documentation blocks for signals & constants on compile-time --- godot-core/src/docs.rs | 61 ++++++++++---- godot-core/src/registry/plugin.rs | 4 +- godot-macros/src/docs.rs | 82 ++++++++++++------- godot/tests/docs.rs | 52 ++++++++++-- godot/tests/{ => test_data}/docs.xml | 25 ++++-- .../rust/src/register_tests/constant_test.rs | 2 +- 6 files changed, 163 insertions(+), 63 deletions(-) rename godot/tests/{ => test_data}/docs.xml (67%) diff --git a/godot-core/src/docs.rs b/godot-core/src/docs.rs index 4f6da61e9..a4a37ab16 100644 --- a/godot-core/src/docs.rs +++ b/godot-core/src/docs.rs @@ -23,40 +23,56 @@ pub struct StructDocs { pub members: &'static str, } -/// Created for documentation on +/// Keeps documentation for Inherent Implementation such as: /// ```ignore /// #[godot_api] /// impl Struct { /// #[func] /// /// This function panics! /// fn panic() -> f32 { panic!() } +/// #[signal] +/// /// this signal signals +/// fn documented_signal(p: Vector3, w: f64); +/// #[constant] +/// /// this constant consts +/// const CON: i64 = 42; +/// /// } /// ``` #[derive(Clone, Copy, Debug, Default)] pub struct InherentImplDocs { - pub methods: &'static str, - pub signals: &'static str, - pub constants: &'static str, + pub methods: Option<&'static str>, + pub signals_block: &'static str, + pub constants_block: &'static str, } #[derive(Default)] struct DocPieces { definition: StructDocs, inherent: InherentImplDocs, - virtual_methods: &'static str, + virtual_methods: Option<&'static str>, } -#[doc(hidden)] /// This function scours the registered plugins to find their documentation pieces, /// and strings them together. /// -/// It returns an iterator over XML documents. +/// Returns an iterator over XML documents. +/// +/// Documentation for signals and constants is being processed at compile time +/// and can take the form of an already formatted XML ``, or an +/// empty string if no such attribute has been documented. +/// +/// Since documentation for methods comes from two different sources +/// -– inherent Implementation (`methods`) and I* trait implementation (`virtual_method_docs`) –- +/// it is undesirable to merge them at compile time. Instead, they are being kept as an optional +/// strings of not-yet-parented XML tags +/// (or nothing if no method has been documented). pub fn gather_xml_docs() -> impl Iterator { let mut map = HashMap::<&'static str, DocPieces>::new(); crate::private::iterate_plugins(|x| match x.item { - PluginItem::InherentImpl { - docs: Some(docs), .. - } => map.entry(x.class_name.as_str()).or_default().inherent = docs, + PluginItem::InherentImpl { docs, .. } => { + map.entry(x.class_name.as_str()).or_default().inherent = docs + } PluginItem::ITraitImpl { virtual_method_docs, .. @@ -79,20 +95,33 @@ pub fn gather_xml_docs() -> impl Iterator { let InherentImplDocs { methods, - signals, - constants, + signals_block, + constants_block, } = pieces.inherent; let virtual_methods = pieces.virtual_methods; - let brief = description.split_once("[br]").map(|(x, _)| x).unwrap_or_default(); + let methods_block = if methods.is_some() || virtual_methods.is_some() { + format!( + "{m}{vm}", + m=methods.unwrap_or_default(), + vm=virtual_methods.unwrap_or_default()) + } else { + String::new() + }; + + let brief = description + .split_once("[br]") + .map(|(x, _)| x) + .unwrap_or_default(); + format!(r#" {brief} {description} -{methods}{virtual_methods} -{constants} -{signals} +{methods_block} +{constants_block} +{signals_block} {members} "#) }, diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index d5220cf0e..8c7cf956d 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -108,14 +108,14 @@ pub enum PluginItem { /// Always present since that's the entire point of this `impl` block. register_methods_constants_fn: ErasedRegisterFn, #[cfg(all(since_api = "4.3", feature = "docs"))] - docs: Option, + docs: InherentImplDocs, }, /// Collected from `#[godot_api] impl I... for MyClass`. ITraitImpl { #[cfg(all(since_api = "4.3", feature = "docs"))] /// Virtual method documentation. - virtual_method_docs: &'static str, + virtual_method_docs: Option<&'static str>, /// Callback to user-defined `register_class` function. user_register_fn: Option, diff --git a/godot-macros/src/docs.rs b/godot-macros/src/docs.rs index b62be3129..280596a76 100644 --- a/godot-macros/src/docs.rs +++ b/godot-macros/src/docs.rs @@ -21,8 +21,8 @@ pub fn make_definition_docs( let members = members .into_iter() .filter(|x| x.var.is_some() | x.export.is_some()) - .map(member) - .collect::>()?; + .filter_map(member) + .collect::(); Some(quote! { docs: ::godot::docs::StructDocs { base: #base, @@ -39,52 +39,72 @@ pub fn make_inherent_impl_docs( constants: &[ConstDefinition], signals: &[SignalDefinition], ) -> TokenStream { + /// Generates TokenStream containing field definitions for documented methods and documentation blocks for constants and signals. fn pieces( functions: &[FuncDefinition], signals: &[SignalDefinition], constants: &[ConstDefinition], - ) -> Option { + ) -> TokenStream { + let to_tagged = |s: String, tag: &str| -> String { + if s.is_empty() { + s + } else { + format!("<{tag}>{s}") + } + }; + + let signals_block = to_tagged( + signals + .iter() + .filter_map(make_signal_docs) + .collect::(), + "signals", + ); + let constants_block = to_tagged( + constants + .iter() + .map(|ConstDefinition { raw_constant }| raw_constant) + .filter_map(make_constant_docs) + .collect::(), + "constants", + ); + let methods = functions .iter() - .map(make_method_docs) - .collect::>()?; - let signals = signals - .iter() - .map(make_signal_docs) - .collect::>()?; - let constants = constants - .iter() - .map(|ConstDefinition { raw_constant: x }| x) - .map(make_constant_docs) - .collect::>()?; - let field_definition = quote! { + .filter_map(make_method_docs) + .collect::(); + + let methods = if methods.is_empty() { + quote! { None } + } else { + quote! { Some(#methods) } + }; + + quote! { docs: ::godot::docs::InherentImplDocs { methods: #methods, - signals: #signals, - constants: #constants, - }.into() - }; - Some(field_definition) + signals_block: #signals_block, + constants_block: #constants_block, + } + } } - pieces(functions, signals, constants).unwrap_or_else(|| quote! { docs: None }) + pieces(functions, signals, constants) } pub fn make_virtual_impl_docs(vmethods: &[ImplMember]) -> TokenStream { - match vmethods + let virtual_methods = vmethods .iter() .filter_map(|x| match x { venial::ImplMember::AssocFunction(f) => Some(f.clone()), _ => None, }) - .map(make_virtual_method_docs) - .collect::>() - { - Some(vmethods) => quote! { - virtual_method_docs: #vmethods, - }, - None => quote! { - virtual_method_docs: "" - }, + .filter_map(make_virtual_method_docs) + .collect::(); + + if virtual_methods.is_empty() { + quote! { virtual_method_docs: None, } + } else { + quote! { virtual_method_docs: #virtual_methods.into(), } } } diff --git a/godot/tests/docs.rs b/godot/tests/docs.rs index ae99bec91..c493fe45e 100644 --- a/godot/tests/docs.rs +++ b/godot/tests/docs.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "register-docs")] /* * Copyright (c) godot-rust; Bromeon and contributors. * This Source Code Form is subject to the terms of the Mozilla Public @@ -75,10 +76,13 @@ use godot::prelude::*; /// these #[derive(GodotClass)] #[class(base=Node)] -pub struct ExtremelyDocumented { +pub struct FairlyDocumented { #[doc = r#"this is very documented"#] #[var] item: f32, + /// is it documented? + #[var] + item_2: i64, /// this isnt documented _other_item: (), /// nor this @@ -86,46 +90,78 @@ pub struct ExtremelyDocumented { } #[godot_api] -impl INode for ExtremelyDocumented { +impl INode for FairlyDocumented { /// initialize this fn init(base: Base) -> Self { Self { base, item: 883.0, + item_2: 25, _other_item: {}, } } } #[godot_api] -impl ExtremelyDocumented { - #[constant] +impl FairlyDocumented { /// Documentation. + #[constant] const RANDOM: i64 = 4; + #[constant] + const PURPOSE: i64 = 42; + #[func] + fn totally_undocumented_function(&self) -> i64 { + 5 + } + /// huh + #[func] fn ye(&self) -> f32 { self.item } - #[func] + #[func(gd_self, virtual)] + fn virtual_undocumented(_s: Gd) { + panic!("no implementation") + } + + /// some virtual function that should be overridden by a user + /// + /// some multiline doc + #[func(gd_self, virtual)] + fn virtual_documented(_s: Gd) { + panic!("please provide user implementation") + } + /// wow + /// + /// some multiline doc + #[func] fn ne(_x: f32) -> Gd { panic!() } + + #[signal] + fn undocumented_signal(p: Vector3, w: f64); + + /// some user signal + /// + /// some multiline doc + #[signal] + fn documented_signal(p: Vector3, w: f64); } #[test] -#[cfg(feature = "register-docs")] fn correct() { // Uncomment if implementation changes and expected output file should be rewritten. // std::fs::write( - // "tests/docs.xml", + // "tests/test_data/docs.xml", // godot_core::docs::gather_xml_docs().next().unwrap(), // ); assert_eq!( - include_str!("docs.xml"), + include_str!("test_data/docs.xml"), godot_core::docs::gather_xml_docs().next().unwrap() ); } diff --git a/godot/tests/docs.xml b/godot/tests/test_data/docs.xml similarity index 67% rename from godot/tests/docs.xml rename to godot/tests/test_data/docs.xml index fd9f4e92f..0b6a2fe45 100644 --- a/godot/tests/docs.xml +++ b/godot/tests/test_data/docs.xml @@ -1,6 +1,6 @@ - + [i]documented[/i] ~ [b]documented[/b] ~ [AABB] [url=https://github.com/godot-rust/gdext/pull/748]pr[/url] [i]documented[/i] ~ [b]documented[/b] ~ [AABB] [url=https://github.com/godot-rust/gdext/pull/748]pr[/url][br][br]a few tests:[br][br]headings:[br][br]Some heading[br][br]lists:[br][br][br][br][br][br]links with back-references:[br][br]Blah blah [br][br][br][br]footnotes:[br][br]We cannot florbinate the glorb[br][br][br][br]task lists:[br][br]We must ensure that we've completed[br][br][br][br]tables:[br][br][br][br]images:[br][br][img]http://url/a.png[/img][br][br]blockquotes:[br][br][br][br]ordered list:[br][br][br][br]Something here < this is technically header syntax[br][br]And here[br][br]smart punctuation[br][br]codeblocks:[br][br][codeblock]#![no_main] #[link_section=\".text\"] @@ -16,11 +16,19 @@ these + + + + + some virtual function that should be overridden by a user[br][br]some multiline doc + + + - + - wow + wow[br][br]some multiline doc @@ -33,6 +41,13 @@ these Documentation. - -this is very documented + + + + + some user signal[br][br]some multiline doc + + + +this is very documentedis it documented? \ No newline at end of file diff --git a/itest/rust/src/register_tests/constant_test.rs b/itest/rust/src/register_tests/constant_test.rs index 18fdfb62f..d7b25f9c8 100644 --- a/itest/rust/src/register_tests/constant_test.rs +++ b/itest/rust/src/register_tests/constant_test.rs @@ -176,7 +176,7 @@ godot::sys::plugin_add!( raw: ::godot::private::callbacks::register_user_methods_constants::, }, #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs: None, + docs: ::godot::docs::InherentImplDocs::default(), }, init_level: HasOtherConstants::INIT_LEVEL, }