Skip to content

Commit a1bd733

Browse files
Add an @OverRide annotation for enforcing proper override semantics
1 parent 825ef23 commit a1bd733

32 files changed

+270
-44
lines changed

core/object/script_language.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@ class ScriptLanguage : public Object {
318318
int location = LOCATION_OTHER;
319319
String theme_color_name;
320320

321+
/**
322+
* @brief A callback that is executed after this completion option is applied by the code editor (in confirm_code_completion).
323+
* This callback receives both the CodeEdit instance and the current caret that was used for the application.
324+
* For multi-line applications, this callback may be invoked multiple times with different caret values.
325+
*/
326+
Callable on_applied;
327+
321328
CodeCompletionOption() {}
322329

323330
CodeCompletionOption(const String &p_text, CodeCompletionKind p_kind, int p_location = LOCATION_OTHER, const String &p_theme_color_name = "") {

doc/classes/CodeEdit.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
<param index="4" name="icon" type="Resource" default="null" />
5151
<param index="5" name="value" type="Variant" default="null" />
5252
<param index="6" name="location" type="int" default="1024" />
53+
<param index="7" name="on_applied" type="Callable" default="Callable()" />
5354
<description>
5455
Submits an item to the queue of potential candidates for the autocomplete menu. Call [method update_code_completion_options] to update the list.
5556
[param location] indicates location of the option relative to the location of the code completion query. See [enum CodeEdit.CodeCompletionLocation] for how to set this value.

editor/gui/code_editor.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,7 @@ void CodeTextEditor::_complete_request() {
10471047
} else if (e.insert_text.begins_with("#") || e.insert_text.begins_with("//")) {
10481048
font_color = completion_comment_color;
10491049
}
1050-
text_editor->add_code_completion_option((CodeEdit::CodeCompletionKind)e.kind, e.display, e.insert_text, font_color, _get_completion_icon(e), e.default_value, e.location);
1050+
text_editor->add_code_completion_option((CodeEdit::CodeCompletionKind)e.kind, e.display, e.insert_text, font_color, _get_completion_icon(e), e.default_value, e.location, e.on_applied);
10511051
}
10521052
text_editor->update_code_completion_options(forced);
10531053
}

modules/gdscript/doc_classes/@GDScript.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,28 @@
782782
[/codeblock]
783783
</description>
784784
</annotation>
785+
<annotation name="@override">
786+
<return type="void" />
787+
<description>
788+
Marks a function as being an override. Override functions must override a function defined in a parent class, otherwise an error is raised.
789+
Functions that override methods but do not have this annotation will raise a warning.
790+
[codeblock]
791+
class Shape:
792+
func draw()
793+
794+
class Circle extends Shape:
795+
@override func draw(): # No error, no warnings
796+
print("Drawing a circle.")
797+
798+
class Square extends Shape:
799+
func draw(): # Warning due to missing @override
800+
print("Drawing a square.")
801+
802+
@override func junk(): # Error as no parent defines junk()
803+
pass
804+
[/codeblock]
805+
</description>
806+
</annotation>
785807
<annotation name="@rpc">
786808
<return type="void" />
787809
<param index="0" name="mode" type="String" default="&quot;authority&quot;" />

modules/gdscript/gdscript_analyzer.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,12 +1954,52 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
19541954

