Skip to content

Commit 8215c6b

Browse files
committed
Validate that enum replacement parameters/return values truly exist
1 parent 7cd3000 commit 8215c6b

File tree

3 files changed

+126
-95
lines changed

3 files changed

+126
-95
lines changed

godot-codegen/src/models/domain.rs

Lines changed: 66 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ use proc_macro2::{Ident, Literal, TokenStream};
1717
use quote::{format_ident, quote, ToTokens};
1818

1919
use crate::context::Context;
20+
use crate::conv;
2021
use crate::models::json::{JsonMethodArg, JsonMethodReturn};
2122
use crate::util::{ident, option_as_slice, safe_ident};
22-
use crate::{conv, special_cases};
2323

2424
mod enums;
2525

@@ -498,7 +498,6 @@ impl FnQualifier {
498498
}
499499
// ----------------------------------------------------------------------------------------------------------------------------------------------
500500

501-
#[derive(Clone)]
502501
pub struct FnParam {
503502
pub name: Ident,
504503

@@ -511,29 +510,29 @@ pub struct FnParam {
511510

512511
impl FnParam {
513512
/// Creates a new parameter builder for constructing function parameters with configurable options.
514-
pub fn builder<'a>() -> FnParamBuilder<'a> {
513+
pub fn builder() -> FnParamBuilder {
515514
FnParamBuilder::new()
516515
}
517516
}
518517

519518
/// Builder for constructing `FnParam` instances with configurable enum replacements and default value handling.
520-
pub struct FnParamBuilder<'a> {
521-
surrounding_class_method: Option<(&'a TyName, &'a str)>,
519+
pub struct FnParamBuilder {
520+
replacements: EnumReplacements,
522521
no_defaults: bool,
523522
}
524523

525-
impl<'a> FnParamBuilder<'a> {
526-
/// Creates a new parameter builder with default settings (replacements disabled, defaults enabled).
524+
impl FnParamBuilder {
525+
/// Creates a new parameter builder with default settings (no replacements, defaults enabled).
527526
pub fn new() -> Self {
528527
Self {
529-
surrounding_class_method: None,
528+
replacements: &[],
530529
no_defaults: false,
531530
}
532531
}
533532

534-
/// Configures the builder to apply enum replacements for the specified class and method context.
535-
pub fn with_replacements(mut self, class_name: &'a TyName, method_name: &'a str) -> Self {
536-
self.surrounding_class_method = Some((class_name, method_name));
533+
/// Configures the builder to use specific enum replacements.
534+
pub fn enum_replacements(mut self, replacements: EnumReplacements) -> Self {
535+
self.replacements = replacements;
537536
self
538537
}
539538

@@ -543,12 +542,44 @@ impl<'a> FnParamBuilder<'a> {
543542
self
544543
}
545544

545+
/// Builds a single function parameter from the provided JSON method argument.
546+
pub fn build_single(self, method_arg: &JsonMethodArg, ctx: &mut Context) -> FnParam {
547+
self.build_single_impl(method_arg, ctx)
548+
}
549+
550+
/// Builds a vector of function parameters from the provided JSON method arguments.
551+
pub fn build_many(
552+
self,
553+
method_args: &Option<Vec<JsonMethodArg>>,
554+
ctx: &mut Context,
555+
) -> Vec<FnParam> {
556+
option_as_slice(method_args)
557+
.iter()
558+
.map(|arg| self.build_single_impl(arg, ctx))
559+
.collect()
560+
}
561+
546562
/// Core implementation for processing a single JSON method argument into a `FnParam`.
547563
fn build_single_impl(&self, method_arg: &JsonMethodArg, ctx: &mut Context) -> FnParam {
548564
let name = safe_ident(&method_arg.name);
549565
let type_ = conv::to_rust_type(&method_arg.type_, method_arg.meta.as_ref(), ctx);
550-
let type_ =
551-
apply_enum_replacement(Some(&method_arg.name), type_, self.surrounding_class_method);
566+
567+
// Apply enum replacement if one exists for this parameter
568+
let matching_replacement = self
569+
.replacements
570+
.iter()
571+
.find(|(p, ..)| *p == method_arg.name);
572+
let type_ = if let Some((_, enum_name, is_bitfield)) = matching_replacement {
573+
if !type_.is_integer() {
574+
panic!(
575+
"Parameter `{}` is of type {}, but can only replace int with enum",
576+
method_arg.name, type_
577+
);
578+
}
579+
conv::to_enum_type_uncached(enum_name, *is_bitfield)
580+
} else {
581+
type_
582+
};
552583

553584
let default_value = if self.no_defaults {
554585
None
@@ -565,23 +596,6 @@ impl<'a> FnParamBuilder<'a> {
565596
default_value,
566597
}
567598
}
568-
569-
/// Builds a single function parameter from the provided JSON method argument.
570-
pub fn build_single(self, method_arg: &JsonMethodArg, ctx: &mut Context) -> FnParam {
571-
self.build_single_impl(method_arg, ctx)
572-
}
573-
574-
/// Builds a vector of function parameters from the provided JSON method arguments.
575-
pub fn build_many(
576-
self,
577-
method_args: &Option<Vec<JsonMethodArg>>,
578-
ctx: &mut Context,
579-
) -> Vec<FnParam> {
580-
option_as_slice(method_args)
581-
.iter()
582-
.map(|arg| self.build_single_impl(arg, ctx))
583-
.collect()
584-
}
585599
}
586600

587601
impl fmt::Debug for FnParam {
@@ -604,17 +618,30 @@ pub struct FnReturn {
604618

605619
impl FnReturn {
606620
pub fn new(return_value: &Option<JsonMethodReturn>, ctx: &mut Context) -> Self {
607-
Self::new_with_replacements(return_value, None, ctx)
621+
Self::with_enum_replacements(return_value, &[], ctx)
608622
}
609623

610-
pub fn new_with_replacements(
624+
pub fn with_enum_replacements(
611625
return_value: &Option<JsonMethodReturn>,
612-
surrounding_class_method: Option<(&TyName, &str)>,
626+
replacements: EnumReplacements,
613627
ctx: &mut Context,
614628
) -> Self {
615629
if let Some(ret) = return_value {
616630
let ty = conv::to_rust_type(&ret.type_, ret.meta.as_ref(), ctx);
617-
let ty = apply_enum_replacement(None, ty, surrounding_class_method);
631+
632+
// Apply enum replacement if one exists for return type (indicated by empty string)
633+
let matching_replacement = replacements.iter().find(|(p, ..)| p.is_empty());
634+
let ty = if let Some((_, enum_name, is_bitfield)) = matching_replacement {
635+
if !ty.is_integer() {
636+
panic!(
637+
"Return type is of type {}, but can only replace int with enum",
638+
ty
639+
);
640+
}
641+
conv::to_enum_type_uncached(enum_name, *is_bitfield)
642+
} else {
643+
ty
644+
};
618645

619646
Self {
620647
decl: ty.return_decl(),
@@ -649,56 +676,12 @@ impl FnReturn {
649676
}
650677

651678
// ----------------------------------------------------------------------------------------------------------------------------------------------
679+
// Int->enum replacements
652680

653-
/// Replaces int parameters/return types with enums, if applicable.
654-
fn apply_enum_replacement(
655-
param_or_return: Option<&str>, // None for return type, Some(name) for parameter
656-
type_: RustTy,
657-
surrounding: Option<(&TyName, &str)>,
658-
) -> RustTy {
659-
// No surrounding class/method info -> caller doesn't need replacements.
660-
let Some((class_name, method_name)) = surrounding else {
661-
return type_;
662-
};
663-
664-
let replacements =
665-
special_cases::get_class_method_param_enum_replacement(class_name, method_name);
666-
667-
let matching_replacement = match param_or_return {
668-
// Look for a specific parameter name.
669-
Some(param_name) => replacements.iter().find(|(p, ..)| *p == param_name),
670-
671-
// Look for return type (empty string).
672-
None => replacements.iter().find(|(p, ..)| p.is_empty()),
673-
};
674-
675-
if let Some((_, enum_name, is_bitfield)) = matching_replacement {
676-
if !type_.is_integer() {
677-
let what = format_param_or_return(class_name, method_name, param_or_return);
678-
panic!("{what} is of type {type_}, but can only replace int with enum");
679-
}
680-
681-
conv::to_enum_type_uncached(enum_name, *is_bitfield)
682-
} else {
683-
// No replacement.
684-
type_
685-
}
686-
}
687-
688-
fn format_param_or_return(
689-
class_name: &TyName,
690-
method_name: &str,
691-
param_or_return: Option<&str>,
692-
) -> String {
693-
let what = if let Some(param_name) = param_or_return {
694-
format!("parameter `{param_name}`")
695-
} else {
696-
"return type".to_string()
697-
};
698-
699-
let class_name = &class_name.godot_ty;
700-
format!("{class_name}::{method_name} {what}")
701-
}
681+
/// Replacement of int->enum in engine APIs; each tuple being `(param_name, enum_type, is_bitfield)`.
682+
///
683+
/// Empty string `""` is used as `param_name` to denote return type replacements.
684+
pub type EnumReplacements = &'static [(&'static str, &'static str, bool)];
702685

703686
// ----------------------------------------------------------------------------------------------------------------------------------------------
704687
// Godot type

godot-codegen/src/models/domain_mapping.rs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ use crate::context::Context;
1313
use crate::models::domain::{
1414
BuildConfiguration, BuiltinClass, BuiltinMethod, BuiltinSize, BuiltinVariant, Class,
1515
ClassCommons, ClassConstant, ClassConstantValue, ClassMethod, ClassSignal, Constructor, Enum,
16-
Enumerator, EnumeratorValue, ExtensionApi, FnDirection, FnParam, FnQualifier, FnReturn,
17-
FunctionCommon, GodotApiVersion, ModName, NativeStructure, Operator, RustTy, Singleton, TyName,
18-
UtilityFunction,
16+
EnumReplacements, Enumerator, EnumeratorValue, ExtensionApi, FnDirection, FnParam, FnQualifier,
17+
FnReturn, FunctionCommon, GodotApiVersion, ModName, NativeStructure, Operator, RustTy,
18+
Singleton, TyName, UtilityFunction,
1919
};
2020
use crate::models::json::{
2121
JsonBuiltinClass, JsonBuiltinMethod, JsonBuiltinSizes, JsonClass, JsonClassConstant,
2222
JsonClassMethod, JsonConstructor, JsonEnum, JsonEnumConstant, JsonExtensionApi, JsonHeader,
23-
JsonMethodReturn, JsonNativeStructure, JsonOperator, JsonSignal, JsonSingleton,
23+
JsonMethodArg, JsonMethodReturn, JsonNativeStructure, JsonOperator, JsonSignal, JsonSingleton,
2424
JsonUtilityFunction,
2525
};
2626
use crate::util::{get_api_level, ident, option_as_slice};
@@ -520,14 +520,22 @@ impl ClassMethod {
520520
is_required_in_json
521521
};
522522

523+
// Ensure that parameters/return types listed in the replacement truly exist in the method.
524+
// The validation function now returns the validated replacement slice for reuse.
525+
let enum_replacements = validate_enum_replacements(
526+
class_name,
527+
&method.name,
528+
option_as_slice(&method.arguments),
529+
method.return_value.is_some(),
530+
);
531+
523532
let parameters = FnParam::builder()
524-
.with_replacements(class_name, &method.name)
533+
.enum_replacements(enum_replacements)
525534
.build_many(&method.arguments, ctx);
526-
let return_value = FnReturn::new_with_replacements(
527-
&method.return_value,
528-
Some((class_name, &method.name)),
529-
ctx,
530-
);
535+
536+
let return_value =
537+
FnReturn::with_enum_replacements(&method.return_value, enum_replacements, ctx);
538+
531539
let is_unsafe = Self::function_uses_pointers(&parameters, &return_value);
532540

533541
// Future note: if further changes are made to the virtual method name, make sure to make it reversible so that #[godot_api]
@@ -745,6 +753,44 @@ impl ClassConstant {
745753
}
746754
}
747755

756+
// ----------------------------------------------------------------------------------------------------------------------------------------------
757+
758+
/// Validates that all parameters and non-unit return types declared in an enum replacement slices actually exist in the method.
759+
///
760+
/// This is a measure to prevent accidental typos or listing inexistent parameters, which would have no effect.
761+
fn validate_enum_replacements(
762+
class_ty: &TyName,
763+
godot_method_name: &str,
764+
method_arguments: &[JsonMethodArg],
765+
has_return_type: bool,
766+
) -> EnumReplacements {
767+
let replacements =
768+
special_cases::get_class_method_param_enum_replacement(class_ty, godot_method_name);
769+
770+
for (param_name, enum_name, _) in replacements {
771+
if param_name.is_empty() {
772+
assert!(has_return_type,
773+
"Method `{class}.{godot_method_name}` has no return type, but replacement with `{enum_name}` is declared",
774+
class = class_ty.godot_ty
775+
);
776+
} else if !method_arguments.iter().any(|arg| arg.name == *param_name) {
777+
let available_params = method_arguments
778+
.iter()
779+
.map(|arg| format!(" * {}: {}", arg.name, arg.type_))
780+
.collect::<Vec<_>>()
781+
.join("\n");
782+
783+
panic!(
784+
"Method `{class}.{godot_method_name}` has no parameter `{param_name}`, but a replacement with `{enum_name}` is declared\n\
785+
\n{count} parameters available:\n{available_params}\n",
786+
class = class_ty.godot_ty, count = method_arguments.len(),
787+
);
788+
}
789+
}
790+
791+
replacements
792+
}
793+
748794
// ----------------------------------------------------------------------------------------------------------------------------------------------
749795
// Native structures
750796

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ use std::borrow::Cow;
3232
use proc_macro2::Ident;
3333

3434
use crate::conv::to_enum_type_uncached;
35-
use crate::models::domain::{ClassCodegenLevel, Enum, RustTy, TyName, VirtualMethodPresence};
35+
use crate::models::domain::{
36+
ClassCodegenLevel, Enum, EnumReplacements, RustTy, TyName, VirtualMethodPresence,
37+
};
3638
use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonSignal, JsonUtilityFunction};
3739
use crate::special_cases::codegen_special_cases;
3840
use crate::util::option_as_slice;
@@ -427,7 +429,7 @@ pub fn is_class_method_replaced_with_type_safe(class_ty: &TyName, godot_method_n
427429
pub fn get_class_method_param_enum_replacement(
428430
class_ty: &TyName,
429431
godot_method_name: &str,
430-
) -> &'static [(&'static str, &'static str, bool)] {
432+
) -> EnumReplacements {
431433
let godot_class_name = class_ty.godot_ty.as_str();
432434

433435
// Notes on replacement mechanism:

0 commit comments

Comments
 (0)