From 5b38f82c709d5471fd3cc9c613f832f97c9181fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20J=2E=20Est=C3=A9banez?= Date: Thu, 30 Jan 2025 10:46:20 +0100 Subject: [PATCH 1/2] GDScript: Include function return type in function stringification --- modules/gdscript/gdscript_parser.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 646391787173..508d46a1a7bd 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -5854,7 +5854,12 @@ void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const } print_parameter(p_function->parameters[i]); } - push_line(" ) :"); + push_text(" ) "); + if (p_function->return_type) { + push_text("-> "); + print_type(p_function->return_type); + } + push_line(" :"); increase_indent(); print_suite(p_function->body); decrease_indent(); From 1e4ee3dc6cfb2a930b879090df1e202d430e92ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20J=2E=20Est=C3=A9banez?= Date: Thu, 30 Jan 2025 12:05:09 +0100 Subject: [PATCH 2/2] GDScript: Implement @if_features annotation --- core/os/os.cpp | 5 +- editor/export/editor_export_platform.cpp | 1 + modules/gdscript/doc_classes/@GDScript.xml | 11 ++ modules/gdscript/gdscript_cache.cpp | 12 +- modules/gdscript/gdscript_cache.h | 1 + modules/gdscript/gdscript_compiler.cpp | 6 + modules/gdscript/gdscript_editor.cpp | 30 ++++ modules/gdscript/gdscript_parser.cpp | 164 +++++++++++++++++- modules/gdscript/gdscript_parser.h | 34 +++- modules/gdscript/register_types.cpp | 83 ++++++++- ...atures_function_clashes_with_other_type.gd | 8 + ...tures_function_clashes_with_other_type.out | 2 + ...s_functions_with_different_signatures_1.gd | 10 ++ ..._functions_with_different_signatures_1.out | 2 + ...s_functions_with_different_signatures_2.gd | 10 ++ ..._functions_with_different_signatures_2.out | 2 + .../parser/features/if_features_annotation.gd | 99 +++++++++++ .../features/if_features_annotation.out | 5 + tests/test_main.cpp | 4 + 19 files changed, 470 insertions(+), 19 deletions(-) create mode 100644 modules/gdscript/tests/scripts/parser/errors/if_features_function_clashes_with_other_type.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/if_features_function_clashes_with_other_type.out create mode 100644 modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_1.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_1.out create mode 100644 modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_2.gd create mode 100644 modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_2.out create mode 100644 modules/gdscript/tests/scripts/parser/features/if_features_annotation.gd create mode 100644 modules/gdscript/tests/scripts/parser/features/if_features_annotation.out diff --git a/core/os/os.cpp b/core/os/os.cpp index c161b2212f40..2ab072a44818 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -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; } diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp index 8b142198cd8c..abf5f23565db 100644 --- a/editor/export/editor_export_platform.cpp +++ b/editor/export/editor_export_platform.cpp @@ -531,6 +531,7 @@ HashSet EditorExportPlatform::get_features(const Ref result.insert(E); } + result.insert("runtime"); result.insert("template"); if (p_debug) { result.insert("debug"); diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index caed2a808d45..7bfe7c237667 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -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). + + + + + 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. + + diff --git a/modules/gdscript/gdscript_cache.cpp b/modules/gdscript/gdscript_cache.cpp index c9a5f26c82b4..2fb91030d383 100644 --- a/modules/gdscript/gdscript_cache.cpp +++ b/modules/gdscript/gdscript_cache.cpp @@ -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; } @@ -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); } diff --git a/modules/gdscript/gdscript_cache.h b/modules/gdscript/gdscript_cache.h index 4903da92b4f3..fd6079331749 100644 --- a/modules/gdscript/gdscript_cache.h +++ b/modules/gdscript/gdscript_cache.h @@ -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(); diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index e52486e209e5..2c3bc527fea3 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -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) { diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 743fe5711e92..e1b167a69afe 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -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 @@ -133,6 +134,9 @@ bool GDScriptLanguage::validate(const String &p_script, const String &p_path, Li GDScriptParser parser; GDScriptAnalyzer analyzer(&parser); +#ifdef TOOLS_ENABLED + parser.set_for_edition(); +#endif Error err = parser.parse(p_script, p_path, false); if (err == OK) { err = analyzer.analyze(); @@ -982,6 +986,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 features; + for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) { + const Ref &preset = EditorExport::get_singleton()->get_export_preset(i); + for (const String &feature : preset->get_custom_features().split(",", false)) { + features.insert(feature.strip_edges()); + } + List 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 } } @@ -3207,6 +3231,9 @@ ::Error GDScriptLanguage::complete_code(const String &p_code, const String &p_pa GDScriptParser parser; GDScriptAnalyzer analyzer(&parser); +#ifdef TOOLS_ENABLED + parser.set_for_edition(); +#endif parser.parse(p_code, p_path, true); analyzer.analyze(); @@ -4023,6 +4050,9 @@ ::Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symb } GDScriptParser parser; +#ifdef TOOLS_ENABLED + parser.set_for_edition(); +#endif parser.parse(p_code, p_path, true); GDScriptParser::CompletionContext context = parser.get_completion_context(); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 508d46a1a7bd..b9b3e857df31 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -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 @@ -944,11 +946,22 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b // Consume annotations. List annotations; +#ifdef TOOLS_ENABLED + constexpr bool parsing_function = std::is_same_v; + 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(); @@ -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::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) { @@ -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(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 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(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::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::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> &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)); @@ -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 "); @@ -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) { @@ -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 diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 22b14e8bb5c1..b300380f1328 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -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 + 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)); @@ -862,6 +869,12 @@ class GDScriptParser { #ifdef TOOLS_ENABLED MemberDocData doc_data; int min_local_doc_line = 0; + struct { + bool used = false; + bool is_default_impl = false; + bool fitting_verified = false; + int potential_candidate_index = -1; + } if_features; #endif // TOOLS_ENABLED bool resolved_signature = false; @@ -1335,6 +1348,11 @@ class GDScriptParser { bool _is_tool = false; String script_path; bool for_completion = false; +#ifdef TOOLS_ENABLED + bool for_export = false; + HashSet export_features; + bool for_edition = false; +#endif bool parse_body = true; bool panic_mode = false; bool can_break = false; @@ -1519,6 +1537,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); @@ -1590,6 +1609,18 @@ class GDScriptParser { void get_annotation_list(List *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 &p_features) { + for_export = true; + export_features = p_features; + } + void set_for_edition() { + for_edition = true; + } + void collect_unfitting_functions(ClassNode *p_class, LocalVector> &r_functions); +#endif + const List &get_errors() const { return errors; } const List get_dependencies() const { // TODO: Keep track of deps. @@ -1636,7 +1667,7 @@ class GDScriptParser { void print_expression(ExpressionNode *p_expression); void print_enum(EnumNode *p_enum); void print_for(ForNode *p_for); - void print_function(FunctionNode *p_function, const String &p_context = "Function"); + void print_function(FunctionNode *p_function, const String &p_context = "Function", bool p_signature_only = false); void print_get_node(GetNodeNode *p_get_node); void print_if(IfNode *p_if, bool p_is_elif = false); void print_identifier(IdentifierNode *p_identifier); @@ -1662,6 +1693,7 @@ class GDScriptParser { public: void print_tree(const GDScriptParser &p_parser); + String strinfigy_function_declaration(FunctionNode *p_function); }; #endif // DEBUG_ENABLED static void cleanup(); diff --git a/modules/gdscript/register_types.cpp b/modules/gdscript/register_types.cpp index 7ae81db13403..c2388f802bd7 100644 --- a/modules/gdscript/register_types.cpp +++ b/modules/gdscript/register_types.cpp @@ -31,6 +31,7 @@ #include "register_types.h" #include "gdscript.h" +#include "gdscript_analyzer.h" #include "gdscript_cache.h" #include "gdscript_parser.h" #include "gdscript_tokenizer_buffer.h" @@ -78,6 +79,9 @@ Ref gdscript_translation_parser_plugin; class EditorExportGDScript : public EditorExportPlugin { GDCLASS(EditorExportGDScript, EditorExportPlugin); + uint32_t customization_hash = 0; + HashSet features; + static constexpr int DEFAULT_SCRIPT_MODE = EditorExportPreset::MODE_SCRIPT_BINARY_TOKENS_COMPRESSED; int script_mode = DEFAULT_SCRIPT_MODE; @@ -89,27 +93,88 @@ class EditorExportGDScript : public EditorExportPlugin { if (preset.is_valid()) { script_mode = preset->get_script_export_mode(); } + + // If features change, the scripts must be reanalyzed. + for (const String &feature : p_features) { + customization_hash = hash_murmur3_one_64(feature.hash64(), customization_hash); + } + features = p_features; + } + + virtual uint64_t _get_customization_configuration_hash() const override { + return customization_hash; } virtual void _export_file(const String &p_path, const String &p_type, const HashSet &p_features) override { - if (p_path.get_extension() != "gd" || script_mode == EditorExportPreset::MODE_SCRIPT_TEXT) { + if (p_path.get_extension() != "gd") { return; } - Vector file = FileAccess::get_file_as_bytes(p_path); - if (file.is_empty()) { + PackedByteArray source_bytes = FileAccess::get_file_as_bytes(p_path); + if (source_bytes.is_empty()) { return; } String source; - source.parse_utf8(reinterpret_cast(file.ptr()), file.size()); - GDScriptTokenizerBuffer::CompressMode compress_mode = script_mode == EditorExportPreset::MODE_SCRIPT_BINARY_TOKENS_COMPRESSED ? GDScriptTokenizerBuffer::COMPRESS_ZSTD : GDScriptTokenizerBuffer::COMPRESS_NONE; - file = GDScriptTokenizerBuffer::parse_code_string(source, compress_mode); - if (file.is_empty()) { - return; + source.parse_utf8(reinterpret_cast(source_bytes.ptr()), source_bytes.size()); + + // Avoid parsing if the script doesn't look like it actually uses @if_features. + bool source_changed = false; + if (source.contains("@if_features")) { + // 1. Parse and analyze script, as little as needed to have annotations processed. + GDScriptParserRef parser; + { + parser.set_path(ResourceLoader::path_remap(p_path)); + Error err = parser.get_parser()->parse(source, p_path, false); + ERR_FAIL_COND(err); + parser.get_parser()->set_export_features(p_features); // Needed for the analyzer step. If done early, parser's clear would remove this info. + err = parser.get_analyzer()->resolve_interface(); // Enough for annotations to be applied. + ERR_FAIL_COND(err); + } + + // 2. Strip functions unfitting @if_features. + { + LocalVector> unfitting_functions; + parser.get_parser()->collect_unfitting_functions(parser.get_parser()->get_tree(), unfitting_functions); + if (unfitting_functions.size()) { + Vector lines = source.split("\n"); + for (const Pair &class_and_func : unfitting_functions) { + GDScriptParser::FunctionNode *function = class_and_func.second; + const String &class_part = class_and_func.first->identifier ? String(class_and_func.first->identifier->name) + ":" : String(); + + // Strip annotations as well (not covered by function's start_line). + int start_line = function->start_line; + if (function->annotations.size()) { + start_line = function->annotations.front()->get()->start_line; + } + + print_verbose(vformat("Stripping function %s%s:%s (%d-%d)", p_path, class_part, function->identifier->name, start_line, function->end_line)); + for (int i = start_line - 1; i <= function->end_line - 1; i++) { + lines.write[i] = ""; + } + } + source = String("\n").join(lines); + source_changed = true; + } + } } - add_file(p_path.get_basename() + ".gdc", file, true); + PackedByteArray res; + if (script_mode == EditorExportPreset::MODE_SCRIPT_TEXT) { + if (source_changed) { + skip(); + source_bytes = source.to_utf8_buffer(); + add_file(p_path, source_bytes, false); + } + } else { + GDScriptTokenizerBuffer::CompressMode compress_mode = script_mode == EditorExportPreset::MODE_SCRIPT_BINARY_TOKENS_COMPRESSED ? GDScriptTokenizerBuffer::COMPRESS_ZSTD : GDScriptTokenizerBuffer::COMPRESS_NONE; + source_bytes = GDScriptTokenizerBuffer::parse_code_string(source, compress_mode); + if (source_bytes.is_empty()) { + return; + } + + add_file(p_path.get_basename() + ".gdc", source_bytes, true); + } } public: diff --git a/modules/gdscript/tests/scripts/parser/errors/if_features_function_clashes_with_other_type.gd b/modules/gdscript/tests/scripts/parser/errors/if_features_function_clashes_with_other_type.gd new file mode 100644 index 000000000000..30ec08facf67 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/if_features_function_clashes_with_other_type.gd @@ -0,0 +1,8 @@ +func _test(): + pass + +var _other + +@if_features() +func _other(): + pass diff --git a/modules/gdscript/tests/scripts/parser/errors/if_features_function_clashes_with_other_type.out b/modules/gdscript/tests/scripts/parser/errors/if_features_function_clashes_with_other_type.out new file mode 100644 index 000000000000..874285c40ac3 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/if_features_function_clashes_with_other_type.out @@ -0,0 +1,2 @@ +GDTEST_PARSER_ERROR +Function "_other" has the same name as a previously declared variable. diff --git a/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_1.gd b/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_1.gd new file mode 100644 index 000000000000..acd7a66b19e0 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_1.gd @@ -0,0 +1,10 @@ +func _test(): + pass + +@if_features() +func _func(a: int, b: String) -> Node: + pass + +@if_features() +func _func(a: float, b: String) -> Node: + pass diff --git a/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_1.out b/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_1.out new file mode 100644 index 000000000000..d63098279cea --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_1.out @@ -0,0 +1,2 @@ +GDTEST_PARSER_ERROR +Function "_func" does not match the signature of a previously declared function of the same name. diff --git a/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_2.gd b/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_2.gd new file mode 100644 index 000000000000..ff332bc35821 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_2.gd @@ -0,0 +1,10 @@ +func _test(): + pass + +@if_features() +func _func(a: int) -> int: + return 1 + +@if_features() +func _func(a: int) -> void: + pass diff --git a/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_2.out b/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_2.out new file mode 100644 index 000000000000..d63098279cea --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/if_features_functions_with_different_signatures_2.out @@ -0,0 +1,2 @@ +GDTEST_PARSER_ERROR +Function "_func" does not match the signature of a previously declared function of the same name. diff --git a/modules/gdscript/tests/scripts/parser/features/if_features_annotation.gd b/modules/gdscript/tests/scripts/parser/features/if_features_annotation.gd new file mode 100644 index 000000000000..705211ee4b6e --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/if_features_annotation.gd @@ -0,0 +1,99 @@ +func test(): + _implementation() + _various() + var inner = InnerClass.new() + inner._implementation() + inner._various() + +# --------------------------------------------- + +# - First match after default prevails. +# - One default can't override another default. +# - Non-fitting are ignored. + +@if_features() +func _implementation(): + print("default") + +@if_features("non_existent_feature") +func _implementation(): + print("nope") + +@if_features("test_feature_1") +func _implementation(): + print("test 1") + +@if_features("test_feature_2") +func _implementation(): + print("test 2") + +@if_features() +func _implementation(): + print("default 2") + +# --------------------------------------------- + +# - Non-default match can't be overridden. +# - Non-fitting are ignored. + +@if_features("non_existent_feature") +func _various(): + print("other nope") + +@if_features("test_feature_1", "test_feature_2") +func _various(): + print("other 1") + +@if_features("test_feature_1", "test_feature_2") +func _various(): + print("other 1 again") + +@if_features() +func _various(): + print("other default") + +class InnerClass: + # - First match after default prevails. + # - One default can't override another default. + # - Non-fitting are ignored. + + @if_features() + func _implementation(): + print("inner default") + + @if_features("non_existent_feature") + func _implementation(): + print("inner nope") + + @if_features("test_feature_1") + func _implementation(): + print("inner test 1") + + @if_features("test_feature_2") + func _implementation(): + print("inner test 2") + + @if_features() + func _implementation(): + print("inner default 2") + + # --------------------------------------------- + + # - Non-default match can't be overridden. + # - Non-fitting are ignored. + + @if_features("non_existent_feature") + func _various(): + print("inner other nope") + + @if_features("test_feature_1", "test_feature_2") + func _various(): + print("inner other 1") + + @if_features("test_feature_1", "test_feature_2") + func _various(): + print("inner other 1 again") + + @if_features() + func _various(): + print("inner other default") diff --git a/modules/gdscript/tests/scripts/parser/features/if_features_annotation.out b/modules/gdscript/tests/scripts/parser/features/if_features_annotation.out new file mode 100644 index 000000000000..217b6e0b2c8c --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/if_features_annotation.out @@ -0,0 +1,5 @@ +GDTEST_OK +test 1 +other 1 +inner test 1 +inner other 1 diff --git a/tests/test_main.cpp b/tests/test_main.cpp index af334dbbf4a6..fec8f1222c55 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -348,6 +348,10 @@ struct GodotTestCaseListener : public doctest::IReporter { return; } + OS::get_singleton()->set_has_server_feature_callback([](const String &p_feature) -> bool { + return p_feature == "test_feature_1" || p_feature == "test_feature_2"; + }); + if (name.contains("[Audio]")) { // The last driver index should always be the dummy driver. int dummy_idx = AudioDriverManager::get_driver_count() - 1;