Skip to content

Commit d18785d

Browse files
authored
Merge pull request #1182 from godot-rust/feature/final-classes-no-interface
Final classes no longer have a `I*` interface trait
2 parents cfa0d29 + d62f1f1 commit d18785d

File tree

7 files changed

+272
-47
lines changed

7 files changed

+272
-47
lines changed

godot-codegen/src/context.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub struct Context<'a> {
2323
native_structures_types: HashSet<&'a str>,
2424
singletons: HashSet<&'a str>,
2525
inheritance_tree: InheritanceTree,
26+
/// Which interface traits are generated (`false` for "Godot-abstract"/final classes).
27+
classes_final: HashMap<TyName, bool>,
2628
cached_rust_types: HashMap<GodotTy, RustTy>,
2729
notifications_by_class: HashMap<TyName, Vec<(Ident, i32)>>,
2830
classes_with_signals: HashSet<TyName>,
@@ -64,13 +66,16 @@ impl<'a> Context<'a> {
6466
continue;
6567
}
6668

67-
// Populate class lookup by name
69+
// Populate class lookup by name.
6870
engine_classes.insert(class_name.clone(), class);
6971

7072
if !option_as_slice(&class.signals).is_empty() {
7173
ctx.classes_with_signals.insert(class_name.clone());
7274
}
7375

76+
ctx.classes_final
77+
.insert(class_name.clone(), ctx.is_class_final(&class_name));
78+
7479
// Populate derived-to-base relations
7580
if let Some(base) = class.inherits.as_ref() {
7681
let base_name = TyName::from_godot(base);
@@ -245,6 +250,21 @@ impl<'a> Context<'a> {
245250
.unwrap_or_else(|| panic!("did not register table index for key {:?}", key))
246251
}
247252

253+
/// Whether an interface trait is generated for a class.
254+
///
255+
/// False if the class is "Godot-abstract"/final, thus there are no virtual functions to inherit.
256+
fn is_class_final(&self, class_name: &TyName) -> bool {
257+
debug_assert!(
258+
!self.singletons.is_empty(),
259+
"initialize singletons before final-check"
260+
);
261+
262+
self.singletons.contains(class_name.godot_ty.as_str())
263+
|| special_cases::is_class_abstract(class_name)
264+
}
265+
266+
// ------------------------------------------------------------------------------------------------------------------------------------------
267+
248268
/// Checks if this is a builtin type (not `Object`).
249269
///
250270
/// Note that builtins != variant types.
@@ -275,6 +295,15 @@ impl<'a> Context<'a> {
275295
self.singletons.contains(class_name.godot_ty.as_str())
276296
}
277297

298+
pub fn is_final(&self, class_name: &TyName) -> bool {
299+
*self.classes_final.get(class_name).unwrap_or_else(|| {
300+
panic!(
301+
"queried final status for class {}, but it is not registered",
302+
class_name.godot_ty
303+
)
304+
})
305+
}
306+
278307
pub fn inheritance_tree(&self) -> &InheritanceTree {
279308
&self.inheritance_tree
280309
}
@@ -317,7 +346,6 @@ impl<'a> Context<'a> {
317346
assert!(prev.is_none(), "no overwrites of RustTy");
318347
}
319348
}
320-
321349
// ----------------------------------------------------------------------------------------------------------------------------------------------
322350

323351
#[derive(Clone)]
@@ -379,7 +407,7 @@ impl InheritanceTree {
379407
self.derived_to_base.get(derived_name).cloned()
380408
}
381409

382-
/// Returns all base classes, without the class itself, in order from nearest to furthest (object).
410+
/// Returns all base classes, without the class itself, in order from nearest to furthest (`Object`).
383411
pub fn collect_all_bases(&self, derived_name: &TyName) -> Vec<TyName> {
384412
let mut upgoer = derived_name;
385413
let mut result = vec![];

godot-codegen/src/generator/classes.rs

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::models::domain::{
1515
ModName, TyName,
1616
};
1717
use crate::util::{ident, make_string_name};
18-
use crate::{conv, util, SubmitFn};
18+
use crate::{util, SubmitFn};
1919
use proc_macro2::{Ident, TokenStream};
2020
use quote::{format_ident, quote};
2121
use std::path::Path;
@@ -45,6 +45,7 @@ pub fn generate_class_files(
4545
own_notification_enum_name: generated_class.notification_enum.try_to_own_name(),
4646
inherits_macro_ident: generated_class.inherits_macro_ident,
4747
is_pub_sidecar: generated_class.has_sidecar_module,
48+
has_interface_trait: generated_class.has_interface_trait,
4849
});
4950
}
5051

