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