diff options
Diffstat (limited to 'modules')
45 files changed, 619 insertions, 162 deletions
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 32acad76aa..e05b17168d 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -565,14 +565,22 @@ </annotation> <annotation name="@rpc" qualifiers="vararg"> <return type="void" /> - <param index="0" name="mode" type="String" default="""" /> - <param index="1" name="sync" type="String" default="""" /> - <param index="2" name="transfer_mode" type="String" default="""" /> + <param index="0" name="mode" type="String" default=""authority"" /> + <param index="1" name="sync" type="String" default=""call_remote"" /> + <param index="2" name="transfer_mode" type="String" default=""unreliable"" /> <param index="3" name="transfer_channel" type="int" default="0" /> <description> Mark the following method for remote procedure calls. See [url=$DOCS_URL/tutorials/networking/high_level_multiplayer.html]High-level multiplayer[/url]. + The order of [code]mode[/code], [code]sync[/code] and [code]transfer_mode[/code] does not matter and all arguments can be omitted, but [code]transfer_channel[/code] always has to be the last argument. The accepted values for [code]mode[/code] are [code]"any_peer"[/code] or [code]"authority"[/code], for [code]sync[/code] are [code]"call_remote"[/code] or [code]"call_local"[/code] and for [code]transfer_mode[/code] are [code]"unreliable"[/code], [code]"unreliable_ordered"[/code] or [code]"reliable"[/code]. [codeblock] - @rpc() + @rpc + func fn(): pass + + @rpc("any_peer", "unreliable_ordered") + func fn_update_pos(): pass + + @rpc("authority", "call_remote", "unreliable", 0) # Equivalent to @rpc + func fn_default(): pass [/codeblock] </description> </annotation> diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index fd04d3c660..cd1dcf9a78 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -138,13 +138,25 @@ static GDScriptParser::DataType make_enum_type(const StringName &p_enum_name, co } static GDScriptParser::DataType make_native_enum_type(const StringName &p_enum_name, const StringName &p_native_class, const bool p_meta = true) { - GDScriptParser::DataType type = make_enum_type(p_enum_name, p_native_class, p_meta); + // Find out which base class declared the enum, so the name is always the same even when coming from other contexts. + StringName native_base = p_native_class; + while (true && native_base != StringName()) { + if (ClassDB::has_enum(native_base, p_enum_name, true)) { + break; + } + native_base = ClassDB::get_parent_class_nocheck(native_base); + } + + GDScriptParser::DataType type = make_enum_type(p_enum_name, native_base, p_meta); + if (p_meta) { + type.builtin_type = Variant::NIL; // Native enum types are not Dictionaries + } List<StringName> enum_values; - ClassDB::get_enum_constants(p_native_class, p_enum_name, &enum_values); + ClassDB::get_enum_constants(native_base, p_enum_name, &enum_values, true); for (const StringName &E : enum_values) { - type.enum_values[E] = ClassDB::get_integer_constant(p_native_class, E); + type.enum_values[E] = ClassDB::get_integer_constant(native_base, E); } return type; @@ -782,6 +794,22 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, resolving_datatype.kind = GDScriptParser::DataType::RESOLVING; { +#ifdef DEBUG_ENABLED + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + GDScriptParser::Node *member_node = member.get_source_node(); + if (member_node && member_node->type != GDScriptParser::Node::ANNOTATION) { + // Apply @warning_ignore annotations before resolving member. + for (GDScriptParser::AnnotationNode *&E : member_node->annotations) { + if (E->name == SNAME("@warning_ignore")) { + resolve_annotation(E); + E->apply(parser, member.variable); + } + } + for (GDScriptWarning::Code ignored_warning : member_node->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); + } + } +#endif switch (member.type) { case GDScriptParser::ClassNode::Member::VARIABLE: { check_class_member_name_conflict(p_class, member.variable->identifier->name, member.variable); @@ -790,9 +818,48 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, // Apply annotations. for (GDScriptParser::AnnotationNode *&E : member.variable->annotations) { - resolve_annotation(E); - E->apply(parser, member.variable); + if (E->name != SNAME("@warning_ignore")) { + resolve_annotation(E); + E->apply(parser, member.variable); + } + } +#ifdef DEBUG_ENABLED + if (member.variable->exported && member.variable->onready) { + parser->push_warning(member.variable, GDScriptWarning::ONREADY_WITH_EXPORT); } + if (member.variable->initializer) { + // Check if it is call to get_node() on self (using shorthand $ or not), so we can check if @onready is needed. + // This could be improved by traversing the expression fully and checking the presence of get_node at any level. + if (!member.variable->onready && member.variable->initializer && (member.variable->initializer->type == GDScriptParser::Node::GET_NODE || member.variable->initializer->type == GDScriptParser::Node::CALL || member.variable->initializer->type == GDScriptParser::Node::CAST)) { + GDScriptParser::Node *expr = member.variable->initializer; + if (expr->type == GDScriptParser::Node::CAST) { + expr = static_cast<GDScriptParser::CastNode *>(expr)->operand; + } + bool is_get_node = expr->type == GDScriptParser::Node::GET_NODE; + bool is_using_shorthand = is_get_node; + if (!is_get_node && expr->type == GDScriptParser::Node::CALL) { + is_using_shorthand = false; + GDScriptParser::CallNode *call = static_cast<GDScriptParser::CallNode *>(expr); + if (call->function_name == SNAME("get_node")) { + switch (call->get_callee_type()) { + case GDScriptParser::Node::IDENTIFIER: { + is_get_node = true; + } break; + case GDScriptParser::Node::SUBSCRIPT: { + GDScriptParser::SubscriptNode *subscript = static_cast<GDScriptParser::SubscriptNode *>(call->callee); + is_get_node = subscript->is_attribute && subscript->base->type == GDScriptParser::Node::SELF; + } break; + default: + break; + } + } + } + if (is_get_node) { + parser->push_warning(member.variable, GDScriptWarning::GET_NODE_DEFAULT_WITHOUT_ONREADY, is_using_shorthand ? "$" : "get_node()"); + } + } + } +#endif } break; case GDScriptParser::ClassNode::Member::CONSTANT: { check_class_member_name_conflict(p_class, member.constant->identifier->name, member.constant); @@ -878,6 +945,10 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, } } break; case GDScriptParser::ClassNode::Member::FUNCTION: + for (GDScriptParser::AnnotationNode *&E : member.function->annotations) { + resolve_annotation(E); + E->apply(parser, member.function); + } resolve_function_signature(member.function, p_source); break; case GDScriptParser::ClassNode::Member::ENUM_VALUE: { @@ -931,6 +1002,9 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, ERR_PRINT("Trying to resolve undefined member."); break; } +#ifdef DEBUG_ENABLED + parser->ignored_warnings = previously_ignored_warnings; +#endif } parser->current_class = previous_class; @@ -1059,19 +1133,7 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co resolve_annotation(E); E->apply(parser, member.function); } - -#ifdef DEBUG_ENABLED - HashSet<uint32_t> previously_ignored = parser->ignored_warning_codes; - for (uint32_t ignored_warning : member.function->ignored_warnings) { - parser->ignored_warning_codes.insert(ignored_warning); - } -#endif // DEBUG_ENABLED - resolve_function_body(member.function); - -#ifdef DEBUG_ENABLED - parser->ignored_warning_codes = previously_ignored; -#endif // DEBUG_ENABLED } else if (member.type == GDScriptParser::ClassNode::Member::VARIABLE && member.variable->property != GDScriptParser::VariableNode::PROP_NONE) { if (member.variable->property == GDScriptParser::VariableNode::PROP_INLINE) { if (member.variable->getter != nullptr) { @@ -1102,9 +1164,9 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co GDScriptParser::ClassNode::Member member = p_class->members[i]; if (member.type == GDScriptParser::ClassNode::Member::VARIABLE) { #ifdef DEBUG_ENABLED - HashSet<uint32_t> previously_ignored = parser->ignored_warning_codes; - for (uint32_t ignored_warning : member.function->ignored_warnings) { - parser->ignored_warning_codes.insert(ignored_warning); + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + for (GDScriptWarning::Code ignored_warning : member.variable->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); } if (member.variable->usages == 0 && String(member.variable->identifier->name).begins_with("_")) { parser->push_warning(member.variable->identifier, GDScriptWarning::UNUSED_PRIVATE_CLASS_VARIABLE, member.variable->identifier->name); @@ -1179,7 +1241,7 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co } } #ifdef DEBUG_ENABLED - parser->ignored_warning_codes = previously_ignored; + parser->ignored_warnings = previously_ignored_warnings; #endif // DEBUG_ENABLED } } @@ -1289,6 +1351,11 @@ void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node, bool p_is_root void GDScriptAnalyzer::resolve_annotation(GDScriptParser::AnnotationNode *p_annotation) { ERR_FAIL_COND_MSG(!parser->valid_annotations.has(p_annotation->name), vformat(R"(Annotation "%s" not found to validate.)", p_annotation->name)); + if (p_annotation->is_resolved) { + return; + } + p_annotation->is_resolved = true; + const MethodInfo &annotation_info = parser->valid_annotations[p_annotation->name].info; const List<PropertyInfo>::Element *E = annotation_info.arguments.front(); @@ -1355,6 +1422,13 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * } p_function->resolved_signature = true; +#ifdef DEBUG_ENABLED + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + for (GDScriptWarning::Code ignored_warning : p_function->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); + } +#endif + GDScriptParser::FunctionNode *previous_function = parser->current_function; parser->current_function = p_function; @@ -1421,7 +1495,8 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * int default_par_count = 0; bool is_static = false; bool is_vararg = false; - if (!p_is_lambda && get_function_signature(p_function, false, base_type, function_name, parent_return_type, parameters_types, default_par_count, is_static, is_vararg)) { + StringName native_base; + if (!p_is_lambda && get_function_signature(p_function, false, base_type, function_name, parent_return_type, parameters_types, default_par_count, is_static, is_vararg, &native_base)) { bool valid = p_function->is_static == is_static; valid = valid && parent_return_type == p_function->get_datatype(); @@ -1447,8 +1522,8 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * parameter = "Variant"; } parent_signature += parameter; - if (j == parameters_types.size() - default_par_count) { - parent_signature += " = default"; + if (j >= parameters_types.size() - default_par_count) { + parent_signature += " = <default>"; } j++; @@ -1464,6 +1539,11 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * push_error(vformat(R"(The function signature doesn't match the parent. Parent signature is "%s".)", parent_signature), p_function); } +#ifdef DEBUG_ENABLED + if (native_base != StringName()) { + parser->push_warning(p_function, GDScriptWarning::NATIVE_METHOD_OVERRIDE, function_name, native_base); + } +#endif } #endif // TOOLS_ENABLED } @@ -1472,6 +1552,9 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * p_function->set_datatype(prev_datatype); } +#ifdef DEBUG_ENABLED + parser->ignored_warnings = previously_ignored_warnings; +#endif parser->current_function = previous_function; } @@ -1481,6 +1564,13 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun } p_function->resolved_body = true; +#ifdef DEBUG_ENABLED + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + for (GDScriptWarning::Code ignored_warning : p_function->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); + } +#endif + GDScriptParser::FunctionNode *previous_function = parser->current_function; parser->current_function = p_function; @@ -1498,6 +1588,9 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun } } +#ifdef DEBUG_ENABLED + parser->ignored_warnings = previously_ignored_warnings; +#endif parser->current_function = previous_function; } @@ -1538,16 +1631,16 @@ void GDScriptAnalyzer::resolve_suite(GDScriptParser::SuiteNode *p_suite) { } #ifdef DEBUG_ENABLED - HashSet<uint32_t> previously_ignored = parser->ignored_warning_codes; - for (uint32_t ignored_warning : stmt->ignored_warnings) { - parser->ignored_warning_codes.insert(ignored_warning); + HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings; + for (GDScriptWarning::Code ignored_warning : stmt->ignored_warnings) { + parser->ignored_warnings.insert(ignored_warning); } #endif // DEBUG_ENABLED resolve_node(stmt); #ifdef DEBUG_ENABLED - parser->ignored_warning_codes = previously_ignored; + parser->ignored_warnings = previously_ignored_warnings; #endif // DEBUG_ENABLED decide_suite_type(p_suite, stmt); @@ -1599,6 +1692,11 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi } else if (initializer_type.kind == GDScriptParser::DataType::BUILTIN && initializer_type.builtin_type == Variant::NIL && !is_constant) { push_error(vformat(R"(Cannot infer the type of "%s" %s because the value is "null".)", p_assignable->identifier->name, p_kind), p_assignable->initializer); } +#ifdef DEBUG_ENABLED + if (initializer_type.is_hard_type() && initializer_type.is_variant()) { + parser->push_warning(p_assignable, GDScriptWarning::INFERENCE_ON_VARIANT, p_kind); + } +#endif } else { if (!initializer_type.is_set()) { push_error(vformat(R"(Could not resolve type for %s "%s".)", p_kind, p_assignable->identifier->name), p_assignable->initializer); @@ -2373,30 +2471,27 @@ void GDScriptAnalyzer::reduce_await(GDScriptParser::AwaitNode *p_await) { return; } - GDScriptParser::DataType awaiting_type; - if (p_await->to_await->type == GDScriptParser::Node::CALL) { reduce_call(static_cast<GDScriptParser::CallNode *>(p_await->to_await), true); - awaiting_type = p_await->to_await->get_datatype(); } else { reduce_expression(p_await->to_await); } - if (p_await->to_await->is_constant) { + GDScriptParser::DataType await_type = p_await->to_await->get_datatype(); + // We cannot infer the type of the result of waiting for a signal. + if (await_type.is_hard_type() && await_type.kind == GDScriptParser::DataType::BUILTIN && await_type.builtin_type == Variant::SIGNAL) { + await_type.kind = GDScriptParser::DataType::VARIANT; + await_type.type_source = GDScriptParser::DataType::UNDETECTED; + } else if (p_await->to_await->is_constant) { p_await->is_constant = p_await->to_await->is_constant; p_await->reduced_value = p_await->to_await->reduced_value; - - awaiting_type = p_await->to_await->get_datatype(); - } else { - awaiting_type.kind = GDScriptParser::DataType::VARIANT; - awaiting_type.type_source = GDScriptParser::DataType::UNDETECTED; } - - p_await->set_datatype(awaiting_type); + await_type.is_coroutine = false; + p_await->set_datatype(await_type); #ifdef DEBUG_ENABLED - awaiting_type = p_await->to_await->get_datatype(); - if (!(awaiting_type.has_no_type() || awaiting_type.is_coroutine || awaiting_type.builtin_type == Variant::SIGNAL)) { + GDScriptParser::DataType to_await_type = p_await->to_await->get_datatype(); + if (!(to_await_type.has_no_type() || to_await_type.is_coroutine || to_await_type.builtin_type == Variant::SIGNAL)) { parser->push_warning(p_await, GDScriptWarning::REDUNDANT_AWAIT); } #endif @@ -2953,7 +3048,11 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a // Enums do not have functions other than the built-in dictionary ones. if (base_type.kind == GDScriptParser::DataType::ENUM && base_type.is_meta_type) { - push_error(vformat(R"*(Enums only have Dictionary built-in methods. Function "%s()" does not exist for enum "%s".)*", p_call->function_name, base_type.enum_type), p_call->callee); + if (base_type.builtin_type == Variant::DICTIONARY) { + push_error(vformat(R"*(Enums only have Dictionary built-in methods. Function "%s()" does not exist for enum "%s".)*", p_call->function_name, base_type.enum_type), p_call->callee); + } else { + push_error(vformat(R"*(The native enum "%s" does not behave like Dictionary and does not have methods of its own.)*", base_type.enum_type), p_call->callee); + } } else if (!p_call->is_super && callee_type != GDScriptParser::Node::NONE) { // Check if the name exists as something else. GDScriptParser::IdentifierNode *callee_id; if (callee_type == GDScriptParser::Node::IDENTIFIER) { @@ -4011,7 +4110,6 @@ void GDScriptAnalyzer::reduce_ternary_op(GDScriptParser::TernaryOpNode *p_ternar if (!is_type_compatible(true_type, false_type)) { result = false_type; if (!is_type_compatible(false_type, true_type)) { - result.type_source = GDScriptParser::DataType::UNDETECTED; result.kind = GDScriptParser::DataType::VARIANT; #ifdef DEBUG_ENABLED parser->push_warning(p_ternary_op, GDScriptWarning::INCOMPATIBLE_TERNARY); @@ -4019,6 +4117,7 @@ void GDScriptAnalyzer::reduce_ternary_op(GDScriptParser::TernaryOpNode *p_ternar } } } + result.type_source = true_type.is_hard_type() && false_type.is_hard_type() ? GDScriptParser::DataType::ANNOTATED_INFERRED : GDScriptParser::DataType::INFERRED; p_ternary_op->set_datatype(result); } @@ -4334,15 +4433,27 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo } elem_type.is_constant = false; result.set_container_element_type(elem_type); + } else if (p_property.type == Variant::INT) { + // Check if it's enum. + if (p_property.class_name != StringName()) { + Vector<String> names = String(p_property.class_name).split("."); + if (names.size() == 2) { + result = make_native_enum_type(names[1], names[0], false); + result.is_constant = false; + } + } } } return result; } -bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType p_base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg) { +bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType p_base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg, StringName *r_native_class) { r_static = false; r_vararg = false; r_default_arg_count = 0; + if (r_native_class) { + *r_native_class = StringName(); + } StringName function_name = p_function; bool was_enum = false; @@ -4477,6 +4588,12 @@ bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bo if (valid && Engine::get_singleton()->has_singleton(base_native)) { r_static = true; } +#ifdef DEBUG_ENABLED + MethodBind *native_method = ClassDB::get_method(base_native, function_name); + if (native_method && r_native_class) { + *r_native_class = native_method->get_instance_class(); + } +#endif return valid; } diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h index 75d52509a4..a4c84db6b9 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -117,7 +117,7 @@ class GDScriptAnalyzer { static GDScriptParser::DataType type_from_metatype(const GDScriptParser::DataType &p_meta_type); GDScriptParser::DataType type_from_property(const PropertyInfo &p_property, bool p_is_arg = false, bool p_is_readonly = false) const; GDScriptParser::DataType make_global_class_meta_type(const StringName &p_class_name, const GDScriptParser::Node *p_source); - bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg); + bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg, StringName *r_native_class = nullptr); bool function_signature_from_info(const MethodInfo &p_info, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg); void validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call); void validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call); diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 12c10642ec..8cfd48b52b 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -800,6 +800,15 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a warning.insert_text = warning.display.quote(p_quote_style); r_result.insert(warning.display, warning); } + } else if (p_annotation->name == SNAME("@rpc")) { + if (p_argument == 0 || p_argument == 1 || p_argument == 2) { + static const char *options[7] = { "call_local", "call_remote", "any_peer", "authority", "reliable", "unreliable", "unreliable_ordered" }; + for (int i = 0; i < 7; i++) { + ScriptLanguage::CodeCompletionOption option(options[i], ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); + option.insert_text = option.display.quote(p_quote_style); + r_result.insert(option.display, option); + } + } } } @@ -968,7 +977,7 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, } break; case GDScriptParser::ClassNode::Member::SIGNAL: - if (p_only_functions || outer) { + if (p_only_functions || outer || p_static) { continue; } option = ScriptLanguage::CodeCompletionOption(member.signal->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); @@ -1024,6 +1033,14 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, location); r_result.insert(option.display, option); } + + List<MethodInfo> signals; + scr->get_script_signal_list(&signals); + for (const MethodInfo &E : signals) { + int location = p_recursion_depth + _get_signal_location(scr->get_class_name(), E.name); + ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); + r_result.insert(option.display, option); + } } HashMap<StringName, Variant> constants; scr->get_constants(&constants); @@ -1032,14 +1049,6 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base ScriptLanguage::CodeCompletionOption option(E.key.operator String(), ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, location); r_result.insert(option.display, option); } - - List<MethodInfo> signals; - scr->get_script_signal_list(&signals); - for (const MethodInfo &E : signals) { - int location = p_recursion_depth + _get_signal_location(scr->get_class_name(), E.name); - ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); - r_result.insert(option.display, option); - } } List<MethodInfo> methods; @@ -1084,14 +1093,6 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base r_result.insert(option.display, option); } - List<MethodInfo> signals; - ClassDB::get_signal_list(type, &signals); - for (const MethodInfo &E : signals) { - int location = p_recursion_depth + _get_signal_location(type, StringName(E.name)); - ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); - r_result.insert(option.display, option); - } - if (!base_type.is_meta_type || Engine::get_singleton()->has_singleton(type)) { List<PropertyInfo> pinfo; ClassDB::get_property_list(type, &pinfo); @@ -1106,6 +1107,14 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, location); r_result.insert(option.display, option); } + + List<MethodInfo> signals; + ClassDB::get_signal_list(type, &signals); + for (const MethodInfo &E : signals) { + int location = p_recursion_depth + _get_signal_location(type, StringName(E.name)); + ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); + r_result.insert(option.display, option); + } } } @@ -2022,6 +2031,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; r_type.type.script_path = script; r_type.type.class_type = parser->get_parser()->get_tree(); + r_type.type.is_meta_type = true; r_type.type.is_constant = false; r_type.type.kind = GDScriptParser::DataType::CLASS; r_type.value = Variant(); @@ -2133,6 +2143,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext & r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; r_type.type.kind = GDScriptParser::DataType::CLASS; r_type.type.class_type = member.m_class; + r_type.type.is_meta_type = true; return true; case GDScriptParser::ClassNode::Member::GROUP: return false; // No-op, but silences warnings. diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index ed2dce471f..0a1a64cb59 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -109,7 +109,7 @@ GDScriptParser::GDScriptParser() { // Warning annotations. register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS | AnnotationInfo::VARIABLE | AnnotationInfo::SIGNAL | AnnotationInfo::CONSTANT | AnnotationInfo::FUNCTION | AnnotationInfo::STATEMENT, &GDScriptParser::warning_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("", "", "", 0), true); + 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), true); #ifdef DEBUG_ENABLED is_ignoring_warnings = !(bool)GLOBAL_GET("debug/gdscript/warnings/enable"); @@ -158,14 +158,10 @@ void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_ return; } - if (ignored_warning_codes.has(p_code)) { + if (ignored_warnings.has(p_code)) { return; } - String warn_name = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)p_code).to_lower(); - if (ignored_warnings.has(warn_name)) { - return; - } int warn_level = (int)GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(p_code)); if (!warn_level) { return; @@ -180,7 +176,7 @@ void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_ warning.rightmost_column = p_source->rightmost_column; if (warn_level == GDScriptWarning::WarnLevel::ERROR || bool(GLOBAL_GET("debug/gdscript/warnings/treat_warnings_as_errors"))) { - push_error(warning.get_message(), p_source); + push_error(warning.get_message() + String(" (Warning treated as error.)"), p_source); return; } @@ -1837,10 +1833,18 @@ GDScriptParser::IfNode *GDScriptParser::parse_if(const String &p_token) { if (match(GDScriptTokenizer::Token::ELIF)) { SuiteNode *else_block = alloc_node<SuiteNode>(); + else_block->parent_function = current_function; + else_block->parent_block = current_suite; + + SuiteNode *previous_suite = current_suite; + current_suite = else_block; + IfNode *elif = parse_if("elif"); else_block->statements.push_back(elif); complete_extents(else_block); n_if->false_block = else_block; + + current_suite = previous_suite; } else if (match(GDScriptTokenizer::Token::ELSE)) { consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "else".)"); n_if->false_block = parse_suite(R"("else" block)"); @@ -3548,7 +3552,11 @@ const GDScriptParser::SuiteNode::Local &GDScriptParser::SuiteNode::get_local(con return empty; } -bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_target) const { +bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_target) { + if (is_applied) { + return true; + } + is_applied = true; return (p_this->*(p_this->valid_annotations[name].apply))(this, p_target); } @@ -3611,6 +3619,10 @@ bool GDScriptParser::icon_annotation(const AnnotationNode *p_annotation, Node *p bool GDScriptParser::onready_annotation(const AnnotationNode *p_annotation, Node *p_node) { ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)"); + if (current_class && !ClassDB::is_parent_class(current_class->get_datatype().native_type, SNAME("Node"))) { + push_error(R"("@onready" can only be used in classes that inherit "Node".)", p_annotation); + } + VariableNode *variable = static_cast<VariableNode *>(p_node); if (variable->onready) { push_error(R"("@onready" annotation can only be used once per variable.)"); @@ -3685,6 +3697,13 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node variable->export_info.type = Variant::DICTIONARY; return true; + } else if (export_type.builtin_type == Variant::PACKED_STRING_ARRAY) { + String hint_prefix = itos(Variant::STRING) + "/" + itos(variable->export_info.hint); + variable->export_info.hint = PROPERTY_HINT_TYPE_STRING; + variable->export_info.hint_string = hint_prefix + ":" + variable->export_info.hint_string; + variable->export_info.type = Variant::PACKED_STRING_ARRAY; + + return true; } } @@ -3902,26 +3921,46 @@ bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_ push_error(R"(Invalid RPC arguments. At most 4 arguments are allowed, where only the last argument can be an integer to specify the channel.')", p_annotation); return false; } + + unsigned char locality_args = 0; + unsigned char permission_args = 0; + unsigned char transfer_mode_args = 0; + for (int i = last; i >= 0; i--) { - String mode = p_annotation->resolved_arguments[i].operator String(); - if (mode == "any_peer") { - rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_ANY_PEER; - } else if (mode == "authority") { - rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_AUTHORITY; - } else if (mode == "call_local") { + String arg = p_annotation->resolved_arguments[i].operator String(); + if (arg == "call_local") { + locality_args++; rpc_config["call_local"] = true; - } else if (mode == "call_remote") { + } else if (arg == "call_remote") { + locality_args++; rpc_config["call_local"] = false; - } else if (mode == "reliable") { + } else if (arg == "any_peer") { + permission_args++; + rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_ANY_PEER; + } else if (arg == "authority") { + permission_args++; + rpc_config["rpc_mode"] = MultiplayerAPI::RPC_MODE_AUTHORITY; + } else if (arg == "reliable") { + transfer_mode_args++; rpc_config["transfer_mode"] = MultiplayerPeer::TRANSFER_MODE_RELIABLE; - } else if (mode == "unreliable") { + } else if (arg == "unreliable") { + transfer_mode_args++; rpc_config["transfer_mode"] = MultiplayerPeer::TRANSFER_MODE_UNRELIABLE; - } else if (mode == "unreliable_ordered") { + } else if (arg == "unreliable_ordered") { + transfer_mode_args++; rpc_config["transfer_mode"] = MultiplayerPeer::TRANSFER_MODE_UNRELIABLE_ORDERED; } else { - push_error(R"(Invalid RPC argument. Must be one of: 'call_local'/'call_remote' (local calls), 'any_peer'/'authority' (permission), 'reliable'/'unreliable'/'unreliable_ordered' (transfer mode).)", p_annotation); + push_error(R"(Invalid RPC argument. Must be one of: "call_local"/"call_remote" (local calls), "any_peer"/"authority" (permission), "reliable"/"unreliable"/"unreliable_ordered" (transfer mode).)", p_annotation); } } + + if (locality_args > 1) { + push_error(R"(Invalid RPC config. The locality ("call_local"/"call_remote") must be specified no more than once.)", p_annotation); + } else if (permission_args > 1) { + push_error(R"(Invalid RPC config. The permission ("any_peer"/"authority") must be specified no more than once.)", p_annotation); + } else if (transfer_mode_args > 1) { + push_error(R"(Invalid RPC config. The transfer mode ("reliable"/"unreliable"/"unreliable_ordered") must be specified no more than once.)", p_annotation); + } } function->rpc_config = rpc_config; return true; diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index bc0fe58fa7..0ba0d5b6da 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -299,7 +299,9 @@ public: int leftmost_column = 0, rightmost_column = 0; Node *next = nullptr; List<AnnotationNode *> annotations; - Vector<uint32_t> ignored_warnings; +#ifdef DEBUG_ENABLED + Vector<GDScriptWarning::Code> ignored_warnings; +#endif DataType datatype; @@ -331,8 +333,10 @@ public: AnnotationInfo *info = nullptr; PropertyInfo export_info; + bool is_resolved = false; + bool is_applied = false; - bool apply(GDScriptParser *p_this, Node *p_target) const; + bool apply(GDScriptParser *p_this, Node *p_target); bool applies_to(uint32_t p_target_kinds) const; AnnotationNode() { @@ -1265,8 +1269,7 @@ private: #ifdef DEBUG_ENABLED bool is_ignoring_warnings = false; List<GDScriptWarning> warnings; - HashSet<String> ignored_warnings; - HashSet<uint32_t> ignored_warning_codes; + HashSet<GDScriptWarning::Code> ignored_warnings; HashSet<int> unsafe_lines; #endif diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index 9436146bed..ef59a07f1a 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -170,6 +170,21 @@ String GDScriptWarning::get_message() const { case RENAMED_IN_GD4_HINT: { break; // Renamed identifier hint is taken care of by the GDScriptAnalyzer. No message needed here. } + case INFERENCE_ON_VARIANT: { + CHECK_SYMBOLS(1); + return vformat("The %s type is being inferred from a Variant value, so it will be typed as Variant.", symbols[0]); + } + case NATIVE_METHOD_OVERRIDE: { + CHECK_SYMBOLS(2); + return vformat(R"(The method "%s" overrides a method from native class "%s". This won't be called by the engine and may not work as expected.)", symbols[0], symbols[1]); + } + case GET_NODE_DEFAULT_WITHOUT_ONREADY: { + CHECK_SYMBOLS(1); + return vformat(R"*(The default value is using "%s" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this.)*", symbols[0]); + } + case ONREADY_WITH_EXPORT: { + return R"(The "@onready" annotation will make the default value to be set after the "@export" takes effect and will override it.)"; + } case WARNING_MAX: break; // Can't happen, but silences warning } @@ -179,14 +194,8 @@ String GDScriptWarning::get_message() const { } int GDScriptWarning::get_default_value(Code p_code) { - if (get_name_from_code(p_code).to_lower().begins_with("unsafe_")) { - return WarnLevel::IGNORE; - } - // Too spammy by default on common cases (connect, Tween, etc.). - if (p_code == RETURN_VALUE_DISCARDED) { - return WarnLevel::IGNORE; - } - return WarnLevel::WARN; + ERR_FAIL_INDEX_V_MSG(p_code, WARNING_MAX, WarnLevel::IGNORE, "Getting default value of invalid warning code."); + return default_warning_levels[p_code]; } PropertyInfo GDScriptWarning::get_property_info(Code p_code) { @@ -240,7 +249,11 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "INT_AS_ENUM_WITHOUT_MATCH", "STATIC_CALLED_ON_INSTANCE", "CONFUSABLE_IDENTIFIER", - "RENAMED_IN_GODOT_4_HINT" + "RENAMED_IN_GODOT_4_HINT", + "INFERENCE_ON_VARIANT", + "NATIVE_METHOD_OVERRIDE", + "GET_NODE_DEFAULT_WITHOUT_ONREADY", + "ONREADY_WITH_EXPORT", }; static_assert((sizeof(names) / sizeof(*names)) == WARNING_MAX, "Amount of warning types don't match the amount of warning names."); diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index fa2907cdae..f0123c518c 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -82,9 +82,58 @@ public: STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself. CONFUSABLE_IDENTIFIER, // The identifier contains misleading characters that can be confused. E.g. "usеr" (has Cyrillic "е" instead of Latin "e"). RENAMED_IN_GD4_HINT, // A variable or function that could not be found has been renamed in Godot 4 + INFERENCE_ON_VARIANT, // The declaration uses type inference but the value is typed as Variant. + NATIVE_METHOD_OVERRIDE, // The script method overrides a native one, this may not work as intended. + GET_NODE_DEFAULT_WITHOUT_ONREADY, // A class variable uses `get_node()` (or the `$` notation) as its default value, but does not use the @onready annotation. + ONREADY_WITH_EXPORT, // The `@onready` annotation will set the value after `@export` which is likely not intended. WARNING_MAX, }; + constexpr static WarnLevel default_warning_levels[] = { + WARN, // UNASSIGNED_VARIABLE + WARN, // UNASSIGNED_VARIABLE_OP_ASSIGN + WARN, // UNUSED_VARIABLE + WARN, // UNUSED_LOCAL_CONSTANT + WARN, // SHADOWED_VARIABLE + WARN, // SHADOWED_VARIABLE_BASE_CLASS + WARN, // UNUSED_PRIVATE_CLASS_VARIABLE + WARN, // UNUSED_PARAMETER + WARN, // UNREACHABLE_CODE + WARN, // UNREACHABLE_PATTERN + WARN, // STANDALONE_EXPRESSION + WARN, // NARROWING_CONVERSION + WARN, // INCOMPATIBLE_TERNARY + WARN, // UNUSED_SIGNAL + IGNORE, // RETURN_VALUE_DISCARDED // Too spammy by default on common cases (connect, Tween, etc.). + WARN, // PROPERTY_USED_AS_FUNCTION + WARN, // CONSTANT_USED_AS_FUNCTION + WARN, // FUNCTION_USED_AS_PROPERTY + WARN, // INTEGER_DIVISION + IGNORE, // UNSAFE_PROPERTY_ACCESS // Too common in untyped scenarios. + IGNORE, // UNSAFE_METHOD_ACCESS // Too common in untyped scenarios. + IGNORE, // UNSAFE_CAST // Too common in untyped scenarios. + IGNORE, // UNSAFE_CALL_ARGUMENT // Too common in untyped scenarios. + WARN, // UNSAFE_VOID_RETURN + WARN, // DEPRECATED_KEYWORD + WARN, // STANDALONE_TERNARY + WARN, // ASSERT_ALWAYS_TRUE + WARN, // ASSERT_ALWAYS_FALSE + WARN, // REDUNDANT_AWAIT + WARN, // EMPTY_FILE + WARN, // SHADOWED_GLOBAL_IDENTIFIER + WARN, // INT_AS_ENUM_WITHOUT_CAST + WARN, // INT_AS_ENUM_WITHOUT_MATCH + WARN, // STATIC_CALLED_ON_INSTANCE + WARN, // CONFUSABLE_IDENTIFIER + WARN, // RENAMED_IN_GD4_HINT + ERROR, // INFERENCE_ON_VARIANT // Most likely done by accident, usually inference is trying for a particular type. + ERROR, // NATIVE_METHOD_OVERRIDE // May not work as expected. + ERROR, // GET_NODE_DEFAULT_WITHOUT_ONREADY // May not work as expected. + ERROR, // ONREADY_WITH_EXPORT // May not work as expected. + }; + + static_assert((sizeof(default_warning_levels) / sizeof(default_warning_levels[0])) == WARNING_MAX, "Amount of default levels does not match the amount of warnings."); + Code code = WARNING_MAX; int start_line = -1, end_line = -1; int leftmost_column = -1, rightmost_column = -1; diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp index 5b8af0ff34..57405aa1ce 100644 --- a/modules/gdscript/tests/gdscript_test_runner.cpp +++ b/modules/gdscript/tests/gdscript_test_runner.cpp @@ -146,11 +146,11 @@ GDScriptTestRunner::GDScriptTestRunner(const String &p_source_dir, bool p_init_l init_language(p_source_dir); } #ifdef DEBUG_ENABLED - // Enable all warnings for GDScript, so we can test them. + // Set all warning levels to "Warn" in order to test them properly, even the ones that default to error. ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/enable", true); for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) { - String warning = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)i).to_lower(); - ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/" + warning, true); + String warning_setting = GDScriptWarning::get_settings_path_from_code((GDScriptWarning::Code)i); + ProjectSettings::get_singleton()->set_setting(warning_setting, (int)GDScriptWarning::WARN); } #endif diff --git a/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.gd b/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.gd new file mode 100644 index 0000000000..c787d9e50e --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.gd @@ -0,0 +1,4 @@ +signal my_signal() + +func test(): + var _a := await my_signal diff --git a/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.out b/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.out new file mode 100644 index 0000000000..8f8744ad7e --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/await_signal_no_infer.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot infer the type of "_a" variable because the value doesn't have a set type. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/function_dont_match_parent_signature_parameter_default_values.out b/modules/gdscript/tests/scripts/analyzer/errors/function_dont_match_parent_signature_parameter_default_values.out index c70a1df10d..508e46742f 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/function_dont_match_parent_signature_parameter_default_values.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/function_dont_match_parent_signature_parameter_default_values.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -The function signature doesn't match the parent. Parent signature is "my_function(int = default) -> int". +The function signature doesn't match the parent. Parent signature is "my_function(int = <default>) -> int". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.gd b/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.gd new file mode 100644 index 0000000000..91f5071fa9 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.gd @@ -0,0 +1,5 @@ +extends RefCounted + +func test(): + var nope := $Node + print("Cannot use $ without a Node base") diff --git a/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.out b/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.out new file mode 100644 index 0000000000..33365908bf --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/get_node_shorthand_within_non_node.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot use shorthand "get_node()" notation ("$") on a class that isn't a node. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.gd b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.gd new file mode 100644 index 0000000000..e781315266 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.gd @@ -0,0 +1,6 @@ +extends RefCounted + +@onready var nope := 0 + +func test(): + print("Cannot use @onready without a Node base") diff --git a/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.out b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.out new file mode 100644 index 0000000000..8088d28329 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +"@onready" can only be used in classes that inherit "Node". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.gd b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.gd new file mode 100644 index 0000000000..1639bbbd52 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.gd @@ -0,0 +1,7 @@ +extends Node + +class Inner extends RefCounted: + @onready var nope = 0 + +func test(): + print("Cannot use @onready without a Node base") diff --git a/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.out b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.out new file mode 100644 index 0000000000..8088d28329 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/onready_within_non_node_inner_class.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +"@onready" can only be used in classes that inherit "Node". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.gd b/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.gd new file mode 100644 index 0000000000..fac0e8756c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.gd @@ -0,0 +1,6 @@ +func test(): + var left_hard_int := 1 + var right_weak_int = 2 + var result_hm_int := left_hard_int if true else right_weak_int + + print('not ok') diff --git a/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.out b/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.out new file mode 100644 index 0000000000..71d1e2f8ae --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/ternary_weak_infer.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot infer the type of "result_hm_int" variable because the value doesn't have a set type. diff --git a/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.gd b/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.gd new file mode 100644 index 0000000000..a9004a346b --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.gd @@ -0,0 +1,18 @@ +extends Node + +@onready var shorthand = $Node +@onready var call = get_node(^"Node") +@onready var shorthand_with_cast = $Node as Node +@onready var call_with_cast = get_node(^"Node") as Node + +func _init(): + var node := Node.new() + node.name = "Node" + add_child(node) + +func test(): + # Those are expected to be `null` since `_ready()` is never called on tests. + prints("shorthand", shorthand) + prints("call", call) + prints("shorthand_with_cast", shorthand_with_cast) + prints("call_with_cast", call_with_cast) diff --git a/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.out b/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.out new file mode 100644 index 0000000000..eddc2deec0 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/allow_get_node_with_onready.out @@ -0,0 +1,5 @@ +GDTEST_OK +shorthand <null> +call <null> +shorthand_with_cast <null> +call_with_cast <null> diff --git a/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.gd b/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.gd new file mode 100644 index 0000000000..02120db868 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.gd @@ -0,0 +1,13 @@ +# https://github.com/godotengine/godot/issues/72501 +extends Node + +func test(): + prints("before", process_mode) + process_mode = PROCESS_MODE_PAUSABLE + prints("after", process_mode) + + var node := Node.new() + add_child(node) + prints("before", node.process_mode) + node.process_mode = PROCESS_MODE_PAUSABLE + prints("after", node.process_mode) diff --git a/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.out b/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.out new file mode 100644 index 0000000000..1eb045a4e4 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/assign_to_native_enum_property.out @@ -0,0 +1,5 @@ +GDTEST_OK +before 0 +after 1 +before 0 +after 1 diff --git a/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.gd b/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.gd new file mode 100644 index 0000000000..9d8cfc7f99 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.gd @@ -0,0 +1,15 @@ +func coroutine() -> int: + @warning_ignore("redundant_await") + await 0 + return 1 + +func not_coroutine() -> int: + return 2 + +func test(): + var a := await coroutine() + @warning_ignore("redundant_await") + var b := await not_coroutine() + @warning_ignore("redundant_await") + var c := await 3 + prints(a, b, c) diff --git a/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.out b/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.out new file mode 100644 index 0000000000..2920e2ce9c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/await_type_inference.out @@ -0,0 +1,2 @@ +GDTEST_OK +1 2 3 diff --git a/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd b/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd index 48a804ff54..b447180ea8 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd @@ -2,16 +2,18 @@ func variant() -> Variant: return null var member_weak = variant() var member_typed: Variant = variant() +@warning_ignore("inference_on_variant") var member_inferred := variant() func param_weak(param = variant()) -> void: print(param) func param_typed(param: Variant = variant()) -> void: print(param) +@warning_ignore("inference_on_variant") func param_inferred(param := variant()) -> void: print(param) func return_untyped(): return variant() func return_typed() -> Variant: return variant() -@warning_ignore("unused_variable") +@warning_ignore("unused_variable", "inference_on_variant") func test() -> void: var weak = variant() var typed: Variant = variant() diff --git a/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.gd b/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.gd new file mode 100644 index 0000000000..1ac03c2181 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.gd @@ -0,0 +1,7 @@ +extends RefCounted + +func test(): + print("ok") + +class Inner extends Node: + @onready var okay = 0 diff --git a/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.out b/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/onready_on_inner_class_with_non_node_outer.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd new file mode 100644 index 0000000000..44ca5f4dd0 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd @@ -0,0 +1,15 @@ +func test(): + var left_hard_int := 1 + var right_hard_int := 2 + var result_hard_int := left_hard_int if true else right_hard_int + assert(result_hard_int == 1) + + @warning_ignore("inference_on_variant") + var left_hard_variant := 1 as Variant + @warning_ignore("inference_on_variant") + var right_hard_variant := 2.0 as Variant + @warning_ignore("inference_on_variant") + var result_hard_variant := left_hard_variant if true else right_hard_variant + assert(result_hard_variant == 1) + + print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.out b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd b/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd index e1e6134fd4..092ae49d00 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd @@ -201,4 +201,10 @@ func test(): assert(typed_enums.get_typed_builtin() == TYPE_INT) + var a := A.new() + var typed_natives: Array[RefCounted] = [a] + var typed_scripts = Array(typed_natives, TYPE_OBJECT, "RefCounted", A) + assert(typed_scripts[0] == a) + + print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.gd b/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.gd new file mode 100644 index 0000000000..849df0921e --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.gd @@ -0,0 +1,17 @@ +extends Node + +var add_node = do_add_node() # Hack to have one node on init and not fail at runtime. + +var shorthand = $Node +var with_self = self.get_node(^"Node") +var without_self = get_node(^"Node") +var with_cast = get_node(^"Node") as Node +var shorthand_with_cast = $Node as Node + +func test(): + print("warn") + +func do_add_node(): + var node = Node.new() + node.name = "Node" + add_child(node) diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.out b/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.out new file mode 100644 index 0000000000..62b3ae291f --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/get_node_without_onready.out @@ -0,0 +1,22 @@ +GDTEST_OK +>> WARNING +>> Line: 5 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "$" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +>> WARNING +>> Line: 6 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "get_node()" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +>> WARNING +>> Line: 7 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "get_node()" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +>> WARNING +>> Line: 8 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "get_node()" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +>> WARNING +>> Line: 9 +>> GET_NODE_DEFAULT_WITHOUT_ONREADY +>> The default value is using "$" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. +warn diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.gd b/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.gd new file mode 100644 index 0000000000..024e91b7c6 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.gd @@ -0,0 +1,6 @@ +func test(): + var inferred_with_variant := return_variant() + print(inferred_with_variant) + +func return_variant() -> Variant: + return "warn" diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.out b/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.out new file mode 100644 index 0000000000..1d4078d2f7 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/inference_with_variant.out @@ -0,0 +1,6 @@ +GDTEST_OK +>> WARNING +>> Line: 2 +>> INFERENCE_ON_VARIANT +>> The variable type is being inferred from a Variant value, so it will be typed as Variant. +warn diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.gd b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.gd new file mode 100644 index 0000000000..0b358ca5f2 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.gd @@ -0,0 +1,6 @@ +extends Node + +@onready @export var conflict = "" + +func test(): + print("warn") diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out new file mode 100644 index 0000000000..ff184f9f04 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out @@ -0,0 +1,6 @@ +GDTEST_OK +>> WARNING +>> Line: 3 +>> ONREADY_WITH_EXPORT +>> The "@onready" annotation will make the default value to be set after the "@export" takes effect and will override it. +warn diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.gd b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.gd new file mode 100644 index 0000000000..19d40f8ec8 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.gd @@ -0,0 +1,5 @@ +func test(): + print("warn") + +func get(_property: StringName) -> Variant: + return null diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out new file mode 100644 index 0000000000..793faa05d4 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out @@ -0,0 +1,6 @@ +GDTEST_OK +>> WARNING +>> Line: 4 +>> NATIVE_METHOD_OVERRIDE +>> The method "get" overrides a method from native class "Object". This won't be called by the engine and may not work as expected. +warn diff --git a/modules/gltf/editor/editor_import_blend_runner.cpp b/modules/gltf/editor/editor_import_blend_runner.cpp index c203a91834..659a60e6a1 100644 --- a/modules/gltf/editor/editor_import_blend_runner.cpp +++ b/modules/gltf/editor/editor_import_blend_runner.cpp @@ -181,7 +181,18 @@ Error EditorImportBlendRunner::start_blender(const String &p_python_script, bool Error EditorImportBlendRunner::do_import(const Dictionary &p_options) { if (is_using_rpc()) { - return do_import_rpc(p_options); + Error err = do_import_rpc(p_options); + if (err != OK) { + // Retry without using RPC (slow, but better than the import failing completely). + if (err == ERR_CONNECTION_ERROR) { + // Disable RPC if the connection could not be established. + print_error(vformat("Failed to connect to Blender via RPC, switching to direct imports of .blend files. Check your proxy and firewall settings, then RPC can be re-enabled by changing the editor setting `filesystem/import/blender/rpc_port` to %d.", rpc_port)); + EditorSettings::get_singleton()->set_manually("filesystem/import/blender/rpc_port", 0); + rpc_port = 0; + } + err = do_import_direct(p_options); + } + return err; } else { return do_import_direct(p_options); } diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 1a09b5bdcc..bd3ee1881f 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -54,6 +54,9 @@ #include "modules/modules_enabled.gen.h" // For csg, gridmap. +#ifdef TOOLS_ENABLED +#include "editor/editor_file_system.h" +#endif #ifdef MODULE_CSG_ENABLED #include "modules/csg/csg_shape.h" #endif // MODULE_CSG_ENABLED @@ -3232,54 +3235,38 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_p p_state->source_images.push_back(Ref<Image>()); } else { Error err = OK; - bool must_import = false; + bool must_import = true; + Vector<uint8_t> img_data = img->get_data(); + Dictionary generator_parameters; String file_path = p_state->get_base_path() + "/" + p_state->filename.get_basename() + "_" + img->get_name() + ".png"; - if (!FileAccess::exists(file_path + ".import")) { + if (FileAccess::exists(file_path + ".import")) { Ref<ConfigFile> config; config.instantiate(); - config->set_value("remap", "importer", "texture"); - config->set_value("remap", "type", "Texture2D"); - // Currently, it will likely use project defaults of Detect 3D, so textures will be reimported again. - if (!config->has_section_key("params", "mipmaps/generate")) { - config->set_value("params", "mipmaps/generate", true); + config->load(file_path + ".import"); + if (config->has_section_key("remap", "generator_parameters")) { + generator_parameters = (Dictionary)config->get_value("remap", "generator_parameters"); } - - if (ProjectSettings::get_singleton()->has_setting("importer_defaults/texture")) { - //use defaults if exist - Dictionary importer_defaults = GLOBAL_GET("importer_defaults/texture"); - List<Variant> importer_def_keys; - importer_defaults.get_key_list(&importer_def_keys); - for (const Variant &key : importer_def_keys) { - if (!config->has_section_key("params", (String)key)) { - config->set_value("params", (String)key, importer_defaults[key]); - } - } + if (!generator_parameters.has("md5")) { + must_import = false; // Didn't come form a gltf document; don't overwrite. } - err = config->save(file_path + ".import"); - ERR_FAIL_COND_V(err != OK, err); - must_import = true; - } - Vector<uint8_t> png_buffer = img->save_png_to_buffer(); - if (ResourceLoader::exists(file_path)) { - Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::READ, &err); - if (err == OK && file.is_valid()) { - Vector<uint8_t> orig_png_buffer = file->get_buffer(file->get_length()); - if (png_buffer != orig_png_buffer) { - must_import = true; - } + String existing_md5 = generator_parameters["md5"]; + unsigned char md5_hash[16]; + CryptoCore::md5(img_data.ptr(), img_data.size(), md5_hash); + String new_md5 = String::hex_encode_buffer(md5_hash, 16); + generator_parameters["md5"] = new_md5; + if (new_md5 == existing_md5) { + must_import = false; } - } else { - must_import = true; } if (must_import) { - Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::WRITE, &err); + err = img->save_png(file_path); ERR_FAIL_COND_V(err != OK, err); - ERR_FAIL_COND_V(file.is_null(), FAILED); - file->store_buffer(png_buffer); - file->flush(); - file.unref(); // ResourceLoader::import will crash if not is_editor_hint(), so this case is protected above and will fall through to uncompressed. - ResourceLoader::import(file_path); + HashMap<StringName, Variant> custom_options; + custom_options[SNAME("mipmaps/generate")] = true; + // Will only use project settings defaults if custom_importer is empty. + EditorFileSystem::get_singleton()->update_file(file_path); + EditorFileSystem::get_singleton()->reimport_append(file_path, custom_options, String(), generator_parameters); } Ref<Texture2D> saved_image = ResourceLoader::load(file_path, "Texture2D"); if (saved_image.is_valid()) { diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs index 02d0226e90..93baf4e51c 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs +++ b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs @@ -30,7 +30,7 @@ namespace GodotPlugins if (baseDirectory != null) { if (!Path.EndsInDirectorySeparator(baseDirectory)) - baseDirectory += Path.PathSeparator; + baseDirectory += Path.DirectorySeparatorChar; // This SetData call effectively sets AppContext.BaseDirectory // See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/AppContext.cs#L21-L25 AppDomain.CurrentDomain.SetData("APP_CONTEXT_BASE_DIRECTORY", baseDirectory); diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 9b474bf2ce..b55188ce0c 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -6118,20 +6118,22 @@ int64_t TextServerAdvanced::_is_confusable(const String &p_string, const PackedS Vector<UChar *> skeletons; skeletons.resize(p_dict.size()); - USpoofChecker *sc = uspoof_open(&status); - uspoof_setChecks(sc, USPOOF_CONFUSABLE, &status); + if (sc_conf == nullptr) { + sc_conf = uspoof_open(&status); + uspoof_setChecks(sc_conf, USPOOF_CONFUSABLE, &status); + } for (int i = 0; i < p_dict.size(); i++) { Char16String word = p_dict[i].utf16(); - int32_t len = uspoof_getSkeleton(sc, 0, word.get_data(), -1, NULL, 0, &status); + int32_t len = uspoof_getSkeleton(sc_conf, 0, word.get_data(), -1, NULL, 0, &status); skeletons.write[i] = (UChar *)memalloc(++len * sizeof(UChar)); status = U_ZERO_ERROR; - uspoof_getSkeleton(sc, 0, word.get_data(), -1, skeletons.write[i], len, &status); + uspoof_getSkeleton(sc_conf, 0, word.get_data(), -1, skeletons.write[i], len, &status); } - int32_t len = uspoof_getSkeleton(sc, 0, utf16.get_data(), -1, NULL, 0, &status); + int32_t len = uspoof_getSkeleton(sc_conf, 0, utf16.get_data(), -1, NULL, 0, &status); UChar *skel = (UChar *)memalloc(++len * sizeof(UChar)); status = U_ZERO_ERROR; - uspoof_getSkeleton(sc, 0, utf16.get_data(), -1, skel, len, &status); + uspoof_getSkeleton(sc_conf, 0, utf16.get_data(), -1, skel, len, &status); for (int i = 0; i < skeletons.size(); i++) { if (u_strcmp(skel, skeletons[i]) == 0) { match_index = i; @@ -6143,7 +6145,6 @@ int64_t TextServerAdvanced::_is_confusable(const String &p_string, const PackedS for (int i = 0; i < skeletons.size(); i++) { memfree(skeletons.write[i]); } - uspoof_close(sc); ERR_FAIL_COND_V_MSG(U_FAILURE(status), -1, u_errorName(status)); @@ -6159,19 +6160,18 @@ bool TextServerAdvanced::_spoof_check(const String &p_string) const { UErrorCode status = U_ZERO_ERROR; Char16String utf16 = p_string.utf16(); - USet *allowed = uset_openEmpty(); - uset_addAll(allowed, uspoof_getRecommendedSet(&status)); - uset_addAll(allowed, uspoof_getInclusionSet(&status)); - - USpoofChecker *sc = uspoof_open(&status); - uspoof_setAllowedChars(sc, allowed, &status); - uspoof_setRestrictionLevel(sc, USPOOF_MODERATELY_RESTRICTIVE); - - int32_t bitmask = uspoof_check(sc, utf16.get_data(), -1, NULL, &status); - - uspoof_close(sc); - uset_close(allowed); + if (allowed == nullptr) { + allowed = uset_openEmpty(); + uset_addAll(allowed, uspoof_getRecommendedSet(&status)); + uset_addAll(allowed, uspoof_getInclusionSet(&status)); + } + if (sc_spoof == nullptr) { + sc_spoof = uspoof_open(&status); + uspoof_setAllowedChars(sc_spoof, allowed, &status); + uspoof_setRestrictionLevel(sc_spoof, USPOOF_MODERATELY_RESTRICTIVE); + } + int32_t bitmask = uspoof_check(sc_spoof, utf16.get_data(), -1, NULL, &status); ERR_FAIL_COND_V_MSG(U_FAILURE(status), false, u_errorName(status)); return (bitmask != 0); @@ -6587,5 +6587,17 @@ TextServerAdvanced::~TextServerAdvanced() { FT_Done_FreeType(ft_library); } #endif + if (sc_spoof != nullptr) { + uspoof_close(sc_spoof); + sc_spoof = nullptr; + } + if (sc_conf != nullptr) { + uspoof_close(sc_conf); + sc_conf = nullptr; + } + if (allowed != nullptr) { + uset_close(allowed); + allowed = nullptr; + } u_cleanup(); } diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index c7fe46d554..1acf5b21f0 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -159,6 +159,9 @@ class TextServerAdvanced : public TextServerExtension { // ICU support data. bool icu_data_loaded = false; + mutable USet *allowed = nullptr; + mutable USpoofChecker *sc_spoof = nullptr; + mutable USpoofChecker *sc_conf = nullptr; // Font cache data. |