@@ -63,6 +64,7 @@ struct GeneratedClass {
6364
inherits_macro_ident: Option<Ident>,
6465
/// Sidecars are the associated modules with related enum/flag types, such as `node_3d` for `Node3D` class.
6566
has_sidecar_module: bool,
67+
has_interface_trait: bool,
6668
}
6769

6870
struct GeneratedClassModule {
@@ -71,6 +73,7 @@ struct GeneratedClassModule {
7173
own_notification_enum_name: Option<Ident>,
7274
inherits_macro_ident: Option<Ident>,
7375
is_pub_sidecar: bool,
76+
has_interface_trait: bool,
7477
}
7578

7679
struct Construction {
@@ -86,13 +89,11 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
8689
// Strings
8790
let godot_class_str = &class_name.godot_ty;
8891
let class_name_cstr = util::c_str(godot_class_str);
89-
let virtual_trait_str = class_name.virtual_trait_name();
9092

9193
// Idents and tokens
92-
let (base_ty, base_ident_opt) = match class.inherits.as_ref() {
93-
Some(base) => {
94-
let base = ident(&conv::to_pascal_case(base));
95-
(quote! { crate::classes::#base }, Some(base))
94+
let (base_ty, base_ident_opt) = match class.base_class.as_ref() {
95+
Some(TyName { rust_ty, .. }) => {
96+
(quote! { crate::classes::#rust_ty }, Some(rust_ty.clone()))
9697
}
9798
None => (quote! { crate::obj::NoBase }, None),
9899
};
@@ -157,25 +158,32 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
157158
// This checks if token streams (i.e. code) is empty.
158159
let has_sidecar_module = !enums.is_empty() || !builders.is_empty() || has_own_signals;
159160

161+
let module_doc = docs::make_module_doc(class_name);
162+
163+
// Classes that can't be inherited from don't need to provide an interface with overridable virtual methods.
164+
let has_interface_trait = !class.is_final;
165+
let interface_trait = if has_interface_trait {
166+
virtual_traits::make_virtual_methods_trait(
167+
class,
168+
&all_bases,
169+
&notification_enum_name,
170+
&cfg_attributes,
171+
view,
172+
ctx,
173+
)
174+
} else {
175+
TokenStream::new()
176+
};
177+
160178
let class_doc = docs::make_class_doc(
161179
class_name,
162180
base_ident_opt,
163181
notification_enum.is_some(),
164182
has_sidecar_module,
183+
has_interface_trait,
165184
has_own_signals,
166185
);
167186

168-
let module_doc = docs::make_module_doc(class_name);
169-
170-
let virtual_trait = virtual_traits::make_virtual_methods_trait(
171-
class,
172-
&all_bases,
173-
&virtual_trait_str,
174-
&notification_enum_name,
175-
&cfg_attributes,
176-
view,
177-
);
178-
179187
// notify() and notify_reversed() are added after other methods, to list others first in docs.
180188
let notify_methods = notifications::make_notify_methods(class_name, ctx);
181189

@@ -220,7 +228,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
220228
// The RawGd<T>'s identity field can be None because of generality (it can represent null pointers, as opposed to Gd<T>).
221229
rtti: Option<crate::private::ObjectRtti>,
222230
}
223-
#virtual_trait
231+
#interface_trait
224232
#notification_enum
225233
impl #class_name {
226234
#constructor
@@ -274,6 +282,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
274282
},
275283
inherits_macro_ident,
276284
has_sidecar_module,
285+
has_interface_trait,
277286
}
278287
}
279288

@@ -294,7 +303,7 @@ fn make_inherits_macro(class: &Class, all_bases: &[TyName]) -> (Option<Ident>, T
294303
// For final classes, we can directly create a meaningful compile error.
295304
if class.is_final {
296305
let error_msg = format!(
297-
"Class `{}` is final and cannot be inherited from.",
306+
"Class `{}` is final, meaning it cannot be inherited in GDExtension or GDScript.",
298307
class_name.rust_ty
299308
);
300309

@@ -351,10 +360,14 @@ fn make_class_module_file(classes_and_modules: Vec<GeneratedClassModule>) -> Tok
351360

352361
let vis = is_pub.then_some(quote! { pub });
353362

363+
let interface_reexport = m.has_interface_trait.then(|| {
364+
quote! { pub use #module_name::re_export::#virtual_trait_name; }
365+
});
366+
354367
let class_decl = quote! {
355368
#vis mod #module_name;
356369
pub use #module_name::re_export::#class_name;
357-
pub use #module_name::re_export::#virtual_trait_name;
370+
#interface_reexport
358371
};
359372
class_decls.push(class_decl);
360373

@@ -452,7 +465,8 @@ fn make_constructor_and_default(class: &Class, ctx: &Context) -> Construction {
452465
let final_doc = if class.is_final {
453466
Some(
454467
"\n\n# Final class\n\n\
455-
This class is _final_, meaning you cannot inherit from it.",
468+
This class is _final_, meaning you cannot inherit from it, and it comes without `I*` interface trait. \
469+
It is still possible that other Godot classes inherit from it, but that is limited to the engine itself.",
456470
)
457471
} else {
458472
None

godot-codegen/src/generator/docs.rs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub fn make_class_doc(
1919
base_ident_opt: Option<Ident>,
2020
has_notification_enum: bool,
2121
has_sidecar_module: bool,
22+
has_interface_trait: bool,
2223
has_signal_collection: bool,
2324
) -> String {
2425
let TyName { rust_ty, godot_ty } = class_name;
@@ -62,7 +63,12 @@ pub fn make_class_doc(
6263
godot_ty.to_ascii_lowercase()
6364
);
6465

65-
let trait_name = class_name.virtual_trait_name();
66+
let interface_trait_line = if has_interface_trait {
67+
let trait_name = class_name.virtual_trait_name();
68+
format!("* [`{trait_name}`][crate::classes::{trait_name}]: virtual methods\n")
69+
} else {
70+
String::new()
71+
};
6672

6773
let notes = special_cases::get_class_extra_docs(class_name)
6874
.map(|notes| format!("# Specific notes for this class\n\n{}", notes))
@@ -75,15 +81,19 @@ pub fn make_class_doc(
7581
\
7682
Related symbols:\n\n\
7783
{sidecar_signal_lines}\
78-
* [`{trait_name}`][crate::classes::{trait_name}]: virtual methods\n\
84+
{interface_trait_line}\
7985
{signal_line}\
8086
{notify_line}\
8187
\n\n\
8288
See also [Godot docs for `{godot_ty}`]({online_link}).\n\n{notes}",
8389
)
8490
}
8591

86-
pub fn make_virtual_trait_doc(trait_name_str: &str, class_name: &TyName) -> String {
92+
pub fn make_virtual_trait_doc(
93+
trait_name_str: &str,
94+
base_traits: &[(String, bool)],
95+
class_name: &TyName,
96+
) -> String {
8797
let TyName { rust_ty, godot_ty } = class_name;
8898

8999
let online_link = format!(
@@ -95,12 +105,40 @@ pub fn make_virtual_trait_doc(trait_name_str: &str, class_name: &TyName) -> Stri
95105
.map(|notes| format!("# Specific notes for this interface\n\n{}", notes))
96106
.unwrap_or_default();
97107

108+
// Detect if a base interface exists. This is not the case if intermediate Godot classes are marked "abstract" (aka final for GDExtension).
109+
// In such cases, still show interfaces as strikethrough.
110+
let inherits_line = if base_traits.is_empty() {
111+
String::new()
112+
} else {
113+
let mut parts = vec![];
114+
let mut strikethrough_explanation = "";
115+
for (trait_name, is_generated) in base_traits {
116+
let part = if *is_generated {
117+
format!("[`{trait_name}`][crate::classes::{trait_name}]")
118+
} else {
119+
strikethrough_explanation =
120+
" \n(Strike-through means some intermediate Godot classes are marked final, \
121+
and can thus not be inherited by GDExtension.)\n\n";
122+
format!("~~`{trait_name}`~~")
123+
};
124+
parts.push(part);
125+
}
126+
127+
format!(
128+
"\n\nBase interfaces: {}.{}",
129+
parts.join(" > "),
130+
strikethrough_explanation
131+
)
132+
};
133+
98134
format!(
99-
"Virtual methods for class [`{rust_ty}`][crate::classes::{rust_ty}].\
100-
\n\n\
101-
These methods represent constructors (`init`) or callbacks invoked by the engine.\
135+
"# Interface trait for class [`{rust_ty}`][crate::classes::{rust_ty}].\
102136
\n\n\
103-
See also [Godot docs for `{godot_ty}` methods]({online_link}).\n\n{notes}"
137+
Functions in this trait represent constructors (`init`) or virtual method callbacks invoked by the engine.\
138+
\n\n{notes}\
139+
\n\n# Related symbols\
140+
{inherits_line}\
141+
\n\nSee also [Godot docs for `{godot_ty}` methods]({online_link})."
104142
)
105143
}
106144

godot-codegen/src/generator/virtual_traits.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8+
use crate::context::Context;
89
use crate::generator::functions_common::FnCode;
910
use crate::generator::{docs, functions_common};
1011
use crate::models::domain::{
@@ -19,18 +20,20 @@ use std::fmt::Write;
1920
pub fn make_virtual_methods_trait(
2021
class: &Class,
2122
all_base_names: &[TyName],
22-
trait_name_str: &str,
2323
notification_enum_name: &Ident,
2424
cfg_attributes: &TokenStream,
2525
view: &ApiView,
26+
ctx: &Context,
2627
) -> TokenStream {
27-
let trait_name = ident(trait_name_str);
2828
let class_name = &class.name().rust_ty;
29+
let trait_name_str = class.name().virtual_trait_name();
30+
let trait_name = ident(&trait_name_str);
2931

3032
let (virtual_method_fns, extra_docs) = make_all_virtual_methods(class, all_base_names, view);
3133
let special_virtual_methods = make_special_virtual_methods(notification_enum_name);
3234

33-
let trait_doc = docs::make_virtual_trait_doc(trait_name_str, class.name());
35+
let base_traits = collect_base_traits(all_base_names, ctx);
36+
let trait_doc = docs::make_virtual_trait_doc(&trait_name_str, &base_traits, class.name());
3437

3538
quote! {
3639
#[doc = #trait_doc]
@@ -45,6 +48,22 @@ pub fn make_virtual_methods_trait(
4548
}
4649
}
4750

51+
/// Collects base traits (without current) in order towards root.
52+
///
53+
/// Results contain the name of the trait `I*` and whether code for it is generated (e.g. `false` for final classes).
54+
fn collect_base_traits(all_base_classes: &[TyName], ctx: &Context) -> Vec<(String, bool)> {
55+
let mut base_traits = vec![];
56+
57+
for class_name in all_base_classes {
58+
let trait_name = class_name.virtual_trait_name();
59+
let has_interface_trait = !ctx.is_final(class_name);
60+
61+
base_traits.push((trait_name, has_interface_trait))
62+
}
63+
64+
base_traits
65+
}
66+
4867
// ----------------------------------------------------------------------------------------------------------------------------------------------
4968
// Implementation
5069

godot-codegen/src/models/domain.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,8 @@ pub struct Class {
161161
pub is_refcounted: bool,
162162
pub is_instantiable: bool,
163163
pub is_experimental: bool,
164-
/// `true` if inheriting the class is disallowed.
165164
pub is_final: bool,
166-
pub inherits: Option<String>,
165+
pub base_class: Option<TyName>,
167166
pub api_level: ClassCodegenLevel,
168167
pub constants: Vec<ClassConstant>,
169168
pub enums: Vec<Enum>,
@@ -762,6 +761,9 @@ impl TyName {
762761
}
763762
}
764763

764+
/// Get name of virtual interface trait.
765+
///
766+
/// This is also valid if the outer class generates no traits (e.g. to explicitly mention absence in docs).
765767
pub fn virtual_trait_name(&self) -> String {
766768
format!("I{}", self.rust_ty)
767769
}

0 commit comments

Comments
 (0)