19551955
push_error(vformat(R"(The function signature doesn't match the parent. Parent signature is "%s".)", parent_signature), p_function);
19561956
}
1957+
1958+
// Mark the function as an override if the check succeeds.
1959+
p_function->is_override = true;
1960+
1961+
// Then if we don't see the @override annotation, raise a warning.
1962+
if (!p_function->has_override_annot) {
1963+
// Resolve the name of the base class that owns the overridden for error reporting
1964+
StringName base_class_name{ "**UnknownNativeClass**" };
1965+
1966+
const GDScriptParser::DataType *cur_base_type = &base_type;
1967+
while (cur_base_type) {
1968+
if (cur_base_type->kind == GDScriptParser::DataType::Kind::NATIVE) {
1969+
MethodInfo method_info;
1970+
if (ClassDB::get_method_info(cur_base_type->native_type, function_name, &method_info)) {
1971+
base_class_name = cur_base_type->native_type;
1972+
}
1973+
break; // Break unconditionally since this method should always give us the appropriate native class name.
1974+
} else {
1975+
const GDScriptParser::ClassNode *base_class = base_type.class_type;
1976+
if (base_class->has_function(function_name)) {
1977+
if (base_class->identifier) {
1978+
base_class_name = base_class->identifier->name;
1979+
} else {
1980+
base_class_name = "**UnidentifiedClass**";
1981+
}
1982+
break;
1983+
}
1984+
cur_base_type = &base_class->base_type;
1985+
}
1986+
}
1987+
1988+
parser->push_warning(p_function, GDScriptWarning::IMPLICIT_FUNCTION_OVERRIDE, function_name, base_class_name);
1989+
}
1990+
19571991
#ifdef DEBUG_ENABLED
19581992
if (native_base != StringName()) {
19591993
parser->push_warning(p_function, GDScriptWarning::NATIVE_METHOD_OVERRIDE, function_name, native_base);
19601994
}
19611995
#endif // DEBUG_ENABLED
19621996
}
1997+
1998+
// If a function with `@override` doesn't override anything, raise an error.
1999+
if (p_function->has_override_annot && !p_function->is_override) {
2000+
push_error(vformat(R"(The function %s has the @override annotation, but does not override anything.)", function_name), p_function);
2001+
}
2002+
19632003
#endif // TOOLS_ENABLED
19642004
}
19652005

modules/gdscript/gdscript_analyzer.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,24 @@ class GDScriptAnalyzer {
128128
GDScriptParser::DataType type_from_variant(const Variant &p_value, const GDScriptParser::Node *p_source);
129129
GDScriptParser::DataType type_from_property(const PropertyInfo &p_property, bool p_is_arg = false, bool p_is_readonly = false) const;
130130
GDScriptParser::DataType make_global_class_meta_type(const StringName &p_class_name, const GDScriptParser::Node *p_source);
131+
132+
/**
133+
* @brief Attempts to look up the function signature for a function based on name and base type.
134+
*
135+
* @param[in] p_source The source node in context of which the signature is being resolved. Used for error reporting.
136+
* @param[in] p_is_constructor If the target function call is a constructor. In this case `p_function` is always "new".
137+
* @param[in] base_type The target type to resolve the function on. If looking for a function `SomeClass.a_function()`, this is `SomeClass`.
138+
* @param[in] p_function The name of the target function.
139+
* @param[out] r_return_type The return type of the found function.
140+
* @param[out] r_par_types A list of the parameters types of the found function, in order from left to right.
141+
* @param[out] r_default_arg_count The number of parameters which have default values.
142+
* @param[out] r_method_flags The flags of the found function.
143+
* @param[out] r_native_class If DEBUG_ENABLED=1, and the found function is a native method, the string name of the owning native class. Otherwise an empty string name.
144+
*
145+
* @return True if the function signature was successfully resolved, false otherwise. See the remaining output variables for other data.
146+
*/
131147
bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, BitField<MethodFlags> &r_method_flags, StringName *r_native_class = nullptr);
148+
132149
bool function_signature_from_info(const MethodInfo &p_info, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, BitField<MethodFlags> &r_method_flags);
133150
void validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call);
134151
void validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call);

modules/gdscript/gdscript_editor.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
#include "editor/editor_string_names.h"
5353
#include "editor/file_system/editor_file_system.h"
5454
#include "editor/settings/editor_settings.h"
55+
#include "scene/gui/code_edit.h"
5556
#endif
5657

