Skip to content

Commit a658bfe

Browse files
committed
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)
1 parent 4407cdb commit a658bfe

22 files changed

+777
-50
lines changed

crates/bevy_reflect/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ keywords = ["bevy"]
1010
readme = "README.md"
1111

1212
[features]
13+
default = []
14+
# Provides Bevy-related reflection implementations
1315
bevy = ["glam", "smallvec", "bevy_math"]
16+
# When enabled, allows documentation comments to be accessed via reflection
17+
documentation = ["bevy_reflect_derive/documentation"]
1418

1519
[dependencies]
1620
# bevy
@@ -31,3 +35,8 @@ glam = { version = "0.21", features = ["serde"], optional = true }
3135

3236
[dev-dependencies]
3337
ron = "0.8.0"
38+
39+
[[example]]
40+
name = "reflect_docs"
41+
path = "examples/reflect_docs.rs"
42+
required-features = ["documentation"]

crates/bevy_reflect/bevy_reflect_derive/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ keywords = ["bevy"]
1111
[lib]
1212
proc-macro = true
1313

14+
[features]
15+
default = []
16+
# When enabled, allows documentation comments to be processed by the reflection macros
17+
documentation = []
18+
1419
[dependencies]
1520
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.9.0-dev" }
1621

crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ pub(crate) struct ReflectMeta<'a> {
3939
generics: &'a Generics,
4040
/// A cached instance of the path to the `bevy_reflect` crate.
4141
bevy_reflect_path: Path,
42+
/// The documentation for this type, if any
43+
#[cfg(feature = "documentation")]
44+
docs: crate::documentation::Documentation,
4245
}
4346

4447
/// Struct data used by derive macros for `Reflect` and `FromReflect`.
@@ -86,6 +89,9 @@ pub(crate) struct StructField<'a> {
8689
pub attrs: ReflectFieldAttr,
8790
/// The index of this field within the struct.
8891
pub index: usize,
92+
/// The documentation for this field, if any
93+
#[cfg(feature = "documentation")]
94+
pub doc: crate::documentation::Documentation,
8995
}
9096

9197
/// Represents a variant on an enum.
@@ -100,6 +106,9 @@ pub(crate) struct EnumVariant<'a> {
100106
/// The index of this variant within the enum.
101107
#[allow(dead_code)]
102108
pub index: usize,
109+
/// The documentation for this variant, if any
110+
#[cfg(feature = "documentation")]
111+
pub doc: crate::documentation::Documentation,
103112
}
104113

