Skip to content

Commit 1e4ee3d

Browse files
committed
GDScript: Implement @if_features annotation
1 parent 5b38f82 commit 1e4ee3d

19 files changed

+470
-19
lines changed

core/os/os.cpp

+4-1
Original file line numberDiff line numberDiff line change
@@ -441,12 +441,15 @@ bool OS::has_feature(const String &p_feature) {
441441
}
442442
if (p_feature == "editor_hint") {
443443
return _in_editor;
444-
} else if (p_feature == "editor_runtime") {
444+
} else if (p_feature == "editor_runtime" || p_feature == "runtime") {
445445
return !_in_editor;
446446
} else if (p_feature == "embedded_in_editor") {
447447
return _embedded_in_editor;
448448
}
449449
#else
450+
if (p_feature == "runtime") {
451+
return true;
452+
}
450453
if (p_feature == "template") {
451454
return true;
452455
}

editor/export/editor_export_platform.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,7 @@ HashSet<String> EditorExportPlatform::get_features(const Ref<EditorExportPreset>
531531
result.insert(E);
532532
}
533533

534+
result.insert("runtime");
534535
result.insert("template");
535536
if (p_debug) {
536537
result.insert("debug");

modules/gdscript/doc_classes/@GDScript.xml

+11
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,17 @@
730730
[b]Note:[/b] Unlike most other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported).
731731
</description>
732732
</annotation>
733+
<annotation name="@if_features" qualifiers="vararg">
734+
<return type="void" />
735+
<param index="0" name="feature" type="String" default="null" />
736+
<description>
737+
Marks the following function to be taken into account only if all the features passed as arguments are declared by the platform or export preset.
738+
This is meant to be applied to a set of functions with the same name so at runtime you have a single implementation of some feature according to the features supported. All the functions in such a set must have the exact same signature.
739+
If the annotation has no features specified, it's considered a default one as so it's only used if none of the other functions with the same name are proper fits.
740+
At runtime from an editor build, the features are checked at the moment the script is parsed, against what the OS advertises.
741+
At export time, the features are checked against the target platform's OS as well as the list of custom features specified in an export preset. Moreover, [b]the functions not matching the features, are removed from the source code[/b] so the exported project won't contain them at all.
742+
</description>
743+
</annotation>
733744
<annotation name="@onready">
734745
<return type="void" />
735746
<description>

modules/gdscript/gdscript_cache.cpp

+11-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ String GDScriptParserRef::get_path() const {
4646
return path;
4747
}
4848

49+
void GDScriptParserRef::set_path(const String &p_path) {
50+
path = p_path;
51+
}
52+
4953
uint32_t GDScriptParserRef::get_source_hash() const {
5054
return source_hash;
5155
}
@@ -134,9 +138,15 @@ void GDScriptParserRef::clear() {
134138
}
135139