5758
Vector<String> GDScriptLanguage::get_comment_delimiters() const {
@@ -3443,6 +3444,55 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
34433444
r_forced = r_result.size() > 0;
34443445
}
34453446

3447+
namespace {
3448+
3449+
/**
3450+
* @brief Used as the `on_applied` callback for GDScript completions of category `COMPLETION_OVERRIDE_METHOD`.
3451+
* This method will attempt to automatically apply the `@override` annotation when using completion to generate an override.
3452+
* If the `@override` annotation is already present, nothing will occur.
3453+
*
3454+
* @param code_edit The code editor instance.
3455+
* @param caret The caret that was used for this application of the completion.
3456+
*/
3457+
void _update_overrides_after_completion(CodeEdit *code_edit, int caret) {
3458+
int line = code_edit->get_caret_line(caret);
3459+
// Backtrack in the current line and upwards a few lines to search for `@override`. If we don't see it, apply it inline before the `func` keyword.
3460+
bool has_override_annot = false;
3461+
int lines_traversed = 0;
3462+
int func_line = -1; // Generally speaking we expect the line with `func` to be the current line, but for sanity we'll search for it anyway.
3463+
3464+
while (!has_override_annot && lines_traversed < 10 && line >= 0) {
3465+
String text = code_edit->get_line(line);
3466+
3467+
if (text.contains("func")) {
3468+
if (func_line == -1) {
3469+
func_line = line;
3470+
} else {
3471+
break; // We've hit another `func` decl. Anything in or above this line doesn't apply to this function anymore.
3472+
}
3473+
}
3474+
3475+
if (text.contains("@override")) {
3476+
has_override_annot = true;
3477+
break;
3478+
}
3479+
3480+
line--;
3481+
lines_traversed++;
3482+
}
3483+
3484+
// TODO: Review stage - I'd prefer to insert the `@override` on the line before `func`, but I'm not sure how to do that.
3485+
if (!has_override_annot && func_line != -1) {
3486+
String text = code_edit->get_line(func_line);
3487+
code_edit->set_line(func_line, "@override " + text);
3488+
int caret_col = code_edit->get_caret_column(caret);
3489+
code_edit->set_caret_line(func_line, true, true, 0, caret);
3490+
code_edit->set_caret_column(caret_col + strlen("@override "), false, caret);
3491+
}
3492+
}
3493+
3494+
} // namespace
3495+
34463496
::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path, Object *p_owner, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_forced, String &r_call_hint) {
34473497
const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\"";
34483498

@@ -3704,6 +3754,10 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa
37043754
String display_name = member.function->identifier->name;
37053755
display_name += member.function->signature + ":";
37063756
ScriptLanguage::CodeCompletionOption option(display_name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION);
3757+
3758+
// When inserting a completion for a function override, we want to automatically add the @override annotation to the completion.
3759+
option.on_applied = callable_mp_static(_update_overrides_after_completion);
3760+
37073761
options.insert(member.function->identifier->name, option); // Insert name instead of display to track duplicates.
37083762
}
37093763
native_type = native_type.class_type->base_type;
@@ -3778,6 +3832,10 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa
37783832
method_hint += ":";
37793833

37803834
ScriptLanguage::CodeCompletionOption option(method_hint, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION);
3835+
3836+
// When inserting a completion for a function override, we want to automatically add the @override annotation to the completion.
3837+
option.on_applied = callable_mp_static(_update_overrides_after_completion);
3838+
37813839
options.insert(option.display, option);
37823840
}
37833841
} break;

