Skip to content

Commit 6101ea0

Browse files
authored
Merge pull request #839 from godot-rust/qol/export-restrictions
Disallow `Export` if class doesn't inherit `Node` or `Resource`
2 parents b28cc97 + 94e20af commit 6101ea0

File tree

7 files changed

+95
-23
lines changed

7 files changed

+95
-23
lines changed

godot-codegen/src/context.rs

+22-3
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,32 @@ impl InheritanceTree {
329329

330330
/// Returns all base classes, without the class itself, in order from nearest to furthest (object).
331331
pub fn collect_all_bases(&self, derived_name: &TyName) -> Vec<TyName> {
332-
let mut maybe_base = derived_name;
332+
let mut upgoer = derived_name;
333333
let mut result = vec![];
334334

335-
while let Some(base) = self.derived_to_base.get(maybe_base) {
335+
while let Some(base) = self.derived_to_base.get(upgoer) {
336336
result.push(base.clone());
337-
maybe_base = base;
337+
upgoer = base;
338338
}
339339
result
340340
}
341+
342+
/// Whether a class is a direct or indirect subclass of another (true for derived == base).
343+
pub fn inherits(&self, derived: &TyName, base_name: &str) -> bool {
344+
// Reflexive: T inherits T.
345+
if derived.godot_ty == base_name {
346+
return true;
347+
}
348+
349+
let mut upgoer = derived;
350+
351+
while let Some(next_base) = self.derived_to_base.get(upgoer) {
352+
if next_base.godot_ty == base_name {
353+
return true;
354+
}
355+
upgoer = next_base;
356+
}
357+
358+
false
359+
}
341360
}

godot-codegen/src/generator/classes.rs

+14-4
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
155155
// notify() and notify_reversed() are added after other methods, to list others first in docs.
156156
let notify_methods = notifications::make_notify_methods(class_name, ctx);
157157

158-
let (assoc_memory, assoc_dyn_memory) = make_bounds(class);
158+
let (assoc_memory, assoc_dyn_memory, is_exportable) = make_bounds(class, ctx);
159159

160160
let internal_methods = quote! {
161161
fn __checked_id(&self) -> Option<crate::obj::InstanceId> {
@@ -228,6 +228,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
228228
type Memory = crate::obj::bounds::#assoc_memory;
229229
type DynMemory = crate::obj::bounds::#assoc_dyn_memory;
230230
type Declarer = crate::obj::bounds::DeclEngine;
231+
type Exportable = crate::obj::bounds::#is_exportable;
231232
}
232233

233234
#(
@@ -420,8 +421,10 @@ fn make_deref_impl(class_name: &TyName, base_ty: &TokenStream) -> TokenStream {
420421
}
421422
}
422423

423-
fn make_bounds(class: &Class) -> (Ident, Ident) {
424-
let assoc_dyn_memory = if class.name().rust_ty == "Object" {
424+
fn make_bounds(class: &Class, ctx: &mut Context) -> (Ident, Ident, Ident) {
425+
let c = class.name();
426+
427+
let assoc_dyn_memory = if c.rust_ty == "Object" {
425428
ident("MemDynamic")
426429
} else if class.is_refcounted {
427430
ident("MemRefCounted")
@@ -435,7 +438,14 @@ fn make_bounds(class: &Class) -> (Ident, Ident) {
435438
ident("MemManual")
436439
};
437440

438-
(assoc_memory, assoc_dyn_memory)
441+
let tree = ctx.inheritance_tree();
442+
let is_exportable = if tree.inherits(c, "Node") || tree.inherits(c, "Resource") {
443+
ident("Yes")
444+
} else {
445+
ident("No")
446+
};
447+
448+
(assoc_memory, assoc_dyn_memory, is_exportable)
439449
}
440450

441451
fn make_class_methods(

godot-core/src/obj/bounds.rs

+24-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ use private::Sealed;
5858
// Sealed trait
5959

6060
pub(super) mod private {
61-
use super::{Declarer, DynMemory, Memory};
61+
use super::{Declarer, DynMemory, Exportable, Memory};
6262

6363
// Bounds trait declared here for code locality; re-exported in crate::obj.
6464

@@ -97,6 +97,12 @@ pub(super) mod private {
9797
/// Whether this class is a core Godot class provided by the engine, or declared by the user as a Rust struct.
9898
// TODO what about GDScript user classes?
9999
type Declarer: Declarer;
100+
101+
/// True if *either* `T: Inherits<Node>` *or* `T: Inherits<Resource>` is fulfilled.
102+
///
103+
/// Enables `#[export]` for those classes.
104+
#[doc(hidden)]
105+
type Exportable: Exportable;
100106
}
101107

102108
/// Implements [`Bounds`] for a user-defined class.
@@ -130,6 +136,7 @@ pub(super) mod private {
130136
type Memory = <<$UserClass as $crate::obj::GodotClass>::Base as $crate::obj::Bounds>::Memory;
131137
type DynMemory = <<$UserClass as $crate::obj::GodotClass>::Base as $crate::obj::Bounds>::DynMemory;
132138
type Declarer = $crate::obj::bounds::DeclUser;
139+
type Exportable = <<$UserClass as $crate::obj::GodotClass>::Base as $crate::obj::Bounds>::Exportable;
133140
}
134141
};
135142
}
@@ -417,3 +424,19 @@ impl Declarer for DeclUser {
417424
}
418425
}
419426
}
427+
428+
// ----------------------------------------------------------------------------------------------------------------------------------------------
429+
// Exportable bounds (still hidden)
430+
431+
#[doc(hidden)]
432+
pub trait Exportable: Sealed {}
433+
434+
#[doc(hidden)]
435+
pub enum Yes {}
436+
impl Sealed for Yes {}
437+
impl Exportable for Yes {}
438+
439+
#[doc(hidden)]
440+
pub enum No {}
441+
impl Sealed for No {}
442+
impl Exportable for No {}

godot-core/src/obj/gd.rs

+26-12
Original file line numberDiff line numberDiff line change
@@ -738,16 +738,27 @@ impl<T: GodotClass> GodotType for Gd<T> {
738738

739739
impl<T: GodotClass> ArrayElement for Gd<T> {
740740
fn element_type_string() -> String {
741-
match Self::export_hint().hint {
742-
hint @ (PropertyHint::RESOURCE_TYPE | PropertyHint::NODE_TYPE) => {
743-
format!(
744-
"{}/{}:{}",
745-
VariantType::OBJECT.ord(),
746-
hint.ord(),
747-
T::class_name()
748-
)
749-
}
750-
_ => format!("{}:", VariantType::OBJECT.ord()),
741+
// See also impl Export for Gd<T>.
742+
743+
let hint = if T::inherits::<classes::Resource>() {
744+
Some(PropertyHint::RESOURCE_TYPE)
745+
} else if T::inherits::<classes::Node>() {
746+
Some(PropertyHint::NODE_TYPE)
747+
} else {
748+
None
749+
};
750+
751+
// Exportable classes (Resource/Node based) include the {RESOURCE|NODE}_TYPE hint + the class name.
752+
if let Some(export_hint) = hint {
753+
format!(
754+
"{variant}/{hint}:{class}",
755+
variant = VariantType::OBJECT.ord(),
756+
hint = export_hint.ord(),
757+
class = T::class_name()
758+
)
759+
} else {
760+
// Previous impl: format!("{variant}:", variant = VariantType::OBJECT.ord())
761+
unreachable!("element_type_string() should only be invoked for exportable classes")
751762
}
752763
}
753764
}
@@ -793,14 +804,17 @@ impl<T: GodotClass> Var for Gd<T> {
793804
}
794805
}
795806

796-
impl<T: GodotClass> Export for Gd<T> {
807+
impl<T> Export for Gd<T>
808+
where
809+
T: GodotClass + Bounds<Exportable = bounds::Yes>,
810+
{
797811
fn export_hint() -> PropertyHintInfo {
798812
let hint = if T::inherits::<classes::Resource>() {
799813
PropertyHint::RESOURCE_TYPE
800814
} else if T::inherits::<classes::Node>() {
801815
PropertyHint::NODE_TYPE
802816
} else {
803-
PropertyHint::NONE
817+
unreachable!("classes not inheriting from Resource or Node should not be exportable")
804818
};
805819

806820
// Godot does this by default too; the hint is needed when the class is a resource/node,

godot-core/src/obj/traits.rs

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ unsafe impl Bounds for NoBase {
8080
type Memory = bounds::MemManual;
8181
type DynMemory = bounds::MemManual;
8282
type Declarer = bounds::DeclEngine;
83+
type Exportable = bounds::No;
8384
}
8485

8586
/// Non-strict inheritance relationship in the Godot class hierarchy.

godot-core/src/registry/property.rs

+7-3
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ use crate::meta::{FromGodot, GodotConvert, GodotType, PropertyHintInfo, ToGodot}
2222
/// This does not require [`FromGodot`] or [`ToGodot`], so that something can be used as a property even if it can't be used in function
2323
/// arguments/return types.
2424
25-
// We also mention #[export] here, because we can't control the order of error messages. Missing Export often also means missing Var trait,
26-
// and so the Var error message appears first.
25+
// on_unimplemented: we also mention #[export] here, because we can't control the order of error messages.
26+
// Missing Export often also means missing Var trait, and so the Var error message appears first.
2727
#[diagnostic::on_unimplemented(
2828
message = "`#[var]` properties require `Var` trait; #[export] ones require `Export` trait",
2929
label = "type cannot be used as a property",
@@ -41,7 +41,10 @@ pub trait Var: GodotConvert {
4141
}
4242

4343
/// Trait implemented for types that can be used as `#[export]` fields.
44-
// Mentioning both Var + Export: see above.
44+
///
45+
/// `Export` is only implemented for objects `Gd<T>` if either `T: Inherits<Node>` or `T: Inherits<Resource>`, just like GDScript.
46+
/// This means you cannot use `#[export]` with `Gd<RefCounted>`, for example.
47+
// on_unimplemented: mentioning both Var + Export; see above.
4548
#[diagnostic::on_unimplemented(
4649
message = "`#[var]` properties require `Var` trait; #[export] ones require `Export` trait",
4750
label = "type cannot be used as a property",
@@ -414,6 +417,7 @@ mod export_impls {
414417

415418
// Dictionary: will need to be done manually once they become typed.
416419
impl_property_by_godot_convert!(Dictionary);
420+
impl_property_by_godot_convert!(Variant);
417421

418422
// Packed arrays: we manually implement `Export`.
419423
impl_property_by_godot_convert!(PackedByteArray, no_export);

godot-macros/src/class/derive_godot_class.rs

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
129129
type Memory = <<Self as ::godot::obj::GodotClass>::Base as ::godot::obj::Bounds>::Memory;
130130
type DynMemory = <<Self as ::godot::obj::GodotClass>::Base as ::godot::obj::Bounds>::DynMemory;
131131
type Declarer = ::godot::obj::bounds::DeclUser;
132+
type Exportable = <<Self as ::godot::obj::GodotClass>::Base as ::godot::obj::Bounds>::Exportable;
132133
}
133134

134135
#godot_init_impl

0 commit comments

Comments
 (0)