From 0dd82752015dfcc73a9fa56cb794184b3722b3d4 Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Thu, 2 Oct 2025 23:47:21 -0700 Subject: [PATCH 01/10] Implement a @meta(...) annotation accepting constant variant args that does nothing --- modules/gdscript/gdscript_analyzer.cpp | 27 +++++++++++++++++--------- modules/gdscript/gdscript_parser.cpp | 6 ++++++ modules/gdscript/gdscript_parser.h | 1 + 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 916283a2ddb7..14d170487f82 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -1707,17 +1707,21 @@ void GDScriptAnalyzer::resolve_annotation(GDScriptParser::AnnotationNode *p_anno return; } - Variant converted_to; - const Variant *converted_from = &value; - Callable::CallError call_error; - Variant::construct(argument_info.type, converted_to, &converted_from, 1, call_error); + // Only validate the conversion if the argument's type isn't explicitly Variant. + if (!(argument_info.type == Variant::NIL && (argument_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT))) { - if (call_error.error != Callable::CallError::CALL_OK) { - push_error(vformat(R"(Cannot convert argument %d of annotation "%s" from "%s" to "%s".)", i + 1, p_annotation->name, Variant::get_type_name(value.get_type()), Variant::get_type_name(argument_info.type)), argument); - return; - } + Variant converted_to; + const Variant *converted_from = &value; + Callable::CallError call_error; + Variant::construct(argument_info.type, converted_to, &converted_from, 1, call_error); + + if (call_error.error != Callable::CallError::CALL_OK) { + push_error(vformat(R"(Cannot convert argument %d of annotation "%s" from "%s" to "%s".)", i + 1, p_annotation->name, Variant::get_type_name(value.get_type()), Variant::get_type_name(argument_info.type)), argument); + return; + } - value = converted_to; + value = converted_to; + } } p_annotation->resolved_arguments.push_back(value); @@ -2690,9 +2694,11 @@ void GDScriptAnalyzer::reduce_expression(GDScriptParser::ExpressionNode *p_expre } void GDScriptAnalyzer::reduce_array(GDScriptParser::ArrayNode *p_array) { + p_array->is_constant = true; for (int i = 0; i < p_array->elements.size(); i++) { GDScriptParser::ExpressionNode *element = p_array->elements[i]; reduce_expression(element); + p_array->is_constant &= element->is_constant; } // It's array in any case. @@ -3835,12 +3841,15 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) { void GDScriptAnalyzer::reduce_dictionary(GDScriptParser::DictionaryNode *p_dictionary) { HashMap elements; + p_dictionary->is_constant = true; for (int i = 0; i < p_dictionary->elements.size(); i++) { const GDScriptParser::DictionaryNode::Pair &element = p_dictionary->elements[i]; if (p_dictionary->style == GDScriptParser::DictionaryNode::PYTHON_DICT) { reduce_expression(element.key); + p_dictionary->is_constant &= element.key->is_constant; } reduce_expression(element.value); + p_dictionary->is_constant &= element.value->is_constant; if (element.key->is_constant) { if (elements.has(element.key->reduced_value)) { diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index cb9de0053caa..0ce0e8a374b7 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -128,6 +128,8 @@ GDScriptParser::GDScriptParser() { register_annotation(MethodInfo("@export_category", PropertyInfo(Variant::STRING, "name")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations); register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations, varray("")); register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations, varray("")); + // Metadata annotation. + register_annotation(MethodInfo("@meta", PropertyInfo(Variant::NIL, "metadata", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT)), AnnotationInfo::CLASS_LEVEL, &GDScriptParser::meta_annotation, varray(), true); // Warning annotations. register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS_LEVEL | AnnotationInfo::STATEMENT, &GDScriptParser::warning_ignore_annotation, varray(), true); register_annotation(MethodInfo("@warning_ignore_start", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::STANDALONE, &GDScriptParser::warning_ignore_region_annotations, varray(), true); @@ -5173,6 +5175,10 @@ bool GDScriptParser::rpc_annotation(AnnotationNode *p_annotation, Node *p_target return true; } +bool GDScriptParser::meta_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { + return true; +} + GDScriptParser::DataType GDScriptParser::SuiteNode::Local::get_datatype() const { switch (type) { case CONSTANT: diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 8a8ab5af4587..ba4b6054dbae 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1540,6 +1540,7 @@ class GDScriptParser { bool warning_ignore_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); bool warning_ignore_region_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool meta_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); // Statements. Node *parse_statement(); VariableNode *parse_variable(bool p_is_static); From e95613f649072c7129fd8035abe28bcc7d4d00e3 Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Sat, 4 Oct 2025 22:40:04 -0700 Subject: [PATCH 02/10] Implement metadata collection for the @meta annotation, accessed via methods on Script --- core/object/script_language.cpp | 87 ++++++++++++++++++++++++++++ core/object/script_language.h | 17 ++++++ modules/gdscript/gdscript.cpp | 17 ++++++ modules/gdscript/gdscript_parser.cpp | 33 ++++++++++- modules/gdscript/gdscript_parser.h | 8 +++ 5 files changed, 161 insertions(+), 1 deletion(-) diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index 2c4d2eec04be..b142e921cf0c 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -159,6 +159,82 @@ PropertyInfo Script::get_class_category() const { #endif // TOOLS_ENABLED +bool Script::has_class_meta(const StringName &p_name) const { + return script_metadata.has(p_name); +} + +void Script::set_class_meta(const StringName &p_name, const Variant &p_value) { + script_metadata[p_name] = p_value; +} + +void Script::remove_class_meta(const StringName &p_name) { + script_metadata.erase(p_name); +} + +Variant Script::get_class_meta(const StringName &p_name, const Variant &p_default) const { + if (!script_metadata.has(p_name)) { + if (p_default != Variant()) { + return p_default; + } else { + ERR_FAIL_V_MSG(Variant(), vformat("The script does not have any 'meta' values with the key '%s'.", p_name)); + } + } + return script_metadata[p_name]; +} + +TypedArray Script::get_class_meta_list() const { + TypedArray result; + for (const auto &[key, _] : script_metadata) { + result.append(key); + } + return result; +} + +bool Script::has_member_meta(const StringName &p_member, const StringName &p_name) const { + return member_metadata.has(p_name); +} + +void Script::set_member_meta(const StringName &p_member, const StringName &p_name, const Variant &p_value) { + if (!member_metadata.has(p_member)) { + member_metadata.insert(p_member, HashMap()); + } + member_metadata[p_member][p_name] = p_value; +} + +void Script::remove_member_meta(const StringName &p_member, const StringName &p_name) { + if (member_metadata.has(p_member)) { + member_metadata.erase(p_name); + } +} + +Variant Script::get_member_meta(const StringName &p_member, const StringName &p_name, const Variant &p_default) const { + if (member_metadata.has(p_member)) { + if (!member_metadata[p_member].has(p_name)) { + if (p_default != Variant()) { + return p_default; + } else { + ERR_FAIL_V_MSG(Variant(), vformat("The member does not have any 'meta' values with the key '%s'.", p_name)); + } + } + return member_metadata[p_member][p_name]; + } + if (p_default != Variant()) { + return p_default; + } else { + ERR_FAIL_V_MSG(Variant(), vformat("The script does not have a member '%s' with 'meta' values.", p_member)); + } +} + +TypedArray Script::get_member_meta_list(const StringName &p_member) const { + TypedArray result; + if (member_metadata.has(p_member)) { + for (const auto &[key, _] : member_metadata[p_member]) { + result.append(key); + } + } + return result; +} + void Script::_bind_methods() { ClassDB::bind_method(D_METHOD("can_instantiate"), &Script::can_instantiate); //ClassDB::bind_method(D_METHOD("instance_create","base_object"),&Script::instance_create); @@ -185,6 +261,17 @@ void Script::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rpc_config"), &Script::_get_rpc_config_bind); + ClassDB::bind_method(D_METHOD("has_class_meta", "name"), &Script::has_class_meta); + ClassDB::bind_method(D_METHOD("set_class_meta", "name", "value"), &Script::set_class_meta); + ClassDB::bind_method(D_METHOD("remove_class_meta", "name"), &Script::remove_class_meta); + ClassDB::bind_method(D_METHOD("get_class_meta", "name", "default"), &Script::get_class_meta, DEFVAL(Variant())); + ClassDB::bind_method(D_METHOD("get_class_meta_list"), &Script::get_class_meta_list); + ClassDB::bind_method(D_METHOD("has_member_meta", "name"), &Script::has_member_meta); + ClassDB::bind_method(D_METHOD("set_member_meta", "name", "value"), &Script::set_member_meta); + ClassDB::bind_method(D_METHOD("remove_member_meta", "name"), &Script::remove_member_meta); + ClassDB::bind_method(D_METHOD("get_member_meta", "name", "default"), &Script::get_member_meta, DEFVAL(Variant())); + ClassDB::bind_method(D_METHOD("get_member_meta_list"), &Script::get_member_meta_list); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "source_code", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_source_code", "get_source_code"); } diff --git a/core/object/script_language.h b/core/object/script_language.h index 77a1b060a2e7..7e8dba5c6f8d 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -139,6 +139,10 @@ class Script : public Resource { return get_rpc_config().duplicate(true); } +private: + HashMap script_metadata; + HashMap> member_metadata; + public: static constexpr AncestralClass static_ancestral_class = AncestralClass::SCRIPT; @@ -160,6 +164,19 @@ class Script : public Resource { virtual void set_source_code(const String &p_code) = 0; virtual Error reload(bool p_keep_state = false) = 0; + // TODO: Is there a better interface that works for both class- and member-level metadata? + bool has_class_meta(const StringName &p_name) const; + void set_class_meta(const StringName &p_name, const Variant &p_value); + void remove_class_meta(const StringName &p_name); + Variant get_class_meta(const StringName &p_name, const Variant &p_default = Variant()) const; + TypedArray get_class_meta_list() const; + + bool has_member_meta(const StringName &p_member, const StringName &p_name) const; + void set_member_meta(const StringName &p_member, const StringName &p_name, const Variant &p_value); + void remove_member_meta(const StringName &p_member, const StringName &p_name); + Variant get_member_meta(const StringName &p_member, const StringName &p_name, const Variant &p_default = Variant()) const; + TypedArray get_member_meta_list(const StringName &p_member) const; + #ifdef TOOLS_ENABLED virtual StringName get_doc_class_name() const = 0; virtual Vector get_documentation() const = 0; diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index aaae85d85efa..a4fdf96f3c5f 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -579,6 +579,14 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call, PlaceHolderSc break; // Nothing. } } + for (const auto& [key, value] : parser.get_script_meta_annotations()) { + set_class_meta(key, value); + } + for (const auto& [member, member_meta] : parser.get_member_meta_annotations()) { + for (const auto& [key, value] : member_meta) { + set_member_meta(member, key, value); + } + } } else { placeholder_fallback_enabled = true; return false; @@ -853,6 +861,15 @@ Error GDScript::reload(bool p_keep_state) { can_run = ScriptServer::is_scripting_enabled() || parser.is_tool(); + for (const auto& [key, value] : parser.get_script_meta_annotations()) { + set_class_meta(key, value); + } + for (const auto& [member, member_meta] : parser.get_member_meta_annotations()) { + for (const auto& [key, value] : member_meta) { + set_member_meta(member, key, value); + } + } + GDScriptCompiler compiler; err = compiler.compile(&parser, this, p_keep_state); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 0ce0e8a374b7..7163670601a1 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -129,7 +129,8 @@ GDScriptParser::GDScriptParser() { register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations, varray("")); register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations, varray("")); // Metadata annotation. - register_annotation(MethodInfo("@meta", PropertyInfo(Variant::NIL, "metadata", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT)), AnnotationInfo::CLASS_LEVEL, &GDScriptParser::meta_annotation, varray(), true); + // TODO: Can we make it so that "value" is optional and defaults to "true", making it easy to do e.g. @meta("some_tag")? + register_annotation(MethodInfo("@meta", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::NIL, "value", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT)), AnnotationInfo::CLASS_LEVEL, &GDScriptParser::meta_annotation, varray(), false); // Warning annotations. register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS_LEVEL | AnnotationInfo::STATEMENT, &GDScriptParser::warning_ignore_annotation, varray(), true); register_annotation(MethodInfo("@warning_ignore_start", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::STANDALONE, &GDScriptParser::warning_ignore_region_annotations, varray(), true); @@ -5176,6 +5177,36 @@ bool GDScriptParser::rpc_annotation(AnnotationNode *p_annotation, Node *p_target } bool GDScriptParser::meta_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { + ERR_FAIL_COND_V(p_annotation->resolved_arguments.size() < 2, false); + ERR_FAIL_COND_V(!Variant::can_convert(p_annotation->resolved_arguments[0].get_type(), Variant::STRING_NAME), false); + + StringName member_name; + switch (p_target->type) { + case Node::CLASS: + // TODO: Can we make it work for inner classes and inner class members? + ERR_FAIL_COND_V_MSG(static_cast(p_target)->outer != nullptr, false, R"("@meta" annotation does not support inner classes.)"); + script_meta_annotations.insert(p_annotation->resolved_arguments[0], p_annotation->resolved_arguments[1]); + return true; + case Node::VARIABLE: + member_name = static_cast(p_target)->identifier->name; + break; + case Node::CONSTANT: + member_name = static_cast(p_target)->identifier->name; + break; + case Node::SIGNAL: + member_name = static_cast(p_target)->identifier->name; + break; + case Node::FUNCTION: + member_name = static_cast(p_target)->identifier->name; + break; + default: + push_error(R"("@meta" annotation does not apply here.)"); + return false; + } + if (!member_meta_annotations.has(member_name)) { + member_meta_annotations.insert(member_name, HashMap()); + } + member_meta_annotations[member_name].insert(p_annotation->resolved_arguments[0], p_annotation->resolved_arguments[1]); return true; } diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index ba4b6054dbae..05d46cc21b5e 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1029,6 +1029,7 @@ class GDScriptParser { type = PATTERN; } }; + struct PreloadNode : public ExpressionNode { ExpressionNode *path = nullptr; String resolved_path; @@ -1399,6 +1400,10 @@ class GDScriptParser { static HashMap valid_annotations; List annotation_stack; + // member name -> metadata name -> value + HashMap> member_meta_annotations; + HashMap script_meta_annotations; + typedef ExpressionNode *(GDScriptParser::*ParseFunction)(ExpressionNode *p_previous_operand, bool p_can_assign); // Higher value means higher precedence (i.e. is evaluated first). enum Precedence { @@ -1616,6 +1621,9 @@ class GDScriptParser { // TODO: Keep track of deps. return List(); } + const HashMap get_script_meta_annotations() const { return script_meta_annotations; } + const HashMap> get_member_meta_annotations() const { return member_meta_annotations; } + #ifdef DEBUG_ENABLED const List &get_warnings() const { return warnings; } const HashSet &get_unsafe_lines() const { return unsafe_lines; } From 3d77bea0ede3166c74f27feba27a927554a43b0a Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Sun, 5 Oct 2025 18:11:47 -0700 Subject: [PATCH 03/10] Simplify the @meta interface; add support for inner classes --- core/object/script_language.cpp | 103 ++++++++------------------- core/object/script_language.h | 37 ++++++---- modules/gdscript/gdscript.cpp | 18 +---- modules/gdscript/gdscript_parser.cpp | 74 +++++++++++++------ modules/gdscript/gdscript_parser.h | 7 +- 5 files changed, 108 insertions(+), 131 deletions(-) diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index b142e921cf0c..f789239b0787 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -159,80 +159,38 @@ PropertyInfo Script::get_class_category() const { #endif // TOOLS_ENABLED -bool Script::has_class_meta(const StringName &p_name) const { - return script_metadata.has(p_name); -} - -void Script::set_class_meta(const StringName &p_name, const Variant &p_value) { - script_metadata[p_name] = p_value; -} - -void Script::remove_class_meta(const StringName &p_name) { - script_metadata.erase(p_name); +Dictionary ScriptMetadata::to_dictionary() const { + Dictionary result; + result[StringName("target_container")] = target_container; + result[StringName("target_name")] = target_name; + result[StringName("target_type")] = target_type; + result[StringName("value")] = value; + return result; } -Variant Script::get_class_meta(const StringName &p_name, const Variant &p_default) const { - if (!script_metadata.has(p_name)) { - if (p_default != Variant()) { - return p_default; - } else { - ERR_FAIL_V_MSG(Variant(), vformat("The script does not have any 'meta' values with the key '%s'.", p_name)); +TypedArray Script::get_script_meta(const StringName &p_name) const { + Array result; + if (script_metadata.has(p_name)) { + for (const auto& meta : script_metadata[p_name]) { + result.append(meta.to_dictionary()); } } - return script_metadata[p_name]; -} - -TypedArray Script::get_class_meta_list() const { - TypedArray result; - for (const auto &[key, _] : script_metadata) { - result.append(key); - } return result; } -bool Script::has_member_meta(const StringName &p_member, const StringName &p_name) const { - return member_metadata.has(p_name); -} - -void Script::set_member_meta(const StringName &p_member, const StringName &p_name, const Variant &p_value) { - if (!member_metadata.has(p_member)) { - member_metadata.insert(p_member, HashMap()); - } - member_metadata[p_member][p_name] = p_value; -} - -void Script::remove_member_meta(const StringName &p_member, const StringName &p_name) { - if (member_metadata.has(p_member)) { - member_metadata.erase(p_name); - } -} - -Variant Script::get_member_meta(const StringName &p_member, const StringName &p_name, const Variant &p_default) const { - if (member_metadata.has(p_member)) { - if (!member_metadata[p_member].has(p_name)) { - if (p_default != Variant()) { - return p_default; - } else { - ERR_FAIL_V_MSG(Variant(), vformat("The member does not have any 'meta' values with the key '%s'.", p_name)); - } - } - return member_metadata[p_member][p_name]; - } - if (p_default != Variant()) { - return p_default; - } else { - ERR_FAIL_V_MSG(Variant(), vformat("The script does not have a member '%s' with 'meta' values.", p_member)); +PackedStringArray Script::get_script_meta_list() const { + PackedStringArray result; + for (const auto &[meta_name, _] : script_metadata) { + result.append(meta_name); } + return result; } -TypedArray Script::get_member_meta_list(const StringName &p_member) const { - TypedArray result; - if (member_metadata.has(p_member)) { - for (const auto &[key, _] : member_metadata[p_member]) { - result.append(key); - } +void Script::copy_script_meta_from(const HashMap> &p_source) { + script_metadata.clear(); + for (const auto &[key, value] : p_source) { + script_metadata.insert(key, value); } - return result; } void Script::_bind_methods() { @@ -261,18 +219,17 @@ void Script::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rpc_config"), &Script::_get_rpc_config_bind); - ClassDB::bind_method(D_METHOD("has_class_meta", "name"), &Script::has_class_meta); - ClassDB::bind_method(D_METHOD("set_class_meta", "name", "value"), &Script::set_class_meta); - ClassDB::bind_method(D_METHOD("remove_class_meta", "name"), &Script::remove_class_meta); - ClassDB::bind_method(D_METHOD("get_class_meta", "name", "default"), &Script::get_class_meta, DEFVAL(Variant())); - ClassDB::bind_method(D_METHOD("get_class_meta_list"), &Script::get_class_meta_list); - ClassDB::bind_method(D_METHOD("has_member_meta", "name"), &Script::has_member_meta); - ClassDB::bind_method(D_METHOD("set_member_meta", "name", "value"), &Script::set_member_meta); - ClassDB::bind_method(D_METHOD("remove_member_meta", "name"), &Script::remove_member_meta); - ClassDB::bind_method(D_METHOD("get_member_meta", "name", "default"), &Script::get_member_meta, DEFVAL(Variant())); - ClassDB::bind_method(D_METHOD("get_member_meta_list"), &Script::get_member_meta_list); + ClassDB::bind_method(D_METHOD("get_script_meta", "name"), &Script::get_script_meta); + ClassDB::bind_method(D_METHOD("get_script_meta_list"), &Script::get_script_meta_list); ADD_PROPERTY(PropertyInfo(Variant::STRING, "source_code", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_source_code", "get_source_code"); + + // TODO: Figure out why BIND_ENUM_CONSTANT doesn't work here. + ClassDB::bind_integer_constant(get_class_static(), StringName("MetaTargetType"), "META_TARGET_CLASS", META_TARGET_CLASS); + ClassDB::bind_integer_constant(get_class_static(), StringName("MetaTargetType"), "META_TARGET_VARIABLE", META_TARGET_VARIABLE); + ClassDB::bind_integer_constant(get_class_static(), StringName("MetaTargetType"), "META_TARGET_CONSTANT", META_TARGET_CONSTANT); + ClassDB::bind_integer_constant(get_class_static(), StringName("MetaTargetType"), "META_TARGET_SIGNAL", META_TARGET_SIGNAL); + ClassDB::bind_integer_constant(get_class_static(), StringName("MetaTargetType"), "META_TARGET_FUNCTION", META_TARGET_FUNCTION); } void Script::reload_from_file() { diff --git a/core/object/script_language.h b/core/object/script_language.h index 7e8dba5c6f8d..cca6cc1939b7 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -112,6 +112,17 @@ class ScriptServer { class PlaceHolderScriptInstance; +class ScriptMetadata { +public: + StringName name; + StringName target_container; + StringName target_name; + int target_type; // Script::MetaTargetType + Variant value; + + Dictionary to_dictionary() const; +}; + class Script : public Resource { GDCLASS(Script, Resource); OBJ_SAVE_TYPE(Script); @@ -140,12 +151,19 @@ class Script : public Resource { } private: - HashMap script_metadata; - HashMap> member_metadata; + HashMap> script_metadata; public: static constexpr AncestralClass static_ancestral_class = AncestralClass::SCRIPT; + enum MetaTargetType { + META_TARGET_CLASS, + META_TARGET_VARIABLE, + META_TARGET_CONSTANT, + META_TARGET_SIGNAL, + META_TARGET_FUNCTION + }; + virtual void reload_from_file() override; virtual bool can_instantiate() const = 0; @@ -164,18 +182,9 @@ class Script : public Resource { virtual void set_source_code(const String &p_code) = 0; virtual Error reload(bool p_keep_state = false) = 0; - // TODO: Is there a better interface that works for both class- and member-level metadata? - bool has_class_meta(const StringName &p_name) const; - void set_class_meta(const StringName &p_name, const Variant &p_value); - void remove_class_meta(const StringName &p_name); - Variant get_class_meta(const StringName &p_name, const Variant &p_default = Variant()) const; - TypedArray get_class_meta_list() const; - - bool has_member_meta(const StringName &p_member, const StringName &p_name) const; - void set_member_meta(const StringName &p_member, const StringName &p_name, const Variant &p_value); - void remove_member_meta(const StringName &p_member, const StringName &p_name); - Variant get_member_meta(const StringName &p_member, const StringName &p_name, const Variant &p_default = Variant()) const; - TypedArray get_member_meta_list(const StringName &p_member) const; + TypedArray get_script_meta(const StringName &p_name) const; + PackedStringArray get_script_meta_list() const; + void copy_script_meta_from(const HashMap> &p_source); #ifdef TOOLS_ENABLED virtual StringName get_doc_class_name() const = 0; diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index a4fdf96f3c5f..bfc45b1c7a8c 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -579,14 +579,7 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call, PlaceHolderSc break; // Nothing. } } - for (const auto& [key, value] : parser.get_script_meta_annotations()) { - set_class_meta(key, value); - } - for (const auto& [member, member_meta] : parser.get_member_meta_annotations()) { - for (const auto& [key, value] : member_meta) { - set_member_meta(member, key, value); - } - } + copy_script_meta_from(parser.get_script_metadata()); } else { placeholder_fallback_enabled = true; return false; @@ -861,14 +854,7 @@ Error GDScript::reload(bool p_keep_state) { can_run = ScriptServer::is_scripting_enabled() || parser.is_tool(); - for (const auto& [key, value] : parser.get_script_meta_annotations()) { - set_class_meta(key, value); - } - for (const auto& [member, member_meta] : parser.get_member_meta_annotations()) { - for (const auto& [key, value] : member_meta) { - set_member_meta(member, key, value); - } - } + copy_script_meta_from(parser.get_script_metadata()); GDScriptCompiler compiler; err = compiler.compile(&parser, this, p_keep_state); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 7163670601a1..891189cd17b2 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -5180,33 +5180,61 @@ bool GDScriptParser::meta_annotation(AnnotationNode *p_annotation, Node *p_targe ERR_FAIL_COND_V(p_annotation->resolved_arguments.size() < 2, false); ERR_FAIL_COND_V(!Variant::can_convert(p_annotation->resolved_arguments[0].get_type(), Variant::STRING_NAME), false); - StringName member_name; + Script::MetaTargetType target_type; + StringName target_name; switch (p_target->type) { - case Node::CLASS: - // TODO: Can we make it work for inner classes and inner class members? - ERR_FAIL_COND_V_MSG(static_cast(p_target)->outer != nullptr, false, R"("@meta" annotation does not support inner classes.)"); - script_meta_annotations.insert(p_annotation->resolved_arguments[0], p_annotation->resolved_arguments[1]); - return true; - case Node::VARIABLE: - member_name = static_cast(p_target)->identifier->name; - break; - case Node::CONSTANT: - member_name = static_cast(p_target)->identifier->name; - break; - case Node::SIGNAL: - member_name = static_cast(p_target)->identifier->name; - break; - case Node::FUNCTION: - member_name = static_cast(p_target)->identifier->name; - break; + case Node::CLASS: { + target_type = Script::MetaTargetType::META_TARGET_CLASS; + ClassNode *class_node = static_cast(p_target); + if (class_node->identifier != nullptr) { + target_name = class_node->identifier->name; + } + } break; + case Node::VARIABLE: { + target_type = Script::MetaTargetType::META_TARGET_VARIABLE; + VariableNode *variable_node = static_cast(p_target); + target_name = variable_node->identifier->name; + } break; + case Node::CONSTANT: { + target_type = Script::MetaTargetType::META_TARGET_CONSTANT; + ConstantNode *constant_node = static_cast(p_target); + target_name = constant_node->identifier->name; + } break; + case Node::SIGNAL: { + target_type = Script::MetaTargetType::META_TARGET_SIGNAL; + SignalNode *signal_node = static_cast(p_target); + target_name = signal_node->identifier->name; + } break; + case Node::FUNCTION: { + target_type = Script::MetaTargetType::META_TARGET_FUNCTION; + FunctionNode *function_noade = static_cast(p_target); + target_name = function_noade->identifier->name; + } break; default: - push_error(R"("@meta" annotation does not apply here.)"); - return false; + ERR_FAIL_V_MSG(false, R"("@meta" annotation does not apply here.)"); } - if (!member_meta_annotations.has(member_name)) { - member_meta_annotations.insert(member_name, HashMap()); + + // Figure out the fully qualified path of the class containing this target. + PackedStringArray class_path; + ClassNode *containing_class = p_class; + while (containing_class != nullptr) { + if (containing_class->identifier != nullptr) { + class_path.insert(0, containing_class->identifier->name); + } + containing_class = containing_class->outer; + } + + StringName name = p_annotation->resolved_arguments[0]; + ScriptMetadata metadata; + metadata.name = name; + metadata.value = p_annotation->resolved_arguments[1]; // <- Only works for primitives??; + metadata.target_name = target_name; + metadata.target_container = String(".").join(class_path); + metadata.target_type = target_type; + if (!script_metadata.has(name)) { + script_metadata.insert(name, Vector()); } - member_meta_annotations[member_name].insert(p_annotation->resolved_arguments[0], p_annotation->resolved_arguments[1]); + script_metadata[name].append(metadata); return true; } diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 05d46cc21b5e..f74e4f80bcf2 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1400,9 +1400,7 @@ class GDScriptParser { static HashMap valid_annotations; List annotation_stack; - // member name -> metadata name -> value - HashMap> member_meta_annotations; - HashMap script_meta_annotations; + HashMap> script_metadata; typedef ExpressionNode *(GDScriptParser::*ParseFunction)(ExpressionNode *p_previous_operand, bool p_can_assign); // Higher value means higher precedence (i.e. is evaluated first). @@ -1621,8 +1619,7 @@ class GDScriptParser { // TODO: Keep track of deps. return List(); } - const HashMap get_script_meta_annotations() const { return script_meta_annotations; } - const HashMap> get_member_meta_annotations() const { return member_meta_annotations; } + const HashMap> get_script_metadata() const { return script_metadata; } #ifdef DEBUG_ENABLED const List &get_warnings() const { return warnings; } From dafcd50b8d5f03ced0c0c10c1f0df29ba48941bc Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Sun, 5 Oct 2025 18:44:33 -0700 Subject: [PATCH 04/10] Fix constant dictionary and array expressions having null value in annotation->resolved_arguments --- modules/gdscript/gdscript_analyzer.cpp | 42 +++++++++++++++++--------- modules/gdscript/gdscript_parser.cpp | 2 +- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 14d170487f82..82cd89ce1841 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -1695,7 +1695,7 @@ void GDScriptAnalyzer::resolve_annotation(GDScriptParser::AnnotationNode *p_anno Variant value = argument->reduced_value; - if (value.get_type() != argument_info.type) { + if (value.get_type() != argument_info.type && !(argument_info.type == Variant::NIL && (argument_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT))) { #ifdef DEBUG_ENABLED if (argument_info.type == Variant::INT && value.get_type() == Variant::FLOAT) { parser->push_warning(argument, GDScriptWarning::NARROWING_CONVERSION); @@ -1707,21 +1707,17 @@ void GDScriptAnalyzer::resolve_annotation(GDScriptParser::AnnotationNode *p_anno return; } - // Only validate the conversion if the argument's type isn't explicitly Variant. - if (!(argument_info.type == Variant::NIL && (argument_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT))) { + Variant converted_to; + const Variant *converted_from = &value; + Callable::CallError call_error; + Variant::construct(argument_info.type, converted_to, &converted_from, 1, call_error); - Variant converted_to; - const Variant *converted_from = &value; - Callable::CallError call_error; - Variant::construct(argument_info.type, converted_to, &converted_from, 1, call_error); - - if (call_error.error != Callable::CallError::CALL_OK) { - push_error(vformat(R"(Cannot convert argument %d of annotation "%s" from "%s" to "%s".)", i + 1, p_annotation->name, Variant::get_type_name(value.get_type()), Variant::get_type_name(argument_info.type)), argument); - return; - } - - value = converted_to; + if (call_error.error != Callable::CallError::CALL_OK) { + push_error(vformat(R"(Cannot convert argument %d of annotation "%s" from "%s" to "%s".)", i + 1, p_annotation->name, Variant::get_type_name(value.get_type()), Variant::get_type_name(argument_info.type)), argument); + return; } + + value = converted_to; } p_annotation->resolved_arguments.push_back(value); @@ -2701,6 +2697,15 @@ void GDScriptAnalyzer::reduce_array(GDScriptParser::ArrayNode *p_array) { p_array->is_constant &= element->is_constant; } + // If the array is constant we can assign a reduced value. + if (p_array->is_constant) { + Array reduced_value; + for (const auto &element : p_array->elements) { + reduced_value.append(element->reduced_value); + } + p_array->reduced_value = reduced_value; + } + // It's array in any case. GDScriptParser::DataType arr_type; arr_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; @@ -3860,6 +3865,15 @@ void GDScriptAnalyzer::reduce_dictionary(GDScriptParser::DictionaryNode *p_dicti } } + // If the dictionary is constant, we can assign a reduced value. + if (p_dictionary->is_constant) { + Dictionary reduced_value; + for (const auto &[key, value] : p_dictionary->elements) { + reduced_value[key->reduced_value] = value->reduced_value; + } + p_dictionary->reduced_value = reduced_value; + } + // It's dictionary in any case. GDScriptParser::DataType dict_type; dict_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 891189cd17b2..f1a3a651a6a4 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -5227,7 +5227,7 @@ bool GDScriptParser::meta_annotation(AnnotationNode *p_annotation, Node *p_targe StringName name = p_annotation->resolved_arguments[0]; ScriptMetadata metadata; metadata.name = name; - metadata.value = p_annotation->resolved_arguments[1]; // <- Only works for primitives??; + metadata.value = p_annotation->resolved_arguments[1]; metadata.target_name = target_name; metadata.target_container = String(".").join(class_path); metadata.target_type = target_type; From 5a790d63f5be7916f31f2ac6e3f4c361a5fcf3b2 Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Sun, 5 Oct 2025 20:28:15 -0700 Subject: [PATCH 05/10] Paste error message into comment for future investigation --- core/object/script_language.cpp | 3 ++- core/object/script_language.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index f789239b0787..4e16a4d81037 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -224,7 +224,8 @@ void Script::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "source_code", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_source_code", "get_source_code"); - // TODO: Figure out why BIND_ENUM_CONSTANT doesn't work here. + // TODO: Why doesn't e.g. BIND_ENUM_CONSTANT(META_TARGET_CLASS) work here? + // error: incomplete type 'GetTypeInfo' used in nested name specifier ClassDB::bind_integer_constant(get_class_static(), StringName("MetaTargetType"), "META_TARGET_CLASS", META_TARGET_CLASS); ClassDB::bind_integer_constant(get_class_static(), StringName("MetaTargetType"), "META_TARGET_VARIABLE", META_TARGET_VARIABLE); ClassDB::bind_integer_constant(get_class_static(), StringName("MetaTargetType"), "META_TARGET_CONSTANT", META_TARGET_CONSTANT); diff --git a/core/object/script_language.h b/core/object/script_language.h index cca6cc1939b7..85f5e2e3c33c 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -161,7 +161,7 @@ class Script : public Resource { META_TARGET_VARIABLE, META_TARGET_CONSTANT, META_TARGET_SIGNAL, - META_TARGET_FUNCTION + META_TARGET_FUNCTION, }; virtual void reload_from_file() override; From bc46b3299308a50cdbaae2f78dca561f08caff91 Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Sun, 5 Oct 2025 21:19:35 -0700 Subject: [PATCH 06/10] Add documentation --- doc/classes/Script.xml | 33 ++++++++++++++++++++++ modules/gdscript/doc_classes/@GDScript.xml | 20 +++++++++++++ 2 files changed, 53 insertions(+) diff --git a/doc/classes/Script.xml b/doc/classes/Script.xml index d8d6f4fd2115..1d7971bc85ad 100644 --- a/doc/classes/Script.xml +++ b/doc/classes/Script.xml @@ -70,6 +70,22 @@ Returns a dictionary containing constant names and their values. + + + + Returns an [Array] of dictionaries describing each target in this [Script] that was annotated with a [annotation @GDScript.@meta] annotation having its first argument equal to [param name]. Each [Dictionary] contains the following entries: + - [code]target_type[/code] is a [enum MetaTargetType] indicating the type of the annotation target. + - [code]target_name[/code] is the [StringName] identifier of the target, if any. + - [code]target_container[/code] is a [StringName] fully specifying the class containing this annotation target. + - [code]value[/code] is the [Variant] value that was passed as the second argument to the [code]@meta[/code] annotation. + + + + + + Returns the list of all the [StringNames] that were passed as the first argument to [annotation @GDScript.@meta] annotations in this [Script]. + + @@ -137,4 +153,21 @@ The script source code or an empty string if source code is not available. When set, does not reload the class implementation automatically. + + + Indicates that the target of a [code]@meta[/code] annotation is a class. + + + Indicates that the target of a [code]@meta[/code] annotation is a variable. + + + Indicates that the target of a [code]@meta[/code] annotation is a constant. + + + Indicates that the target of a [code]@meta[/code] annotation is a signal. + + + Indicates that the target of a [code]@meta[/code] annotation is a function. + + diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index ab3363ab4c44..1b7cca0e22cb 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -773,6 +773,26 @@ [b]Note:[/b] Unlike most other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported). + + + + + + Attach a piece of named metadata to the given class or class member. The metadata can then be read back at runtime using [method Script.get_script_meta]. + [codeblock] + class_name MyClass + extends RefCounted + + @meta("my_tag", true) + var my_property: String + + func _init(): + var script = get_script() + for metadata in script.get_script_meta("my_tag"): + print("The member %s was annotated with @meta(\"my_tag\", %s)" % [metadata.target_name, metadata.value]) + [/codeblock] + + From 8015372d67576db6299b11e93e6059b46065222f Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Sun, 5 Oct 2025 21:30:26 -0700 Subject: [PATCH 07/10] Make the second argument to @meta optional --- modules/gdscript/doc_classes/@GDScript.xml | 2 +- modules/gdscript/gdscript_parser.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 1b7cca0e22cb..9d6b38901798 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -776,7 +776,7 @@ - + Attach a piece of named metadata to the given class or class member. The metadata can then be read back at runtime using [method Script.get_script_meta]. [codeblock] diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index f1a3a651a6a4..1794bd6208b0 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -129,8 +129,7 @@ GDScriptParser::GDScriptParser() { register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations, varray("")); register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations, varray("")); // Metadata annotation. - // TODO: Can we make it so that "value" is optional and defaults to "true", making it easy to do e.g. @meta("some_tag")? - register_annotation(MethodInfo("@meta", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::NIL, "value", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT)), AnnotationInfo::CLASS_LEVEL, &GDScriptParser::meta_annotation, varray(), false); + register_annotation(MethodInfo("@meta", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::NIL, "value", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT)), AnnotationInfo::CLASS_LEVEL, &GDScriptParser::meta_annotation, varray(true)); // Warning annotations. register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS_LEVEL | AnnotationInfo::STATEMENT, &GDScriptParser::warning_ignore_annotation, varray(), true); register_annotation(MethodInfo("@warning_ignore_start", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::STANDALONE, &GDScriptParser::warning_ignore_region_annotations, varray(), true); @@ -5177,7 +5176,7 @@ bool GDScriptParser::rpc_annotation(AnnotationNode *p_annotation, Node *p_target } bool GDScriptParser::meta_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { - ERR_FAIL_COND_V(p_annotation->resolved_arguments.size() < 2, false); + ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false); ERR_FAIL_COND_V(!Variant::can_convert(p_annotation->resolved_arguments[0].get_type(), Variant::STRING_NAME), false); Script::MetaTargetType target_type; @@ -5225,9 +5224,10 @@ bool GDScriptParser::meta_annotation(AnnotationNode *p_annotation, Node *p_targe } StringName name = p_annotation->resolved_arguments[0]; + Variant value = p_annotation->resolved_arguments.size() < 2 ? Variant(true) : p_annotation->resolved_arguments[1]; ScriptMetadata metadata; metadata.name = name; - metadata.value = p_annotation->resolved_arguments[1]; + metadata.value = value; metadata.target_name = target_name; metadata.target_container = String(".").join(class_path); metadata.target_type = target_type; From a6099c2120e1385c336e9fbccbdb0f389d177c05 Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Sun, 5 Oct 2025 21:45:14 -0700 Subject: [PATCH 08/10] Update @meta docs for clarity --- doc/classes/Script.xml | 18 +++++++++--------- modules/gdscript/doc_classes/@GDScript.xml | 8 +++----- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/doc/classes/Script.xml b/doc/classes/Script.xml index 1d7971bc85ad..ba462aab8f55 100644 --- a/doc/classes/Script.xml +++ b/doc/classes/Script.xml @@ -73,17 +73,17 @@ - Returns an [Array] of dictionaries describing each target in this [Script] that was annotated with a [annotation @GDScript.@meta] annotation having its first argument equal to [param name]. Each [Dictionary] contains the following entries: + Returns an [Array] of dictionaries describing each [annotation @GDScript.@meta] annotation target in this [Script] with first argument [param name]. Each [Dictionary] contains the following entries: - [code]target_type[/code] is a [enum MetaTargetType] indicating the type of the annotation target. - [code]target_name[/code] is the [StringName] identifier of the target, if any. - - [code]target_container[/code] is a [StringName] fully specifying the class containing this annotation target. - - [code]value[/code] is the [Variant] value that was passed as the second argument to the [code]@meta[/code] annotation. + - [code]target_container[/code] is a [StringName] fully specifying the class containing the annotation target. For example, if an inner class property is annotated the [code]target_container[/code] might be [code]"OuterClass.InnerClass"[/code]. + - [code]value[/code] holds the [Variant] passed as the second argument to the [annotation @GDScript.@meta] annotation. - Returns the list of all the [StringNames] that were passed as the first argument to [annotation @GDScript.@meta] annotations in this [Script]. + Returns the list of all the distinct [StringName] values passed as [code]name[/code] arguments to [annotation @GDScript.@meta] annotations in this [Script]. @@ -155,19 +155,19 @@ - Indicates that the target of a [code]@meta[/code] annotation is a class. + Indicates that the target of a [annotation @GDScript.@meta] annotation is a class. - Indicates that the target of a [code]@meta[/code] annotation is a variable. + Indicates that the target of a [annotation @GDScript.@meta] annotation is a variable. - Indicates that the target of a [code]@meta[/code] annotation is a constant. + Indicates that the target of a [annotation @GDScript.@meta] annotation is a constant. - Indicates that the target of a [code]@meta[/code] annotation is a signal. + Indicates that the target of a [annotation @GDScript.@meta] annotation is a signal. - Indicates that the target of a [code]@meta[/code] annotation is a function. + Indicates that the target of a [annotation @GDScript.@meta] annotation is a function. diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 9d6b38901798..a5b40fab5c69 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -780,16 +780,14 @@ Attach a piece of named metadata to the given class or class member. The metadata can then be read back at runtime using [method Script.get_script_meta]. [codeblock] - class_name MyClass - extends RefCounted - - @meta("my_tag", true) + @meta("my_tag", "my_data") var my_property: String func _init(): var script = get_script() for metadata in script.get_script_meta("my_tag"): - print("The member %s was annotated with @meta(\"my_tag\", %s)" % [metadata.target_name, metadata.value]) + # prints "my_property has my_data" + print("%s has %s" % [metadata.target_name, metadata.value]) [/codeblock] From 11d73f5e7b78b97dd392f0f69fe968ec916868f1 Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Sun, 5 Oct 2025 22:54:24 -0700 Subject: [PATCH 09/10] Resolve code style and docs pre-commit findings --- core/object/script_language.cpp | 2 +- doc/classes/Script.xml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index 4e16a4d81037..df78b3adf2ff 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -171,7 +171,7 @@ Dictionary ScriptMetadata::to_dictionary() const { TypedArray Script::get_script_meta(const StringName &p_name) const { Array result; if (script_metadata.has(p_name)) { - for (const auto& meta : script_metadata[p_name]) { + for (const auto &meta : script_metadata[p_name]) { result.append(meta.to_dictionary()); } } diff --git a/doc/classes/Script.xml b/doc/classes/Script.xml index ba462aab8f55..30ba75ea20b0 100644 --- a/doc/classes/Script.xml +++ b/doc/classes/Script.xml @@ -71,7 +71,8 @@ - + + Returns an [Array] of dictionaries describing each [annotation @GDScript.@meta] annotation target in this [Script] with first argument [param name]. Each [Dictionary] contains the following entries: - [code]target_type[/code] is a [enum MetaTargetType] indicating the type of the annotation target. @@ -81,7 +82,7 @@ - + Returns the list of all the distinct [StringName] values passed as [code]name[/code] arguments to [annotation @GDScript.@meta] annotations in this [Script]. From b5734ddc1525327d8f1b064ee204cbb59741e2df Mon Sep 17 00:00:00 2001 From: Karl Hellstern Date: Mon, 6 Oct 2025 19:49:18 -0700 Subject: [PATCH 10/10] Resolve docs pre-submit finding --- doc/classes/Script.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/classes/Script.xml b/doc/classes/Script.xml index 30ba75ea20b0..61f0023e0a56 100644 --- a/doc/classes/Script.xml +++ b/doc/classes/Script.xml @@ -71,8 +71,8 @@ - + Returns an [Array] of dictionaries describing each [annotation @GDScript.@meta] annotation target in this [Script] with first argument [param name]. Each [Dictionary] contains the following entries: - [code]target_type[/code] is a [enum MetaTargetType] indicating the type of the annotation target.