modules/gdscript/gdscript_parser.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ GDScriptParser::GDScriptParser() {
9797
register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation);
9898
register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);
9999
register_annotation(MethodInfo("@abstract"), AnnotationInfo::SCRIPT | AnnotationInfo::CLASS | AnnotationInfo::FUNCTION, &GDScriptParser::abstract_annotation);
100+
register_annotation(MethodInfo("@override"), AnnotationInfo::FUNCTION, &GDScriptParser::override_annotation);
100101
// Onready annotation.
101102
register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation);
102103
// Export annotations.
@@ -4397,6 +4398,20 @@ bool GDScriptParser::abstract_annotation(AnnotationNode *p_annotation, Node *p_t
43974398
ERR_FAIL_V_MSG(false, R"("@abstract" annotation can only be applied to classes and functions.)");
43984399
}
43994400

4401+
bool GDScriptParser::override_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
4402+
if (p_target->type == Node::FUNCTION) {
4403+
FunctionNode *function_node = static_cast<FunctionNode *>(p_target);
4404+
if (function_node->is_static) {
4405+
push_error(R"("@override" annotation cannot be applied to static functions.)", p_annotation);
4406+
return false;
4407+
}
4408+
// The only thing we can do here is record that we have the annotation. The analyzer gets to do the rest.
4409+
function_node->has_override_annot = true;
4410+
return true;
4411+
}
4412+
ERR_FAIL_V_MSG(false, R"("@override" annotation can only be applied to functions.)");
4413+
}
4414+
44004415
bool GDScriptParser::onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
44014416
ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)");
44024417

modules/gdscript/gdscript_parser.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,15 @@ class GDScriptParser {
857857
bool is_abstract = false;
858858
bool is_static = false; // For lambdas it's determined in the analyzer.
859859
bool is_coroutine = false;
860+
861+
// If this function is known to override a function in the parent type.
862+
// If a function is an override, but does not have the @override annotation, a warning is raised.
863+
// The value of this field is undefined unless resolved_signature is true.
864+
bool is_override = false;
865+
866+
// If this function node has the @override annotation. This does not indicate if the function is actually an override or not.
867+
bool has_override_annot = false;
868+
860869
Variant rpc_config;
861870
MethodInfo info;
862871
LambdaNode *source_lambda = nullptr;
@@ -867,6 +876,7 @@ class GDScriptParser {
867876
String signature; // For autocompletion.
868877
#endif // TOOLS_ENABLED
869878

879+
// True if GDScriptAnalyzer::resolve_function_signature has been called for this function node.
870880
bool resolved_signature = false;
871881
bool resolved_body = false;
872882

@@ -1529,6 +1539,7 @@ class GDScriptParser {
15291539
bool icon_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15301540
bool static_unload_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15311541
bool abstract_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
1542+
bool override_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15321543
bool onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15331544
template <PropertyHint t_hint, Variant::Type t_type>
15341545
bool export_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);

modules/gdscript/gdscript_warning.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ String GDScriptWarning::get_message() const {
162162
return vformat(R"*(The default value uses "%s" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this.)*", symbols[0]);
163163
case ONREADY_WITH_EXPORT:
164164
return R"("@onready" will set the default value after "@export" takes effect and will override it.)";
165+
case IMPLICIT_FUNCTION_OVERRIDE:
166+
CHECK_SYMBOLS(2);
167+
return vformat(R"*(The method "%s()" overrides "%s::%s()" but does not have the @override annotation.)*", symbols[0], symbols[1], symbols[0]);
165168
#ifndef DISABLE_DEPRECATED
166169
// Never produced. These warnings migrated from 3.x by mistake.
167170
case PROPERTY_USED_AS_FUNCTION: // There is already an error.
@@ -238,6 +241,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) {
238241
PNAME("NATIVE_METHOD_OVERRIDE"),
239242
PNAME("GET_NODE_DEFAULT_WITHOUT_ONREADY"),
240243
PNAME("ONREADY_WITH_EXPORT"),
244+
PNAME("IMPLICIT_FUNCTION_OVERRIDE"),
241245
#ifndef DISABLE_DEPRECATED
242246
"PROPERTY_USED_AS_FUNCTION",
243247
"CONSTANT_USED_AS_FUNCTION",

0 commit comments

Comments
 (0)