Skip to content
45 changes: 45 additions & 0 deletions core/object/script_language.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Dictionary> 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<StringName, Vector<ScriptMetadata>> &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);
Expand All @@ -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<Script::MetaTargetType, void>' 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() {
Expand Down
26 changes: 26 additions & 0 deletions core/object/script_language.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -139,9 +150,20 @@ class Script : public Resource {
return get_rpc_config().duplicate(true);
}

private:
HashMap<StringName, Vector<ScriptMetadata>> 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;
Expand All @@ -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<Dictionary> get_script_meta(const StringName &p_name) const;
PackedStringArray get_script_meta_list() const;
void copy_script_meta_from(const HashMap<StringName, Vector<ScriptMetadata>> &p_source);

#ifdef TOOLS_ENABLED
virtual StringName get_doc_class_name() const = 0;
virtual Vector<DocData::ClassDoc> get_documentation() const = 0;
Expand Down
34 changes: 34 additions & 0 deletions doc/classes/Script.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@
Returns a dictionary containing constant names and their values.
</description>
</method>
<method name="get_script_meta">
<return type="Dictionary[]" />
<param index="0" name="name" type="StringName" />
<description>
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.
</description>
</method>
<method name="get_script_meta_list">
<return type="StringName[]" />
<description>
Returns the list of all the distinct [StringName] values passed as [code]name[/code] arguments to [annotation @GDScript.@meta] annotations in this [Script].
</description>
</method>
<method name="get_script_method_list">
<return type="Dictionary[]" />
<description>
Expand Down Expand Up @@ -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.
</member>
</members>
<constants>
<constant name="META_TARGET_CLASS" value="0" enum="MetaTargetType">
Indicates that the target of a [annotation @GDScript.@meta] annotation is a class.
</constant>
<constant name="META_TARGET_VARIABLE" value="1" enum="MetaTargetType">
Indicates that the target of a [annotation @GDScript.@meta] annotation is a variable.
</constant>
<constant name="META_TARGET_CONSTANT" value="2" enum="MetaTargetType">
Indicates that the target of a [annotation @GDScript.@meta] annotation is a constant.
</constant>
<constant name="META_TARGET_SIGNAL" value="3" enum="MetaTargetType">
Indicates that the target of a [annotation @GDScript.@meta] annotation is a signal.
</constant>
<constant name="META_TARGET_FUNCTION" value="3" enum="MetaTargetType">
Indicates that the target of a [annotation @GDScript.@meta] annotation is a function.
</constant>
</constants>
</class>
18 changes: 18 additions & 0 deletions modules/gdscript/doc_classes/@GDScript.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
</description>
</annotation>
<annotation name="@meta">
<return type="void" />
<param index="0" name="name" type="StringName" />
<param index="1" name="value" type="Variant" default="true" />
<description>
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]
</description>
</annotation>
<annotation name="@onready">
<return type="void" />
<description>
Expand Down
3 changes: 3 additions & 0 deletions modules/gdscript/gdscript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
25 changes: 24 additions & 1 deletion modules/gdscript/gdscript_analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -3835,12 +3846,15 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) {
void GDScriptAnalyzer::reduce_dictionary(GDScriptParser::DictionaryNode *p_dictionary) {
HashMap<Variant, GDScriptParser::ExpressionNode *, VariantHasher, StringLikeVariantComparator> 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)) {
Expand All @@ -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;
Expand Down
65 changes: 65 additions & 0 deletions modules/gdscript/gdscript_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ GDScriptParser::GDScriptParser() {
register_annotation(MethodInfo("@export_category", PropertyInfo(Variant::STRING, "name")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_CATEGORY>);
register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_GROUP>, varray(""));
register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_SUBGROUP>, 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);
Expand Down Expand Up @@ -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<ClassNode *>(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<VariableNode *>(p_target);
target_name = variable_node->identifier->name;
} break;
case Node::CONSTANT: {
target_type = Script::MetaTargetType::META_TARGET_CONSTANT;
ConstantNode *constant_node = static_cast<ConstantNode *>(p_target);
target_name = constant_node->identifier->name;
} break;
case Node::SIGNAL: {
target_type = Script::MetaTargetType::META_TARGET_SIGNAL;
SignalNode *signal_node = static_cast<SignalNode *>(p_target);
target_name = signal_node->identifier->name;
} break;
case Node::FUNCTION: {
target_type = Script::MetaTargetType::META_TARGET_FUNCTION;
FunctionNode *function_noade = static_cast<FunctionNode *>(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<ScriptMetadata>());
}
script_metadata[name].append(metadata);
return true;
}

GDScriptParser::DataType GDScriptParser::SuiteNode::Local::get_datatype() const {
switch (type) {
case CONSTANT:
Expand Down
6 changes: 6 additions & 0 deletions modules/gdscript/gdscript_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,7 @@ class GDScriptParser {
type = PATTERN;
}
};

struct PreloadNode : public ExpressionNode {
ExpressionNode *path = nullptr;
String resolved_path;
Expand Down Expand Up @@ -1399,6 +1400,8 @@ class GDScriptParser {
static HashMap<StringName, AnnotationInfo> valid_annotations;
List<AnnotationNode *> annotation_stack;

HashMap<StringName, Vector<ScriptMetadata>> 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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1615,6 +1619,8 @@ class GDScriptParser {
// TODO: Keep track of deps.
return List<String>();
}
const HashMap<StringName, Vector<ScriptMetadata>> get_script_metadata() const { return script_metadata; }

#ifdef DEBUG_ENABLED
const List<GDScriptWarning> &get_warnings() const { return warnings; }
const HashSet<int> &get_unsafe_lines() const { return unsafe_lines; }
Expand Down
Loading