From 5b4403c9a5650e36feb11e45f91431eacfed4050 Mon Sep 17 00:00:00 2001 From: Danil Alexeev Date: Wed, 26 Jul 2023 18:26:48 +0300 Subject: [PATCH] GDScript: Add validation for `@export_node_path` annotation arguments Co-authored-by: George Marques --- modules/gdscript/doc_classes/@GDScript.xml | 1 + modules/gdscript/gdscript_editor.cpp | 18 +++++++++++++++--- modules/gdscript/gdscript_parser.cpp | 18 ++++++++++++++++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 0c300eade48c..9cc1fc0c31f7 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -533,6 +533,7 @@ [codeblock] @export_node_path("Button", "TouchScreenButton") var some_button [/codeblock] + [b]Note:[/b] The type must be a native class or a globally registered script (using the [code]class_name[/code] keyword) that inherits [Node]. diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 2a7346940bf6..2a0a36a0629e 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -808,9 +808,10 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a ScriptLanguage::CodeCompletionOption node("Node", ScriptLanguage::CODE_COMPLETION_KIND_CLASS); node.insert_text = node.display.quote(p_quote_style); r_result.insert(node.display, node); - List node_types; - ClassDB::get_inheriters_from_class("Node", &node_types); - for (const StringName &E : node_types) { + + List native_classes; + ClassDB::get_inheriters_from_class("Node", &native_classes); + for (const StringName &E : native_classes) { if (!ClassDB::is_class_exposed(E)) { continue; } @@ -818,6 +819,17 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a option.insert_text = option.display.quote(p_quote_style); r_result.insert(option.display, option); } + + List global_script_classes; + ScriptServer::get_global_class_list(&global_script_classes); + for (const StringName &E : global_script_classes) { + if (!ClassDB::is_parent_class(ScriptServer::get_global_class_native_base(E), "Node")) { + continue; + } + ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_CLASS); + option.insert_text = option.display.quote(p_quote_style); + r_result.insert(option.display, option); + } } else if (p_annotation->name == SNAME("@warning_ignore")) { for (int warning_code = 0; warning_code < GDScriptWarning::WARNING_MAX; warning_code++) { ScriptLanguage::CodeCompletionOption warning(GDScriptWarning::get_name_from_code((GDScriptWarning::Code)warning_code).to_lower(), ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 2b91ba8f860f..6197cbdcf425 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -3867,6 +3867,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } } + // WARNING: Do not merge with the previous `if` because there `!=`, not `==`! if (p_annotation->name == SNAME("@export_flags")) { const int64_t max_flags = 32; Vector t = arg_string.split(":", true, 1); @@ -3892,6 +3893,18 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": Starting from argument %d, the flag value must be specified explicitly.)", i + 1, max_flags + 1), p_annotation->arguments[i]); return false; } + } else if (p_annotation->name == SNAME("@export_node_path")) { + String native_class = arg_string; + if (ScriptServer::is_global_class(arg_string)) { + native_class = ScriptServer::get_global_class_native_base(arg_string); + } + if (!ClassDB::class_exists(native_class)) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_node_path": The class "%s" was not found in the global scope.)", i + 1, arg_string), p_annotation->arguments[i]); + return false; + } else if (!ClassDB::is_parent_class(native_class, SNAME("Node"))) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_node_path": The class "%s" does not inherit "Node".)", i + 1, arg_string), p_annotation->arguments[i]); + return false; + } } if (i > 0) { @@ -3909,8 +3922,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node if (export_type.builtin_type == Variant::INT) { variable->export_info.type = Variant::INT; } - } - if (p_annotation->name == SNAME("@export_multiline")) { + } else if (p_annotation->name == SNAME("@export_multiline")) { if (export_type.builtin_type == Variant::ARRAY && export_type.has_container_element_type()) { DataType inner_type = export_type.get_container_element_type(); if (inner_type.builtin_type != Variant::STRING) { @@ -3938,6 +3950,8 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } } + // WARNING: Do not merge with the previous `else if`! Otherwise `else` (default variable type check) + // will not work for the above annotations. `@export` and `@export_enum` validate the type separately. if (p_annotation->name == SNAME("@export")) { if (variable->datatype_specifier == nullptr && variable->initializer == nullptr) { push_error(R"(Cannot use simple "@export" annotation with variable without type or initializer, since type can't be inferred.)", p_annotation);