Skip to content

Commit 270bf9c

Browse files
committed
Fix visibility where class is wider than signal
Addresses immediate issue where a `pub struct MyClass` has a private #[signal] declaration. This caused "leaks private type" problems with the associated type <MyClass as WithSignals>::SignalCollection, that mentions a generated (private) type. This approach can possibly be extended to the inverse case later, to allow the inverse case: pub #[signal]s in private classes.
1 parent 446c51e commit 270bf9c

File tree

4 files changed

+76
-20
lines changed

4 files changed

+76
-20
lines changed

godot-macros/src/class/data_models/signal.rs

+26-17
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ struct SignalDetails<'a> {
112112
individual_struct_name: Ident,
113113
/// Visibility, e.g. `pub(crate)`
114114
vis_marker: Option<venial::VisMarker>,
115-
/// Detected visibility as strongly typed enum.
116-
vis_classified: SignalVisibility,
115+
// /// Detected visibility as strongly typed enum.
116+
// vis_classified: SignalVisibility,
117117
}
118118

119119
impl<'a> SignalDetails<'a> {
@@ -150,7 +150,7 @@ impl<'a> SignalDetails<'a> {
150150
let individual_struct_name = format_ident!("__godot_Signal_{}_{}", class_name, signal_name);
151151

152152
let vis_marker = &fn_signature.vis_marker;
153-
let Some(vis_classified) = SignalVisibility::try_parse(vis_marker.as_ref()) else {
153+
let Some(_vis_classified) = SignalVisibility::try_parse(vis_marker.as_ref()) else {
154154
return bail!(
155155
vis_marker,
156156
"invalid visibility `{}` for #[signal]; supported are `pub`, `pub(crate)`, `pub(super)` and private (no visibility marker)",
@@ -170,7 +170,7 @@ impl<'a> SignalDetails<'a> {
170170
signal_cfg_attrs,
171171
individual_struct_name,
172172
vis_marker: vis_marker.clone(),
173-
vis_classified,
173+
// vis_classified,
174174
})
175175
}
176176
}
@@ -187,8 +187,8 @@ pub fn make_signal_registrations(
187187

188188
#[cfg(since_api = "4.2")]
189189
let mut collection_api = SignalCollection::default();
190-
#[cfg(since_api = "4.2")]
191-
let mut max_visibility = SignalVisibility::Priv;
190+
// #[cfg(since_api = "4.2")]
191+
// let mut max_visibility = SignalVisibility::Priv;
192192

193193
for signal in signals {
194194
let SignalDefinition {
@@ -203,15 +203,15 @@ pub fn make_signal_registrations(
203203
#[cfg(since_api = "4.2")]
204204
if *has_builder {
205205
collection_api.extend_with(&details);
206-
max_visibility = max_visibility.max(details.vis_classified);
206+
// max_visibility = max_visibility.max(details.vis_classified);
207207
}
208208

209209
let registration = make_signal_registration(&details, class_name_obj);
210210
signal_registrations.push(registration);
211211
}
212212

213213
#[cfg(since_api = "4.2")]
214-
let signal_symbols = make_signal_symbols(class_name, collection_api, max_visibility);
214+
let signal_symbols = make_signal_symbols(class_name, collection_api);
215215
#[cfg(before_api = "4.2")]
216216
let signal_symbols = None;
217217

@@ -391,7 +391,7 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream {
391391
fn make_signal_symbols(
392392
class_name: &Ident,
393393
collection_api: SignalCollection,
394-
max_visibility: SignalVisibility,
394+
// max_visibility: SignalVisibility,
395395
) -> Option<TokenStream> {
396396
// Future note: if we add Rust->Rust inheritance, then the WithSignals trait must be unconditionally implemented.
397397
if collection_api.is_empty() {
@@ -405,8 +405,13 @@ fn make_signal_symbols(
405405
let individual_structs = collection_api.individual_structs;
406406

407407
// The collection cannot be `pub` because `Deref::Target` contains the class type, which leads to "leak private type" errors.
408-
// Max visibility: the user decides which visibility is acceptable for individual #[signal]s, which is bounded by the class' own
409-
// visibility. Since we assume that decision is correct, the collection itself can also share the widest visibility of any #[signal].
408+
// We thus adopt the visibility of the #[derive(GodotClass)] struct, imported via macro trick.
409+
//
410+
// A previous approach (that cannot access the struct visibility) used "max visibility": the user decides which visibility is acceptable f
411+
// or individual #[signal]s. They can all be at most the class visibility. Since we assume that decision is correct, the signal collection
412+
// itself can also share the widest visibility of any #[signal]. This approach however still led to problems because there's a 2-way
413+
// dependency: `impl WithSignals for MyClass` has an associated type `SignalCollection` that mentions the generated collection type. If
414+
// that collection type has *lower* visibility than the class, we *also* run into "leak private type" errors.
410415

411416
// Unrelated, we could use the following for encapsulation:
412417
// #[cfg(since_api = "4.2")]
@@ -430,13 +435,17 @@ fn make_signal_symbols(
430435
// Downside is slightly higher complexity and introducing signals in secondary blocks becomes harder (although we could use another
431436
// module name, we'd need a way to create unique names).
432437

438+
let visibility_macro = util::format_class_visibility_macro(class_name);
439+
433440
let code = quote! {
434-
#[allow(non_camel_case_types)]
435-
#[doc(hidden)] // Only on struct, not methods, to allow completion in IDEs.
436-
#max_visibility struct #collection_struct_name<'c, C> {
437-
// Hiding necessary because it's in the same scope as the user-defined class, so appearing in IDE completion.
438-
#[doc(hidden)]
439-
__internal_obj: Option<::godot::private::UserSignalObject<'c, C>>
441+
#visibility_macro! {
442+
#[allow(non_camel_case_types)]
443+
#[doc(hidden)] // Only on struct, not methods, to allow completion in IDEs.
444+
struct #collection_struct_name<'c, C> {
445+
// Hiding necessary because it's in the same scope as the user-defined class, so appearing in IDE completion.
446+
#[doc(hidden)]
447+
__internal_obj: Option<::godot::private::UserSignalObject<'c, C>>
448+
}
440449
}
441450

442451
impl<'c, C> #collection_struct_name<'c, C>

godot-macros/src/class/derive_godot_class.rs

+29-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8-
use proc_macro2::{Ident, Punct, TokenStream};
9-
use quote::{format_ident, quote, quote_spanned};
10-
118
use crate::class::{
129
make_property_impl, make_virtual_callback, BeforeKind, Field, FieldCond, FieldDefault,
1310
FieldExport, FieldVar, Fields, SignatureInfo,
@@ -17,6 +14,8 @@ use crate::util::{
1714
require_api_version, KvParser,
1815
};
1916
use crate::{handle_mutually_exclusive_keys, util, ParseResult};
17+
use proc_macro2::{Ident, Punct, TokenStream};
18+
use quote::{format_ident, quote, quote_spanned};
2019

2120
pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
2221
let class = item.as_struct().ok_or_else(|| {
@@ -143,6 +142,8 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
143142
pub struct #funcs_collection_struct_name {}
144143
};
145144

145+
let visibility_macro = make_visibility_macro(class_name, class.vis_marker.as_ref());
146+
146147
Ok(quote! {
147148
impl ::godot::obj::GodotClass for #class_name {
148149
type Base = #base_class;
@@ -172,6 +173,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
172173
#godot_exports_impl
173174
#user_class_impl
174175
#init_expecter
176+
#visibility_macro
175177
#( #deprecations )*
176178
#( #errors )*
177179

@@ -185,6 +187,30 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
185187
})
186188
}
187189

190+
/// Generates code for a decl-macro, which takes any item and prepends it with the visibility marker of the class.
191+
///
192+
/// Used to access the visibility of the class in other proc-macros like `#[godot_api]`.
193+
fn make_visibility_macro(
194+
class_name: &Ident,
195+
vis_marker: Option<&venial::VisMarker>,
196+
) -> TokenStream {
197+
let macro_name = util::format_class_visibility_macro(class_name);
198+
199+
quote! {
200+
macro_rules! #macro_name {
201+
(
202+
$( #[$meta:meta] )*
203+
struct $( $tt:tt )+
204+
) => {
205+
$( #[$meta] )*
206+
#vis_marker struct $( $tt )+
207+
};
208+
209+
// Can be expanded to `fn` etc. if needed.
210+
}
211+
}
212+
}
213+
188214
/// Checks at compile time that a function with the given name exists on `Self`.
189215
#[must_use]
190216
pub fn make_existence_check(ident: &Ident) -> TokenStream {

godot-macros/src/util/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -407,3 +407,8 @@ pub fn format_funcs_collection_constant(_class_name: &Ident, func_name: &Ident)
407407
pub fn format_funcs_collection_struct(class_name: &Ident) -> Ident {
408408
format_ident!("__godot_{class_name}_Funcs")
409409
}
410+
411+
/// Returns the name of the macro used to communicate the `struct` (class) visibility to other symbols.
412+
pub fn format_class_visibility_macro(class_name: &Ident) -> Ident {
413+
format_ident!("__godot_{class_name}_vis_macro")
414+
}

itest/rust/src/builtin_tests/containers/signal_test.rs

+16
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,22 @@ impl Receiver {
446446
}
447447
}
448448

449+
// ----------------------------------------------------------------------------------------------------------------------------------------------
450+
451+
// Class which is deliberately `pub` but has only private `#[signal]` declaration.
452+
// Regression test, as this caused "leaked private types" in the past.
453+
#[derive(GodotClass)]
454+
#[class(init, base=Object)]
455+
pub struct PubClassPrivSignal {
456+
_base: Base<Object>,
457+
}
458+
459+
#[godot_api]
460+
impl PubClassPrivSignal {
461+
#[signal]
462+
fn private_signal();
463+
}
464+
449465
// ----------------------------------------------------------------------------------------------------------------------------------------------
450466
// 4.2+ custom callables
451467

0 commit comments

Comments
 (0)