bevy_reflect: Reflect doc comments (#6234)

# Objective

Resolves #6197

Make it so that doc comments can be retrieved via reflection.

## Solution

Adds the new `documentation` feature to `bevy_reflect` (disabled by default).

When enabled, documentation can be found using `TypeInfo::doc` for reflected types:

```rust
/// Some struct.
///
/// # Example
///
/// ```ignore
/// let some_struct = SomeStruct;
/// ```
#[derive(Reflect)]
struct SomeStruct;

let info = <SomeStruct as Typed>::type_info();
assert_eq!(
    Some(" Some struct.\n\n # Example\n\n ```ignore\n let some_struct = SomeStruct;\n ```"),
    info.docs()
);
```

### Notes for Reviewers

The bulk of the files simply added the same 16 lines of code (with slightly different documentation). Most of the real changes occur in the `bevy_reflect_derive` files as well as in the added tests.

---

## Changelog

* Added `documentation` feature to `bevy_reflect`
* Added `TypeInfo::docs` method (and similar methods for all info types)
This commit is contained in:
Gino Valente 2022-10-18 13:49:57 +00:00
parent 4407cdb423
commit a658bfef19
22 changed files with 777 additions and 50 deletions

View File

@ -10,7 +10,11 @@ keywords = ["bevy"]
readme = "README.md"
[features]
default = []
# Provides Bevy-related reflection implementations
bevy = ["glam", "smallvec", "bevy_math"]
# When enabled, allows documentation comments to be accessed via reflection
documentation = ["bevy_reflect_derive/documentation"]
[dependencies]
# bevy
@ -31,3 +35,8 @@ glam = { version = "0.21", features = ["serde"], optional = true }
[dev-dependencies]
ron = "0.8.0"
[[example]]
name = "reflect_docs"
path = "examples/reflect_docs.rs"
required-features = ["documentation"]

View File

@ -11,6 +11,11 @@ keywords = ["bevy"]
[lib]
proc-macro = true
[features]
default = []
# When enabled, allows documentation comments to be processed by the reflection macros
documentation = []
[dependencies]
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.9.0-dev" }

View File

@ -39,6 +39,9 @@ pub(crate) struct ReflectMeta<'a> {
generics: &'a Generics,
/// A cached instance of the path to the `bevy_reflect` crate.
bevy_reflect_path: Path,
/// The documentation for this type, if any
#[cfg(feature = "documentation")]
docs: crate::documentation::Documentation,
}
/// Struct data used by derive macros for `Reflect` and `FromReflect`.
@ -86,6 +89,9 @@ pub(crate) struct StructField<'a> {
pub attrs: ReflectFieldAttr,
/// The index of this field within the struct.
pub index: usize,
/// The documentation for this field, if any
#[cfg(feature = "documentation")]
pub doc: crate::documentation::Documentation,
}
/// Represents a variant on an enum.
@ -100,6 +106,9 @@ pub(crate) struct EnumVariant<'a> {
/// The index of this variant within the enum.
#[allow(dead_code)]
pub index: usize,
/// The documentation for this variant, if any
#[cfg(feature = "documentation")]
pub doc: crate::documentation::Documentation,
}
pub(crate) enum EnumVariantFields<'a> {
@ -123,6 +132,9 @@ impl<'a> ReflectDerive<'a> {
// Should indicate whether `#[reflect_value]` was used
let mut reflect_mode = None;
#[cfg(feature = "documentation")]
let mut doc = crate::documentation::Documentation::default();
for attribute in input.attrs.iter().filter_map(|attr| attr.parse_meta().ok()) {
match attribute {
Meta::List(meta_list) if meta_list.path.is_ident(REFLECT_ATTRIBUTE_NAME) => {
@ -159,25 +171,31 @@ impl<'a> ReflectDerive<'a> {
reflect_mode = Some(ReflectMode::Value);
}
#[cfg(feature = "documentation")]
Meta::NameValue(pair) if pair.path.is_ident("doc") => {
if let syn::Lit::Str(lit) = pair.lit {
doc.push(lit.value());
}
}
_ => continue,
}
}
let meta = ReflectMeta::new(&input.ident, &input.generics, traits);
#[cfg(feature = "documentation")]
let meta = meta.with_docs(doc);
// Use normal reflection if unspecified
let reflect_mode = reflect_mode.unwrap_or(ReflectMode::Normal);
if reflect_mode == ReflectMode::Value {
return Ok(Self::Value(ReflectMeta::new(
&input.ident,
&input.generics,
traits,
)));
return Ok(Self::Value(meta));
}
return match &input.data {
Data::Struct(data) => {
let fields = Self::collect_struct_fields(&data.fields)?;
let meta = ReflectMeta::new(&input.ident, &input.generics, traits);
let reflect_struct = ReflectStruct {
meta,
serialization_denylist: members_to_serialization_denylist(
@ -194,7 +212,6 @@ impl<'a> ReflectDerive<'a> {
}
Data::Enum(data) => {
let variants = Self::collect_enum_variants(&data.variants)?;
let meta = ReflectMeta::new(&input.ident, &input.generics, traits);
let reflect_enum = ReflectEnum { meta, variants };
Ok(Self::Enum(reflect_enum))
@ -216,6 +233,8 @@ impl<'a> ReflectDerive<'a> {
index,
attrs,
data: field,
#[cfg(feature = "documentation")]
doc: crate::documentation::Documentation::from_attributes(&field.attrs),
})
})
.fold(
@ -245,6 +264,8 @@ impl<'a> ReflectDerive<'a> {
attrs: parse_field_attrs(&variant.attrs)?,
data: variant,
index,
#[cfg(feature = "documentation")]
doc: crate::documentation::Documentation::from_attributes(&variant.attrs),
})
})
.fold(
@ -263,9 +284,17 @@ impl<'a> ReflectMeta<'a> {
type_name,
generics,
bevy_reflect_path: utility::get_bevy_reflect_path(),
#[cfg(feature = "documentation")]
docs: Default::default(),
}
}
/// Sets the documentation for this type.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: crate::documentation::Documentation) -> Self {
Self { docs, ..self }
}
/// The registered reflect traits on this struct.
pub fn traits(&self) -> &ReflectTraits {
&self.traits
@ -296,6 +325,12 @@ impl<'a> ReflectMeta<'a> {
None,
)
}
/// The collection of docstrings for this type, if any.
#[cfg(feature = "documentation")]
pub fn doc(&self) -> &crate::documentation::Documentation {
&self.docs
}
}
impl<'a> ReflectStruct<'a> {

View File

@ -0,0 +1,77 @@
//! Contains code related to documentation reflection (requires the `documentation` feature).
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{Attribute, Lit, Meta};
/// A struct used to represent a type's documentation, if any.
///
/// When converted to a [`TokenStream`], this will output an `Option<String>`
/// containing the collection of doc comments.
#[derive(Default)]
pub(crate) struct Documentation {
docs: Vec<String>,
}
impl Documentation {
/// Create a new [`Documentation`] from a type's attributes.
///
/// This will collect all `#[doc = "..."]` attributes, including the ones generated via `///` and `//!`.
pub fn from_attributes<'a>(attributes: impl IntoIterator<Item = &'a Attribute>) -> Self {
let docs = attributes
.into_iter()
.filter_map(|attr| {
let meta = attr.parse_meta().ok()?;
match meta {
Meta::NameValue(pair) if pair.path.is_ident("doc") => {
if let Lit::Str(lit) = pair.lit {
Some(lit.value())
} else {
None
}
}
_ => None,
}
})
.collect();
Self { docs }
}
/// The full docstring, if any.
pub fn doc_string(&self) -> Option<String> {
if self.docs.is_empty() {
return None;
}
let len = self.docs.len();
Some(
self.docs
.iter()
.enumerate()
.map(|(index, doc)| {
if index < len - 1 {
format!("{}\n", doc)
} else {
doc.to_owned()
}
})
.collect(),
)
}
/// Push a new docstring to the collection
pub fn push(&mut self, doc: String) {
self.docs.push(doc);
}
}
impl ToTokens for Documentation {
fn to_tokens(&self, tokens: &mut TokenStream) {
if let Some(doc) = self.doc_string() {
quote!(Some(#doc)).to_tokens(tokens);
} else {
quote!(None).to_tokens(tokens);
}
}
}

View File

@ -1,9 +1,10 @@
use crate::derive_data::{EnumVariantFields, ReflectEnum, StructField};
use crate::derive_data::{EnumVariant, EnumVariantFields, ReflectEnum, StructField};
use crate::enum_utility::{get_variant_constructors, EnumVariantConstructors};
use crate::impls::impl_typed;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::Fields;
pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> TokenStream {
let bevy_reflect_path = reflect_enum.meta().bevy_reflect_path();
@ -55,12 +56,28 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> TokenStream {
});
let string_name = enum_name.to_string();
#[cfg(feature = "documentation")]
let info_generator = {
let doc = reflect_enum.meta().doc();
quote! {
#bevy_reflect_path::EnumInfo::new::<Self>(#string_name, &variants).with_docs(#doc)
}
};
#[cfg(not(feature = "documentation"))]
let info_generator = {
quote! {
#bevy_reflect_path::EnumInfo::new::<Self>(#string_name, &variants)
}
};
let typed_impl = impl_typed(
enum_name,
reflect_enum.meta().generics(),
quote! {
let variants = [#(#variant_info),*];
let info = #bevy_reflect_path::EnumInfo::new::<Self>(#string_name, &variants);
let info = #info_generator;
#bevy_reflect_path::TypeInfo::Enum(info)
},
bevy_reflect_path,
@ -286,6 +303,18 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden
let name = ident.to_string();
let unit = reflect_enum.get_unit(ident);
let variant_type_ident = match variant.data.fields {
Fields::Unit => Ident::new("Unit", Span::call_site()),
Fields::Unnamed(..) => Ident::new("Tuple", Span::call_site()),
Fields::Named(..) => Ident::new("Struct", Span::call_site()),
};
let variant_info_ident = match variant.data.fields {
Fields::Unit => Ident::new("UnitVariantInfo", Span::call_site()),
Fields::Unnamed(..) => Ident::new("TupleVariantInfo", Span::call_site()),
Fields::Named(..) => Ident::new("StructVariantInfo", Span::call_site()),
};
enum_variant_name.push(quote! {
#unit{..} => #name
});
@ -293,10 +322,10 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden
#unit{..} => #variant_index
});
fn for_fields(
fn get_field_args(
fields: &[StructField],
mut generate_for_field: impl FnMut(usize, usize, &StructField) -> proc_macro2::TokenStream,
) -> (usize, Vec<proc_macro2::TokenStream>) {
) -> Vec<proc_macro2::TokenStream> {
let mut constructor_argument = Vec::new();
let mut reflect_idx = 0;
for field in fields.iter() {
@ -307,41 +336,64 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden
constructor_argument.push(generate_for_field(reflect_idx, field.index, field));
reflect_idx += 1;
}
(reflect_idx, constructor_argument)
constructor_argument
}
let mut add_fields_branch = |variant, info_type, arguments, field_len| {
let variant = Ident::new(variant, Span::call_site());
let info_type = Ident::new(info_type, Span::call_site());
variant_info.push(quote! {
#bevy_reflect_path::VariantInfo::#variant(
#bevy_reflect_path::#info_type::new(#arguments)
)
});
enum_field_len.push(quote! {
#unit{..} => #field_len
});
enum_variant_type.push(quote! {
#unit{..} => #bevy_reflect_path::VariantType::#variant
});
};
let mut push_variant =
|_variant: &EnumVariant, arguments: proc_macro2::TokenStream, field_len: usize| {
#[cfg(feature = "documentation")]
let with_docs = {
let doc = quote::ToTokens::to_token_stream(&_variant.doc);
Some(quote!(.with_docs(#doc)))
};
#[cfg(not(feature = "documentation"))]
let with_docs: Option<proc_macro2::TokenStream> = None;
variant_info.push(quote! {
#bevy_reflect_path::VariantInfo::#variant_type_ident(
#bevy_reflect_path::#variant_info_ident::new(#arguments)
#with_docs
)
});
enum_field_len.push(quote! {
#unit{..} => #field_len
});
enum_variant_type.push(quote! {
#unit{..} => #bevy_reflect_path::VariantType::#variant_type_ident
});
};
match &variant.fields {
EnumVariantFields::Unit => {
add_fields_branch("Unit", "UnitVariantInfo", quote!(#name), 0usize);
push_variant(variant, quote!(#name), 0);
}
EnumVariantFields::Unnamed(fields) => {
let (field_len, argument) = for_fields(fields, |reflect_idx, declar, field| {
let declar_field = syn::Index::from(declar);
let args = get_field_args(fields, |reflect_idx, declaration_index, field| {
let declar_field = syn::Index::from(declaration_index);
enum_field_at.push(quote! {
#unit { #declar_field : value, .. } if #ref_index == #reflect_idx => Some(value)
});
#[cfg(feature = "documentation")]
let with_docs = {
let doc = quote::ToTokens::to_token_stream(&field.doc);
Some(quote!(.with_docs(#doc)))
};
#[cfg(not(feature = "documentation"))]
let with_docs: Option<proc_macro2::TokenStream> = None;
let field_ty = &field.data.ty;
quote! { #bevy_reflect_path::UnnamedField::new::<#field_ty>(#reflect_idx) }
quote! {
#bevy_reflect_path::UnnamedField::new::<#field_ty>(#reflect_idx)
#with_docs
}
});
let arguments = quote!(#name, &[ #(#argument),* ]);
add_fields_branch("Tuple", "TupleVariantInfo", arguments, field_len);
let field_len = args.len();
push_variant(variant, quote!(#name, &[ #(#args),* ]), field_len);
}
EnumVariantFields::Named(fields) => {
let (field_len, argument) = for_fields(fields, |reflect_idx, _, field| {
let args = get_field_args(fields, |reflect_idx, _, field| {
let field_ident = field.data.ident.as_ref().unwrap();
let field_name = field_ident.to_string();
enum_field.push(quote! {
@ -357,11 +409,23 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden
#unit{ .. } if #ref_index == #reflect_idx => Some(#field_name)
});
#[cfg(feature = "documentation")]
let with_docs = {
let doc = quote::ToTokens::to_token_stream(&field.doc);
Some(quote!(.with_docs(#doc)))
};
#[cfg(not(feature = "documentation"))]
let with_docs: Option<proc_macro2::TokenStream> = None;
let field_ty = &field.data.ty;
quote! { #bevy_reflect_path::NamedField::new::<#field_ty>(#field_name) }
quote! {
#bevy_reflect_path::NamedField::new::<#field_ty>(#field_name)
#with_docs
}
});
let arguments = quote!(#name, &[ #(#argument),* ]);
add_fields_branch("Struct", "StructVariantInfo", arguments, field_len);
let field_len = args.len();
push_variant(variant, quote!(#name, &[ #(#args),* ]), field_len);
}
};
}

View File

@ -51,15 +51,46 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> TokenStream {
}
});
#[cfg(feature = "documentation")]
let field_generator = {
let docs = reflect_struct
.active_fields()
.map(|field| quote::ToTokens::to_token_stream(&field.doc));
quote! {
#(#bevy_reflect_path::NamedField::new::<#field_types>(#field_names).with_docs(#docs) ,)*
}
};
#[cfg(not(feature = "documentation"))]
let field_generator = {
quote! {
#(#bevy_reflect_path::NamedField::new::<#field_types>(#field_names) ,)*
}
};
let string_name = struct_name.to_string();
#[cfg(feature = "documentation")]
let info_generator = {
let doc = reflect_struct.meta().doc();
quote! {
#bevy_reflect_path::StructInfo::new::<Self>(#string_name, &fields).with_docs(#doc)
}
};
#[cfg(not(feature = "documentation"))]
let info_generator = {
quote! {
#bevy_reflect_path::StructInfo::new::<Self>(#string_name, &fields)
}
};
let typed_impl = impl_typed(
struct_name,
reflect_struct.meta().generics(),
quote! {
let fields = [
#(#bevy_reflect_path::NamedField::new::<#field_types>(#field_names),)*
];
let info = #bevy_reflect_path::StructInfo::new::<Self>(#string_name, &fields);
let fields = [#field_generator];
let info = #info_generator;
#bevy_reflect_path::TypeInfo::Struct(info)
},
bevy_reflect_path,

View File

@ -35,15 +35,46 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> TokenStream {
}
});
#[cfg(feature = "documentation")]
let field_generator = {
let docs = reflect_struct
.active_fields()
.map(|field| quote::ToTokens::to_token_stream(&field.doc));
quote! {
#(#bevy_reflect_path::UnnamedField::new::<#field_types>(#field_idents).with_docs(#docs) ,)*
}
};
#[cfg(not(feature = "documentation"))]
let field_generator = {
quote! {
#(#bevy_reflect_path::UnnamedField::new::<#field_types>(#field_idents) ,)*
}
};
let string_name = struct_name.to_string();
#[cfg(feature = "documentation")]
let info_generator = {
let doc = reflect_struct.meta().doc();
quote! {
#bevy_reflect_path::TupleStructInfo::new::<Self>(#string_name, &fields).with_docs(#doc)
}
};
#[cfg(not(feature = "documentation"))]
let info_generator = {
quote! {
#bevy_reflect_path::TupleStructInfo::new::<Self>(#string_name, &fields)
}
};
let typed_impl = impl_typed(
struct_name,
reflect_struct.meta().generics(),
quote! {
let fields = [
#(#bevy_reflect_path::UnnamedField::new::<#field_types>(#field_idents),)*
];
let info = #bevy_reflect_path::TupleStructInfo::new::<Self>(#string_name, &fields);
let fields = [#field_generator];
let info = #info_generator;
#bevy_reflect_path::TypeInfo::TupleStruct(info)
},
bevy_reflect_path,

View File

@ -12,11 +12,19 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> TokenStream {
let partial_eq_fn = meta.traits().get_partial_eq_impl(bevy_reflect_path);
let debug_fn = meta.traits().get_debug_impl();
#[cfg(feature = "documentation")]
let with_docs = {
let doc = quote::ToTokens::to_token_stream(meta.doc());
Some(quote!(.with_docs(#doc)))
};
#[cfg(not(feature = "documentation"))]
let with_docs: Option<proc_macro2::TokenStream> = None;
let typed_impl = impl_typed(
type_name,
meta.generics(),
quote! {
let info = #bevy_reflect_path::ValueInfo::new::<Self>();
let info = #bevy_reflect_path::ValueInfo::new::<Self>() #with_docs;
#bevy_reflect_path::TypeInfo::Value(info)
},
bevy_reflect_path,

View File

@ -16,6 +16,8 @@ extern crate proc_macro;
mod container_attributes;
mod derive_data;
#[cfg(feature = "documentation")]
mod documentation;
mod enum_utility;
mod field_attributes;
mod from_reflect;
@ -95,11 +97,16 @@ pub fn reflect_trait(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro]
pub fn impl_reflect_value(input: TokenStream) -> TokenStream {
let def = parse_macro_input!(input as ReflectValueDef);
impls::impl_value(&ReflectMeta::new(
let meta = ReflectMeta::new(
&def.type_name,
&def.generics,
def.traits.unwrap_or_default(),
))
);
#[cfg(feature = "documentation")]
let meta = meta.with_docs(documentation::Documentation::from_attributes(&def.attrs));
impls::impl_value(&meta)
}
/// A replacement for `#[derive(Reflect)]` to be used with foreign types which

View File

@ -2,7 +2,7 @@ use crate::container_attributes::ReflectTraits;
use proc_macro2::Ident;
use syn::parse::{Parse, ParseStream};
use syn::token::{Paren, Where};
use syn::{parenthesized, Generics};
use syn::{parenthesized, Attribute, Generics};
/// A struct used to define a simple reflected value type (such as primitives).
///
@ -19,6 +19,8 @@ use syn::{parenthesized, Generics};
/// foo<T1, T2> where T1: Bar (TraitA, TraitB)
/// ```
pub(crate) struct ReflectValueDef {
#[allow(dead_code)]
pub attrs: Vec<Attribute>,
pub type_name: Ident,
pub generics: Generics,
pub traits: Option<ReflectTraits>,
@ -26,6 +28,7 @@ pub(crate) struct ReflectValueDef {
impl Parse for ReflectValueDef {
fn parse(input: ParseStream) -> syn::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let type_ident = input.parse::<Ident>()?;
let generics = input.parse::<Generics>()?;
let mut lookahead = input.lookahead1();
@ -43,6 +46,7 @@ impl Parse for ReflectValueDef {
}
Ok(ReflectValueDef {
attrs,
type_name: type_ident,
generics: Generics {
where_clause,

View File

@ -0,0 +1,50 @@
//! This example illustrates how you can reflect doc comments.
//!
//! There may be cases where you may want to collect a reflected item's documentation.
//! For example, you may want to generate schemas or other external documentation for scripting.
//! Or perhaps you want your custom editor to display tooltips for certain properties that match the documentation.
//!
//! These scenarios can readily be achieved by using `bevy_reflect` with the `documentation` feature.
use bevy_reflect::{Reflect, TypeInfo, Typed};
fn main() {
//! This function will simply demonstrate how you can access a type's documentation.
//!
//! Please note that the code below uses a standard struct with named fields; however, this isn't
//! exclusive to them. It can work for all kinds of data types including tuple structs and enums too!
/// The struct that defines our player.
///
/// # Example
///
/// ```
/// let player = Player::new("Urist McPlayer");
/// ```
#[derive(Reflect)]
struct Player {
/// The player's name.
name: String,
/// The player's current health points.
hp: u8,
// Some regular comment (i.e. not a doc comment)
max_hp: u8,
}
// Using `TypeInfo` we can access all of the doc comments on the `Player` struct above:
let player_info = <Player as Typed>::type_info();
// From here, we already have access to the struct's docs:
let player_docs = player_info.docs().unwrap();
assert_eq!(" The struct that defines our player.\n\n # Example\n\n ```\n let player = Player::new(\"Urist McPlayer\");\n ```", player_docs);
println!("=====[ Player ]=====\n{}", player_docs);
// We can then iterate through our struct's fields to get their documentation as well:
if let TypeInfo::Struct(struct_info) = player_info {
for field in struct_info.iter() {
let field_name = field.name();
let field_docs = field.docs().unwrap_or("<NO_DOCUMENTATION>");
println!("-----[ Player::{} ]-----\n{}", field_name, field_docs);
}
}
}

View File

@ -49,6 +49,8 @@ pub struct ArrayInfo {
item_type_name: &'static str,
item_type_id: TypeId,
capacity: usize,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl ArrayInfo {
@ -65,9 +67,17 @@ impl ArrayInfo {
item_type_name: std::any::type_name::<TItem>(),
item_type_id: TypeId::of::<TItem>(),
capacity,
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this array.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// The compile-time capacity of the array.
pub fn capacity(&self) -> usize {
self.capacity
@ -106,6 +116,12 @@ impl ArrayInfo {
pub fn item_is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.item_type_id
}
/// The docstring of this array, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// A fixed-size list of reflected values.

View File

@ -137,6 +137,8 @@ pub struct EnumInfo {
type_id: TypeId,
variants: Box<[VariantInfo]>,
variant_indices: HashMap<&'static str, usize>,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl EnumInfo {
@ -160,9 +162,17 @@ impl EnumInfo {
type_id: TypeId::of::<TEnum>(),
variants: variants.to_vec().into_boxed_slice(),
variant_indices,
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this enum.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// Get a variant with the given name.
pub fn variant(&self, name: &str) -> Option<&VariantInfo> {
self.variant_indices
@ -227,6 +237,12 @@ impl EnumInfo {
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
/// The docstring of this enum, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// An iterator over the fields in the current enum variant.

View File

@ -72,6 +72,16 @@ impl VariantInfo {
Self::Unit(info) => info.name(),
}
}
/// The docstring of the underlying variant, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&str> {
match self {
Self::Struct(info) => info.docs(),
Self::Tuple(info) => info.docs(),
Self::Unit(info) => info.docs(),
}
}
}
/// Type info for struct variants.
@ -80,6 +90,8 @@ pub struct StructVariantInfo {
name: &'static str,
fields: Box<[NamedField]>,
field_indices: HashMap<&'static str, usize>,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl StructVariantInfo {
@ -90,9 +102,17 @@ impl StructVariantInfo {
name,
fields: fields.to_vec().into_boxed_slice(),
field_indices,
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this variant.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// The name of this variant.
pub fn name(&self) -> &'static str {
self.name
@ -132,6 +152,12 @@ impl StructVariantInfo {
.map(|(index, field)| (field.name(), index))
.collect()
}
/// The docstring of this variant, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// Type info for tuple variants.
@ -139,6 +165,8 @@ impl StructVariantInfo {
pub struct TupleVariantInfo {
name: &'static str,
fields: Box<[UnnamedField]>,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl TupleVariantInfo {
@ -147,9 +175,17 @@ impl TupleVariantInfo {
Self {
name,
fields: fields.to_vec().into_boxed_slice(),
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this variant.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// The name of this variant.
pub fn name(&self) -> &'static str {
self.name
@ -169,22 +205,46 @@ impl TupleVariantInfo {
pub fn field_len(&self) -> usize {
self.fields.len()
}
/// The docstring of this variant, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// Type info for unit variants.
#[derive(Clone, Debug)]
pub struct UnitVariantInfo {
name: &'static str,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl UnitVariantInfo {
/// Create a new [`UnitVariantInfo`].
pub fn new(name: &'static str) -> Self {
Self { name }
Self {
name,
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this variant.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// The name of this variant.
pub fn name(&self) -> &'static str {
self.name
}
/// The docstring of this variant, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}

View File

@ -7,6 +7,8 @@ pub struct NamedField {
name: &'static str,
type_name: &'static str,
type_id: TypeId,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl NamedField {
@ -16,9 +18,17 @@ impl NamedField {
name,
type_name: std::any::type_name::<T>(),
type_id: TypeId::of::<T>(),
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this field.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// The name of the field.
pub fn name(&self) -> &'static str {
self.name
@ -40,6 +50,12 @@ impl NamedField {
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
/// The docstring of this field, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// The unnamed field of a reflected tuple or tuple struct.
@ -48,6 +64,8 @@ pub struct UnnamedField {
index: usize,
type_name: &'static str,
type_id: TypeId,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl UnnamedField {
@ -56,9 +74,17 @@ impl UnnamedField {
index,
type_name: std::any::type_name::<T>(),
type_id: TypeId::of::<T>(),
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this field.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// Returns the index of the field.
pub fn index(&self) -> usize {
self.index
@ -80,4 +106,10 @@ impl UnnamedField {
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
/// The docstring of this field, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}

View File

@ -835,6 +835,160 @@ mod tests {
assert!(info.is::<MyDynamic>());
}
#[cfg(feature = "documentation")]
mod docstrings {
use super::*;
#[test]
fn should_not_contain_docs() {
// Regular comments do not count as doc comments,
// and are therefore not reflected.
#[derive(Reflect)]
struct SomeStruct;
let info = <SomeStruct as Typed>::type_info();
assert_eq!(None, info.docs());
/*
* Block comments do not count as doc comments,
* and are therefore not reflected.
*/
#[derive(Reflect)]
struct SomeOtherStruct;
let info = <SomeOtherStruct as Typed>::type_info();
assert_eq!(None, info.docs());
}
#[test]
fn should_contain_docs() {
/// Some struct.
///
/// # Example
///
/// ```ignore
/// let some_struct = SomeStruct;
/// ```
#[derive(Reflect)]
struct SomeStruct;
let info = <SomeStruct as Typed>::type_info();
assert_eq!(
Some(" Some struct.\n\n # Example\n\n ```ignore\n let some_struct = SomeStruct;\n ```"),
info.docs()
);
#[doc = "The compiler automatically converts `///`-style comments into `#[doc]` attributes."]
#[doc = "Of course, you _could_ use the attribute directly if you wanted to."]
#[doc = "Both will be reflected."]
#[derive(Reflect)]
struct SomeOtherStruct;
let info = <SomeOtherStruct as Typed>::type_info();
assert_eq!(
Some("The compiler automatically converts `///`-style comments into `#[doc]` attributes.\nOf course, you _could_ use the attribute directly if you wanted to.\nBoth will be reflected."),
info.docs()
);
/// Some tuple struct.
#[derive(Reflect)]
struct SomeTupleStruct(usize);
let info = <SomeTupleStruct as Typed>::type_info();
assert_eq!(Some(" Some tuple struct."), info.docs());
/// Some enum.
#[derive(Reflect)]
enum SomeEnum {
Foo,
}
let info = <SomeEnum as Typed>::type_info();
assert_eq!(Some(" Some enum."), info.docs());
#[derive(Clone)]
struct SomePrimitive;
impl_reflect_value!(
/// Some primitive for which we have attributed custom documentation.
SomePrimitive
);
let info = <SomePrimitive as Typed>::type_info();
assert_eq!(
Some(" Some primitive for which we have attributed custom documentation."),
info.docs()
);
}
#[test]
fn fields_should_contain_docs() {
#[derive(Reflect)]
struct SomeStruct {
/// The name
name: String,
/// The index
index: usize,
// Not documented...
data: Vec<i32>,
}
let info = <SomeStruct as Typed>::type_info();
if let TypeInfo::Struct(info) = info {
let mut fields = info.iter();
assert_eq!(Some(" The name"), fields.next().unwrap().docs());
assert_eq!(Some(" The index"), fields.next().unwrap().docs());
assert_eq!(None, fields.next().unwrap().docs());
} else {
panic!("expected struct info");
}
}
#[test]
fn variants_should_contain_docs() {
#[derive(Reflect)]
enum SomeEnum {
// Not documented...
Nothing,
/// Option A
A(
/// Index
usize,
),
/// Option B
B {
/// Name
name: String,
},
}
let info = <SomeEnum as Typed>::type_info();
if let TypeInfo::Enum(info) = info {
let mut variants = info.iter();
assert_eq!(None, variants.next().unwrap().docs());
let variant = variants.next().unwrap();
assert_eq!(Some(" Option A"), variant.docs());
if let VariantInfo::Tuple(variant) = variant {
let field = variant.field_at(0).unwrap();
assert_eq!(Some(" Index"), field.docs());
} else {
panic!("expected tuple variant")
}
let variant = variants.next().unwrap();
assert_eq!(Some(" Option B"), variant.docs());
if let VariantInfo::Struct(variant) = variant {
let field = variant.field_at(0).unwrap();
assert_eq!(Some(" Name"), field.docs());
} else {
panic!("expected struct variant")
}
} else {
panic!("expected enum info");
}
}
}
#[test]
fn as_reflect() {
trait TestTrait: Reflect {}

View File

@ -34,6 +34,8 @@ pub struct ListInfo {
type_id: TypeId,
item_type_name: &'static str,
item_type_id: TypeId,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl ListInfo {
@ -44,9 +46,17 @@ impl ListInfo {
type_id: TypeId::of::<TList>(),
item_type_name: std::any::type_name::<TItem>(),
item_type_id: TypeId::of::<TItem>(),
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this list.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// The [type name] of the list.
///
/// [type name]: std::any::type_name
@ -80,6 +90,12 @@ impl ListInfo {
pub fn item_is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.item_type_id
}
/// The docstring of this list, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// A list of reflected values.

View File

@ -68,6 +68,8 @@ pub struct MapInfo {
key_type_id: TypeId,
value_type_name: &'static str,
value_type_id: TypeId,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl MapInfo {
@ -80,9 +82,17 @@ impl MapInfo {
key_type_id: TypeId::of::<TKey>(),
value_type_name: std::any::type_name::<TValue>(),
value_type_id: TypeId::of::<TValue>(),
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this map.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// The [type name] of the map.
///
/// [type name]: std::any::type_name
@ -133,6 +143,12 @@ impl MapInfo {
pub fn value_is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.value_type_id
}
/// The docstring of this map, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
const HASH_ERROR: &str = "the given key does not support hashing";

View File

@ -74,6 +74,8 @@ pub struct StructInfo {
type_id: TypeId,
fields: Box<[NamedField]>,
field_indices: HashMap<&'static str, usize>,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl StructInfo {
@ -97,9 +99,17 @@ impl StructInfo {
type_id: TypeId::of::<T>(),
fields: fields.to_vec().into_boxed_slice(),
field_indices,
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this struct.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// Get the field with the given name.
pub fn field(&self, name: &str) -> Option<&NamedField> {
self.field_indices
@ -152,6 +162,12 @@ impl StructInfo {
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
/// The docstring of this struct, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// An iterator over the field values of a struct.

View File

@ -134,6 +134,8 @@ pub struct TupleInfo {
type_name: &'static str,
type_id: TypeId,
fields: Box<[UnnamedField]>,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl TupleInfo {
@ -148,9 +150,17 @@ impl TupleInfo {
type_name: std::any::type_name::<T>(),
type_id: TypeId::of::<T>(),
fields: fields.to_vec().into_boxed_slice(),
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this tuple.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// Get the field at the given index.
pub fn field_at(&self, index: usize) -> Option<&UnnamedField> {
self.fields.get(index)
@ -182,6 +192,12 @@ impl TupleInfo {
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
/// The docstring of this tuple, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// A tuple which allows fields to be added at runtime.

View File

@ -53,6 +53,8 @@ pub struct TupleStructInfo {
type_name: &'static str,
type_id: TypeId,
fields: Box<[UnnamedField]>,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl TupleStructInfo {
@ -69,9 +71,17 @@ impl TupleStructInfo {
type_name: std::any::type_name::<T>(),
type_id: TypeId::of::<T>(),
fields: fields.to_vec().into_boxed_slice(),
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this struct.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// Get the field at the given index.
pub fn field_at(&self, index: usize) -> Option<&UnnamedField> {
self.fields.get(index)
@ -112,6 +122,12 @@ impl TupleStructInfo {
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
/// The docstring of this struct, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// An iterator over the field values of a tuple struct.

View File

@ -146,6 +146,22 @@ impl TypeInfo {
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id()
}
/// The docstring of the underlying type, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&str> {
match self {
Self::Struct(info) => info.docs(),
Self::TupleStruct(info) => info.docs(),
Self::Tuple(info) => info.docs(),
Self::List(info) => info.docs(),
Self::Array(info) => info.docs(),
Self::Map(info) => info.docs(),
Self::Enum(info) => info.docs(),
Self::Value(info) => info.docs(),
Self::Dynamic(info) => info.docs(),
}
}
}
/// A container for compile-time info related to general value types, including primitives.
@ -160,6 +176,8 @@ impl TypeInfo {
pub struct ValueInfo {
type_name: &'static str,
type_id: TypeId,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl ValueInfo {
@ -167,9 +185,17 @@ impl ValueInfo {
Self {
type_name: std::any::type_name::<T>(),
type_id: TypeId::of::<T>(),
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this value.
#[cfg(feature = "documentation")]
pub fn with_docs(self, doc: Option<&'static str>) -> Self {
Self { docs: doc, ..self }
}
/// The [type name] of the value.
///
/// [type name]: std::any::type_name
@ -186,6 +212,12 @@ impl ValueInfo {
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
/// The docstring of this dynamic value, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}
/// A container for compile-time info related to Bevy's _dynamic_ types, including primitives.
@ -200,6 +232,8 @@ impl ValueInfo {
pub struct DynamicInfo {
type_name: &'static str,
type_id: TypeId,
#[cfg(feature = "documentation")]
docs: Option<&'static str>,
}
impl DynamicInfo {
@ -207,9 +241,17 @@ impl DynamicInfo {
Self {
type_name: std::any::type_name::<T>(),
type_id: TypeId::of::<T>(),
#[cfg(feature = "documentation")]
docs: None,
}
}
/// Sets the docstring for this dynamic value.
#[cfg(feature = "documentation")]
pub fn with_docs(self, docs: Option<&'static str>) -> Self {
Self { docs, ..self }
}
/// The [type name] of the dynamic value.
///
/// [type name]: std::any::type_name
@ -226,4 +268,10 @@ impl DynamicInfo {
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
/// The docstring of this value, if any.
#[cfg(feature = "documentation")]
pub fn docs(&self) -> Option<&'static str> {
self.docs
}
}