Skip to content

Commit

Permalink
GDScript: Implement @if_features annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
RandomShaper committed Jan 27, 2025
1 parent 1b7b009 commit 9c1483a
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 10 deletions.
11 changes: 11 additions & 0 deletions modules/gdscript/doc_classes/@GDScript.xml
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,17 @@
[b]Note:[/b] Unlike most other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported).
</description>
</annotation>
<annotation name="@if_features" qualifiers="vararg">
<return type="void" />
<param index="0" name="feature" type="String" default="null" />
<description>
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.
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.
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.
At runtime from an editor build, the features are checked at the moment the script is parsed, against what the OS advertises.
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.
</description>
</annotation>
<annotation name="@onready">
<return type="void" />
<description>
Expand Down
12 changes: 11 additions & 1 deletion modules/gdscript/gdscript_cache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ String GDScriptParserRef::get_path() const {
return path;
}

void GDScriptParserRef::set_path(const String &p_path) {
path = p_path;
}

uint32_t GDScriptParserRef::get_source_hash() const {
return source_hash;
}
Expand Down Expand Up @@ -134,9 +138,15 @@ void GDScriptParserRef::clear() {
}

GDScriptParserRef::~GDScriptParserRef() {
#ifdef TOOLS_ENABLED
bool remove_from_map = !parser || !parser->is_for_export();
#else
bool remove_from_map = true;
#endif

clear();

if (!abandoned) {
if (remove_from_map && !abandoned) {
MutexLock lock(GDScriptCache::singleton->mutex);
GDScriptCache::singleton->parser_map.erase(path);
}
Expand Down
1 change: 1 addition & 0 deletions modules/gdscript/gdscript_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class GDScriptParserRef : public RefCounted {
public:
Status get_status() const;
String get_path() const;
void set_path(const String &p_path);
uint32_t get_source_hash() const;
GDScriptParser *get_parser();
GDScriptAnalyzer *get_analyzer();
Expand Down
8 changes: 8 additions & 0 deletions modules/gdscript/gdscript_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2947,6 +2947,14 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
const GDScriptParser::ClassNode::Member &member = p_class->members[i];
if (member.type == member.FUNCTION) {
const GDScriptParser::FunctionNode *function = member.function;
#ifdef TOOLS_ENABLED
if (!Engine::get_singleton()->is_editor_hint()) {
// Runtime in editor build. Ignore @if_features-decorated function if unfitting.
if (function->if_features.potential_candidate_index != -1) {
continue;
}
}
#endif
Error err = OK;
_parse_function(err, p_script, p_class, function);
if (err) {
Expand Down
21 changes: 21 additions & 0 deletions modules/gdscript/gdscript_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "gdscript_utility_functions.h"

#ifdef TOOLS_ENABLED
#include "editor/export/editor_export.h"
#include "editor/gdscript_docgen.h"
#include "editor/script_templates/templates.gen.h"
#endif
Expand Down Expand Up @@ -982,6 +983,26 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a
r_result.insert(option.display, option);
}
}
} else if (p_annotation->name == SNAME("@if_features")) {
#ifdef TOOLS_ENABLED
HashSet<String> features;
for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
const Ref<EditorExportPreset> &preset = EditorExport::get_singleton()->get_export_preset(i);
for (const String &feature : preset->get_custom_features().split(",", false)) {
features.insert(feature.strip_edges());
}
List<String> platform_features;
preset->get_platform()->get_platform_features(&platform_features);
for (const String &feature : platform_features) {
features.insert(feature.strip_edges());
}
}
for (const String &feature : features) {
ScriptLanguage::CodeCompletionOption option(feature, ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT);
option.insert_text = option.display.quote(p_quote_style);
r_result.insert(option.display, option);
}
#endif
}
}

Expand Down
115 changes: 115 additions & 0 deletions modules/gdscript/gdscript_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ GDScriptParser::GDScriptParser() {
register_annotation(MethodInfo("@warning_ignore_restore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::STANDALONE, &GDScriptParser::warning_ignore_region_annotations, varray(), true);
// Networking.
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));
// Preprocessing.
register_annotation(MethodInfo("@if_features", PropertyInfo(Variant::STRING, "feature")), AnnotationInfo::FUNCTION, &GDScriptParser::if_features_annotation, varray(Variant()), true);
}

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

