From d1134d3fe13d03e8b1a7047f81e6efae9759b94d Mon Sep 17 00:00:00 2001 From: chocola-mint <56677134+chocola-mint@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:20:13 +0900 Subject: [PATCH] Add user annotations --- editor/code_editor.cpp | 1 + modules/gdscript/config.py | 5 + modules/gdscript/doc_classes/GDScript.xml | 19 ++ .../doc_classes/GDScriptAnnotation.xml | 26 ++ .../doc_classes/GDScriptClassAnnotation.xml | 27 ++ .../GDScriptFunctionAnnotation.xml | 43 +++ .../doc_classes/GDScriptSignalAnnotation.xml | 33 +++ .../GDScriptVariableAnnotation.xml | 57 ++++ .../gdscript/editor/gdscript_highlighter.cpp | 6 +- modules/gdscript/gdscript.cpp | 31 +- modules/gdscript/gdscript.h | 18 +- modules/gdscript/gdscript_analyzer.cpp | 271 ++++++++++++++---- modules/gdscript/gdscript_analyzer.h | 19 ++ modules/gdscript/gdscript_annotation.cpp | 85 ++++++ modules/gdscript/gdscript_annotation.h | 87 ++++++ modules/gdscript/gdscript_editor.cpp | 23 +- .../gdscript/gdscript_member_annotations.cpp | 79 +++++ .../gdscript/gdscript_member_annotations.h | 201 +++++++++++++ modules/gdscript/gdscript_parser.cpp | 140 ++++++++- modules/gdscript/gdscript_parser.h | 12 + modules/gdscript/gdscript_tokenizer.cpp | 10 +- modules/gdscript/register_types.cpp | 6 + scene/gui/code_edit.cpp | 47 ++- scene/gui/code_edit.h | 1 + tests/scene/test_code_edit.h | 6 +- 25 files changed, 1180 insertions(+), 73 deletions(-) create mode 100644 modules/gdscript/doc_classes/GDScriptAnnotation.xml create mode 100644 modules/gdscript/doc_classes/GDScriptClassAnnotation.xml create mode 100644 modules/gdscript/doc_classes/GDScriptFunctionAnnotation.xml create mode 100644 modules/gdscript/doc_classes/GDScriptSignalAnnotation.xml create mode 100644 modules/gdscript/doc_classes/GDScriptVariableAnnotation.xml create mode 100644 modules/gdscript/gdscript_annotation.cpp create mode 100644 modules/gdscript/gdscript_annotation.h create mode 100644 modules/gdscript/gdscript_member_annotations.cpp create mode 100644 modules/gdscript/gdscript_member_annotations.h diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index ef5d878ca0aa..130f1411b434 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -1956,6 +1956,7 @@ CodeTextEditor::CodeTextEditor() { cs.push_back("="); cs.push_back("$"); cs.push_back("@"); + cs.push_back("@@"); cs.push_back("\""); cs.push_back("\'"); text_editor->set_code_completion_prefixes(cs); diff --git a/modules/gdscript/config.py b/modules/gdscript/config.py index ecd33a5daccc..865fcaf98552 100644 --- a/modules/gdscript/config.py +++ b/modules/gdscript/config.py @@ -11,7 +11,12 @@ def get_doc_classes(): return [ "@GDScript", "GDScript", + "GDScriptAnnotation", + "GDScriptClassAnnotation", + "GDScriptFunctionAnnotation", + "GDScriptSignalAnnotation", "GDScriptSyntaxHighlighter", + "GDScriptVariableAnnotation", ] diff --git a/modules/gdscript/doc_classes/GDScript.xml b/modules/gdscript/doc_classes/GDScript.xml index c3fa59dc23f6..46107d3d001a 100644 --- a/modules/gdscript/doc_classes/GDScript.xml +++ b/modules/gdscript/doc_classes/GDScript.xml @@ -12,6 +12,25 @@ $DOCS_URL/tutorials/scripting/gdscript/index.html + + + + Returns all [GDScriptAnnotation]s targeting the top-level class of this script. + + + + + + + Returns all [GDScriptAnnotation]s targeting [param member]. + + + + + + Returns all members of this script that is targeted by at least one [GDScriptAnnotation]. + + diff --git a/modules/gdscript/doc_classes/GDScriptAnnotation.xml b/modules/gdscript/doc_classes/GDScriptAnnotation.xml new file mode 100644 index 000000000000..11f5fa87950e --- /dev/null +++ b/modules/gdscript/doc_classes/GDScriptAnnotation.xml @@ -0,0 +1,26 @@ + + + + A user annotation implemented in the GDScript language. + + + A user annotation implemented in the GDScript language. User annotations are metadata that can be associated with a GDScript. + You can create a new GDScriptAnnotation by extending GDScriptAnnotation's subclasses, such as [GDScriptVariableAnnotation] and [GDScriptFunctionAnnotation]. + User annotations must have a class name, cannot be a nested class, and must define the constructor [code]_init[/code], which defines the annotation's parameters. + + + + + + + + Returns the annotation's name, which is equal to the class name. + + + + + + The annotation's current error message. Setting this to a non-empty value when the annotation is being compiled will cause the GDScript parser to emit an error. + + + diff --git a/modules/gdscript/doc_classes/GDScriptClassAnnotation.xml b/modules/gdscript/doc_classes/GDScriptClassAnnotation.xml new file mode 100644 index 000000000000..1552f3b3ac7b --- /dev/null +++ b/modules/gdscript/doc_classes/GDScriptClassAnnotation.xml @@ -0,0 +1,27 @@ + + + + A user annotation that can only target classes. + + + A user annotation that can only target classes. + + + + + + + + + Override to access the targeted class's information. + - [param name] is the class's name. + + + + + + Override to specify if there can be multiple instances of this annotation on the same target. If not overridden, the default is [code]false[/code]. + + + + diff --git a/modules/gdscript/doc_classes/GDScriptFunctionAnnotation.xml b/modules/gdscript/doc_classes/GDScriptFunctionAnnotation.xml new file mode 100644 index 000000000000..528a2c86f6a6 --- /dev/null +++ b/modules/gdscript/doc_classes/GDScriptFunctionAnnotation.xml @@ -0,0 +1,43 @@ + + + + A user annotation that can only target functions. + + + A user annotation that can only target functions. + + + + + + + + + + + + + + + + + Override to access the targeted function's information. This can be used for validation, for example requiring the function to have a specific signature. + - [param name] is the function's name; + - [param parameter_names] is an array containing every parameter's name. + - [param parameter_type_names] is an array containing every parameter's type name. + - [param parameter_builtin_types] is an array containing every parameter's type, as an [int] (see [enum Variant.Type]); + - [param return_type_name] is the return type's type name; + - [param return_builtin_type] is the return type, as an [int] (see [enum Variant.Type]); + - [param default_arguments] is an array containing default arguments for the function; + - [param is_static] is whether the function is static or not; + - [param is_coroutine] is whether the function is a coroutine or not. + + + + + + Override to specify if there can be multiple instances of this annotation on the same target. If not overridden, the default is [code]false[/code]. + + + + diff --git a/modules/gdscript/doc_classes/GDScriptSignalAnnotation.xml b/modules/gdscript/doc_classes/GDScriptSignalAnnotation.xml new file mode 100644 index 000000000000..ce4a201818e2 --- /dev/null +++ b/modules/gdscript/doc_classes/GDScriptSignalAnnotation.xml @@ -0,0 +1,33 @@ + + + + A user annotation that can only target signals. + + + A user annotation that can only target signals. + + + + + + + + + + + + Override to access the targeted signal's information. This can be used for validation, for example requiring the signal to have a specific signature. + - [param name] is the signal's name; + - [param parameter_names] is an array containing every parameter's name. + - [param parameter_type_names] is an array containing every parameter's type name. + - [param parameter_builtin_types] is an array containing every parameter's type, as an [int] (see [enum Variant.Type]). + + + + + + Override to specify if there can be multiple instances of this annotation on the same target. If not overridden, the default is [code]false[/code]. + + + + diff --git a/modules/gdscript/doc_classes/GDScriptVariableAnnotation.xml b/modules/gdscript/doc_classes/GDScriptVariableAnnotation.xml new file mode 100644 index 000000000000..21989329e86e --- /dev/null +++ b/modules/gdscript/doc_classes/GDScriptVariableAnnotation.xml @@ -0,0 +1,57 @@ + + + + A user annotation that can only target variables. + + + A user annotation that can only target variables. By overriding [method _is_export_annotation], this annotation can also act as an export annotation like [annotation @GDScript.@export_custom]. + + + + + + + + + + + + Override to access the targeted variable's information. This can be used for validation, for example requiring the variable to be a certain type. + - [param name] is the variable's name; + - [param type_name] is the variable's type name; + - [param type] is the variable's type, as an [int] (see [enum Variant.Type]); + - [param is_static] is whether the variable is static or not. + + + + + + Override to specify if there can be multiple instances of this annotation on the same target. If not overridden, the default is [code]false[/code]. + + + + + + Override to specify the annotation's property hint. If not overridden, the default is [constant PROPERTY_HINT_NONE]. + + + + + + Override to specify the annotation's property hint string. If not overridden, the default is an empty string. + + + + + + Override to specify the annotation's property usage. If not overridden, the default is [constant PROPERTY_USAGE_DEFAULT]. + + + + + + Override to specify if the annotation should be treated as an export annotation. If not overridden, the default is [code]false[/code]. + + + + diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp index acb74c1a6dcd..4173e7c44c2f 100644 --- a/modules/gdscript/editor/gdscript_highlighter.cpp +++ b/modules/gdscript/editor/gdscript_highlighter.cpp @@ -57,6 +57,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l bool in_node_path = false; bool in_node_ref = false; bool in_annotation = false; + bool in_user_annotation = false; bool in_string_name = false; bool is_hex_notation = false; bool is_bin_notation = false; @@ -593,8 +594,11 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l if (!in_annotation && in_region == -1 && str[j] == '@') { in_annotation = true; + } else if (in_annotation && in_region == -1 && str[j] == '@') { + in_user_annotation = true; } else if (in_region != -1 || is_a_symbol) { in_annotation = false; + in_user_annotation = false; } const bool in_raw_string_prefix = in_region == -1 && str[j] == 'r' && j + 1 < line_length && (str[j + 1] == '"' || str[j + 1] == '\''); @@ -604,7 +608,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l } else if (in_node_ref) { next_type = NODE_REF; color = node_ref_color; - } else if (in_annotation) { + } else if (in_annotation || in_user_annotation) { next_type = ANNOTATION; color = annotation_color; } else if (in_string_name) { diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 8ff0c63bd451..7924625f00fb 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -31,6 +31,7 @@ #include "gdscript.h" #include "gdscript_analyzer.h" +#include "gdscript_annotation.h" #include "gdscript_cache.h" #include "gdscript_compiler.h" #include "gdscript_parser.h" @@ -150,7 +151,7 @@ void GDScript::_super_implicit_constructor(GDScript *p_script, GDScriptInstance } } -GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_is_ref_counted, Callable::CallError &r_error) { +GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_is_ref_counted, Callable::CallError &r_error, bool p_show_error) { /* STEP 1, CREATE */ GDScriptInstance *instance = memnew(GDScriptInstance); @@ -182,7 +183,9 @@ GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argco MutexLock lock(GDScriptLanguage::singleton->mutex); instances.erase(p_owner); } - ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance: " + error_text); + if (p_show_error) { + ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance: " + error_text); + } } if (p_argcount < 0) { @@ -200,7 +203,9 @@ GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argco MutexLock lock(GDScriptLanguage::singleton->mutex); instances.erase(p_owner); } - ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance: " + error_text); + if (p_show_error) { + ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance: " + error_text); + } } } //@TODO make thread safe @@ -208,6 +213,10 @@ GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argco } Variant GDScript::_new(const Variant **p_args, int p_argcount, Callable::CallError &r_error) { + return _new_internal(p_args, p_argcount, r_error, true); +} + +Variant GDScript::_new_internal(const Variant **p_args, int p_argcount, Callable::CallError &r_error, bool p_show_error) { /* STEP 1, CREATE */ if (!valid) { @@ -237,7 +246,7 @@ Variant GDScript::_new(const Variant **p_args, int p_argcount, Callable::CallErr ref = Ref(r); } - GDScriptInstance *instance = _create_instance(p_args, p_argcount, owner, r != nullptr, r_error); + GDScriptInstance *instance = _create_instance(p_args, p_argcount, owner, r != nullptr, r_error, p_show_error); if (!instance) { if (ref.is_null()) { memdelete(owner); //no owner, sorry @@ -581,6 +590,11 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call, PlaceHolderSc break; // Nothing. } } + + member_annotations.clear(); + for (int i = 0; i < parser.get_member_annotations().size(); i++) { + member_annotations[parser.get_member_annotations()[i].first] = parser.get_member_annotations()[i].second; + } } else { placeholder_fallback_enabled = true; return false; @@ -850,6 +864,12 @@ Error GDScript::reload(bool p_keep_state) { can_run = ScriptServer::is_scripting_enabled() || parser.is_tool(); + class_annotations = parser.get_class_annotations(); + member_annotations.clear(); + for (int i = 0; i < parser.get_member_annotations().size(); i++) { + member_annotations[parser.get_member_annotations()[i].first] = parser.get_member_annotations()[i].second; + } + GDScriptCompiler compiler; err = compiler.compile(&parser, this, p_keep_state); @@ -1073,6 +1093,9 @@ void GDScript::_get_property_list(List *p_properties) const { void GDScript::_bind_methods() { ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "new", &GDScript::_new, MethodInfo("new")); + ClassDB::bind_method(D_METHOD("get_members_with_annotations"), &GDScript::get_members_with_annotations); + ClassDB::bind_method(D_METHOD("get_member_annotations", "member"), &GDScript::get_member_annotations); + ClassDB::bind_method(D_METHOD("get_class_annotations"), &GDScript::get_class_annotations); } void GDScript::set_path(const String &p_path, bool p_take_over) { diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index fa141d65ac5e..0ff8dd1d7b31 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -110,6 +110,9 @@ class GDScript : public Script { HashMap _signals; Dictionary rpc_config; + Array class_annotations; + HashMap member_annotations; + public: struct LambdaInfo { int capture_count; @@ -194,7 +197,7 @@ class GDScript : public Script { GDScriptFunction *_super_constructor(GDScript *p_script); void _super_implicit_constructor(GDScript *p_script, GDScriptInstance *p_instance, Callable::CallError &r_error); - GDScriptInstance *_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_is_ref_counted, Callable::CallError &r_error); + GDScriptInstance *_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_is_ref_counted, Callable::CallError &r_error, bool p_show_error = true); String _get_debug_path() const; @@ -286,6 +289,7 @@ class GDScript : public Script { StringName debug_get_static_var_by_index(int p_idx) const; Variant _new(const Variant **p_args, int p_argcount, Callable::CallError &r_error); + Variant _new_internal(const Variant **p_args, int p_argcount, Callable::CallError &r_error, bool p_show_error); virtual bool can_instantiate() const override; virtual Ref