diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index 2c4d2eec04be..df78b3adf2ff 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -159,6 +159,40 @@ PropertyInfo Script::get_class_category() const { #endif // TOOLS_ENABLED +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; +} + +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 result; +} + +PackedStringArray Script::get_script_meta_list() const { + PackedStringArray result; + for (const auto &[meta_name, _] : script_metadata) { + result.append(meta_name); + } + return result; +} + +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); + } +} + 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,7 +219,18 @@ void Script::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rpc_config"), &Script::_get_rpc_config_bind); + 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: 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); + 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 77a1b060a2e7..85f5e2e3c33c 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); @@ -139,9 +150,20 @@ class Script : public Resource { return get_rpc_config().duplicate(true); } +private: + 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; @@ -160,6 +182,10 @@ class Script : public Resource { virtual void set_source_code(const String &p_code) = 0; virtual Error reload(bool p_keep_state = false) = 0; + 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; virtual Vector get_documentation() const = 0; diff --git a/doc/classes/Script.xml b/doc/classes/Script.xml index d8d6f4fd2115..61f0023e0a56 100644 --- a/doc/classes/Script.xml +++ b/doc/classes/Script.xml @@ -70,6 +70,23 @@ Returns a dictionary containing constant names and their values. + + + + + 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 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 distinct [StringName] values passed as [code]name[/code] arguments to [annotation @GDScript.@meta] annotations in this [Script]. + + @@ -137,4 +154,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 [annotation @GDScript.@meta] annotation is a class. + + + Indicates that the target of a [annotation @GDScript.@meta] annotation is a variable. + + + Indicates that the target of a [annotation @GDScript.@meta] annotation is a constant. + + + Indicates that the target of a [annotation @GDScript.@meta] annotation is a signal. + + + 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 ab3363ab4c44..a5b40fab5c69 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -773,6 +773,24 @@ [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] + @meta("my_tag", "my_data") + var my_property: String + + func _init(): + var script = get_script() + for metadata in script.get_script_meta("my_tag"): + # prints "my_property has my_data" + print("%s has %s" % [metadata.target_name, metadata.value]) + [/codeblock] + + diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index aaae85d85efa..bfc45b1c7a8c 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -579,6 +579,7 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call, PlaceHolderSc break; // Nothing. } } + copy_script_meta_from(parser.get_script_metadata()); } else { placeholder_fallback_enabled = true; return false; @@ -853,6 +854,8 @@ Error GDScript::reload(bool p_keep_state) { can_run = ScriptServer::is_scripting_enabled() || parser.is_tool(); + copy_script_meta_from(parser.get_script_metadata()); + GDScriptCompiler compiler; err = compiler.compile(&parser, this, p_keep_state); diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 916283a2ddb7..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); @@ -2690,9 +2690,20 @@ 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; + } + + // 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. @@ -3835,12 +3846,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)) { @@ -3851,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 cb9de0053caa..1794bd6208b0 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::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); @@ -5173,6 +5175,69 @@ 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) { + 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; + StringName target_name; + switch (p_target->type) { + 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: + ERR_FAIL_V_MSG(false, R"("@meta" annotation does not apply here.)"); + } + + // 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]; + Variant value = p_annotation->resolved_arguments.size() < 2 ? Variant(true) : p_annotation->resolved_arguments[1]; + ScriptMetadata metadata; + metadata.name = name; + metadata.value = value; + 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()); + } + script_metadata[name].append(metadata); + 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..f74e4f80bcf2 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,8 @@ class GDScriptParser { static HashMap valid_annotations; List annotation_stack; + 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). enum Precedence { @@ -1540,6 +1543,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); @@ -1615,6 +1619,8 @@ class GDScriptParser { // TODO: Keep track of deps. return List(); } + const HashMap> get_script_metadata() const { return script_metadata; } + #ifdef DEBUG_ENABLED const List &get_warnings() const { return warnings; } const HashSet &get_unsafe_lines() const { return unsafe_lines; }