Skip to content

Commit d62f1f1

Browse files
committed
Determine interface trait presence by abstract/final status
Collect information about all "abstract" classes in Godot. Disable `I*` traits whenever they are final, meaning either: * a singleton * abstract (in Godot terms), i.e. non-inheritable outside engine itself Document the trait relation, and explicitly exclude intermediate interfaces associated with final classes.
1 parent d249103 commit d62f1f1

File tree

7 files changed

+237
-33
lines changed

7 files changed

+237
-33
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: 8 additions & 9 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;
@@ -91,10 +91,9 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
9191
let class_name_cstr = util::c_str(godot_class_str);
9292

9393
// Idents and tokens
94-
let (base_ty, base_ident_opt) = match class.inherits.as_ref() {
95-
Some(base) => {
96-
let base = ident(&conv::to_pascal_case(base));
97-
(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()))
9897
}
9998
None => (quote! { crate::obj::NoBase }, None),
10099
};
@@ -164,14 +163,13 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
164163
// Classes that can't be inherited from don't need to provide an interface with overridable virtual methods.
165164
let has_interface_trait = !class.is_final;
166165
let interface_trait = if has_interface_trait {
167-
let virtual_trait_str = class_name.virtual_trait_name();
168166
virtual_traits::make_virtual_methods_trait(
169167
class,
170168
&all_bases,
171-
&virtual_trait_str,
172169
&notification_enum_name,
173170
&cfg_attributes,
174171
view,
172+
ctx,
175173
)
176174
} else {
177175
TokenStream::new()
@@ -305,7 +303,7 @@ fn make_inherits_macro(class: &Class, all_bases: &[TyName]) -> (Option<Ident>, T
305303
// For final classes, we can directly create a meaningful compile error.
306304
if class.is_final {
307305
let error_msg = format!(
308-
"Class `{}` is final and cannot be inherited from.",
306+
"Class `{}` is final, meaning it cannot be inherited in GDExtension or GDScript.",
309307
class_name.rust_ty
310308
);
311309

@@ -467,7 +465,8 @@ fn make_constructor_and_default(class: &Class, ctx: &Context) -> Construction {
467465
let final_doc = if class.is_final {
468466
Some(
469467
"\n\n# Final class\n\n\
470-
This class is _final_, meaning you cannot inherit from it, and it comes without `I*` interface trait.",
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.",
471470
)
472471
} else {
473472
None

godot-codegen/src/generator/docs.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ pub fn make_class_doc(
8989
)
9090
}
9191

92-
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 {
9397
let TyName { rust_ty, godot_ty } = class_name;
9498

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

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+
104134
format!(
105-
"Virtual methods for class [`{rust_ty}`][crate::classes::{rust_ty}].\
106-
\n\n\
107-
These methods represent constructors (`init`) or callbacks invoked by the engine.\
135+
"# Interface trait for class [`{rust_ty}`][crate::classes::{rust_ty}].\
108136
\n\n\
109-
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})."
110142
)
111143
}
112144

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
}

godot-codegen/src/models/domain_mapping.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,11 @@ impl Class {
9191

9292
// Already checked in is_class_deleted(), but code remains more maintainable if those are separate, and it's cheap to validate.
9393
let is_experimental = special_cases::is_class_experimental(&ty_name.godot_ty);
94-
let is_instantiable =
95-
special_cases::is_class_instantiable(&ty_name).unwrap_or(json.is_instantiable);
96-
let is_final = ctx.is_singleton(&ty_name) || special_cases::is_class_final(&ty_name);
94+
95+
let is_instantiable = special_cases::is_class_instantiable(&ty_name) //.
96+
.unwrap_or(json.is_instantiable);
97+
98+
let is_final = ctx.is_final(&ty_name);
9799

98100
let mod_name = ModName::from_godot(&ty_name.godot_ty);
99101

@@ -126,6 +128,11 @@ impl Class {
126128
})
127129
.collect();
128130

131+
let base_class = json
132+
.inherits
133+
.as_ref()
134+
.map(|godot_name| TyName::from_godot(godot_name));
135+
129136
Some(Self {
130137
common: ClassCommons {
131138
name: ty_name,
@@ -135,7 +142,7 @@ impl Class {
135142
is_instantiable,
136143
is_experimental,
137144
is_final,
138-
inherits: json.inherits.clone(),
145+
base_class,
139146
api_level: get_api_level(json),
140147
constants,
141148
enums,

0 commit comments

Comments
 (0)