Skip to content

Commit e7c0baa

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

17 files changed

+452
-18
lines changed

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

+8
Original file line numberDiff line numberDiff line change
@@ -2947,6 +2947,14 @@ 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+
if (!Engine::get_singleton()->is_editor_hint()) {
2952+
// Runtime in editor build. Ignore @if_features-decorated function if unfitting.
2953+
if (function->if_features.potential_candidate_index != -1) {
2954+
continue;
2955+
}
2956+
}
2957+
#endif
29502958
Error err = OK;
29512959
_parse_function(err, p_script, p_class, function);
29522960
if (err) {

modules/gdscript/gdscript_editor.cpp

+21
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
@@ -982,6 +983,26 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a
982983
r_result.insert(option.display, option);
983984
}
984985
}
986+
} else if (p_annotation->name == SNAME("@if_features")) {
987+
#ifdef TOOLS_ENABLED
988+
HashSet<String> features;
989+
for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
990+
const Ref<EditorExportPreset> &preset = EditorExport::get_singleton()->get_export_preset(i);
991+
for (const String &feature : preset->get_custom_features().split(",", false)) {
992+
features.insert(feature.strip_edges());
993+
}
994+
List<String> platform_features;
995+
preset->get_platform()->get_platform_features(&platform_features);
996+
for (const String &feature : platform_features) {
997+
features.insert(feature.strip_edges());
998+
}
999+
}
1000+
for (const String &feature : features) {
1001+
ScriptLanguage::CodeCompletionOption option(feature, ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);
1002+
option.insert_text = option.display.quote(p_quote_style);
1003+
r_result.insert(option.display, option);
1004+
}
1005+
#endif
9851006
}
9861007
}
9871008

modules/gdscript/gdscript_parser.cpp

+155-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,106 @@ 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+
if (Engine::get_singleton()->is_editor_hint() && !for_export) {
5042+
// At edit time there's nothing else to process.
5043+
return true;
5044+
}
5045+
5046+
// The idea is to keep the first one fitting, with only the default possibly overridden by another one coming later.
5047+
5048+
FunctionNode *target_function = static_cast<FunctionNode *>(p_target);
5049+
const StringName &function_name = target_function->identifier->name;
5050+
5051+
auto _check_incoming_fits = [&]() -> bool {
5052+
if (features.is_empty()) {
5053+
return false; // Defaults are considered non-fitting.
5054+
}
5055+
bool fitting = true;
5056+
for (const String &feature : features) {
5057+
if (for_export) {
5058+
// Export time in editor build.
5059+
if (!export_features.has(feature)) {
5060+
fitting = false;
5061+
break;
5062+
}
5063+
} else {
5064+
// Runtime in editor build.
5065+
if (!OS::get_singleton()->has_feature(feature)) {
5066+
fitting = false;
5067+
break;
5068+
}
5069+
}
5070+
}
5071+
return fitting;
5072+
};
5073+
5074+
if (target_function->if_features.potential_candidate_index == -1) {
5075+
// Chosen one at parsing time because it was the first one found. Only keep if fitting.
5076+
if (!_check_incoming_fits()) {
5077+
HashMap<StringName, int>::Iterator E = p_class->members_indices.find(function_name);
5078+
int64_t current_match_index = E->value;
5079+
target_function->if_features.potential_candidate_index = current_match_index;
5080+
p_class->members_indices.remove(E);
5081+
}
5082+
} else {
5083+
HashMap<StringName, int>::Iterator E = p_class->members_indices.find(function_name);
5084+
if ((bool)E) {
5085+
// There's a current one. Override if current is default and incoming fits.
5086+
bool current_match_is_default = p_class->members[E->value].function->if_features.is_default_impl;
5087+
if (current_match_is_default && _check_incoming_fits()) {
5088+
E->value = target_function->if_features.potential_candidate_index;
5089+
target_function->if_features.potential_candidate_index = -1;
5090+
}
5091+
} else {
5092+
// Incoming fits and there's no current chosen. Pick if it's default or fits.
5093+
if (target_function->if_features.is_default_impl || _check_incoming_fits()) {
5094+
p_class->members_indices.insert(function_name, target_function->if_features.potential_candidate_index);
5095+
target_function->if_features.potential_candidate_index = -1;
5096+
}
5097+
}
5098+
}
5099+
#endif
5100+
5101+
return true;
5102+
}
5103+
5104+
#ifdef TOOLS_ENABLED
5105+
void GDScriptParser::collect_unfitting_functions(ClassNode *p_class, LocalVector<Pair<ClassNode *, FunctionNode *>> &r_functions) {
5106+
for (const ClassNode::Member &member : p_class->members) {
5107+
if (member.type == ClassNode::Member::CLASS) {
5108+
collect_unfitting_functions(member.m_class, r_functions);
5109+
}
5110+
if (member.type == ClassNode::Member::FUNCTION && member.function->if_features.potential_candidate_index != -1) {
5111+
r_functions.push_back(Pair(p_class, member.function));
5112+
}
5113+
}
5114+
}
5115+
#endif
5116+
49785117
bool GDScriptParser::rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
49795118
ERR_FAIL_COND_V_MSG(p_target->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name));
49805119

