Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user annotations #102516

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions editor/code_editor.cpp
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 5 additions & 0 deletions modules/gdscript/config.py
Original file line number Diff line number Diff line change
@@ -11,7 +11,12 @@ def get_doc_classes():
return [
"@GDScript",
"GDScript",
"GDScriptAnnotation",
"GDScriptClassAnnotation",
"GDScriptFunctionAnnotation",
"GDScriptSignalAnnotation",
"GDScriptSyntaxHighlighter",
"GDScriptVariableAnnotation",
]


19 changes: 19 additions & 0 deletions modules/gdscript/doc_classes/GDScript.xml
Original file line number Diff line number Diff line change
@@ -12,6 +12,25 @@
<link title="GDScript documentation index">$DOCS_URL/tutorials/scripting/gdscript/index.html</link>
</tutorials>
<methods>
<method name="get_class_annotations" qualifiers="const">
<return type="Array" />
<description>
Returns all [GDScriptAnnotation]s targeting the top-level class of this script.
</description>
</method>
<method name="get_member_annotations" qualifiers="const">
<return type="Array" />
<param index="0" name="member" type="StringName" />
<description>
Returns all [GDScriptAnnotation]s targeting [param member].
</description>
</method>
<method name="get_members_with_annotations" qualifiers="const">
<return type="PackedStringArray" />
<description>
Returns all members of this script that is targeted by at least one [GDScriptAnnotation].
</description>
</method>
<method name="new" qualifiers="vararg">
<return type="Variant" />
<description>
26 changes: 26 additions & 0 deletions modules/gdscript/doc_classes/GDScriptAnnotation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="GDScriptAnnotation" inherits="RefCounted" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
A user annotation implemented in the GDScript language.
</brief_description>
<description>
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.
</description>
<tutorials>
</tutorials>
<methods>
<method name="get_name" qualifiers="const">
<return type="StringName" />
<description>
Returns the annotation's name, which is equal to the class name.
</description>
</method>
</methods>
<members>
<member name="error_message" type="String" setter="set_error_message" getter="get_error_message">
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.
</member>
</members>
</class>
27 changes: 27 additions & 0 deletions modules/gdscript/doc_classes/GDScriptClassAnnotation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="GDScriptClassAnnotation" inherits="GDScriptAnnotation" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
A user annotation that can only target classes.
</brief_description>
<description>
A user annotation that can only target classes.
</description>
<tutorials>
</tutorials>
<methods>
<method name="_analyze" qualifiers="virtual">
<return type="void" />
<param index="0" name="name" type="StringName" />
<description>
Override to access the targeted class's information.
- [param name] is the class's name.
</description>
</method>
<method name="_get_allow_multiple" qualifiers="virtual const">
<return type="bool" />
<description>
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].
</description>
</method>
</methods>
</class>
43 changes: 43 additions & 0 deletions modules/gdscript/doc_classes/GDScriptFunctionAnnotation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="GDScriptFunctionAnnotation" inherits="GDScriptAnnotation" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
A user annotation that can only target functions.
</brief_description>
<description>
A user annotation that can only target functions.
</description>
<tutorials>
</tutorials>
<methods>
<method name="_analyze" qualifiers="virtual">
<return type="void" />
<param index="0" name="name" type="StringName" />
<param index="1" name="parameter_names" type="PackedStringArray" />
<param index="2" name="parameter_type_names" type="PackedStringArray" />
<param index="3" name="parameter_builtin_types" type="PackedInt32Array" />
<param index="4" name="return_type_name" type="StringName" />
<param index="5" name="return_builtin_type" type="int" enum="Variant.Type" />
<param index="6" name="default_arguments" type="Array" />
<param index="7" name="is_static" type="bool" />
<param index="8" name="is_coroutine" type="bool" />
<description>
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.
</description>
</method>
<method name="_get_allow_multiple" qualifiers="virtual const">
<return type="bool" />
<description>
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].
</description>
</method>
</methods>
</class>
33 changes: 33 additions & 0 deletions modules/gdscript/doc_classes/GDScriptSignalAnnotation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="GDScriptSignalAnnotation" inherits="GDScriptAnnotation" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
A user annotation that can only target signals.
</brief_description>
<description>
A user annotation that can only target signals.
</description>
<tutorials>
</tutorials>
<methods>
<method name="_analyze" qualifiers="virtual">
<return type="void" />
<param index="0" name="name" type="StringName" />
<param index="1" name="parameter_names" type="PackedStringArray" />
<param index="2" name="parameter_type_names" type="PackedStringArray" />
<param index="3" name="parameter_builtin_types" type="PackedInt32Array" />
<description>
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]).
</description>
</method>
<method name="_get_allow_multiple" qualifiers="virtual const">
<return type="bool" />
<description>
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].
</description>
</method>
</methods>
</class>
57 changes: 57 additions & 0 deletions modules/gdscript/doc_classes/GDScriptVariableAnnotation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="GDScriptVariableAnnotation" inherits="GDScriptAnnotation" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
A user annotation that can only target variables.
</brief_description>
<description>
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].
</description>
<tutorials>
</tutorials>
<methods>
<method name="_analyze" qualifiers="virtual">
<return type="void" />
<param index="0" name="name" type="StringName" />
<param index="1" name="type_name" type="StringName" />
<param index="2" name="type" type="int" enum="Variant.Type" />
<param index="3" name="is_static" type="bool" />
<description>
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.
</description>
</method>
<method name="_get_allow_multiple" qualifiers="virtual const">
<return type="bool" />
<description>
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].
</description>
</method>
<method name="_get_property_hint" qualifiers="virtual const">
<return type="int" />
<description>
Override to specify the annotation's property hint. If not overridden, the default is [constant PROPERTY_HINT_NONE].
</description>
</method>
<method name="_get_property_hint_string" qualifiers="virtual const">
<return type="String" />
<description>
Override to specify the annotation's property hint string. If not overridden, the default is an empty string.
</description>
</method>
<method name="_get_property_usage" qualifiers="virtual const">
<return type="int" />
<description>
Override to specify the annotation's property usage. If not overridden, the default is [constant PROPERTY_USAGE_DEFAULT].
</description>
</method>
<method name="_is_export_annotation" qualifiers="virtual const">
<return type="bool" />
<description>
Override to specify if the annotation should be treated as an export annotation. If not overridden, the default is [code]false[/code].
</description>
</method>
</methods>
</class>
6 changes: 5 additions & 1 deletion modules/gdscript/editor/gdscript_highlighter.cpp
Original file line number Diff line number Diff line change
@@ -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) {
31 changes: 27 additions & 4 deletions modules/gdscript/gdscript.cpp
Original file line number Diff line number Diff line change
@@ -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,14 +203,20 @@ 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
return instance;
}

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<RefCounted>(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<PropertyInfo> *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) {
18 changes: 17 additions & 1 deletion modules/gdscript/gdscript.h
Original file line number Diff line number Diff line change
@@ -110,6 +110,9 @@ class GDScript : public Script {
HashMap<StringName, MethodInfo> _signals;
Dictionary rpc_config;

Array class_annotations;
HashMap<StringName, Array> 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<Script> get_base_script() const override;
@@ -342,6 +346,18 @@ class GDScript : public Script {

virtual void get_constants(HashMap<StringName, Variant> *p_constants) override;
virtual void get_members(HashSet<StringName> *p_members) override;
PackedStringArray get_members_with_annotations() const {
PackedStringArray arr;
for (const KeyValue<StringName, Array> &E : member_annotations) {
arr.push_back(E.key);
}
return arr;
}
Array get_member_annotations(const StringName &p_member) const {
const Array *ret = member_annotations.getptr(p_member);
return ret ? *ret : Array();
}
Array get_class_annotations() const { return class_annotations; }

virtual Variant get_rpc_config() const override;

271 changes: 219 additions & 52 deletions modules/gdscript/gdscript_analyzer.cpp

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions modules/gdscript/gdscript_analyzer.h
Original file line number Diff line number Diff line change
@@ -73,6 +73,25 @@ class GDScriptAnalyzer {
void decide_suite_type(GDScriptParser::Node *p_suite, GDScriptParser::Node *p_statement);

void resolve_annotation(GDScriptParser::AnnotationNode *p_annotation);
template <typename T>
Array resolve_and_apply_annotations(T *p_target, GDScriptParser::ClassNode *p_class) {
HashSet<StringName> annotation_name_set;
Array annotations;
for (GDScriptParser::AnnotationNode *E : p_target->annotations) {
resolve_annotation(E);
if (E->annotation_object.is_valid() && !E->annotation_object->get_allow_multiple() && annotation_name_set.has(E->name)) {
push_error(vformat(R"(Annotation "%s" can only be applied once.)", E->name), E);
E->annotation_object = nullptr;
continue;
}
E->apply(parser, p_target, p_class);
if (E->annotation_object.is_valid()) {
annotations.push_back(E->annotation_object);
annotation_name_set.insert(E->name);
}
}
return annotations;
}
void resolve_class_member(GDScriptParser::ClassNode *p_class, const StringName &p_name, const GDScriptParser::Node *p_source = nullptr);
void resolve_class_member(GDScriptParser::ClassNode *p_class, int p_index, const GDScriptParser::Node *p_source = nullptr);
void resolve_class_interface(GDScriptParser::ClassNode *p_class, const GDScriptParser::Node *p_source = nullptr);
85 changes: 85 additions & 0 deletions modules/gdscript/gdscript_annotation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**************************************************************************/
/* gdscript_annotation.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#include "gdscript_annotation.h"
#include "core/object/script_language.h"
#include "gdscript.h"

void GDScriptAnnotation::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_name"), &GDScriptAnnotation::get_name);

ClassDB::bind_method(D_METHOD("set_error_message", "error_message"), &GDScriptAnnotation::set_error_message);
ClassDB::bind_method(D_METHOD("get_error_message"), &GDScriptAnnotation::get_error_message);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "error_message"), "set_error_message", "get_error_message");
}

StringName GDScriptAnnotation::get_name() const {
return name;
}

void GDScriptAnnotation::set_error_message(const String &p_error_message) {
error_message = p_error_message;
}

String GDScriptAnnotation::get_error_message() const {
return error_message;
}

void GDScriptAnnotation::find_user_annotations(List<MethodInfo> *r_annotations) {
List<StringName> global_classes;
ScriptServer::get_global_class_list(&global_classes);
for (const StringName &global_class : global_classes) {
if (ScriptServer::get_global_class_language(global_class) == GDScript::get_class_static()) {
if (ClassDB::is_parent_class(ScriptServer::get_global_class_native_base(global_class), get_class_static())) {
const String path = ScriptServer::get_global_class_path(global_class);
Ref<GDScript> script = ResourceLoader::load(path, GDScript::get_class_static());
if (script->has_method("_init")) {
MethodInfo mi = script->get_method_info("_init");
mi.name = "@@" + global_class;
r_annotations->push_back(mi);
}
}
}
}
}

void GDScriptAnnotation::find_native_user_annotations(List<MethodInfo> *r_annotations) {
List<StringName> annotation_classes;
ClassDB::get_inheriters_from_class(get_class_static(), &annotation_classes);
for (const StringName &annotation_class : annotation_classes) {
if (!ClassDB::is_abstract(annotation_class) && !ClassDB::is_virtual(annotation_class)) {
MethodInfo mi;
if (ClassDB::get_method_info(annotation_class, GDScriptLanguage::get_singleton()->strings._init, &mi)) {
mi.name = "@@" + annotation_class;
r_annotations->push_back(mi);
}
}
}
}
87 changes: 87 additions & 0 deletions modules/gdscript/gdscript_annotation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**************************************************************************/
/* gdscript_annotation.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#ifndef GDSCRIPT_ANNOTATION_H
#define GDSCRIPT_ANNOTATION_H

#include "core/object/gdvirtual.gen.inc"
#include "core/object/ref_counted.h"

class GDScriptAnnotation : public RefCounted {
GDCLASS(GDScriptAnnotation, RefCounted);

friend class GDScriptAnalyzer;

public:
enum TargetFlags : int {
TARGET_NONE = 0,
TARGET_VARIABLE = 1 << 0,
TARGET_FUNCTION = 1 << 1,
TARGET_SIGNAL = 1 << 2,
TARGET_CLASS = 1 << 3,
};

protected:
StringName name;
String error_message;

static void _bind_methods();

public:
StringName get_name() const;

void set_error_message(const String &p_error_message);
String get_error_message() const;
_FORCE_INLINE_ bool has_error_message() const { return !error_message.is_empty(); }

virtual TargetFlags get_target_mask() = 0;

virtual bool get_allow_multiple() = 0;

static _FORCE_INLINE_ constexpr const char *target_to_name(TargetFlags p_target) {
switch (p_target) {
case TARGET_VARIABLE:
return "Variable";
case TARGET_FUNCTION:
return "Function";
case TARGET_SIGNAL:
return "Signal";
case TARGET_CLASS:
return "Class";
default:
return "Unknown";
}
}

static void find_user_annotations(List<MethodInfo> *r_annotations);
static void find_native_user_annotations(List<MethodInfo> *r_annotations);
};

#endif // GDSCRIPT_ANNOTATION_H
23 changes: 22 additions & 1 deletion modules/gdscript/gdscript_editor.cpp
Original file line number Diff line number Diff line change
@@ -889,7 +889,12 @@ static void _get_directory_contents(EditorFileSystemDirectory *p_dir, HashMap<St
}

static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_annotation, int p_argument, const String p_quote_style, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, String &r_arghint) {
r_arghint = _make_arguments_hint(p_annotation->info->info, p_argument, true);
if (p_annotation->is_builtin) {
r_arghint = _make_arguments_hint(p_annotation->info->info, p_argument, true);
} else {
r_arghint = _make_arguments_hint(p_annotation->annotation_object_info, p_argument, true);
return;
}
if (p_annotation->name == SNAME("@export_range")) {
if (p_argument == 3 || p_argument == 4 || p_argument == 5) {
// Slider hint.
@@ -3234,6 +3239,19 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa
}
r_forced = true;
} break;
case GDScriptParser::COMPLETION_USER_ANNOTATION: {
List<MethodInfo> annotations;
GDScriptAnnotation::find_user_annotations(&annotations);
GDScriptAnnotation::find_native_user_annotations(&annotations);
for (const MethodInfo &E : annotations) {
ScriptLanguage::CodeCompletionOption option(E.name.substr(2), ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);
if (E.arguments.size() > 0) {
option.insert_text += "(";
}
options.insert(option.display, option);
}
r_forced = true;
} break;
case GDScriptParser::COMPLETION_ANNOTATION_ARGUMENTS: {
if (completion_context.node == nullptr || completion_context.node->type != GDScriptParser::Node::ANNOTATION) {
break;
@@ -4336,6 +4354,9 @@ ::Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symb
return OK;
}
} break;
case GDScriptParser::COMPLETION_USER_ANNOTATION: {
// Nothing here. Should have been resolved as a class lookup.
} break;
default: {
}
}
79 changes: 79 additions & 0 deletions modules/gdscript/gdscript_member_annotations.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**************************************************************************/
/* gdscript_member_annotations.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#include "gdscript_member_annotations.h"

void GDScriptVariableAnnotation::_bind_methods() {
GDVIRTUAL_BIND(_analyze, "name", "type_name", "type", "is_static");
GDVIRTUAL_BIND(_get_allow_multiple);
GDVIRTUAL_BIND(_is_export_annotation);
GDVIRTUAL_BIND(_get_property_hint);
GDVIRTUAL_BIND(_get_property_hint_string);
GDVIRTUAL_BIND(_get_property_usage);
}

bool GDScriptVariableAnnotation::apply(GDScriptParser::VariableNode *p_target, GDScriptParser::ClassNode *p_class) {
if (is_export_annotation()) {
if (p_target->is_static) {
error_message = vformat(R"(Annotation "%s" cannot be applied to a static variable.)", name);
return false;
}
if (p_target->exported) {
error_message = vformat(R"(Annotation "%s" cannot be used with another export annotation.)", name);
return false;
}

p_target->exported = true;

GDScriptParser::DataType export_type = p_target->get_datatype();

p_target->export_info.type = export_type.builtin_type;
p_target->export_info.hint = get_property_hint();
p_target->export_info.hint_string = get_property_hint_string();
p_target->export_info.usage = get_property_usage();
}

return true;
}

void GDScriptFunctionAnnotation::_bind_methods() {
GDVIRTUAL_BIND(_analyze, "name", "parameter_names", "parameter_type_names", "parameter_builtin_types", "return_type_name", "return_builtin_type", "default_arguments", "is_static", "is_coroutine");
GDVIRTUAL_BIND(_get_allow_multiple);
}

void GDScriptSignalAnnotation::_bind_methods() {
GDVIRTUAL_BIND(_analyze, "name", "parameter_names", "parameter_type_names", "parameter_builtin_types");
GDVIRTUAL_BIND(_get_allow_multiple);
}

void GDScriptClassAnnotation::_bind_methods() {
GDVIRTUAL_BIND(_analyze, "name");
GDVIRTUAL_BIND(_get_allow_multiple);
}
201 changes: 201 additions & 0 deletions modules/gdscript/gdscript_member_annotations.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**************************************************************************/
/* gdscript_member_annotations.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#ifndef GDSCRIPT_MEMBER_ANNOTATIONS_H
#define GDSCRIPT_MEMBER_ANNOTATIONS_H

#include "gdscript_annotation.h"
#include "gdscript_parser.h"

class GDScriptVariableAnnotation : public GDScriptAnnotation {
GDCLASS(GDScriptVariableAnnotation, GDScriptAnnotation);

protected:
static void _bind_methods();

public:
GDVIRTUAL0RC(bool, _get_allow_multiple)
virtual bool get_allow_multiple() override final {
if (GDVIRTUAL_IS_OVERRIDDEN(_get_allow_multiple)) {
bool ret = false;
GDVIRTUAL_CALL(_get_allow_multiple, ret);
return ret;
}
return false;
}

virtual TargetFlags get_target_mask() override final {
return TARGET_VARIABLE;
}

GDVIRTUAL4(_analyze, StringName, StringName, Variant::Type, bool)
virtual void analyze(const StringName &p_name, const StringName &p_type_name, Variant::Type p_builtin_type, bool p_is_static) {
if (GDVIRTUAL_IS_OVERRIDDEN(_analyze)) {
GDVIRTUAL_CALL(_analyze, p_name, p_type_name, p_builtin_type, p_is_static);
}
}

GDVIRTUAL0RC(bool, _is_export_annotation)
virtual bool is_export_annotation() const {
if (GDVIRTUAL_IS_OVERRIDDEN(_is_export_annotation)) {
bool ret = false;
GDVIRTUAL_CALL(_is_export_annotation, ret);
return ret;
} else {
return false;
}
}

GDVIRTUAL0RC(int, _get_property_hint)
virtual PropertyHint get_property_hint() const {
if (GDVIRTUAL_IS_OVERRIDDEN(_get_property_hint)) {
int ret = PROPERTY_HINT_NONE;
GDVIRTUAL_CALL(_get_property_hint, ret);
return (PropertyHint)ret;
} else {
return PROPERTY_HINT_NONE;
}
}

GDVIRTUAL0RC(String, _get_property_hint_string)
virtual String get_property_hint_string() const {
if (GDVIRTUAL_IS_OVERRIDDEN(_get_property_hint_string)) {
String ret;
GDVIRTUAL_CALL(_get_property_hint_string, ret);
return ret;
} else {
return String();
}
}

GDVIRTUAL0RC(int, _get_property_usage)
virtual PropertyUsageFlags get_property_usage() const {
if (GDVIRTUAL_IS_OVERRIDDEN(_get_property_usage)) {
int ret = PROPERTY_USAGE_DEFAULT;
GDVIRTUAL_CALL(_get_property_usage, ret);
return (PropertyUsageFlags)ret;
} else {
return PROPERTY_USAGE_DEFAULT;
}
}

// Default implementation is roughly equivalent to using @export_custom.
// This means no validation is performed on the hint string. The user is responsible for validation in _init.
virtual bool apply(GDScriptParser::VariableNode *p_target, GDScriptParser::ClassNode *p_class);
};

class GDScriptFunctionAnnotation : public GDScriptAnnotation {
GDCLASS(GDScriptFunctionAnnotation, GDScriptAnnotation);

protected:
static void _bind_methods();

public:
GDVIRTUAL0RC(bool, _get_allow_multiple)
virtual bool get_allow_multiple() override final {
if (GDVIRTUAL_IS_OVERRIDDEN(_get_allow_multiple)) {
bool ret = false;
GDVIRTUAL_CALL(_get_allow_multiple, ret);
return ret;
}
return false;
}

virtual TargetFlags get_target_mask() override final {
return TARGET_FUNCTION;
}

GDVIRTUAL9(_analyze, StringName, PackedStringArray, PackedStringArray, PackedInt32Array, StringName, Variant::Type, Array, bool, bool)
virtual void analyze(const StringName &p_name, const PackedStringArray &p_parameter_names, const PackedStringArray &p_parameter_type_names, const PackedInt32Array &p_parameter_builtin_types, const StringName &p_return_type_name, Variant::Type p_return_builtin_type, const Array &p_default_arguments, bool p_is_static, bool p_is_coroutine) {
if (GDVIRTUAL_IS_OVERRIDDEN(_analyze)) {
GDVIRTUAL_CALL(_analyze, p_name, p_parameter_names, p_parameter_type_names, p_parameter_builtin_types, p_return_type_name, p_return_builtin_type, p_default_arguments, p_is_static, p_is_coroutine);
}
}
};

class GDScriptSignalAnnotation : public GDScriptAnnotation {
GDCLASS(GDScriptSignalAnnotation, GDScriptAnnotation);

protected:
static void _bind_methods();

public:
GDVIRTUAL0RC(bool, _get_allow_multiple)
virtual bool get_allow_multiple() override final {
if (GDVIRTUAL_IS_OVERRIDDEN(_get_allow_multiple)) {
bool ret = false;
GDVIRTUAL_CALL(_get_allow_multiple, ret);
return ret;
}
return false;
}

virtual TargetFlags get_target_mask() override final {
return TARGET_SIGNAL;
}

GDVIRTUAL4(_analyze, StringName, PackedStringArray, PackedStringArray, PackedInt32Array)
virtual void analyze(const StringName &p_name, const PackedStringArray &p_parameter_names, const PackedStringArray &p_parameter_type_names, const PackedInt32Array &p_parameter_builtin_types) {
if (GDVIRTUAL_IS_OVERRIDDEN(_analyze)) {
GDVIRTUAL_CALL(_analyze, p_name, p_parameter_names, p_parameter_type_names, p_parameter_builtin_types);
}
}
};

class GDScriptClassAnnotation : public GDScriptAnnotation {
GDCLASS(GDScriptClassAnnotation, GDScriptAnnotation);

protected:
static void _bind_methods();

public:
GDVIRTUAL0RC(bool, _get_allow_multiple)
virtual bool get_allow_multiple() override final {
if (GDVIRTUAL_IS_OVERRIDDEN(_get_allow_multiple)) {
bool ret = false;
GDVIRTUAL_CALL(_get_allow_multiple, ret);
return ret;
}
return false;
}

virtual TargetFlags get_target_mask() override final {
return TARGET_CLASS;
}

GDVIRTUAL1(_analyze, StringName)
virtual void analyze(const StringName &p_name) {
if (GDVIRTUAL_IS_OVERRIDDEN(_analyze)) {
GDVIRTUAL_CALL(_analyze, p_name);
}
}
};

#endif // GDSCRIPT_MEMBER_ANNOTATIONS_H
140 changes: 133 additions & 7 deletions modules/gdscript/gdscript_parser.cpp
Original file line number Diff line number Diff line change
@@ -31,6 +31,8 @@
#include "gdscript_parser.h"

#include "gdscript.h"
#include "gdscript_annotation.h"
#include "gdscript_member_annotations.h"
#include "gdscript_tokenizer_buffer.h"

#include "core/config/project_settings.h"
@@ -1650,13 +1652,15 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) {
GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_valid_targets) {
AnnotationNode *annotation = alloc_node<AnnotationNode>();

annotation->name = previous.literal;
const String name = previous.literal;
annotation->is_builtin = !name.begins_with("@@");
annotation->name = annotation->is_builtin ? name : name.trim_prefix("@@");

make_completion_context(COMPLETION_ANNOTATION, annotation);
make_completion_context(annotation->is_builtin ? COMPLETION_ANNOTATION : COMPLETION_USER_ANNOTATION, annotation);

bool valid = true;

if (!valid_annotations.has(annotation->name)) {
if (annotation->is_builtin && !valid_annotations.has(annotation->name)) {
if (annotation->name == "@deprecated") {
push_error(R"("@deprecated" annotation does not exist. Use "## @deprecated: Reason here." instead.)");
} else if (annotation->name == "@experimental") {
@@ -1669,7 +1673,7 @@ GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_vali
valid = false;
}

if (valid) {
if (annotation->is_builtin && valid) {
annotation->info = &valid_annotations[annotation->name];

if (!annotation->applies_to(p_valid_targets)) {
@@ -1682,7 +1686,7 @@ GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_vali
}
}

if (check(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) {
if (valid && check(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) {
push_multiline(true);
advance();
// Arguments.
@@ -1720,7 +1724,7 @@ GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_vali

match(GDScriptTokenizer::Token::NEWLINE); // Newline after annotation is optional.

if (valid) {
if (annotation->is_builtin && valid) {
valid = validate_annotation_arguments(annotation);
}

@@ -4160,10 +4164,132 @@ bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_targe
return true;
}
is_applied = true;
return (p_this->*(p_this->valid_annotations[name].apply))(this, p_target, p_class);
if (is_builtin) {
return (p_this->*(p_this->valid_annotations[name].apply))(this, p_target, p_class);
} else {
// The annotation object can be invalid if _init has reported an error by user code.
if (annotation_object.is_valid()) {
#define ANNOTATION_MESSAGE_CHECK \
if (annotation_object->has_error_message()) { \
p_this->push_error(annotation_object->get_error_message(), this); \
annotation_object = nullptr; \
return false; \
}

GDScriptAnnotation::TargetFlags target_mask = annotation_object->get_target_mask();
GDScriptAnnotation::TargetFlags target_type = GDScriptAnnotation::TARGET_NONE;
switch (p_target->type) {
case Node::CLASS: {
target_type = GDScriptAnnotation::TARGET_CLASS;
} break;
case Node::FUNCTION: {
target_type = GDScriptAnnotation::TARGET_FUNCTION;
} break;
case Node::SIGNAL: {
target_type = GDScriptAnnotation::TARGET_SIGNAL;
} break;
case Node::VARIABLE: {
target_type = GDScriptAnnotation::TARGET_VARIABLE;
} break;
default: {
target_type = GDScriptAnnotation::TARGET_NONE;
} break;
}

int mask_result = target_mask & target_type;
if (!mask_result) {
p_this->push_error(vformat(R"*(Annotation "%s" is not compatible with target type: %s.)*", name, GDScriptAnnotation::target_to_name(target_type)), this);
return false;
}

// Call _analyze.
switch (p_target->type) {
case Node::CLASS: {
GDScriptClassAnnotation *class_annotation = Object::cast_to<GDScriptClassAnnotation>(annotation_object.ptr());
if (class_annotation) {
ClassNode *m_class = static_cast<ClassNode *>(p_target);
class_annotation->analyze(m_class->get_global_name());
ANNOTATION_MESSAGE_CHECK;
}
} break;
case Node::FUNCTION: {
GDScriptFunctionAnnotation *function_annotation = Object::cast_to<GDScriptFunctionAnnotation>(annotation_object.ptr());
if (function_annotation) {
FunctionNode *function = static_cast<FunctionNode *>(p_target);
StringName function_name = function->identifier->name;
PackedStringArray parameter_names;
PackedStringArray parameter_type_names;
PackedInt32Array parameter_builtin_types;
for (const ParameterNode *parameter : function->parameters) {
parameter_names.push_back(parameter->identifier->name);
parameter_type_names.push_back(parameter->get_datatype().to_string());
parameter_builtin_types.push_back(parameter->get_datatype().builtin_type);
}
Array default_arguments;
for (const Variant &arg : function->default_arg_values) {
default_arguments.push_back(arg);
}
StringName return_type_name = function->return_type->get_datatype().to_string();
Variant::Type return_builtin_type = function->return_type->get_datatype().builtin_type;
function_annotation->analyze(
function_name,
parameter_names,
parameter_type_names,
parameter_builtin_types,
return_type_name,
return_builtin_type,
default_arguments,
function->is_static,
function->is_coroutine);
ANNOTATION_MESSAGE_CHECK;
}
} break;
case Node::SIGNAL: {
GDScriptSignalAnnotation *signal_annotation = Object::cast_to<GDScriptSignalAnnotation>(annotation_object.ptr());
if (signal_annotation) {
SignalNode *signal = static_cast<SignalNode *>(p_target);
StringName signal_name = signal->identifier->name;
PackedStringArray parameter_names;
PackedStringArray parameter_type_names;
PackedInt32Array parameter_builtin_types;
for (const ParameterNode *parameter : signal->parameters) {
parameter_names.push_back(parameter->identifier->name);
parameter_type_names.push_back(parameter->get_datatype().to_string());
parameter_builtin_types.push_back(parameter->get_datatype().builtin_type);
}
signal_annotation->analyze(signal_name, parameter_names, parameter_type_names, parameter_builtin_types);
ANNOTATION_MESSAGE_CHECK;
}
} break;
case Node::VARIABLE: {
GDScriptVariableAnnotation *variable_annotation = Object::cast_to<GDScriptVariableAnnotation>(annotation_object.ptr());
if (variable_annotation) {
VariableNode *variable = static_cast<VariableNode *>(p_target);
variable_annotation->analyze(
variable->identifier->name,
variable->get_datatype().to_string(),
variable->get_datatype().builtin_type,
variable->is_static);
ANNOTATION_MESSAGE_CHECK;
variable_annotation->apply(variable, p_class);
}
} break;
default: {
// Not reachable.
} break;
}

return true;
#undef ANNOTATION_MESSAGE_CHECK
}
return false;
}
}

bool GDScriptParser::AnnotationNode::applies_to(uint32_t p_target_kinds) const {
if (!is_builtin) {
return (AnnotationInfo::CLASS_LEVEL & p_target_kinds) > 0;
}
return (info->target_kind & p_target_kinds) > 0;
}

12 changes: 12 additions & 0 deletions modules/gdscript/gdscript_parser.h
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@
#ifndef GDSCRIPT_PARSER_H
#define GDSCRIPT_PARSER_H

#include "gdscript_annotation.h"
#include "gdscript_cache.h"
#include "gdscript_tokenizer.h"

@@ -371,11 +372,14 @@ class GDScriptParser {
StringName name;
Vector<ExpressionNode *> arguments;
Vector<Variant> resolved_arguments;
Ref<GDScriptAnnotation> annotation_object;
MethodInfo annotation_object_info;

AnnotationInfo *info = nullptr;
PropertyInfo export_info;
bool is_resolved = false;
bool is_applied = false;
bool is_builtin = true;

bool apply(GDScriptParser *p_this, Node *p_target, ClassNode *p_class);
bool applies_to(uint32_t p_target_kinds) const;
@@ -1287,6 +1291,7 @@ class GDScriptParser {
enum CompletionType {
COMPLETION_NONE,
COMPLETION_ANNOTATION, // Annotation (following @).
COMPLETION_USER_ANNOTATION, // User annotation (following @@).
COMPLETION_ANNOTATION_ARGUMENTS, // Annotation arguments hint.
COMPLETION_ASSIGN, // Assignment based on type (e.g. enum values).
COMPLETION_ATTRIBUTE, // After id.| to look for members.
@@ -1331,6 +1336,8 @@ class GDScriptParser {
private:
friend class GDScriptAnalyzer;
friend class GDScriptParserRef;
friend class GDScriptAnnotation;
friend struct GDScriptParserApplyCallbackInterface;

bool _is_tool = false;
String script_path;
@@ -1398,6 +1405,9 @@ class GDScriptParser {
};
static HashMap<StringName, AnnotationInfo> valid_annotations;
List<AnnotationNode *> annotation_stack;
// Class annotations that target the top-level class of the script.
Array class_annotations;
Vector<Pair<StringName, Array>> member_annotations;

typedef ExpressionNode *(GDScriptParser::*ParseFunction)(ExpressionNode *p_previous_operand, bool p_can_assign);
// Higher value means higher precedence (i.e. is evaluated first).
@@ -1589,6 +1599,8 @@ class GDScriptParser {
CompletionCall get_completion_call() const { return completion_call; }
void get_annotation_list(List<MethodInfo> *r_annotations) const;
bool annotation_exists(const String &p_annotation_name) const;
_FORCE_INLINE_ Array get_class_annotations() const { return class_annotations; }
_FORCE_INLINE_ Vector<Pair<StringName, Array>> get_member_annotations() const { return member_annotations; }

const List<ParserError> &get_errors() const { return errors; }
const List<String> get_dependencies() const {
10 changes: 9 additions & 1 deletion modules/gdscript/gdscript_tokenizer.cpp
Original file line number Diff line number Diff line change
@@ -481,9 +481,17 @@ GDScriptTokenizer::Token GDScriptTokenizerText::check_vcs_marker(char32_t p_test
GDScriptTokenizer::Token GDScriptTokenizerText::annotation() {
if (is_unicode_identifier_start(_peek())) {
_advance(); // Consume start character.
} else if (_peek() == '@') {
_advance(); // Consume the second @ in @@ (user annotations).
if (is_unicode_identifier_start(_peek())) {
_advance(); // Consume start character.
} else {
push_error("Expected annotation identifier after \"@@\".");
}
} else {
push_error("Expected annotation identifier after \"@\".");
push_error("Expected annotation identifier or @ after \"@\".");
}

while (is_unicode_identifier_continue(_peek())) {
// Consume all identifier characters.
_advance();
6 changes: 6 additions & 0 deletions modules/gdscript/register_types.cpp
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@

#include "gdscript.h"
#include "gdscript_cache.h"
#include "gdscript_member_annotations.h"
#include "gdscript_parser.h"
#include "gdscript_tokenizer_buffer.h"
#include "gdscript_utility_functions.h"
@@ -153,6 +154,11 @@ void initialize_gdscript_module(ModuleInitializationLevel p_level) {
gdscript_cache = memnew(GDScriptCache);

GDScriptUtilityFunctions::register_functions();
GDREGISTER_ABSTRACT_CLASS(GDScriptAnnotation);
GDREGISTER_VIRTUAL_CLASS(GDScriptVariableAnnotation);
GDREGISTER_VIRTUAL_CLASS(GDScriptFunctionAnnotation);
GDREGISTER_VIRTUAL_CLASS(GDScriptSignalAnnotation);
GDREGISTER_VIRTUAL_CLASS(GDScriptClassAnnotation);
}

#ifdef TOOLS_ENABLED
47 changes: 43 additions & 4 deletions scene/gui/code_edit.cpp
Original file line number Diff line number Diff line change
@@ -581,6 +581,17 @@ void CodeEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
return;
}
if (k->is_action("ui_text_backspace", true)) {
// If the backspace would break a digraph, we need to re-request code completion.
int cofs = get_caret_column();
if (cofs > 1) {
const String line = get_line(get_caret_line());
if (code_completion_digraph_prefixes.has(*(const uint64_t *)(const char *)(&line[cofs - 2]))) {
backspace();
request_code_completion();
accept_event();
return;
}
}
backspace();
_filter_code_completion_candidates_impl();
accept_event();
@@ -2086,7 +2097,13 @@ void CodeEdit::set_code_completion_prefixes(const TypedArray<String> &p_prefixes
const String prefix = p_prefixes[i];

ERR_CONTINUE_MSG(prefix.is_empty(), "Code completion prefix cannot be empty.");
code_completion_prefixes.insert(prefix[0]);
if (prefix.length() == 1) {
code_completion_prefixes.insert(prefix[0]);
} else if (prefix.length() == 2) {
code_completion_digraph_prefixes.insert(*(const uint64_t *)(const char *)(prefix.ptr()));
} else {
ERR_PRINT("Code completion prefix cannot be more than two characters long.");
}
}
}

@@ -2095,6 +2112,10 @@ TypedArray<String> CodeEdit::get_code_completion_prefixes() const {
for (const char32_t &E : code_completion_prefixes) {
prefixes.push_back(String::chr(E));
}
for (const uint64_t &E : code_completion_digraph_prefixes) {
const char32_t *str = (const char32_t *)(const char *)(&E);
prefixes.push_back(String(str, 2));
}
return prefixes;
}

@@ -2156,7 +2177,11 @@ void CodeEdit::request_code_completion(bool p_force) {
String line = get_line(get_caret_line());
int ofs = CLAMP(get_caret_column(), 0, line.length());

if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || !is_symbol(line[ofs - 1]) || code_completion_prefixes.has(line[ofs - 1]))) {
if (ofs > 1 && (is_in_string(get_caret_line(), ofs) != -1 || !is_symbol(line[ofs - 1]) || !is_symbol(line[ofs - 2]) || code_completion_digraph_prefixes.has(*(const uint64_t *)(const char *)(&line[ofs - 2])))) {
emit_signal(SNAME("code_completion_requested"));
} else if (ofs > 2 && line[ofs - 1] == ' ' && code_completion_digraph_prefixes.has(*(const uint64_t *)(const char *)(&line[ofs - 3]))) {
emit_signal(SNAME("code_completion_requested"));
} else if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || !is_symbol(line[ofs - 1]) || code_completion_prefixes.has(line[ofs - 1]))) {
emit_signal(SNAME("code_completion_requested"));
} else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(line[ofs - 2])) {
emit_signal(SNAME("code_completion_requested"));
@@ -2245,6 +2270,11 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
}

char32_t caret_last_completion_char = 0;
union {
char32_t chars[2];
uint64_t digraph;
} caret_last_completion_digraph;
caret_last_completion_digraph.digraph = 0;
begin_complex_operation();
begin_multicaret_edit();

@@ -2312,6 +2342,9 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
char32_t last_completion_char = insert_text[insert_text.length() - 1];
if (i == 0) {
caret_last_completion_char = last_completion_char;
} else if (i == 1) {
caret_last_completion_digraph.chars[0] = caret_last_completion_char;
caret_last_completion_digraph.chars[1] = last_completion_char;
}
char32_t last_completion_char_display = display_text[display_text.length() - 1];

@@ -2350,7 +2383,9 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
end_complex_operation();

cancel_code_completion();
if (code_completion_prefixes.has(caret_last_completion_char)) {
if (code_completion_digraph_prefixes.has(caret_last_completion_digraph.digraph)) {
request_code_completion();
} else if (code_completion_prefixes.has(caret_last_completion_char)) {
request_code_completion();
}
}
@@ -3512,7 +3547,11 @@ void CodeEdit::_filter_code_completion_candidates_impl() {
/* If all else fails, check for a prefix. */
/* Single space between caret and prefix is okay. */
bool prev_is_prefix = false;
if (cofs > 0 && code_completion_prefixes.has(line[cofs - 1])) {
if (cofs > 1 && code_completion_digraph_prefixes.has(*(const uint64_t *)(const char *)(&line[cofs - 2]))) {
prev_is_prefix = true;
} else if (cofs > 2 && line[cofs - 1] == ' ' && code_completion_digraph_prefixes.has(*(const uint64_t *)(const char *)(&line[cofs - 3]))) {
prev_is_prefix = true;
} else if (cofs > 0 && code_completion_prefixes.has(line[cofs - 1])) {
prev_is_prefix = true;
} else if (cofs > 1 && line[cofs - 1] == ' ' && code_completion_prefixes.has(line[cofs - 2])) {
prev_is_prefix = true;
1 change: 1 addition & 0 deletions scene/gui/code_edit.h
Original file line number Diff line number Diff line change
@@ -220,6 +220,7 @@ class CodeEdit : public TextEdit {
float code_completion_pan_offset = 0.0f;

HashSet<char32_t> code_completion_prefixes;
HashSet<uint64_t> code_completion_digraph_prefixes;
List<ScriptLanguage::CodeCompletionOption> code_completion_option_submitted;
List<ScriptLanguage::CodeCompletionOption> code_completion_option_sources;
String code_completion_base;
6 changes: 4 additions & 2 deletions tests/scene/test_code_edit.h
Original file line number Diff line number Diff line change
@@ -3936,20 +3936,22 @@ TEST_CASE("[SceneTree][CodeEdit] completion") {
code_edit->set_code_completion_enabled(true);
CHECK(code_edit->is_code_completion_enabled());

/* Set prefixes, single char only, disallow empty. */
/* Set prefixes, single char or double chars (digraphs) only, disallow empty. */
TypedArray<String> completion_prefixes;
completion_prefixes.push_back("");
completion_prefixes.push_back(".");
completion_prefixes.push_back(".");
completion_prefixes.push_back(",");
completion_prefixes.push_back(",,");

ERR_PRINT_OFF;
code_edit->set_code_completion_prefixes(completion_prefixes);
ERR_PRINT_ON;
completion_prefixes = code_edit->get_code_completion_prefixes();
CHECK(completion_prefixes.size() == 2);
CHECK(completion_prefixes.size() == 3);
CHECK(completion_prefixes.has("."));
CHECK(completion_prefixes.has(","));
CHECK(completion_prefixes.has(",,"));

code_edit->set_text("test\ntest");
CHECK(code_edit->get_text_for_code_completion() == String::chr(0xFFFF) + "test\ntest");