diff options
Diffstat (limited to 'modules/gdscript')
56 files changed, 823 insertions, 210 deletions
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 32acad76aa..923b2fe30d 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -304,7 +304,7 @@ <return type="void" /> <param index="0" name="names" type="String" /> <description> - Export an [int] or [String] property as an enumerated list of options. If the property is an [int], then the index of the value is stored, in the same order the values are provided. You can add specific identifiers for allowed values using a colon. If the property is a [String], then the value is stored. + Export an [int] or [String] property as an enumerated list of options. If the property is an [int], then the index of the value is stored, in the same order the values are provided. You can add explicit values using a colon. If the property is a [String], then the value is stored. See also [constant PROPERTY_HINT_ENUM]. [codeblock] @export_enum("Warrior", "Magician", "Thief") var character_class: int @@ -357,6 +357,20 @@ [codeblock] @export_flags("Fire", "Water", "Earth", "Wind") var spell_elements = 0 [/codeblock] + You can add explicit values using a colon: + [codeblock] + @export_flags("Self:4", "Allies:8", "Foes:16") var spell_targets = 0 + [/codeblock] + You can also combine several flags: + [codeblock] + @export_flags("Self:4", "Allies:8", "Self and Allies:12", "Foes:16") + var spell_targets = 0 + [/codeblock] + [b]Note:[/b] A flag value must be at least [code]1[/code] and at most [code]2 ** 32 - 1[/code]. + [b]Note:[/b] Unlike [annotation @export_enum], the previous explicit value is not taken into account. In the following example, A is 16, B is 2, C is 4. + [codeblock] + @export_flags("A:16", "B", "C") var x + [/codeblock] </description> </annotation> <annotation name="@export_flags_2d_navigation"> @@ -565,14 +579,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.cpp b/modules/gdscript/gdscript.cpp index 8324cb0fe0..fe79f37454 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -1496,7 +1496,12 @@ GDScript::~GDScript() { // Order matters since clearing the stack may already cause // the GDScriptFunctionState to be destroyed and thus removed from the list. pending_func_states.remove(E); - E->self()->_clear_stack(); + GDScriptFunctionState *state = E->self(); + ObjectID state_id = state->get_instance_id(); + state->_clear_connections(); + if (ObjectDB::get_instance(state_id)) { + state->_clear_stack(); + } } } @@ -1920,7 +1925,12 @@ GDScriptInstance::~GDScriptInstance() { // Order matters since clearing the stack may already cause // the GDSCriptFunctionState to be destroyed and thus removed from the list. pending_func_states.remove(E); - E->self()->_clear_stack(); + GDScriptFunctionState *state = E->self(); + ObjectID state_id = state->get_instance_id(); + state->_clear_connections(); + if (ObjectDB::get_instance(state_id)) { + state->_clear_stack(); + } } if (script.is_valid() && owner) { @@ -2568,7 +2578,6 @@ GDScriptLanguage::GDScriptLanguage() { #ifdef DEBUG_ENABLED GLOBAL_DEF("debug/gdscript/warnings/enable", true); - GLOBAL_DEF("debug/gdscript/warnings/treat_warnings_as_errors", false); GLOBAL_DEF("debug/gdscript/warnings/exclude_addons", true); for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) { GDScriptWarning::Code code = (GDScriptWarning::Code)i; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index fd04d3c660..de0dacece3 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -42,6 +42,11 @@ #include "gdscript_utility_functions.h" #include "scene/resources/packed_scene.h" +#if defined(TOOLS_ENABLED) && !defined(DISABLE_DEPRECATED) +#define SUGGEST_GODOT4_RENAMES +#include "editor/renames_map_3_to_4.h" +#endif + #define UNNAMED_ENUM "<anonymous enum>" #define ENUM_SEPARATOR "::" @@ -138,13 +143,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 +799,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 +823,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 +950,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 +1007,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 +1138,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 +1169,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 +1246,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 +1356,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 +1427,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 +1500,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 +1527,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 +1544,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 +1557,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,16 +1569,20 @@ 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; resolve_suite(p_function->body); - GDScriptParser::DataType return_type = p_function->body->get_datatype(); - - if (!p_function->get_datatype().is_hard_type() && return_type.is_set()) { + if (!p_function->get_datatype().is_hard_type() && p_function->body->get_datatype().is_set()) { // Use the suite inferred type if return isn't explicitly set. - return_type.type_source = GDScriptParser::DataType::INFERRED; p_function->set_datatype(p_function->body->get_datatype()); } else if (p_function->get_datatype().is_hard_type() && (p_function->get_datatype().kind != GDScriptParser::DataType::BUILTIN || p_function->get_datatype().builtin_type != Variant::NIL)) { if (!p_function->body->has_return && (p_is_lambda || p_function->identifier->name != GDScriptLanguage::get_singleton()->strings._init)) { @@ -1498,6 +1590,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 +1633,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 +1694,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 +2473,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 @@ -2509,9 +2606,8 @@ void GDScriptAnalyzer::reduce_binary_op(GDScriptParser::BinaryOpNode *p_binary_o p_binary_op->set_datatype(result); } -#ifdef TOOLS_ENABLED -#ifndef DISABLE_DEPRECATED -const char *GDScriptAnalyzer::get_rename_from_map(const char *map[][2], String key) { +#ifdef SUGGEST_GODOT4_RENAMES +const char *get_rename_from_map(const char *map[][2], String key) { for (int index = 0; map[index][0]; index++) { if (map[index][0] == key) { return map[index][1]; @@ -2522,39 +2618,39 @@ const char *GDScriptAnalyzer::get_rename_from_map(const char *map[][2], String k // Checks if an identifier/function name has been renamed in Godot 4, uses ProjectConverter3To4 for rename map. // Returns the new name if found, nullptr otherwise. -const char *GDScriptAnalyzer::check_for_renamed_identifier(String identifier, GDScriptParser::Node::Type type) { +const char *check_for_renamed_identifier(String identifier, GDScriptParser::Node::Type type) { switch (type) { case GDScriptParser::Node::IDENTIFIER: { // Check properties - const char *result = get_rename_from_map(ProjectConverter3To4::gdscript_properties_renames, identifier); + const char *result = get_rename_from_map(RenamesMap3To4::gdscript_properties_renames, identifier); if (result) { return result; } // Check enum values - result = get_rename_from_map(ProjectConverter3To4::enum_renames, identifier); + result = get_rename_from_map(RenamesMap3To4::enum_renames, identifier); if (result) { return result; } // Check color constants - result = get_rename_from_map(ProjectConverter3To4::color_renames, identifier); + result = get_rename_from_map(RenamesMap3To4::color_renames, identifier); if (result) { return result; } // Check type names - result = get_rename_from_map(ProjectConverter3To4::class_renames, identifier); + result = get_rename_from_map(RenamesMap3To4::class_renames, identifier); if (result) { return result; } - return get_rename_from_map(ProjectConverter3To4::builtin_types_renames, identifier); + return get_rename_from_map(RenamesMap3To4::builtin_types_renames, identifier); } case GDScriptParser::Node::CALL: { - const char *result = get_rename_from_map(ProjectConverter3To4::gdscript_function_renames, identifier); + const char *result = get_rename_from_map(RenamesMap3To4::gdscript_function_renames, identifier); if (result) { return result; } // Built-in Types are mistaken for function calls when the built-in type is not found. // Check built-in types if function rename not found - return get_rename_from_map(ProjectConverter3To4::builtin_types_renames, identifier); + return get_rename_from_map(RenamesMap3To4::builtin_types_renames, identifier); } // Signal references don't get parsed through the GDScriptAnalyzer. No support for signal rename hints. default: @@ -2562,8 +2658,7 @@ const char *GDScriptAnalyzer::check_for_renamed_identifier(String identifier, GD return nullptr; } } -#endif // DISABLE_DEPRECATED -#endif // TOOLS_ENABLED +#endif // SUGGEST_GODOT4_RENAMES void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_await, bool p_is_root) { bool all_is_constant = true; @@ -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) { @@ -2982,8 +3081,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a } if (!found && (is_self || (base_type.is_hard_type() && base_type.kind == GDScriptParser::DataType::BUILTIN))) { String base_name = is_self && !p_call->is_super ? "self" : base_type.to_string(); -#ifdef TOOLS_ENABLED -#ifndef DISABLE_DEPRECATED +#ifdef SUGGEST_GODOT4_RENAMES String rename_hint = String(); if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GD4_HINT)).booleanize()) { const char *renamed_function_name = check_for_renamed_identifier(p_call->function_name, p_call->type); @@ -2992,12 +3090,9 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a } } push_error(vformat(R"*(Function "%s()" not found in base %s.%s)*", p_call->function_name, base_name, rename_hint), p_call->is_super ? p_call : p_call->callee); -#else // !DISABLE_DEPRECATED - push_error(vformat(R"*(Function "%s()" not found in base %s.)*", p_call->function_name, base_name), p_call->is_super ? p_call : p_call->callee); -#endif // DISABLE_DEPRECATED #else push_error(vformat(R"*(Function "%s()" not found in base %s.)*", p_call->function_name, base_name), p_call->is_super ? p_call : p_call->callee); -#endif +#endif // SUGGEST_GODOT4_RENAMES } else if (!found && (!p_call->is_super && base_type.is_hard_type() && base_type.kind == GDScriptParser::DataType::NATIVE && base_type.is_meta_type)) { push_error(vformat(R"*(Static function "%s()" not found in base "%s".)*", p_call->function_name, base_type.native_type), p_call); } @@ -3187,8 +3282,7 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod p_identifier->reduced_value = result; p_identifier->set_datatype(type_from_variant(result, p_identifier)); } else if (base.is_hard_type()) { -#ifdef TOOLS_ENABLED -#ifndef DISABLE_DEPRECATED +#ifdef SUGGEST_GODOT4_RENAMES String rename_hint = String(); if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GD4_HINT)).booleanize()) { const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type); @@ -3197,12 +3291,9 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod } } push_error(vformat(R"(Cannot find constant "%s" on base "%s".%s)", name, base.to_string(), rename_hint), p_identifier); -#else // !DISABLE_DEPRECATED - push_error(vformat(R"(Cannot find constant "%s" on base "%s".)", name, base.to_string()), p_identifier); -#endif // DISABLE_DEPRECATED #else push_error(vformat(R"(Cannot find constant "%s" on base "%s".)", name, base.to_string()), p_identifier); -#endif +#endif // SUGGEST_GODOT4_RENAMES } } else { switch (base.builtin_type) { @@ -3231,8 +3322,7 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod } } if (base.is_hard_type()) { -#ifdef TOOLS_ENABLED -#ifndef DISABLE_DEPRECATED +#ifdef SUGGEST_GODOT4_RENAMES String rename_hint = String(); if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GD4_HINT)).booleanize()) { const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type); @@ -3241,12 +3331,9 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod } } push_error(vformat(R"(Cannot find property "%s" on base "%s".%s)", name, base.to_string(), rename_hint), p_identifier); -#else // !DISABLE_DEPRECATED - push_error(vformat(R"(Cannot find property "%s" on base "%s".)", name, base.to_string()), p_identifier); -#endif // DISABLE_DEPRECATED #else push_error(vformat(R"(Cannot find property "%s" on base "%s".)", name, base.to_string()), p_identifier); -#endif +#endif // SUGGEST_GODOT4_RENAMES } } } @@ -3586,8 +3673,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident if (GDScriptUtilityFunctions::function_exists(name)) { push_error(vformat(R"(Built-in function "%s" cannot be used as an identifier.)", name), p_identifier); } else { -#ifdef TOOLS_ENABLED -#ifndef DISABLE_DEPRECATED +#ifdef SUGGEST_GODOT4_RENAMES String rename_hint = String(); if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GD4_HINT)).booleanize()) { const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type); @@ -3596,12 +3682,9 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident } } push_error(vformat(R"(Identifier "%s" not declared in the current scope.%s)", name, rename_hint), p_identifier); -#else // !DISABLE_DEPRECATED - push_error(vformat(R"(Identifier "%s" not declared in the current scope.)", name), p_identifier); -#endif // DISABLE_DEPRECATED #else push_error(vformat(R"(Identifier "%s" not declared in the current scope.)", name), p_identifier); -#endif +#endif // SUGGEST_GODOT4_RENAMES } GDScriptParser::DataType dummy; dummy.kind = GDScriptParser::DataType::VARIANT; @@ -4011,7 +4094,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 +4101,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 +4417,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 +4572,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..cdeba374c7 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -37,10 +37,6 @@ #include "gdscript_cache.h" #include "gdscript_parser.h" -#ifdef TOOLS_ENABLED -#include "editor/project_converter_3_to_4.h" -#endif - class GDScriptAnalyzer { GDScriptParser *parser = nullptr; HashMap<String, Ref<GDScriptParserRef>> depended_parsers; @@ -117,7 +113,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); @@ -137,13 +133,6 @@ class GDScriptAnalyzer { bool is_shadowing(GDScriptParser::IdentifierNode *p_local, const String &p_context); #endif -#ifdef TOOLS_ENABLED -#ifndef DISABLE_DEPRECATED - const char *get_rename_from_map(const char *map[][2], String key); - const char *check_for_renamed_identifier(String identifier, GDScriptParser::Node::Type type); -#endif // DISABLE_DEPRECATED -#endif // TOOLS_ENABLED - public: Error resolve_inheritance(); Error resolve_interface(); diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 12c10642ec..3543c0a79f 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. @@ -3480,6 +3491,14 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co break; } + if (context.current_class) { + if (context.type != GDScriptParser::COMPLETION_SUPER_METHOD) { + base.type = context.current_class->get_datatype(); + } else { + base.type = context.current_class->base_type; + } + } + if (_lookup_symbol_from_base(base.type, p_symbol, is_function, r_result) == OK) { return OK; } diff --git a/modules/gdscript/gdscript_function.cpp b/modules/gdscript/gdscript_function.cpp index 71831a3a97..a6b4dc7981 100644 --- a/modules/gdscript/gdscript_function.cpp +++ b/modules/gdscript/gdscript_function.cpp @@ -296,6 +296,15 @@ void GDScriptFunctionState::_clear_stack() { } } +void GDScriptFunctionState::_clear_connections() { + List<Object::Connection> conns; + get_signals_connected_to_this(&conns); + + for (Object::Connection &c : conns) { + c.signal.disconnect(c.callable); + } +} + void GDScriptFunctionState::_bind_methods() { ClassDB::bind_method(D_METHOD("resume", "arg"), &GDScriptFunctionState::resume, DEFVAL(Variant())); ClassDB::bind_method(D_METHOD("is_valid", "extended_check"), &GDScriptFunctionState::is_valid, DEFVAL(false)); diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index 37416a734d..f45c1f9577 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -628,6 +628,7 @@ public: Variant resume(const Variant &p_arg = Variant()); void _clear_stack(); + void _clear_connections(); GDScriptFunctionState(); ~GDScriptFunctionState(); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index ed2dce471f..b5cb5a4680 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; @@ -179,8 +175,8 @@ void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_ warning.leftmost_column = p_source->leftmost_column; 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); + if (warn_level == GDScriptWarning::WarnLevel::ERROR) { + push_error(warning.get_message() + String(" (Warning treated as error.)"), p_source); return; } @@ -487,24 +483,34 @@ void GDScriptParser::parse_program() { current_class = head; bool can_have_class_or_extends = true; - while (match(GDScriptTokenizer::Token::ANNOTATION)) { - AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL); - if (annotation != nullptr) { - if (annotation->applies_to(AnnotationInfo::SCRIPT)) { - // `@icon` needs to be applied in the parser. See GH-72444. - if (annotation->name == SNAME("@icon")) { - annotation->apply(this, head); + while (!check(GDScriptTokenizer::Token::TK_EOF)) { + if (match(GDScriptTokenizer::Token::ANNOTATION)) { + AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL); + if (annotation != nullptr) { + if (annotation->applies_to(AnnotationInfo::SCRIPT)) { + // `@icon` needs to be applied in the parser. See GH-72444. + if (annotation->name == SNAME("@icon")) { + annotation->apply(this, head); + } else { + head->annotations.push_back(annotation); + } } else { - head->annotations.push_back(annotation); + annotation_stack.push_back(annotation); + // This annotation must appear after script-level annotations + // and class_name/extends (ex: could be @onready or @export), + // so we stop looking for script-level stuff. + can_have_class_or_extends = false; + break; } - } else { - annotation_stack.push_back(annotation); - // This annotation must appear after script-level annotations - // and class_name/extends (ex: could be @onready or @export), - // so we stop looking for script-level stuff. - can_have_class_or_extends = false; - break; } + } else if (check(GDScriptTokenizer::Token::LITERAL) && current.literal.get_type() == Variant::STRING) { + // Allow strings in class body as multiline comments. + advance(); + if (!match(GDScriptTokenizer::Token::NEWLINE)) { + push_error("Expected newline after comment string."); + } + } else { + break; } } @@ -528,6 +534,16 @@ void GDScriptParser::parse_program() { end_statement("superclass"); } break; + case GDScriptTokenizer::Token::LITERAL: + if (current.literal.get_type() == Variant::STRING) { + // Allow strings in class body as multiline comments. + advance(); + if (!match(GDScriptTokenizer::Token::NEWLINE)) { + push_error("Expected newline after comment string."); + } + break; + } + [[fallthrough]]; default: // No tokens are allowed between script annotations and class/extends. can_have_class_or_extends = false; @@ -833,6 +849,16 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { case GDScriptTokenizer::Token::DEDENT: class_end = true; break; + case GDScriptTokenizer::Token::LITERAL: + if (current.literal.get_type() == Variant::STRING) { + // Allow strings in class body as multiline comments. + advance(); + if (!match(GDScriptTokenizer::Token::NEWLINE)) { + push_error("Expected newline after comment string."); + } + break; + } + [[fallthrough]]; default: // Display a completion with identifiers. make_completion_context(COMPLETION_IDENTIFIER, nullptr); @@ -1679,6 +1705,12 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { // Standalone lambdas can't be used, so make this an error. push_error("Standalone lambdas cannot be accessed. Consider assigning it to a variable.", expression); break; + case Node::LITERAL: + if (static_cast<GDScriptParser::LiteralNode *>(expression)->value.get_type() == Variant::STRING) { + // Allow strings as multiline comments. + break; + } + [[fallthrough]]; default: push_warning(expression, GDScriptWarning::STANDALONE_EXPRESSION); } @@ -1837,10 +1869,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)"); @@ -2141,7 +2181,12 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_precedence(Precedence p_pr make_completion_context(COMPLETION_IDENTIFIER, nullptr); GDScriptTokenizer::Token token = current; - ParseFunction prefix_rule = get_rule(token.type)->prefix; + GDScriptTokenizer::Token::Type token_type = token.type; + if (token.is_identifier()) { + // Allow keywords that can be treated as identifiers. + token_type = GDScriptTokenizer::Token::IDENTIFIER; + } + ParseFunction prefix_rule = get_rule(token_type)->prefix; if (prefix_rule == nullptr) { // Expected expression. Let the caller give the proper error message. @@ -3006,7 +3051,14 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p path_state = PATH_STATE_NODE_NAME; } else if (current.is_node_name()) { advance(); - get_node->full_path += previous.get_identifier(); + String identifier = previous.get_identifier(); +#ifdef DEBUG_ENABLED + // Check spoofing. + if (TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY) && TS->spoof_check(identifier)) { + push_warning(get_node, GDScriptWarning::CONFUSABLE_IDENTIFIER, identifier); + } +#endif + get_node->full_path += identifier; path_state = PATH_STATE_NODE_NAME; } else if (!check(GDScriptTokenizer::Token::SLASH) && !check(GDScriptTokenizer::Token::PERCENT)) { @@ -3548,7 +3600,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 +3667,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.)"); @@ -3625,15 +3685,6 @@ template <PropertyHint t_hint, Variant::Type t_type> bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node *p_node) { ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); - { - const int max_flags = 32; - - if (t_hint == PropertyHint::PROPERTY_HINT_FLAGS && p_annotation->resolved_arguments.size() > max_flags) { - push_error(vformat(R"(The argument count limit for "@export_flags" is exceeded (%d/%d).)", p_annotation->resolved_arguments.size(), max_flags), p_annotation); - return false; - } - } - VariableNode *variable = static_cast<VariableNode *>(p_node); if (variable->exported) { push_error(vformat(R"(Annotation "%s" cannot be used with another "@export" annotation.)", p_annotation->name), p_annotation); @@ -3647,14 +3698,50 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node String hint_string; for (int i = 0; i < p_annotation->resolved_arguments.size(); i++) { - if (p_annotation->name != SNAME("@export_placeholder") && String(p_annotation->resolved_arguments[i]).contains(",")) { - push_error(vformat(R"(Argument %d of annotation "%s" contains a comma. Use separate arguments instead.)", i + 1, p_annotation->name), p_annotation->arguments[i]); - return false; + String arg_string = String(p_annotation->resolved_arguments[i]); + + if (p_annotation->name != SNAME("@export_placeholder")) { + if (arg_string.is_empty()) { + push_error(vformat(R"(Argument %d of annotation "%s" is empty.)", i + 1, p_annotation->name), p_annotation->arguments[i]); + return false; + } + if (arg_string.contains(",")) { + push_error(vformat(R"(Argument %d of annotation "%s" contains a comma. Use separate arguments instead.)", i + 1, p_annotation->name), p_annotation->arguments[i]); + return false; + } } + + if (p_annotation->name == SNAME("@export_flags")) { + const int64_t max_flags = 32; + Vector<String> t = arg_string.split(":", true, 1); + if (t[0].is_empty()) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": Expected flag name.)", i + 1), p_annotation->arguments[i]); + return false; + } + if (t.size() == 2) { + if (t[1].is_empty()) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": Expected flag value.)", i + 1), p_annotation->arguments[i]); + return false; + } + if (!t[1].is_valid_int()) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": The flag value must be a valid integer.)", i + 1), p_annotation->arguments[i]); + return false; + } + int64_t value = t[1].to_int(); + if (value < 1 || value >= (1LL << max_flags)) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": The flag value must be at least 1 and at most 2 ** %d - 1.)", i + 1, max_flags), p_annotation->arguments[i]); + return false; + } + } else if (i >= max_flags) { + push_error(vformat(R"(Invalid argument %d of annotation "@export_flags": Starting from argument %d, the flag value must be specified explicitly.)", i + 1, max_flags + 1), p_annotation->arguments[i]); + return false; + } + } + if (i > 0) { hint_string += ","; } - hint_string += String(p_annotation->resolved_arguments[i]); + hint_string += arg_string; } variable->export_info.hint_string = hint_string; @@ -3685,6 +3772,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 +3996,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_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp index d7f1114fd3..d586380c41 100644 --- a/modules/gdscript/gdscript_tokenizer.cpp +++ b/modules/gdscript/gdscript_tokenizer.cpp @@ -168,7 +168,11 @@ bool GDScriptTokenizer::Token::is_identifier() const { switch (type) { case IDENTIFIER: case MATCH: // Used in String.match(). - case CONST_INF: // Used in Vector{2,3,4}.INF + // Allow constants to be treated as regular identifiers. + case CONST_PI: + case CONST_INF: + case CONST_NAN: + case CONST_TAU: return true; default: return false; @@ -188,6 +192,10 @@ bool GDScriptTokenizer::Token::is_node_name() const { case CLASS_NAME: case CLASS: case CONST: + case CONST_PI: + case CONST_INF: + case CONST_NAN: + case CONST_TAU: case CONTINUE: case ELIF: case ELSE: @@ -530,9 +538,12 @@ void GDScriptTokenizer::make_keyword_list() { #endif // DEBUG_ENABLED GDScriptTokenizer::Token GDScriptTokenizer::potential_identifier() { + bool only_ascii = _peek(-1) < 128; + // Consume all identifier characters. while (is_unicode_identifier_continue(_peek())) { - _advance(); + char32_t c = _advance(); + only_ascii = only_ascii && c < 128; } int len = _current - _start; @@ -587,7 +598,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::potential_identifier() { #ifdef DEBUG_ENABLED // Additional checks for identifiers but only in debug and if it's available in TextServer. - if (TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY)) { + if (!only_ascii && TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY)) { int64_t confusable = TS->is_confusable(name, keyword_list); if (confusable >= 0) { push_error(vformat(R"(Identifier "%s" is visually similar to the GDScript keyword "%s" and thus not allowed.)", name, keyword_list[confusable])); diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp index b99f5d2685..7a11ea52f0 100644 --- a/modules/gdscript/gdscript_vm.cpp +++ b/modules/gdscript/gdscript_vm.cpp @@ -447,6 +447,9 @@ void (*type_init_function_table[])(Variant *) = { #define OP_GET_BASIS get_basis #define OP_GET_RID get_rid +#define METHOD_CALL_ON_NULL_VALUE_ERROR(method_pointer) "Cannot call method '" + (method_pointer)->get_name() + "' on a null value." +#define METHOD_CALL_ON_FREED_INSTANCE_ERROR(method_pointer) "Cannot call method '" + (method_pointer)->get_name() + "' on a previously freed instance." + Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_args, int p_argcount, Callable::CallError &r_err, CallState *p_state) { OPCODES_TABLE; @@ -1675,10 +1678,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a bool freed = false; Object *base_obj = base->get_validated_object_with_check(freed); if (freed) { - err_text = "Trying to call a function on a previously freed instance."; + err_text = METHOD_CALL_ON_FREED_INSTANCE_ERROR(method); OPCODE_BREAK; } else if (!base_obj) { - err_text = "Trying to call a function on a null value."; + err_text = METHOD_CALL_ON_NULL_VALUE_ERROR(method); OPCODE_BREAK; } #else @@ -1839,10 +1842,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a bool freed = false; \ Object *base_obj = base->get_validated_object_with_check(freed); \ if (freed) { \ - err_text = "Trying to call a function on a previously freed instance."; \ + err_text = METHOD_CALL_ON_FREED_INSTANCE_ERROR(method); \ OPCODE_BREAK; \ } else if (!base_obj) { \ - err_text = "Trying to call a function on a null value."; \ + err_text = METHOD_CALL_ON_NULL_VALUE_ERROR(method); \ OPCODE_BREAK; \ } \ const void **argptrs = call_args_ptr; \ @@ -1941,10 +1944,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a bool freed = false; Object *base_obj = base->get_validated_object_with_check(freed); if (freed) { - err_text = "Trying to call a function on a previously freed instance."; + err_text = METHOD_CALL_ON_FREED_INSTANCE_ERROR(method); OPCODE_BREAK; } else if (!base_obj) { - err_text = "Trying to call a function on a null value."; + err_text = METHOD_CALL_ON_NULL_VALUE_ERROR(method); OPCODE_BREAK; } #else @@ -1969,7 +1972,13 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a VariantInternal::initialize(ret, Variant::OBJECT); Object **ret_opaque = VariantInternal::get_object(ret); method->ptrcall(base_obj, argptrs, ret_opaque); - VariantInternal::update_object_id(ret); + if (method->is_return_type_raw_object_ptr()) { + // The Variant has to participate in the ref count since the method returns a raw Object *. + VariantInternal::object_assign(ret, *ret_opaque); + } else { + // The method, in case it returns something, returns an already encapsulated object. + VariantInternal::update_object_id(ret); + } #ifdef DEBUG_ENABLED if (GDScriptLanguage::get_singleton()->profiling) { @@ -1996,10 +2005,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a bool freed = false; Object *base_obj = base->get_validated_object_with_check(freed); if (freed) { - err_text = "Trying to call a function on a previously freed instance."; + err_text = METHOD_CALL_ON_FREED_INSTANCE_ERROR(method); OPCODE_BREAK; } else if (!base_obj) { - err_text = "Trying to call a function on a null value."; + err_text = METHOD_CALL_ON_NULL_VALUE_ERROR(method); OPCODE_BREAK; } #else @@ -3427,7 +3436,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a String message_str; if (_code_ptr[ip + 2] != 0) { GET_VARIANT_PTR(message, 1); - message_str = *message; + Variant message_var = *message; + if (message->get_type() != Variant::NIL) { + message_str = message_var; + } } if (message_str.is_empty()) { err_text = "Assertion failed."; 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/language_server/gdscript_text_document.cpp b/modules/gdscript/language_server/gdscript_text_document.cpp index b9e6921034..35fbdca949 100644 --- a/modules/gdscript/language_server/gdscript_text_document.cpp +++ b/modules/gdscript/language_server/gdscript_text_document.cpp @@ -108,6 +108,7 @@ void GDScriptTextDocument::didSave(const Variant &p_param) { scr->reload(true); } scr->update_exports(); + ScriptEditor::get_singleton()->reload_scripts(true); ScriptEditor::get_singleton()->update_docs_from_script(scr); } } 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/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.gd b/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.gd new file mode 100644 index 0000000000..390d314b94 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.gd @@ -0,0 +1,3 @@ +func test(): + var P1 = "ok" # Technically it is visually similar to keyword "PI" but allowed since it's in ASCII range. + print(P1) diff --git a/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.out b/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allow_id_similar_to_keyword_in_ascii.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.gd b/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.gd new file mode 100644 index 0000000000..3ecd65ad9c --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.gd @@ -0,0 +1,21 @@ +""" +This is a comment. +""" + +@tool + +""" +This is also a comment. +""" + +extends RefCounted + +''' +This is a comment too. +''' + +func test(): + """ + This too is a comment. + """ + print("ok") diff --git a/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.out b/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allow_strings_as_comments.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.gd b/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.gd new file mode 100644 index 0000000000..7e1982597c --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.gd @@ -0,0 +1,16 @@ +func test(): + # The following keywords are allowed as identifiers: + var match = "match" + print(match) + + var PI = "PI" + print(PI) + + var INF = "INF" + print(INF) + + var NAN = "NAN" + print(NAN) + + var TAU = "TAU" + print(TAU) diff --git a/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.out b/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.out new file mode 100644 index 0000000000..aae2ae13d5 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/allowed_keywords_as_identifiers.out @@ -0,0 +1,6 @@ +GDTEST_OK +match +PI +INF +NAN +TAU diff --git a/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.gd b/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.gd index e2caac8ffd..41b38c4bba 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.gd +++ b/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.gd @@ -1,5 +1,12 @@ +extends Node + func test(): var port = 0 # Only latin characters. var pοrt = 1 # The "ο" is Greek omicron. prints(port, pοrt) + +# Do not call this since nodes aren't in the tree. It is just a parser check. +func nodes(): + var _node1 = $port # Only latin characters. + var _node2 = $pοrt # The "ο" is Greek omicron. diff --git a/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.out b/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.out index c483396443..c189204285 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.out +++ b/modules/gdscript/tests/scripts/parser/warnings/confusable_identifier.out @@ -1,6 +1,10 @@ GDTEST_OK >> WARNING ->> Line: 3 +>> Line: 5 +>> CONFUSABLE_IDENTIFIER +>> The identifier "pοrt" has misleading characters and might be confused with something else. +>> WARNING +>> Line: 12 >> CONFUSABLE_IDENTIFIER >> The identifier "pοrt" has misleading characters and might be confused with something else. 0 1 diff --git a/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.gd b/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.gd index 18ea260fa2..dc4223ec2d 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.gd +++ b/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.gd @@ -1,6 +1,5 @@ func test(): # The following statements should all be reported as standalone expressions: - "This is a standalone expression" 1234 0.0 + 0.0 Color(1, 1, 1) diff --git a/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.out b/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.out index 99ec87438e..a2c67a6e51 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.out +++ b/modules/gdscript/tests/scripts/parser/warnings/standalone_expression.out @@ -8,14 +8,10 @@ GDTEST_OK >> STANDALONE_EXPRESSION >> Standalone expression (the line has no effect). >> WARNING ->> Line: 5 +>> Line: 6 >> STANDALONE_EXPRESSION >> Standalone expression (the line has no effect). >> WARNING >> Line: 7 >> STANDALONE_EXPRESSION >> Standalone expression (the line has no effect). ->> WARNING ->> Line: 8 ->> STANDALONE_EXPRESSION ->> Standalone expression (the line has no effect). |