Skip to content

Commit

Permalink
bevy_reflect: Reflect doc comments (#6234)
Browse files Browse the repository at this point in the history
# 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)
  • Loading branch information
MrGVSV authored and james7132 committed Oct 19, 2022
1 parent abec929 commit 889e015
Show file tree
Hide file tree
Showing 22 changed files with 777 additions and 50 deletions.
9 changes: 9 additions & 0 deletions crates/bevy_reflect/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
5 changes: 5 additions & 0 deletions crates/bevy_reflect/bevy_reflect_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }

Expand Down
49 changes: 42 additions & 7 deletions crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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.
Expand All @@ -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> {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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(
Expand All @@ -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))
Expand All @@ -216,6 +233,8 @@ impl<'a> ReflectDerive<'a> {
index,
attrs,
data: field,
#[cfg(feature = "documentation")]
doc: crate::documentation::Documentation::from_attributes(&field.attrs),
})
})
.fold(
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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> {
Expand Down
77 changes: 77 additions & 0 deletions crates/bevy_reflect/bevy_reflect_derive/src/documentation.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

0 comments on commit 889e015

Please sign in to comment.