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 Feb 3, 2025
1 parent 5b38f82 commit e176bda
Show file tree
Hide file tree
Showing 19 changed files with 464 additions and 19 deletions.
5 changes: 4 additions & 1 deletion core/os/os.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -441,12 +441,15 @@ bool OS::has_feature(const String &p_feature) {
}
if (p_feature == "editor_hint") {
return _in_editor;
} else if (p_feature == "editor_runtime") {
} else if (p_feature == "editor_runtime" || p_feature == "runtime") {
return !_in_editor;
} else if (p_feature == "embedded_in_editor") {
return _embedded_in_editor;
}
#else
if (p_feature == "runtime") {
return true;
}
if (p_feature == "template") {
return true;
}
Expand Down
1 change: 1 addition & 0 deletions editor/export/editor_export_platform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ HashSet<String> EditorExportPlatform::get_features(const Ref<EditorExportPreset>
result.insert(E);
}

result.insert("runtime");
result.insert("template");
if (p_debug) {
result.insert("debug");
Expand Down
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. All the functions in such a set must have the exact same signature.
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
6 changes: 6 additions & 0 deletions modules/gdscript/gdscript_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2947,6 +2947,12 @@ 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
// Ignore unfitting @if_features-decorated functions.
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
24 changes: 24 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 @@ -133,6 +134,7 @@ bool GDScriptLanguage::validate(const String &p_script, const String &p_path, Li
GDScriptParser parser;
GDScriptAnalyzer analyzer(&parser);

parser.set_for_edition();

Check failure on line 137 in modules/gdscript/gdscript_editor.cpp

View workflow job for this annotation

GitHub Actions / 🍏 iOS / Template (target=template_release)

no member named 'set_for_edition' in 'GDScriptParser'

Check failure on line 137 in modules/gdscript/gdscript_editor.cpp

View workflow job for this annotation

GitHub Actions / 🍎 macOS / Template (target=template_release, tests=yes)

no member named 'set_for_edition' in 'GDScriptParser'

Check failure on line 137 in modules/gdscript/gdscript_editor.cpp

View workflow job for this annotation

GitHub Actions / 🤖 Android / Template arm32 (target=template_release, arch=arm32)

no member named 'set_for_edition' in 'GDScriptParser'

Check failure on line 137 in modules/gdscript/gdscript_editor.cpp

View workflow job for this annotation

GitHub Actions / 🤖 Android / Template arm64 (target=template_release, arch=arm64)

no member named 'set_for_edition' in 'GDScriptParser'

Check failure on line 137 in modules/gdscript/gdscript_editor.cpp

View workflow job for this annotation

GitHub Actions / 🌐 Web / Template w/ threads (target=template_release, threads=yes)

no member named 'set_for_edition' in 'GDScriptParser'

Check failure on line 137 in modules/gdscript/gdscript_editor.cpp

View workflow job for this annotation

GitHub Actions / 🌐 Web / Template w/o threads (target=template_release, threads=no)

no member named 'set_for_edition' in 'GDScriptParser'

Check failure on line 137 in modules/gdscript/gdscript_editor.cpp

View workflow job for this annotation

GitHub Actions / 🏁 Windows / Template (target=template_release, tests=yes)

'set_for_edition': is not a member of 'GDScriptParser'
Error err = parser.parse(p_script, p_path, false);
if (err == OK) {
err = analyzer.analyze();
Expand Down Expand Up @@ -982,6 +984,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 Expand Up @@ -3207,6 +3229,7 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa
GDScriptParser parser;
GDScriptAnalyzer analyzer(&parser);

parser.set_for_edition();
parser.parse(p_code, p_path, true);
analyzer.analyze();

Expand Down Expand Up @@ -4023,6 +4046,7 @@ ::Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symb
}

GDScriptParser parser;
parser.set_for_edition();
parser.parse(p_code, p_path, true);

GDScriptParser::CompletionContext context = parser.get_completion_context();
Expand Down
164 changes: 157 additions & 7 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 *if_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")) {
if_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,32 @@ 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 (if_features) {
// Mark this one as a default implementation if the annotation provides no features.
member->if_features.is_default_impl = if_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.
HashMap<StringName, int>::Iterator E = current_class->members_indices.find(member->identifier->name);
if (E) {
// Member with that name already exists.
const ClassNode::Member &existing = current_class->members[E->value];
if (existing.type == ClassNode::Member::FUNCTION) { // Otherwise, an error is raised anyway.
// HACK: Compare compatibility of functions via TreePrinter.
const String &existing_str = TreePrinter().strinfigy_function_declaration(existing.function);
const String &incoming_str = TreePrinter().strinfigy_function_declaration(member);
if (existing_str != incoming_str) {
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);
} else {
current_class->add_if_features_potential_candidate(member);
}
return;
}
}
}
}
#endif // TOOLS_ENABLED

