Skip to content

Generate documentation from doc comments #748

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions godot-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ homepage = "https://godot-rust.github.io"

[features]
default = []
docs = []
codegen-rustfmt = ["godot-ffi/codegen-rustfmt", "godot-codegen/codegen-rustfmt"]
codegen-full = ["godot-codegen/codegen-full"]
codegen-lazy-fptrs = [
Expand Down
114 changes: 114 additions & 0 deletions godot-core/src/docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
use crate::registry::plugin::PluginItem;
use std::collections::HashMap;

/// Created for documentation on
/// ```ignore
/// #[derive(GodotClass)]
/// /// Documented
/// struct Struct {
/// /// documented
/// x: f32,
/// }
/// ```
#[derive(Clone, Copy, Debug, Default)]
pub struct StructDocs {
pub base: &'static str,
pub description: &'static str,
pub members: &'static str,
}

/// Created for documentation on
/// ```ignore
/// #[godot_api]
/// impl Struct {
/// #[func]
/// /// This function panics!
/// fn panic() -> f32 { panic!() }
/// }
/// ```
#[derive(Clone, Copy, Debug, Default)]
pub struct InherentImplDocs {
pub methods: &'static str,
pub signals: &'static str,
pub constants: &'static str,
}

#[derive(Default)]
struct DocPieces {
definition: StructDocs,
inherent: InherentImplDocs,
virtual_methods: &'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.
pub fn gather_xml_docs() -> impl Iterator<Item = String> {
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::ITraitImpl {
virtual_method_docs,
..
} => {
map.entry(x.class_name.as_str())
.or_default()
.virtual_methods = virtual_method_docs
}
PluginItem::Struct {
docs: Some(docs), ..
} => map.entry(x.class_name.as_str()).or_default().definition = docs,
_ => (),
});
map.into_iter().map(|(class, pieces)| {
let StructDocs {
base,
description,
members,
} = pieces.definition;

let InherentImplDocs {
methods,
signals,
constants,
} = pieces.inherent;

let virtual_methods = pieces.virtual_methods;
let brief = description.split_once("[br]").map(|(x, _)| x).unwrap_or_default();
format!(r#"
<?xml version="1.0" encoding="UTF-8"?>
<class name="{class}" inherits="{base}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>{brief}</brief_description>
<description>{description}</description>
<methods>{methods}{virtual_methods}</methods>
<constants>{constants}</constants>
<signals>{signals}</signals>
<members>{members}</members>
</class>"#)
},
)
}

/// # Safety
///
/// The Godot binding must have been initialized before calling this function.
///
/// If "experimental-threads" is not enabled, then this must be called from the same thread that the bindings were initialized from.
pub unsafe fn register() {
for xml in gather_xml_docs() {
crate::sys::interface_fn!(editor_help_load_xml_from_utf8_chars_and_len)(
xml.as_ptr() as *const std::ffi::c_char,
xml.len() as i64,
);
}
}
13 changes: 10 additions & 3 deletions godot-core/src/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,16 @@ unsafe fn gdext_on_level_init(level: InitLevel) {
// SAFETY: we are in the main thread, initialize has been called, has never been called with this level before.
unsafe { sys::load_class_method_table(level) };

if level == InitLevel::Scene {
// SAFETY: On the main thread, api initialized, `Scene` was initialized above.
unsafe { ensure_godot_features_compatible() };
match level {
InitLevel::Scene => {
// SAFETY: On the main thread, api initialized, `Scene` was initialized above.
unsafe { ensure_godot_features_compatible() };
}
InitLevel::Editor => {
#[cfg(all(since_api = "4.3", feature = "docs"))]
crate::docs::register();
}
_ => (),
}

crate::registry::class::auto_register_classes(level);
Expand Down
13 changes: 13 additions & 0 deletions godot-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
pub mod builder;
pub mod builtin;
pub mod classes;
#[cfg(all(since_api = "4.3", feature = "docs"))]
pub mod docs;
#[doc(hidden)]
pub mod possibly_docs {
#[cfg(all(since_api = "4.3", feature = "docs"))]
pub use crate::docs::*;
}
pub mod global;
pub mod init;
pub mod meta;
Expand Down Expand Up @@ -70,3 +77,9 @@ pub mod log {
godot_error, godot_print, godot_print_rich, godot_script_error, godot_warn,
};
}

// ----
// Validation

#[cfg(all(feature = "docs", before_api = "4.3"))]
compile_error!("Documentation generation requires 4.3.");
6 changes: 6 additions & 0 deletions godot-core/src/registry/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
is_editor_plugin,
is_hidden,
is_instantiable,
#[cfg(all(since_api = "4.3", feature = "docs"))]
docs: _,
} => {
c.parent_class_name = Some(base_class_name);
c.default_virtual_fn = default_get_virtual_fn;
Expand Down Expand Up @@ -282,6 +284,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {

PluginItem::InherentImpl {
register_methods_constants_fn,
#[cfg(all(since_api = "4.3", feature = "docs"))]
docs: _,
} => {
c.register_methods_constants_fn = Some(register_methods_constants_fn);
}
Expand All @@ -299,6 +303,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
user_free_property_list_fn,
user_property_can_revert_fn,
user_property_get_revert_fn,
#[cfg(all(since_api = "4.3", feature = "docs"))]
virtual_method_docs: _,
} => {
c.user_register_fn = user_register_fn;

Expand Down
14 changes: 11 additions & 3 deletions godot-core/src/registry/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use std::any::Any;
use std::fmt;

#[cfg(all(since_api = "4.3", feature = "docs"))]
use crate::docs::*;
use crate::init::InitLevel;
use crate::meta::ClassName;
use crate::sys;
use std::any::Any;
use std::fmt;

// TODO(bromeon): some information coming from the proc-macro API is deferred through PluginItem, while others is directly
// translated to code. Consider moving more code to the PluginItem, which allows for more dynamic registration and will
Expand Down Expand Up @@ -96,6 +97,8 @@ pub enum PluginItem {

/// Whether the class has a default constructor.
is_instantiable: bool,
#[cfg(all(since_api = "4.3", feature = "docs"))]
docs: Option<StructDocs>,
},

/// Collected from `#[godot_api] impl MyClass`.
Expand All @@ -104,10 +107,15 @@ 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<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,
/// Callback to user-defined `register_class` function.
user_register_fn: Option<ErasedRegisterFn>,

Expand Down
3 changes: 3 additions & 0 deletions godot-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ homepage = "https://godot-rust.github.io"

[features]
api-custom = ["godot-bindings/api-custom"]
docs = ["dep:markdown"]

[lib]
proc-macro = true
Expand All @@ -21,6 +22,8 @@ proc-macro = true
proc-macro2 = "1.0.63"
quote = "1.0.29"

# Enabled by `docs`
markdown = { version = "1.0.0-alpha.17", optional = true }
venial = "0.6"

[build-dependencies]
Expand Down
4 changes: 4 additions & 0 deletions godot-macros/src/class/data_models/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub struct Field {
pub var: Option<FieldVar>,
pub export: Option<FieldExport>,
pub is_onready: bool,
#[cfg(feature = "docs")]
pub attributes: Vec<venial::Attribute>,
}

impl Field {
Expand All @@ -26,6 +28,8 @@ impl Field {
var: None,
export: None,
is_onready: false,
#[cfg(feature = "docs")]
attributes: field.attributes.clone(),
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions godot-macros/src/class/data_models/inherent_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult<Toke
let (funcs, signals) = process_godot_fns(&class_name, &mut impl_block)?;
let consts = process_godot_constants(&mut impl_block)?;

#[cfg(all(feature = "docs", since_api = "4.3"))]
let docs = crate::docs::make_inherent_impl_docs(&funcs, &consts, &signals);
#[cfg(not(all(feature = "docs", since_api = "4.3")))]
let docs = quote! {};

let signal_registrations = make_signal_registrations(signals, &class_name_obj);

let method_registrations: Vec<TokenStream> = funcs
Expand Down Expand Up @@ -80,6 +85,7 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult<Toke
register_methods_constants_fn: #prv::ErasedRegisterFn {
raw: #prv::callbacks::register_user_methods_constants::<#class_name>,
},
#docs
},
init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL,
});
Expand Down
6 changes: 5 additions & 1 deletion godot-macros/src/class/data_models/interface_trait_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult<TokenStr
let mut virtual_method_names = vec![];

let prv = quote! { ::godot::private };

#[cfg(all(feature = "docs", since_api = "4.3"))]
let docs = crate::docs::make_virtual_impl_docs(&original_impl.body_items);
#[cfg(not(all(feature = "docs", since_api = "4.3")))]
let docs = quote! {};
for item in original_impl.body_items.iter() {
let method = if let venial::ImplMember::AssocFunction(f) = item {
f
Expand Down Expand Up @@ -400,6 +403,7 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult<TokenStr
user_property_get_revert_fn: #property_get_revert_fn,
user_property_can_revert_fn: #property_can_revert_fn,
get_virtual_fn: #prv::callbacks::get_virtual::<#class_name>,
#docs
},
init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL,
});
Expand Down
20 changes: 15 additions & 5 deletions godot-macros/src/class/derive_godot_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
let is_editor_plugin = struct_cfg.is_editor_plugin;
let is_hidden = struct_cfg.is_hidden;
let base_ty = &struct_cfg.base_ty;
#[cfg(all(feature = "docs", since_api = "4.3"))]
let docs = crate::docs::make_definition_docs(
base_ty.to_string(),
&class.attributes,
&fields.all_fields,
);
#[cfg(not(all(feature = "docs", since_api = "4.3")))]
let docs = quote! {};
let base_class = quote! { ::godot::classes::#base_ty };
let base_class_name_obj = util::class_name_obj(&base_class);
let inherits_macro = format_ident!("unsafe_inherits_transitive_{}", base_ty);
Expand Down Expand Up @@ -75,7 +83,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {

match struct_cfg.init_strategy {
InitStrategy::Generated => {
godot_init_impl = make_godot_init_impl(class_name, fields);
godot_init_impl = make_godot_init_impl(class_name, &fields);
create_fn = quote! { Some(#prv::callbacks::create::<#class_name>) };

if cfg!(since_api = "4.2") {
Expand Down Expand Up @@ -142,6 +150,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
is_editor_plugin: #is_editor_plugin,
is_hidden: #is_hidden,
is_instantiable: #is_instantiable,
#docs
},
init_level: {
let level = <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL;
Expand Down Expand Up @@ -193,17 +202,18 @@ struct ClassAttributes {
rename: Option<Ident>,
}

fn make_godot_init_impl(class_name: &Ident, fields: Fields) -> TokenStream {
let base_init = if let Some(Field { name, .. }) = fields.base_field {
fn make_godot_init_impl(class_name: &Ident, fields: &Fields) -> TokenStream {
let base_init = if let Some(Field { name, .. }) = &fields.base_field {
quote! { #name: base, }
} else {
TokenStream::new()
};

let rest_init = fields.all_fields.into_iter().map(|field| {
let field_name = field.name;
let rest_init = fields.all_fields.iter().map(|field| {
let field_name = field.name.clone();
let value_expr = field
.default
.clone()
.unwrap_or_else(|| quote! { ::std::default::Default::default() });

quote! { #field_name: #value_expr, }
Expand Down
Loading
Loading