@@ -5833,9 +5972,11 @@ void GDScriptParser::TreePrinter::print_for(ForNode *p_for) {
58335972
decrease_indent();
58345973
}
58355974

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);
5975+
void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const String &p_context, bool p_signature_only) {
5976+
if (!p_signature_only) {
5977+
for (const AnnotationNode *E : p_function->annotations) {
5978+
print_annotation(E);
5979+
}
58395980
}
58405981
if (p_function->is_static) {
58415982
push_text("Static ");
@@ -5859,10 +6000,12 @@ void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const
58596000
push_text("-> ");
58606001
print_type(p_function->return_type);
58616002
}
5862-
push_line(" :");
5863-
increase_indent();
5864-
print_suite(p_function->body);
5865-
decrease_indent();
6003+
if (!p_signature_only) {
6004+
push_line(" :");
6005+
increase_indent();
6006+
print_suite(p_function->body);
6007+
decrease_indent();
6008+
}
58666009
}
58676010

58686011
void GDScriptParser::TreePrinter::print_get_node(GetNodeNode *p_get_node) {
@@ -6283,4 +6426,9 @@ void GDScriptParser::TreePrinter::print_tree(const GDScriptParser &p_parser) {
62836426
print_line(String(printed));
62846427
}
62856428

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

modules/gdscript/gdscript_parser.h

+29-1
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,13 @@ class GDScriptParser {
785785
members_indices[p_member_node->identifier->name] = members.size();
786786
members.push_back(Member(p_member_node));
787787
}
788+
#ifdef TOOLS_ENABLED
789+
template <typename T>
790+
void add_if_features_potential_candidate(T *p_member_node) {
791+
p_member_node->if_features.potential_candidate_index = members.size();
792+
members.push_back(Member(p_member_node));
793+
}
794+
#endif
788795
void add_member(const EnumNode::Value &p_enum_value) {
789796
members_indices[p_enum_value.identifier->name] = members.size();
790797
members.push_back(Member(p_enum_value));
@@ -862,6 +869,12 @@ class GDScriptParser {
862869
#ifdef TOOLS_ENABLED
863870
MemberDocData doc_data;
864871
int min_local_doc_line = 0;
872+
struct {
873+
bool used = false;
874+
bool is_default_impl = false;
875+
bool fitting_verified = false;
876+
int potential_candidate_index = -1;
877+
} if_features;
865878
#endif // TOOLS_ENABLED
866879

867880
bool resolved_signature = false;
@@ -1335,6 +1348,10 @@ class GDScriptParser {
13351348
bool _is_tool = false;
13361349
String script_path;
13371350
bool for_completion = false;
1351+
#ifdef TOOLS_ENABLED
1352+
bool for_export = false;
1353+
HashSet<String> export_features;
1354+
#endif
13381355
bool parse_body = true;
13391356
bool panic_mode = false;
13401357
bool can_break = false;
@@ -1519,6 +1536,7 @@ class GDScriptParser {
15191536
bool warning_ignore_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15201537
bool warning_ignore_region_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15211538
bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
1539+
bool if_features_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15221540
// Statements.
15231541
Node *parse_statement();
15241542
VariableNode *parse_variable(bool p_is_static);
@@ -1590,6 +1608,15 @@ class GDScriptParser {
15901608
void get_annotation_list(List<MethodInfo> *r_annotations) const;
15911609
bool annotation_exists(const String &p_annotation_name) const;
15921610

1611+
#ifdef TOOLS_ENABLED
1612+
bool is_for_export() const { return for_export; }
1613+
void set_export_features(const HashSet<String> &p_features) {
1614+
for_export = true;
1615+
export_features = p_features;
1616+
}
1617+
void collect_unfitting_functions(ClassNode *p_class, LocalVector<Pair<ClassNode *, FunctionNode *>> &r_functions);
1618+
#endif
1619+
15931620
const List<ParserError> &get_errors() const { return errors; }
15941621
const List<String> get_dependencies() const {
15951622
// TODO: Keep track of deps.
@@ -1636,7 +1663,7 @@ class GDScriptParser {
16361663
void print_expression(ExpressionNode *p_expression);
16371664
void print_enum(EnumNode *p_enum);
16381665
void print_for(ForNode *p_for);
1639-
void print_function(FunctionNode *p_function, const String &p_context = "Function");
1666+
void print_function(FunctionNode *p_function, const String &p_context = "Function", bool p_signature_only = false);
16401667
void print_get_node(GetNodeNode *p_get_node);
16411668
void print_if(IfNode *p_if, bool p_is_elif = false);
16421669
void print_identifier(IdentifierNode *p_identifier);
@@ -1662,6 +1689,7 @@ class GDScriptParser {
16621689

16631690
public:
16641691
void print_tree(const GDScriptParser &p_parser);
1692+
String strinfigy_function_declaration(FunctionNode *p_function);
16651693
};
16661694
#endif // DEBUG_ENABLED
16671695
static void cleanup();

0 commit comments

Comments
 (0)