if (member->identifier != nullptr) {
Expand Down Expand Up @@ -4975,6 +5014,108 @@ 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;
}
}

// There's nothing to process unless we're exporting or running in the editor.
// Sadly, we can't tell for sure if the script is being run in the editor or just being edited.
// We only know for sure it's the latter if this parsing is done for completion reasons.
if (for_edition) {
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;

auto _check_incoming_fits = [&]() -> bool {
if (features.is_empty()) {
return false; // Defaults are considered non-fitting.
}
bool fitting = true;
for (const String &feature : features) {
if (for_export) {
// Export time in editor build.
if (!export_features.has(feature)) {
fitting = false;
break;
}
} else {
// Run-on-editor.
if (!OS::get_singleton()->has_feature(feature)) {
fitting = false;
break;
}
}
}
return fitting;
};

if (target_function->if_features.potential_candidate_index == -1) {
// Chosen one at parsing time because it was the first one found. Only keep if fitting.
if (!_check_incoming_fits()) {
HashMap<StringName, int>::Iterator E = p_class->members_indices.find(function_name);
int64_t current_match_index = E->value;
target_function->if_features.potential_candidate_index = current_match_index;
p_class->members_indices.remove(E);
}
} else {
HashMap<StringName, int>::Iterator E = p_class->members_indices.find(function_name);
if ((bool)E) {
// There's a current one. Override if current is default and incoming fits.
bool current_match_is_default = p_class->members[E->value].function->if_features.is_default_impl;
if (current_match_is_default && _check_incoming_fits()) {
E->value = target_function->if_features.potential_candidate_index;
target_function->if_features.potential_candidate_index = -1;
}
} else {
// Incoming fits and there's no current chosen. Pick if it's default or fits.
if (target_function->if_features.is_default_impl || _check_incoming_fits()) {
p_class->members_indices.insert(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 Expand Up @@ -5833,9 +5974,11 @@ void GDScriptParser::TreePrinter::print_for(ForNode *p_for) {
decrease_indent();
}

void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const String &p_context) {
for (const AnnotationNode *E : p_function->annotations) {
print_annotation(E);
void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const String &p_context, bool p_signature_only) {
if (!p_signature_only) {
for (const AnnotationNode *E : p_function->annotations) {
print_annotation(E);
}
}
if (p_function->is_static) {
push_text("Static ");
Expand All @@ -5859,10 +6002,12 @@ void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const
push_text("-> ");
print_type(p_function->return_type);
}
push_line(" :");
increase_indent();
print_suite(p_function->body);
decrease_indent();
if (!p_signature_only) {
push_line(" :");
increase_indent();
print_suite(p_function->body);
decrease_indent();
}
}

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

String GDScriptParser::TreePrinter::strinfigy_function_declaration(FunctionNode *p_function) {
print_function(p_function, "Function", true);
return String(printed);
}

#endif // DEBUG_ENABLED
Loading

0 comments on commit e176bda

Please sign in to comment.