105114
pub(crate) enum EnumVariantFields<'a> {
@@ -123,6 +132,9 @@ impl<'a> ReflectDerive<'a> {
123132
// Should indicate whether `#[reflect_value]` was used
124133
let mut reflect_mode = None;
125134

135+
#[cfg(feature = "documentation")]
136+
let mut doc = crate::documentation::Documentation::default();
137+
126138
for attribute in input.attrs.iter().filter_map(|attr| attr.parse_meta().ok()) {
127139
match attribute {
128140
Meta::List(meta_list) if meta_list.path.is_ident(REFLECT_ATTRIBUTE_NAME) => {
@@ -159,25 +171,31 @@ impl<'a> ReflectDerive<'a> {
159171

160172
reflect_mode = Some(ReflectMode::Value);
161173
}
174+
#[cfg(feature = "documentation")]
175+
Meta::NameValue(pair) if pair.path.is_ident("doc") => {
176+
if let syn::Lit::Str(lit) = pair.lit {
177+
doc.push(lit.value());
178+
}
179+
}
162180
_ => continue,
163181
}
164182
}
165183

184+
let meta = ReflectMeta::new(&input.ident, &input.generics, traits);
185+
186+
#[cfg(feature = "documentation")]
187+
let meta = meta.with_docs(doc);
188+
166189
// Use normal reflection if unspecified
167190
let reflect_mode = reflect_mode.unwrap_or(ReflectMode::Normal);
168191

169192
if reflect_mode == ReflectMode::Value {
170-
return Ok(Self::Value(ReflectMeta::new(
171-
&input.ident,
172-
&input.generics,
173-
traits,
174-
)));
193+
return Ok(Self::Value(meta));
175194
}
176195

177196
return match &input.data {
178197
Data::Struct(data) => {
179198
let fields = Self::collect_struct_fields(&data.fields)?;
180-
let meta = ReflectMeta::new(&input.ident, &input.generics, traits);
181199
let reflect_struct = ReflectStruct {
182200
meta,
183201
serialization_denylist: members_to_serialization_denylist(
@@ -194,7 +212,6 @@ impl<'a> ReflectDerive<'a> {
194212
}
195213
Data::Enum(data) => {
196214
let variants = Self::collect_enum_variants(&data.variants)?;
197-
let meta = ReflectMeta::new(&input.ident, &input.generics, traits);
198215

199216
let reflect_enum = ReflectEnum { meta, variants };
200217
Ok(Self::Enum(reflect_enum))
@@ -216,6 +233,8 @@ impl<'a> ReflectDerive<'a> {
216233
index,
217234
attrs,
218235
data: field,
236+
#[cfg(feature = "documentation")]
237+
doc: crate::documentation::Documentation::from_attributes(&field.attrs),
219238
})
220239
})
221240
.fold(
@@ -245,6 +264,8 @@ impl<'a> ReflectDerive<'a> {
245264
attrs: parse_field_attrs(&variant.attrs)?,
246265
data: variant,
247266
index,
267+
#[cfg(feature = "documentation")]
268+
doc: crate::documentation::Documentation::from_attributes(&variant.attrs),
248269
})
249270
})
250271
.fold(
@@ -263,9 +284,17 @@ impl<'a> ReflectMeta<'a> {
263284
type_name,
264285
generics,
265286
bevy_reflect_path: utility::get_bevy_reflect_path(),
287+
#[cfg(feature = "documentation")]
288+
docs: Default::default(),
266289
}
267290
}
268291

292+
/// Sets the documentation for this type.
293+
#[cfg(feature = "documentation")]
294+
pub fn with_docs(self, docs: crate::documentation::Documentation) -> Self {
295+
Self { docs, ..self }
296+
}
297+
269298
/// The registered reflect traits on this struct.
270299
pub fn traits(&self) -> &ReflectTraits {
271300
&self.traits
@@ -296,6 +325,12 @@ impl<'a> ReflectMeta<'a> {
296325
None,
297326
)
298327
}
328+
329+
/// The collection of docstrings for this type, if any.
330+
#[cfg(feature = "documentation")]
331+
pub fn doc(&self) -> &crate::documentation::Documentation {
332+
&self.docs
333+
}
299334
}
300335

301336
impl<'a> ReflectStruct<'a> {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! Contains code related to documentation reflection (requires the `documentation` feature).
2+
3+
use proc_macro2::TokenStream;
4+
use quote::{quote, ToTokens};
5+
use syn::{Attribute, Lit, Meta};
6+
7+
/// A struct used to represent a type's documentation, if any.
8+
///
9+
/// When converted to a [`TokenStream`], this will output an `Option<String>`
10+
/// containing the collection of doc comments.
11+
#[derive(Default)]
12+
pub(crate) struct Documentation {
13+
docs: Vec<String>,
14+
}
15+
16+
impl Documentation {
17+
/// Create a new [`Documentation`] from a type's attributes.
18+
///
19+
/// This will collect all `#[doc = "..."]` attributes, including the ones generated via `///` and `//!`.
20+
pub fn from_attributes<'a>(attributes: impl IntoIterator<Item = &'a Attribute>) -> Self {
21+
let docs = attributes
22+
.into_iter()
23+
.filter_map(|attr| {
24+
let meta = attr.parse_meta().ok()?;
25+
match meta {
26+
Meta::NameValue(pair) if pair.path.is_ident("doc") => {
27+
if let Lit::Str(lit) = pair.lit {
28+
Some(lit.value())
29+
} else {
30+
None
31+
}
32+
}
33+
_ => None,
34+
}
35+
})
36+
.collect();
37+
38+
Self { docs }
39+
}
40+
41+
/// The full docstring, if any.
42+
pub fn doc_string(&self) -> Option<String> {
43+
if self.docs.is_empty() {
44+
return None;
45+
}
46+
47+
let len = self.docs.len();
48+
Some(
49+
self.docs
50+
.iter()
51+
.enumerate()
52+
.map(|(index, doc)| {
53+
if index < len - 1 {
54+
format!("{}\n", doc)
55+
} else {
56+
doc.to_owned()
57+
}
58+
})
59+
.collect(),
60+
)
61+
}
62+
63+
/// Push a new docstring to the collection
64+
pub fn push(&mut self, doc: String) {
65+
self.docs.push(doc);
66+
}
67+
}
68+
69+
impl ToTokens for Documentation {
70+
fn to_tokens(&self, tokens: &mut TokenStream) {
71+
if let Some(doc) = self.doc_string() {
72+
quote!(Some(#doc)).to_tokens(tokens);
73+
} else {
74+
quote!(None).to_tokens(tokens);
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)