// Consume annotations.
List<AnnotationNode *> annotations;
#ifdef TOOLS_ENABLED
constexpr bool parsing_function = std::is_same_v<T, FunctionNode>;
AnnotationNode *îf_features = nullptr;
#endif
while (!annotation_stack.is_empty()) {
AnnotationNode *last_annotation = annotation_stack.back()->get();
if (last_annotation->applies_to(p_target)) {
annotations.push_front(last_annotation);
annotation_stack.pop_back();
#ifdef TOOLS_ENABLED
if constexpr (parsing_function) {
if (last_annotation->name == StringName("@if_features")) {
îf_features = last_annotation;
}
}
#endif
} else {
push_error(vformat(R"(Annotation "%s" cannot be applied to a %s.)", last_annotation->name, p_member_kind));
clear_unused_annotations();
Expand Down Expand Up @@ -994,6 +1007,19 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
}

min_member_doc_line = member->end_line + 1; // Prevent multiple members from using the same doc comment.

if constexpr (parsing_function) {
if (îf_features) {
// Mark this one as a default implementation if the annotation provides no features.
member->if_features.is_default_impl = îf_features->arguments.is_empty();
// Let the first default one with of same name fully in.
// The others are added so they are parsed, but not indexed so name clash error is avoided.
if (!member->if_features.is_default_impl || current_class->members_indices.has(member->identifier->name)) {
current_class->add_if_features_potential_candidate(member);
return;
}
}
}
#endif // TOOLS_ENABLED

if (member->identifier != nullptr) {
Expand Down Expand Up @@ -4975,6 +5001,95 @@ bool GDScriptParser::warning_ignore_region_annotations(AnnotationNode *p_annotat
#endif // DEBUG_ENABLED
}

bool GDScriptParser::if_features_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
#if defined(TOOLS_ENABLED)
ERR_FAIL_COND_V_MSG(p_target->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name));

FunctionNode *function = static_cast<FunctionNode *>(p_target);
if (function->if_features.used) {
push_error("The @if_features annotation can only be used once per function.");
return false;
}

function->if_features.used = true;

thread_local LocalVector<String> features;
features.clear();
for (const ExpressionNode *arg : p_annotation->arguments) {
DEV_ASSERT(arg->reduced);
if (arg->reduced_value.get_type() == Variant::STRING) {
features.push_back(arg->reduced_value);
} else {
push_error("The arguments to @if_features must be strings.");
return false;
}
}

if (Engine::get_singleton()->is_editor_hint() && !for_export) {
// At edit time there's nothing else to process.
return true;
}

// The idea is to keep the first one fitting, with only the default possibly overridden by another one coming later.

FunctionNode *target_function = static_cast<FunctionNode *>(p_target);
const StringName &function_name = target_function->identifier->name;

bool current_match_overridable = false;
if (target_function->if_features.potential_candidate_index != -1) { // Otherwise, it's already the chosen one when parsing the function.
if (p_class->has_function(function_name)) {
bool current_match_is_default = p_class->get_member(function_name).function->if_features.is_default_impl;
bool incoming_candidate_is_default = target_function->if_features.is_default_impl;
current_match_overridable = current_match_is_default && !incoming_candidate_is_default;
}
}

if (current_match_overridable) {
bool fitting = false;
if (features.size()) {
fitting = true;
for (const String &feature : features) {
if (for_export) {
// Export time in editor build.
if (!export_features.has(feature)) {
fitting = false;
break;
}
} else {
// Runtime in editor build.
if (!OS::get_singleton()->has_feature(feature)) {
fitting = false;
break;
}
}
}
}
if (fitting) {
// If fits, replace the current match.
int64_t current_match_index = p_class->members_indices[function_name];
p_class->members[current_match_index].function->if_features.potential_candidate_index = current_match_index;
p_class->members_indices[function_name] = target_function->if_features.potential_candidate_index;
target_function->if_features.potential_candidate_index = -1;
}
}
#endif

return true;
}

#ifdef TOOLS_ENABLED
void GDScriptParser::collect_unfitting_functions(ClassNode *p_class, LocalVector<Pair<ClassNode *, FunctionNode *>> &r_functions) {
for (const ClassNode::Member &member : p_class->members) {
if (member.type == ClassNode::Member::CLASS) {
collect_unfitting_functions(member.m_class, r_functions);
}
if (member.type == ClassNode::Member::FUNCTION && member.function->if_features.potential_candidate_index != -1) {
r_functions.push_back(Pair(p_class, member.function));
}
}
}
#endif

bool GDScriptParser::rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
ERR_FAIL_COND_V_MSG(p_target->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name));

Expand Down
26 changes: 26 additions & 0 deletions modules/gdscript/gdscript_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,13 @@ class GDScriptParser {
members_indices[p_member_node->identifier->name] = members.size();
members.push_back(Member(p_member_node));
}
#ifdef TOOLS_ENABLED
template <typename T>
void add_if_features_potential_candidate(T *p_member_node) {
p_member_node->if_features.potential_candidate_index = members.size();
members.push_back(Member(p_member_node));
}
#endif
void add_member(const EnumNode::Value &p_enum_value) {
members_indices[p_enum_value.identifier->name] = members.size();
members.push_back(Member(p_enum_value));
Expand Down Expand Up @@ -862,6 +869,11 @@ class GDScriptParser {
#ifdef TOOLS_ENABLED
MemberDocData doc_data;
int min_local_doc_line = 0;
struct {
bool used = false;
bool is_default_impl = false;
int potential_candidate_index = -1;
} if_features;
#endif // TOOLS_ENABLED

bool resolved_signature = false;
Expand Down Expand Up @@ -1335,6 +1347,10 @@ class GDScriptParser {
bool _is_tool = false;
String script_path;
bool for_completion = false;
#ifdef TOOLS_ENABLED
bool for_export = false;
HashSet<String> export_features;
#endif
bool parse_body = true;
bool panic_mode = false;
bool can_break = false;
Expand Down Expand Up @@ -1519,6 +1535,7 @@ class GDScriptParser {
bool warning_ignore_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool warning_ignore_region_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool if_features_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
// Statements.
Node *parse_statement();
VariableNode *parse_variable(bool p_is_static);
Expand Down Expand Up @@ -1590,6 +1607,15 @@ class GDScriptParser {
void get_annotation_list(List<MethodInfo> *r_annotations) const;
bool annotation_exists(const String &p_annotation_name) const;

#ifdef TOOLS_ENABLED
bool is_for_export() const { return for_export; }
void set_export_features(const HashSet<String> &p_features) {
for_export = true;
export_features = p_features;
}
void collect_unfitting_functions(ClassNode *p_class, LocalVector<Pair<ClassNode *, FunctionNode *>> &r_functions);
#endif

const List<ParserError> &get_errors() const { return errors; }
const List<String> get_dependencies() const {
// TODO: Keep track of deps.
Expand Down
Loading

0 comments on commit 9c1483a

Please sign in to comment.