136140
GDScriptParserRef::~GDScriptParserRef() {
141+
#ifdef TOOLS_ENABLED
142+
bool remove_from_map = !parser || !parser->is_for_export();
143+
#else
144+
bool remove_from_map = true;
145+
#endif
146+
137147
clear();
138148

139-
if (!abandoned) {
149+
if (remove_from_map && !abandoned) {
140150
MutexLock lock(GDScriptCache::singleton->mutex);
141151
GDScriptCache::singleton->parser_map.erase(path);
142152
}

modules/gdscript/gdscript_cache.h

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class GDScriptParserRef : public RefCounted {
6767
public:
6868
Status get_status() const;
6969
String get_path() const;
70+
void set_path(const String &p_path);
7071
uint32_t get_source_hash() const;
7172
GDScriptParser *get_parser();
7273
GDScriptAnalyzer *get_analyzer();

modules/gdscript/gdscript_compiler.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -2947,6 +2947,12 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
29472947
const GDScriptParser::ClassNode::Member &member = p_class->members[i];
29482948
if (member.type == member.FUNCTION) {
29492949
const GDScriptParser::FunctionNode *function = member.function;
2950+
#ifdef TOOLS_ENABLED
2951+
// Ignore unfitting @if_features-decorated functions.
2952+
if (function->if_features.potential_candidate_index != -1) {
2953+
continue;
2954+
}
2955+
#endif
29502956
Error err = OK;
29512957
_parse_function(err, p_script, p_class, function);
29522958
if (err) {

modules/gdscript/gdscript_editor.cpp

+30
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "gdscript_utility_functions.h"
3737

3838
#ifdef TOOLS_ENABLED
39+
#include "editor/export/editor_export.h"
3940
#include "editor/gdscript_docgen.h"
4041
#include "editor/script_templates/templates.gen.h"
4142
#endif
@@ -133,6 +134,9 @@ bool GDScriptLanguage::validate(const String &p_script, const String &p_path, Li
133134
GDScriptParser parser;
134135
GDScriptAnalyzer analyzer(&parser);
135136

137+
#ifdef TOOLS_ENABLED
138+
parser.set_for_edition();
139+
#endif
136140
Error err = parser.parse(p_script, p_path, false);
137141
if (err == OK) {
138142
err = analyzer.analyze();
@@ -982,6 +986,26 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a
982986
r_result.insert(option.display, option);
983987
}
984988
}
989+
} else if (p_annotation->name == SNAME("@if_features")) {
990+
#ifdef TOOLS_ENABLED
991+
HashSet<String> features;
992+
for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
993+
const Ref<EditorExportPreset> &preset = EditorExport::get_singleton()->get_export_preset(i);
994+
for (const String &feature : preset->get_custom_features().split(",", false)) {
995+
features.insert(feature.strip_edges());
996+
}
997+
List<String> platform_features;
998+
preset->get_platform()->get_platform_features(&platform_features);
999+
for (const String &feature : platform_features) {
1000+
features.insert(feature.strip_edges());
1001+
}
1002+
}
1003+
for (const String &feature : features) {
1004+
ScriptLanguage::CodeCompletionOption option(feature, ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);
1005+
option.insert_text = option.display.quote(p_quote_style);
1006+
r_result.insert(option.display, option);
1007+
}
1008+
#endif
9851009
}
9861010
}
9871011

@@ -3207,6 +3231,9 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa
32073231
GDScriptParser parser;
32083232
GDScriptAnalyzer analyzer(&parser);
32093233

3234+
#ifdef TOOLS_ENABLED
3235+
parser.set_for_edition();
3236+
#endif
32103237
parser.parse(p_code, p_path, true);
32113238
analyzer.analyze();
32123239

@@ -4023,6 +4050,9 @@ ::Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symb
40234050
}
40244051

40254052
GDScriptParser parser;
4053+
#ifdef TOOLS_ENABLED
4054+
parser.set_for_edition();
4055+
#endif
40264056
parser.parse(p_code, p_path, true);
40274057

40284058
GDScriptParser::CompletionContext context = parser.get_completion_context();

modules/gdscript/gdscript_parser.cpp

+157-7
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ GDScriptParser::GDScriptParser() {
132132
register_annotation(MethodInfo("@warning_ignore_restore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::STANDALONE, &GDScriptParser::warning_ignore_region_annotations, varray(), true);
133133
// Networking.
134134
register_annotation(MethodInfo("@rpc", PropertyInfo(Variant::STRING, "mode"), PropertyInfo(Variant::STRING, "sync"), PropertyInfo(Variant::STRING, "transfer_mode"), PropertyInfo(Variant::INT, "transfer_channel")), AnnotationInfo::FUNCTION, &GDScriptParser::rpc_annotation, varray("authority", "call_remote", "unreliable", 0));
135+
// Preprocessing.
136+
register_annotation(MethodInfo("@if_features", PropertyInfo(Variant::STRING, "feature")), AnnotationInfo::FUNCTION, &GDScriptParser::if_features_annotation, varray(Variant()), true);
135137
}
136138

137139
#ifdef DEBUG_ENABLED
@@ -944,11 +946,22 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
944946

945947
// Consume annotations.
946948
List<AnnotationNode *> annotations;
949+
#ifdef TOOLS_ENABLED
950+
constexpr bool parsing_function = std::is_same_v<T, FunctionNode>;
951+
AnnotationNode *if_features = nullptr;
952+
#endif
947953
while (!annotation_stack.is_empty()) {
948954
AnnotationNode *last_annotation = annotation_stack.back()->get();
949955
if (last_annotation->applies_to(p_target)) {
950956
annotations.push_front(last_annotation);
951957
annotation_stack.pop_back();
958+
#ifdef TOOLS_ENABLED
959+
if constexpr (parsing_function) {
960+
if (last_annotation->name == StringName("@if_features")) {
961+
if_features = last_annotation;
962+
}
963+
}
964+
#endif
952965
} else {
953966
push_error(vformat(R"(Annotation "%s" cannot be applied to a %s.)", last_annotation->name, p_member_kind));
954967
clear_unused_annotations();
@@ -994,6 +1007,32 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
9941007
}
9951008

9961009
min_member_doc_line = member->end_line + 1; // Prevent multiple members from using the same doc comment.
1010+
1011+
if constexpr (parsing_function) {
1012+
if (if_features) {
1013+
// Mark this one as a default implementation if the annotation provides no features.
1014+
member->if_features.is_default_impl = if_features->arguments.is_empty();
1015+
1016+
// Let the first default one with of same name fully in.
1017+
// The others are added so they are parsed, but not indexed so name clash error is avoided.
1018+
HashMap<StringName, int>::Iterator E = current_class->members_indices.find(member->identifier->name);
1019+
if (E) {
1020+
// Member with that name already exists.
1021+
const ClassNode::Member &existing = current_class->members[E->value];
1022+
if (existing.type == ClassNode::Member::FUNCTION) { // Otherwise, an error is raised anyway.
1023+
// HACK: Compare compatibility of functions via TreePrinter.
1024+
const String &existing_str = TreePrinter().strinfigy_function_declaration(existing.function);
1025+
const String &incoming_str = TreePrinter().strinfigy_function_declaration(member);
1026+
if (existing_str != incoming_str) {
1027+
push_error(vformat(R"(%s "%s" does not match the signature of a previously declared function of the same name.)", p_member_kind.capitalize(), member->identifier->name), member->identifier);
1028+
} else {
1029+
current_class->add_if_features_potential_candidate(member);
1030+
}
1031+
return;
1032+
}
1033+
}
1034+
}
1035+
}
9971036
#endif // TOOLS_ENABLED
9981037

9991038
if (member->identifier != nullptr) {
@@ -4975,6 +5014,108 @@ bool GDScriptParser::warning_ignore_region_annotations(AnnotationNode *p_annotat
49755014
#endif // DEBUG_ENABLED
49765015
}
49775016

5017+
bool GDScriptParser::if_features_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
5018+
#if defined(TOOLS_ENABLED)
5019+
ERR_FAIL_COND_V_MSG(p_target->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name));
5020+
5021+
FunctionNode *function = static_cast<FunctionNode *>(p_target);
5022+
if (function->if_features.used) {
5023+
push_error("The @if_features annotation can only be used once per function.");
5024+
return false;
5025+
}
5026+
5027+
function->if_features.used = true;
5028+
5029+
thread_local LocalVector<String> features;
5030+
features.clear();
5031+
for (const ExpressionNode *arg : p_annotation->arguments) {
5032+
DEV_ASSERT(arg->reduced);
5033+
if (arg->reduced_value.get_type() == Variant::STRING) {
5034+
features.push_back(arg->reduced_value);
5035+
} else {
5036+
push_error("The arguments to @if_features must be strings.");
5037+
return false;
5038+
}
5039+
}
5040+
5041+
// There's nothing to process unless we're exporting or running in the editor.
5042+
// Sadly, we can't tell for sure if the script is being run in the editor or just being edited.
5043+
// We only know for sure it's the latter if this parsing is done for completion reasons.
5044+
if (for_edition) {
5045+
return true;
5046+
}
5047+
5048+
// The idea is to keep the first one fitting, with only the default possibly overridden by another one coming later.
5049+
5050+
FunctionNode *target_function = static_cast<FunctionNode *>(p_target);
5051+
const StringName &function_name = target_function->identifier->name;
5052+
5053+
auto _check_incoming_fits = [&]() -> bool {
5054+
if (features.is_empty()) {
5055+
return false; // Defaults are considered non-fitting.
5056+
}
5057+
bool fitting = true;
5058+
for (const String &feature : features) {
5059+
if (for_export) {
5060+
// Export time in editor build.
5061+
if (!export_features.has(feature)) {
5062+
fitting = false;
5063+
break;
5064+
}
5065+
} else {
5066+
// Run-on-editor.
5067+
if (!OS::get_singleton()->has_feature(feature)) {
5068+
fitting = false;
5069+
break;
5070+
}
5071+
}
5072+
}
5073+
return fitting;
5074+
};
5075+
5076+
if (target_function->if_features.potential_candidate_index == -1) {
5077+
// Chosen one at parsing time because it was the first one found. Only keep if fitting.
5078+
if (!_check_incoming_fits()) {
5079+
HashMap<StringName, int>::Iterator E = p_class->members_indices.find(function_name);
5080+
int64_t current_match_index = E->value;
5081+
target_function->if_features.potential_candidate_index = current_match_index;
5082+
p_class->members_indices.remove(E);
5083+
}
5084+
} else {
5085+
HashMap<StringName, int>::Iterator E = p_class->members_indices.find(function_name);
5086+
if ((bool)E) {
5087+
// There's a current one. Override if current is default and incoming fits.
5088+
bool current_match_is_default = p_class->members[E->value].function->if_features.is_default_impl;
5089+
if (current_match_is_default && _check_incoming_fits()) {
5090+
E->value = target_function->if_features.potential_candidate_index;
5091+
target_function->if_features.potential_candidate_index = -1;
5092+
}
5093+
} else {
5094+
// Incoming fits and there's no current chosen. Pick if it's default or fits.
5095+
if (target_function->if_features.is_default_impl || _check_incoming_fits()) {
5096+
p_class->members_indices.insert(function_name, target_function->if_features.potential_candidate_index);
5097+
target_function->if_features.potential_candidate_index = -1;
5098+
}
5099+
}
5100+
}
5101+
#endif
5102+
5103+
return true;
5104+
}
5105+
5106+
#ifdef TOOLS_ENABLED
5107+
void GDScriptParser::collect_unfitting_functions(ClassNode *p_class, LocalVector<Pair<ClassNode *, FunctionNode *>> &r_functions) {
5108+
for (const ClassNode::Member &member : p_class->members) {
5109+
if (member.type == ClassNode::Member::CLASS) {
5110+
collect_unfitting_functions(member.m_class, r_functions);
5111+
}
5112+
if (member.type == ClassNode::Member::FUNCTION && member.function->if_features.potential_candidate_index != -1) {
5113+
r_functions.push_back(Pair(p_class, member.function));
5114+
}
5115+
}
5116+
}
5117+
#endif
5118+
49785119
bool GDScriptParser::rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
49795120
ERR_FAIL_COND_V_MSG(p_target->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name));
49805121

@@ -5833,9 +5974,11 @@ void GDScriptParser::TreePrinter::print_for(ForNode *p_for) {
58335974
decrease_indent();
58345975
}
58355976

5836-
void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const String &p_context) {
5837-
for (const AnnotationNode *E : p_function->annotations) {
5838-
print_annotation(E);
5977+
void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const String &p_context, bool p_signature_only) {
5978+
if (!p_signature_only) {
5979+
for (const AnnotationNode *E : p_function->annotations) {
5980+
print_annotation(E);
5981+
}
58395982
}
58405983
if (p_function->is_static) {
58415984
push_text("Static ");
@@ -5859,10 +6002,12 @@ void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const
58596002
push_text("-> ");
58606003
print_type(p_function->return_type);
58616004
}
5862-
push_line(" :");
5863-
increase_indent();
5864-
print_suite(p_function->body);
5865-
decrease_indent();
6005+
if (!p_signature_only) {
6006+
push_line(" :");
6007+
increase_indent();
6008+
print_suite(p_function->body);
6009+
decrease_indent();
6010+
}
58666011
}
58676012

58686013
void GDScriptParser::TreePrinter::print_get_node(GetNodeNode *p_get_node) {
@@ -6283,4 +6428,9 @@ void GDScriptParser::TreePrinter::print_tree(const GDScriptParser &p_parser) {
62836428
print_line(String(printed));
62846429
}
62856430

6431+
String GDScriptParser::TreePrinter::strinfigy_function_declaration(FunctionNode *p_function) {
6432+
print_function(p_function, "Function", true);
6433+
return String(printed);
6434+
}
6435+
62866436
#endif // DEBUG_ENABLED

0 commit comments

Comments
 (0)