diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 6e864316f106..9433abf0d4d0 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -470,11 +470,12 @@ When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an identifier that will be shadowed below in the block is used. - + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a constant is used as a function. When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when deprecated keywords are used. + [b]Note:[/b] There are currently no deprecated keywords, so this warning is never produced. When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an empty file is parsed. @@ -485,7 +486,7 @@ If [code]true[/code], scripts in the [code]res://addons[/code] folder will not generate warnings. - + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when using a function as if it is a property. @@ -519,7 +520,7 @@ When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when the [code]@onready[/code] annotation is used together with the [code]@export[/code] annotation, since it may not behave as expected. - + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when using a property as if it is a function. @@ -593,7 +594,7 @@ When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a private member variable is never used. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a signal is declared but never emitted. + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a signal is declared but never explicitly used in the class. When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a local variable is unused. diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 3ec5098c21ef..6af6460b315e 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -562,6 +562,12 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c class_type.native_type = result.native_type; p_class->set_datatype(class_type); + // Apply annotations. + for (GDScriptParser::AnnotationNode *&E : p_class->annotations) { + resolve_annotation(E); + E->apply(parser, p_class, p_class->outer); + } + parser->current_class = previous_class; return OK; @@ -912,7 +918,6 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, { #ifdef DEBUG_ENABLED - HashSet 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. @@ -922,9 +927,6 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, E->apply(parser, member.variable, p_class); } } - for (GDScriptWarning::Code ignored_warning : member_node->ignored_warnings) { - parser->ignored_warnings.insert(ignored_warning); - } } #endif switch (member.type) { @@ -1061,6 +1063,13 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, enum_type.enum_values[element.identifier->name] = element.value; dictionary[String(element.identifier->name)] = element.value; + +#ifdef DEBUG_ENABLED + // Named enum identifiers do not shadow anything since you can only access them with `NamedEnum.ENUM_VALUE`. + if (member.m_enum->identifier->name == StringName()) { + is_shadowing(element.identifier, "enum member", false); + } +#endif } current_enum = prev_enum; @@ -1133,9 +1142,6 @@ 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; @@ -1146,11 +1152,11 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas p_source = p_class; } + if (!p_class->resolved_interface) { #ifdef DEBUG_ENABLED - bool has_static_data = p_class->has_static_data; + bool has_static_data = p_class->has_static_data; #endif - if (!p_class->resolved_interface) { if (!parser->has_class(p_class)) { String script_path = p_class->get_datatype().script_path; Ref parser_ref = get_parser_for(script_path); @@ -1178,6 +1184,7 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas return; } + p_class->resolved_interface = true; if (resolve_class_inheritance(p_class) != OK) { @@ -1319,10 +1326,6 @@ 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 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); } @@ -1396,10 +1399,12 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co } } } - + } else if (member.type == GDScriptParser::ClassNode::Member::SIGNAL) { #ifdef DEBUG_ENABLED - parser->ignored_warnings = previously_ignored_warnings; -#endif // DEBUG_ENABLED + if (member.signal->usages == 0) { + parser->push_warning(member.signal->identifier, GDScriptWarning::UNUSED_SIGNAL, member.signal->identifier->name); + } +#endif } } @@ -1431,6 +1436,7 @@ void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node, bool p_is_root case GDScriptParser::Node::NONE: break; // Unreachable. case GDScriptParser::Node::CLASS: + // NOTE: Currently this route is never executed, `resolve_class_*()` is called directly. if (OK == resolve_class_inheritance(static_cast(p_node), true)) { resolve_class_interface(static_cast(p_node), true); resolve_class_body(static_cast(p_node), true); @@ -1584,13 +1590,6 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * } p_function->resolved_signature = true; -#ifdef DEBUG_ENABLED - HashSet 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; bool previous_static_context = static_context; @@ -1775,9 +1774,6 @@ 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; static_context = previous_static_context; } @@ -1788,13 +1784,6 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun } p_function->resolved_body = true; -#ifdef DEBUG_ENABLED - HashSet 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; @@ -1812,9 +1801,6 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun } } -#ifdef DEBUG_ENABLED - parser->ignored_warnings = previously_ignored_warnings; -#endif parser->current_function = previous_function; static_context = previous_static_context; } @@ -1852,23 +1838,11 @@ void GDScriptAnalyzer::resolve_suite(GDScriptParser::SuiteNode *p_suite) { // Apply annotations. for (GDScriptParser::AnnotationNode *&E : stmt->annotations) { resolve_annotation(E); - E->apply(parser, stmt, nullptr); + E->apply(parser, stmt, nullptr); // TODO: Provide `p_class`. } -#ifdef DEBUG_ENABLED - HashSet 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); resolve_pending_lambda_bodies(); - -#ifdef DEBUG_ENABLED - parser->ignored_warnings = previously_ignored_warnings; -#endif // DEBUG_ENABLED - decide_suite_type(p_suite, stmt); } } @@ -2257,6 +2231,12 @@ void GDScriptAnalyzer::resolve_match(GDScriptParser::MatchNode *p_match) { } void GDScriptAnalyzer::resolve_match_branch(GDScriptParser::MatchBranchNode *p_match_branch, GDScriptParser::ExpressionNode *p_match_test) { + // Apply annotations. + for (GDScriptParser::AnnotationNode *&E : p_match_branch->annotations) { + resolve_annotation(E); + E->apply(parser, p_match_branch, nullptr); // TODO: Provide `p_class`. + } + for (int i = 0; i < p_match_branch->patterns.size(); i++) { resolve_match_pattern(p_match_branch->patterns[i], p_match_test); } @@ -3746,6 +3726,8 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod if (is_base && !base.is_meta_type) { p_identifier->set_datatype(member.get_datatype()); p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_SIGNAL; + p_identifier->signal_source = member.signal; + member.signal->usages += 1; return; } } break; @@ -3930,6 +3912,8 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident found_source = true; break; case GDScriptParser::IdentifierNode::MEMBER_SIGNAL: + p_identifier->signal_source->usages++; + [[fallthrough]]; case GDScriptParser::IdentifierNode::INHERITED_VARIABLE: mark_lambda_use_self(); break; @@ -5636,21 +5620,20 @@ Error GDScriptAnalyzer::resolve_dependencies() { Error GDScriptAnalyzer::analyze() { parser->errors.clear(); - Error err = OK; - err = resolve_inheritance(); + Error err = resolve_inheritance(); if (err) { return err; } - // Apply annotations. - for (GDScriptParser::AnnotationNode *&E : parser->head->annotations) { - resolve_annotation(E); - E->apply(parser, parser->head, nullptr); - } - resolve_interface(); resolve_body(); + +#ifdef DEBUG_ENABLED + // Apply here, after all `@warning_ignore`s have been resolved and applied. + parser->apply_pending_warnings(); +#endif + if (!parser->errors.is_empty()) { return ERR_PARSE_ERROR; } diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 49341cb670d4..8d98c0b11c10 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -127,7 +127,7 @@ GDScriptParser::GDScriptParser() { register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations, varray("")); register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations, varray("")); // 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); + register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS_LEVEL | 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("authority", "call_remote", "unreliable", 0)); } @@ -181,47 +181,62 @@ void GDScriptParser::push_error(const String &p_message, const Node *p_origin) { #ifdef DEBUG_ENABLED void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_code, const Vector &p_symbols) { ERR_FAIL_NULL(p_source); + ERR_FAIL_INDEX(p_code, GDScriptWarning::WARNING_MAX); + if (is_ignoring_warnings) { return; } if (GLOBAL_GET("debug/gdscript/warnings/exclude_addons").booleanize() && script_path.begins_with("res://addons/")) { return; } - - if (ignored_warnings.has(p_code)) { + GDScriptWarning::WarnLevel warn_level = (GDScriptWarning::WarnLevel)(int)GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(p_code)); + if (warn_level == GDScriptWarning::IGNORE) { return; } - int warn_level = (int)GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(p_code)); - if (!warn_level) { - return; - } + PendingWarning pw; + pw.source = p_source; + pw.code = p_code; + pw.treated_as_error = warn_level == GDScriptWarning::ERROR; + pw.symbols = p_symbols; - GDScriptWarning warning; - warning.code = p_code; - warning.symbols = p_symbols; - warning.start_line = p_source->start_line; - warning.end_line = p_source->end_line; - warning.leftmost_column = p_source->leftmost_column; - warning.rightmost_column = p_source->rightmost_column; + pending_warnings.push_back(pw); +} - if (warn_level == GDScriptWarning::WarnLevel::ERROR) { - push_error(warning.get_message() + String(" (Warning treated as error.)"), p_source); - return; - } +void GDScriptParser::apply_pending_warnings() { + for (const PendingWarning &pw : pending_warnings) { + if (warning_ignored_lines[pw.code].has(pw.source->start_line)) { + continue; + } - List::Element *before = nullptr; - for (List::Element *E = warnings.front(); E; E = E->next()) { - if (E->get().start_line > warning.start_line) { - break; + GDScriptWarning warning; + warning.code = pw.code; + warning.symbols = pw.symbols; + warning.start_line = pw.source->start_line; + warning.end_line = pw.source->end_line; + warning.leftmost_column = pw.source->leftmost_column; + warning.rightmost_column = pw.source->rightmost_column; + + if (pw.treated_as_error) { + push_error(warning.get_message() + String(" (Warning treated as error.)"), pw.source); + continue; + } + + List::Element *before = nullptr; + for (List::Element *E = warnings.front(); E; E = E->next()) { + if (E->get().start_line > warning.start_line) { + break; + } + before = E; + } + if (before) { + warnings.insert_after(before, warning); + } else { + warnings.push_front(warning); } - before = E; - } - if (before) { - warnings.insert_after(before, warning); - } else { - warnings.push_front(warning); } + + pending_warnings.clear(); } #endif @@ -553,25 +568,53 @@ void GDScriptParser::end_statement(const String &p_context) { void GDScriptParser::parse_program() { head = alloc_node(); + head->start_line = 1; + head->end_line = 1; head->fqcn = GDScript::canonicalize_path(script_path); current_class = head; bool can_have_class_or_extends = true; +#define PUSH_PENDING_ANNOTATIONS_TO_HEAD \ + if (!annotation_stack.is_empty()) { \ + for (AnnotationNode * annot : annotation_stack) { \ + head->annotations.push_back(annot); \ + } \ + annotation_stack.clear(); \ + } + while (!check(GDScriptTokenizer::Token::TK_EOF)) { if (match(GDScriptTokenizer::Token::ANNOTATION)) { - AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL); + AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::CLASS_LEVEL | AnnotationInfo::STANDALONE); 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, nullptr); + if (annotation->applies_to(AnnotationInfo::CLASS)) { + // We do not know in advance what the annotation will be applied to: the `head` class or the subsequent inner class. + // If we encounter `class_name`, `extends` or pure `SCRIPT` annotation, then it's `head`, otherwise it's an inner class. + annotation_stack.push_back(annotation); + } else if (annotation->applies_to(AnnotationInfo::SCRIPT)) { + PUSH_PENDING_ANNOTATIONS_TO_HEAD; + if (annotation->name == SNAME("@tool") || annotation->name == SNAME("@icon")) { + // Some annotations need to be resolved in the parser. + annotation->apply(this, head, nullptr); // `head->outer == nullptr`. } else { head->annotations.push_back(annotation); } + } else if (annotation->applies_to(AnnotationInfo::STANDALONE)) { + if (previous.type != GDScriptTokenizer::Token::NEWLINE) { + push_error(R"(Expected newline after a standalone annotation.)"); + } + if (annotation->name == SNAME("@export_category") || annotation->name == SNAME("@export_group") || annotation->name == SNAME("@export_subgroup")) { + head->add_member_group(annotation); + // This annotation must appear after script-level annotations and `class_name`/`extends`, + // so we stop looking for script-level stuff. + can_have_class_or_extends = false; + break; + } else { + // For potential non-group standalone annotations. + push_error(R"(Unexpected standalone annotation.)"); + } } else { annotation_stack.push_back(annotation); - // This annotation must appear after script-level annotations - // and class_name/extends (ex: could be @onready or @export), + // This annotation must appear after script-level annotations and `class_name`/`extends`, // so we stop looking for script-level stuff. can_have_class_or_extends = false; break; @@ -592,6 +635,10 @@ void GDScriptParser::parse_program() { // Order here doesn't matter, but there should be only one of each at most. switch (current.type) { case GDScriptTokenizer::Token::CLASS_NAME: + PUSH_PENDING_ANNOTATIONS_TO_HEAD; + if (head->start_line == 1) { + reset_extents(head, current); + } advance(); if (head->identifier != nullptr) { push_error(R"("class_name" can only be used once.)"); @@ -600,6 +647,10 @@ void GDScriptParser::parse_program() { } break; case GDScriptTokenizer::Token::EXTENDS: + PUSH_PENDING_ANNOTATIONS_TO_HEAD; + if (head->start_line == 1) { + reset_extents(head, current); + } advance(); if (head->extends_used) { push_error(R"("extends" can only be used once.)"); @@ -608,6 +659,10 @@ void GDScriptParser::parse_program() { end_statement("superclass"); } break; + case GDScriptTokenizer::Token::TK_EOF: + PUSH_PENDING_ANNOTATIONS_TO_HEAD; + can_have_class_or_extends = false; + break; case GDScriptTokenizer::Token::LITERAL: if (current.literal.get_type() == Variant::STRING) { // Allow strings in class body as multiline comments. @@ -629,6 +684,8 @@ void GDScriptParser::parse_program() { } } +#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD + parse_class_body(true); complete_extents(head); @@ -907,8 +964,8 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { case GDScriptTokenizer::Token::ANNOTATION: { advance(); - // Check for standalone and class-level annotations. - AnnotationNode *annotation = parse_annotation(AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL); + // Check for class-level and standalone annotations. + AnnotationNode *annotation = parse_annotation(AnnotationInfo::CLASS_LEVEL | AnnotationInfo::STANDALONE); if (annotation != nullptr) { if (annotation->applies_to(AnnotationInfo::STANDALONE)) { if (previous.type != GDScriptTokenizer::Token::NEWLINE) { @@ -918,9 +975,9 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { current_class->add_member_group(annotation); } else { // For potential non-group standalone annotations. - push_error(R"(Unexpected standalone annotation in class body.)"); + push_error(R"(Unexpected standalone annotation.)"); } - } else { + } else { // `AnnotationInfo::CLASS_LEVEL`. annotation_stack.push_back(annotation); } } @@ -1342,22 +1399,9 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { break; // Allow trailing comma. } if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for enum key.)")) { - EnumNode::Value item; GDScriptParser::IdentifierNode *identifier = parse_identifier(); -#ifdef DEBUG_ENABLED - if (!named) { // Named enum identifiers do not shadow anything since you can only access them with NamedEnum.ENUM_VALUE - for (MethodInfo &info : gdscript_funcs) { - if (info.name == identifier->name) { - push_warning(identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, "enum member", identifier->name, "built-in function"); - } - } - if (Variant::has_utility_function(identifier->name)) { - push_warning(identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, "enum member", identifier->name, "built-in function"); - } else if (ClassDB::class_exists(identifier->name)) { - push_warning(identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, "enum member", identifier->name, "global class"); - } - } -#endif + + EnumNode::Value item; item.identifier = identifier; item.parent_enum = enum_node; item.line = previous.start_line; @@ -1701,7 +1745,19 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { bool unreachable = current_suite->has_return && !current_suite->has_unreachable_code; #endif - bool is_annotation = false; + List annotations; + if (current.type != GDScriptTokenizer::Token::ANNOTATION) { + while (!annotation_stack.is_empty()) { + AnnotationNode *last_annotation = annotation_stack.back()->get(); + if (last_annotation->applies_to(AnnotationInfo::STATEMENT)) { + annotations.push_front(last_annotation); + annotation_stack.pop_back(); + } else { + push_error(vformat(R"(Annotation "%s" cannot be applied to a statement.)", last_annotation->name)); + clear_unused_annotations(); + } + } + } switch (current.type) { case GDScriptTokenizer::Token::PASS: @@ -1775,7 +1831,6 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { break; case GDScriptTokenizer::Token::ANNOTATION: { advance(); - is_annotation = true; AnnotationNode *annotation = parse_annotation(AnnotationInfo::STATEMENT); if (annotation != nullptr) { annotation_stack.push_back(annotation); @@ -1804,10 +1859,9 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { #ifdef DEBUG_ENABLED if (expression != nullptr) { switch (expression->type) { - case Node::CALL: case Node::ASSIGNMENT: case Node::AWAIT: - case Node::TERNARY_OPERATOR: + case Node::CALL: // Fine. break; case Node::LAMBDA: @@ -1815,11 +1869,14 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { push_error("Standalone lambdas cannot be accessed. Consider assigning it to a variable.", expression); break; case Node::LITERAL: - if (static_cast(expression)->value.get_type() == Variant::STRING) { - // Allow strings as multiline comments. - break; + // Allow strings as multiline comments. + if (static_cast(expression)->value.get_type() != Variant::STRING) { + push_warning(expression, GDScriptWarning::STANDALONE_EXPRESSION); } - [[fallthrough]]; + break; + case Node::TERNARY_OPERATOR: + push_warning(expression, GDScriptWarning::STANDALONE_TERNARY); + break; default: push_warning(expression, GDScriptWarning::STANDALONE_EXPRESSION); } @@ -1829,14 +1886,9 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { } } - while (!is_annotation && result != nullptr && !annotation_stack.is_empty()) { - AnnotationNode *last_annotation = annotation_stack.back()->get(); - if (last_annotation->applies_to(AnnotationInfo::STATEMENT)) { - result->annotations.push_front(last_annotation); - annotation_stack.pop_back(); - } else { - push_error(vformat(R"(Annotation "%s" cannot be applied to a statement.)", last_annotation->name)); - clear_unused_annotations(); + if (result != nullptr && !annotations.is_empty()) { + for (AnnotationNode *&annotation : annotations) { + result->annotations.push_back(annotation); } } @@ -2017,10 +2069,10 @@ GDScriptParser::IfNode *GDScriptParser::parse_if(const String &p_token) { } GDScriptParser::MatchNode *GDScriptParser::parse_match() { - MatchNode *match = alloc_node(); + MatchNode *match_node = alloc_node(); - match->test = parse_expression(false); - if (match->test == nullptr) { + match_node->test = parse_expression(false); + if (match_node->test == nullptr) { push_error(R"(Expected expression to test after "match".)"); } @@ -2028,20 +2080,45 @@ GDScriptParser::MatchNode *GDScriptParser::parse_match() { consume(GDScriptTokenizer::Token::NEWLINE, R"(Expected a newline after "match" statement.)"); if (!consume(GDScriptTokenizer::Token::INDENT, R"(Expected an indented block after "match" statement.)")) { - complete_extents(match); - return match; + complete_extents(match_node); + return match_node; } bool all_have_return = true; bool have_wildcard = false; + List match_branch_annotation_stack; + while (!check(GDScriptTokenizer::Token::DEDENT) && !is_at_end()) { + if (match(GDScriptTokenizer::Token::PASS)) { + consume(GDScriptTokenizer::Token::NEWLINE, R"(Expected newline after "pass".)"); + continue; + } + + if (match(GDScriptTokenizer::Token::ANNOTATION)) { + AnnotationNode *annotation = parse_annotation(AnnotationInfo::STATEMENT); + if (annotation == nullptr) { + continue; + } + if (annotation->name != SNAME("@warning_ignore")) { + push_error(vformat(R"(Annotation "%s" is not allowed in this level.)", annotation->name), annotation); + continue; + } + match_branch_annotation_stack.push_back(annotation); + continue; + } + MatchBranchNode *branch = parse_match_branch(); if (branch == nullptr) { advance(); continue; } + for (AnnotationNode *annotation : match_branch_annotation_stack) { + branch->annotations.push_back(annotation); + } + match_branch_annotation_stack.clear(); + #ifdef DEBUG_ENABLED if (have_wildcard && !branch->patterns.is_empty()) { push_warning(branch->patterns[0], GDScriptWarning::UNREACHABLE_PATTERN); @@ -2050,9 +2127,9 @@ GDScriptParser::MatchNode *GDScriptParser::parse_match() { have_wildcard = have_wildcard || branch->has_wildcard; all_have_return = all_have_return && branch->block->has_return; - match->branches.push_back(branch); + match_node->branches.push_back(branch); } - complete_extents(match); + complete_extents(match_node); consume(GDScriptTokenizer::Token::DEDENT, R"(Expected an indented block after "match" statement.)"); @@ -2060,7 +2137,12 @@ GDScriptParser::MatchNode *GDScriptParser::parse_match() { current_suite->has_return = true; } - return match; + for (const AnnotationNode *annotation : match_branch_annotation_stack) { + push_error(vformat(R"(Annotation "%s" does not precede a valid target, so it will have no effect.)", annotation->name), annotation); + } + match_branch_annotation_stack.clear(); + + return match_node; } GDScriptParser::MatchBranchNode *GDScriptParser::parse_match_branch() { @@ -2378,7 +2460,7 @@ GDScriptParser::IdentifierNode *GDScriptParser::parse_identifier() { IdentifierNode *identifier = static_cast(parse_identifier(nullptr, false)); #ifdef DEBUG_ENABLED // Check for spoofing here (if available in TextServer) since this isn't called inside expressions. This is only relevant for declarations. - if (identifier && TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY) && TS->spoof_check(identifier->name.operator String())) { + if (identifier && TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY) && TS->spoof_check(identifier->name)) { push_warning(identifier, GDScriptWarning::CONFUSABLE_IDENTIFIER, identifier->name.operator String()); } #endif @@ -3161,6 +3243,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p complete_extents(get_node); return nullptr; } + get_node->full_path += "%"; path_state = PATH_STATE_PERCENT; @@ -3204,6 +3287,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p path_state = PATH_STATE_NODE_NAME; } else if (current.is_node_name()) { advance(); + String identifier = previous.get_identifier(); #ifdef DEBUG_ENABLED // Check spoofing. @@ -3934,7 +4018,7 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation) return false; } - // `@icon`'s argument needs to be resolved in the parser. See GH-72444. + // Some annotations need to be resolved in the parser. if (p_annotation->name == SNAME("@icon")) { ExpressionNode *argument = p_annotation->arguments[0]; @@ -4401,23 +4485,77 @@ bool GDScriptParser::export_group_annotations(const AnnotationNode *p_annotation } bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { -#ifdef DEBUG_ENABLED +#ifndef DEBUG_ENABLED + // Only available in debug builds. + return true; +#else // DEBUG_ENABLED + if (is_ignoring_warnings) { + return true; // We already ignore all warnings, let's optimize it. + } + bool has_error = false; for (const Variant &warning_name : p_annotation->resolved_arguments) { - GDScriptWarning::Code warning = GDScriptWarning::get_code_from_name(String(warning_name).to_upper()); - if (warning == GDScriptWarning::WARNING_MAX) { + GDScriptWarning::Code warning_code = GDScriptWarning::get_code_from_name(String(warning_name).to_upper()); + if (warning_code == GDScriptWarning::WARNING_MAX) { push_error(vformat(R"(Invalid warning name: "%s".)", warning_name), p_annotation); has_error = true; } else { - p_target->ignored_warnings.push_back(warning); + int start_line = p_annotation->start_line; + int end_line = p_target->end_line; + + switch (p_target->type) { +#define SIMPLE_CASE(m_type, m_class, m_property) \ + case m_type: { \ + m_class *node = static_cast(p_target); \ + if (node->m_property == nullptr) { \ + end_line = node->start_line; \ + } else { \ + end_line = node->m_property->end_line; \ + } \ + } break; + + // Can contain properties (set/get). + SIMPLE_CASE(Node::VARIABLE, VariableNode, initializer) + + // Contain bodies. + SIMPLE_CASE(Node::FOR, ForNode, list) + SIMPLE_CASE(Node::IF, IfNode, condition) + SIMPLE_CASE(Node::MATCH, MatchNode, test) + SIMPLE_CASE(Node::WHILE, WhileNode, condition) +#undef SIMPLE_CASE + + case Node::CLASS: { + end_line = p_target->start_line; + for (const AnnotationNode *annotation : p_target->annotations) { + start_line = MIN(start_line, annotation->start_line); + end_line = MAX(end_line, annotation->end_line); + } + } break; + + case Node::FUNCTION: { + // `@warning_ignore` on function has a controversial feature that is used in tests. + // It's better not to remove it for now, while there is no way to mass-ignore warnings. + } break; + + case Node::MATCH_BRANCH: { + MatchBranchNode *branch = static_cast(p_target); + end_line = branch->start_line; + for (int i = 0; i < branch->patterns.size(); i++) { + end_line = MAX(end_line, branch->patterns[i]->end_line); + } + } break; + + default: { + } break; + } + + end_line = MAX(start_line, end_line); // Prevent infinite loop. + for (int line = start_line; line <= end_line; line++) { + warning_ignored_lines[warning_code].insert(line); + } } } - return !has_error; - -#else // ! DEBUG_ENABLED - // Only available in debug builds. - return true; #endif // DEBUG_ENABLED } diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 45ab3f3e3869..583b60bf16ce 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -338,9 +338,6 @@ class GDScriptParser { int leftmost_column = 0, rightmost_column = 0; Node *next = nullptr; List annotations; -#ifdef DEBUG_ENABLED - Vector ignored_warnings; -#endif DataType datatype; @@ -900,9 +897,10 @@ class GDScriptParser { union { ParameterNode *parameter_source = nullptr; - ConstantNode *constant_source; - VariableNode *variable_source; IdentifierNode *bind_source; + VariableNode *variable_source; + ConstantNode *constant_source; + SignalNode *signal_source; }; FunctionNode *source_function = nullptr; @@ -1051,6 +1049,8 @@ class GDScriptParser { MemberDocData doc_data; #endif // TOOLS_ENABLED + int usages = 0; + SignalNode() { type = SIGNAL; } @@ -1334,9 +1334,17 @@ class GDScriptParser { List errors; #ifdef DEBUG_ENABLED + struct PendingWarning { + const Node *source = nullptr; + GDScriptWarning::Code code = GDScriptWarning::WARNING_MAX; + bool treated_as_error = false; + Vector symbols; + }; + bool is_ignoring_warnings = false; List warnings; - HashSet ignored_warnings; + List pending_warnings; + HashSet warning_ignored_lines[GDScriptWarning::WARNING_MAX]; HashSet unsafe_lines; #endif @@ -1368,7 +1376,7 @@ class GDScriptParser { FUNCTION = 1 << 5, STATEMENT = 1 << 6, STANDALONE = 1 << 7, - CLASS_LEVEL = CLASS | VARIABLE | FUNCTION, + CLASS_LEVEL = CLASS | VARIABLE | CONSTANT | SIGNAL | FUNCTION, }; uint32_t target_kind = 0; // Flags. AnnotationAction apply = nullptr; @@ -1438,6 +1446,7 @@ class GDScriptParser { void push_warning(const Node *p_source, GDScriptWarning::Code p_code, const Symbols &...p_symbols) { push_warning(p_source, p_code, Vector{ p_symbols... }); } + void apply_pending_warnings(); #endif void make_completion_context(CompletionType p_type, Node *p_node, int p_argument = -1, bool p_force = false); diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index 1fe9b0425c83..e7d9787eabd9 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -52,13 +52,13 @@ String GDScriptWarning::get_message() const { return vformat(R"(The local constant "%s" is declared but never used in the block. If this is intended, prefix it with an underscore: "_%s".)", symbols[0], symbols[0]); case UNUSED_PRIVATE_CLASS_VARIABLE: CHECK_SYMBOLS(1); - return vformat(R"(The class variable "%s" is declared but never used in the script.)", symbols[0]); + return vformat(R"(The class variable "%s" is declared but never used in the class.)", symbols[0]); case UNUSED_PARAMETER: CHECK_SYMBOLS(2); return vformat(R"*(The parameter "%s" is never used in the function "%s()". If this is intended, prefix it with an underscore: "_%s".)*", symbols[1], symbols[0], symbols[1]); case UNUSED_SIGNAL: CHECK_SYMBOLS(1); - return vformat(R"(The signal "%s" is declared but never emitted.)", symbols[0]); + return vformat(R"(The signal "%s" is declared but never explicitly used in the class.)", symbols[0]); case SHADOWED_VARIABLE: CHECK_SYMBOLS(4); return vformat(R"(The local %s "%s" is shadowing an already-declared %s at line %s.)", symbols[0], symbols[1], symbols[2], symbols[3]); @@ -76,18 +76,9 @@ String GDScriptWarning::get_message() const { case STANDALONE_EXPRESSION: return "Standalone expression (the line has no effect)."; case STANDALONE_TERNARY: - return "Standalone ternary conditional operator: the return value is being discarded."; + return "Standalone ternary operator: the return value is being discarded."; case INCOMPATIBLE_TERNARY: - return "Values of the ternary conditional are not mutually compatible."; - case PROPERTY_USED_AS_FUNCTION: - CHECK_SYMBOLS(2); - return vformat(R"*(The method "%s()" was not found in base "%s" but there's a property with the same name. Did you mean to access it?)*", symbols[0], symbols[1]); - case CONSTANT_USED_AS_FUNCTION: - CHECK_SYMBOLS(2); - return vformat(R"*(The method "%s()" was not found in base "%s" but there's a constant with the same name. Did you mean to access it?)*", symbols[0], symbols[1]); - case FUNCTION_USED_AS_PROPERTY: - CHECK_SYMBOLS(2); - return vformat(R"(The property "%s" was not found in base "%s" but there's a method with the same name. Did you mean to call it?)", symbols[0], symbols[1]); + return "Values of the ternary operator are not mutually compatible."; case UNTYPED_DECLARATION: CHECK_SYMBOLS(2); if (symbols[0] == "Function") { @@ -162,10 +153,17 @@ String GDScriptWarning::get_message() const { 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"("@onready" will set the default value after "@export" takes effect and will override it.)"; +#ifndef DISABLE_DEPRECATED + // Never produced. These warnings migrated from 3.x by mistake. + case PROPERTY_USED_AS_FUNCTION: // There is already an error. + case CONSTANT_USED_AS_FUNCTION: // There is already an error. + case FUNCTION_USED_AS_PROPERTY: // This is valid, returns `Callable`. + break; +#endif case WARNING_MAX: - break; // Can't happen, but silences warning + break; // Can't happen, but silences warning. } - ERR_FAIL_V_MSG(String(), "Invalid GDScript warning code: " + get_name_from_code(code) + "."); + ERR_FAIL_V_MSG(String(), vformat(R"(Invalid GDScript warning "%s".)", get_name_from_code(code))); #undef CHECK_SYMBOLS } @@ -206,9 +204,6 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "STANDALONE_EXPRESSION", "STANDALONE_TERNARY", "INCOMPATIBLE_TERNARY", - "PROPERTY_USED_AS_FUNCTION", - "CONSTANT_USED_AS_FUNCTION", - "FUNCTION_USED_AS_PROPERTY", "UNTYPED_DECLARATION", "INFERRED_DECLARATION", "UNSAFE_PROPERTY_ACCESS", @@ -236,6 +231,11 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "NATIVE_METHOD_OVERRIDE", "GET_NODE_DEFAULT_WITHOUT_ONREADY", "ONREADY_WITH_EXPORT", +#ifndef DISABLE_DEPRECATED + "PROPERTY_USED_AS_FUNCTION", + "CONSTANT_USED_AS_FUNCTION", + "FUNCTION_USED_AS_PROPERTY", +#endif }; 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 1aef6fa81bbe..69cc8c179fbc 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -50,9 +50,9 @@ class GDScriptWarning { UNASSIGNED_VARIABLE_OP_ASSIGN, // Variable never assigned but used in an assignment operation (+=, *=, etc). UNUSED_VARIABLE, // Local variable is declared but never used. UNUSED_LOCAL_CONSTANT, // Local constant is declared but never used. - UNUSED_PRIVATE_CLASS_VARIABLE, // Class variable is declared private ("_" prefix) but never used in the file. + UNUSED_PRIVATE_CLASS_VARIABLE, // Class variable is declared private ("_" prefix) but never used in the class. UNUSED_PARAMETER, // Function parameter is never used. - UNUSED_SIGNAL, // Signal is defined but never emitted. + UNUSED_SIGNAL, // Signal is defined but never explicitly used in the class. SHADOWED_VARIABLE, // Variable name shadowed by other variable in same class. SHADOWED_VARIABLE_BASE_CLASS, // Variable name shadowed by other variable in some base class. SHADOWED_GLOBAL_IDENTIFIER, // A global class or function has the same name as variable. @@ -61,9 +61,6 @@ class GDScriptWarning { STANDALONE_EXPRESSION, // Expression not assigned to a variable. STANDALONE_TERNARY, // Return value of ternary expression is discarded. INCOMPATIBLE_TERNARY, // Possible values of a ternary if are not mutually compatible. - PROPERTY_USED_AS_FUNCTION, // Function not found, but there's a property with the same name. - CONSTANT_USED_AS_FUNCTION, // Function not found, but there's a constant with the same name. - FUNCTION_USED_AS_PROPERTY, // Property not found, but there's a function with the same name. UNTYPED_DECLARATION, // Variable/parameter/function has no static type, explicitly specified or implicitly inferred. INFERRED_DECLARATION, // Variable/constant/parameter has an implicitly inferred static type. UNSAFE_PROPERTY_ACCESS, // Property not found in the detected type (but can be in subtypes). @@ -91,6 +88,11 @@ class GDScriptWarning { 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. +#ifndef DISABLE_DEPRECATED + PROPERTY_USED_AS_FUNCTION, // Function not found, but there's a property with the same name. + CONSTANT_USED_AS_FUNCTION, // Function not found, but there's a constant with the same name. + FUNCTION_USED_AS_PROPERTY, // Property not found, but there's a function with the same name. +#endif WARNING_MAX, }; @@ -110,9 +112,6 @@ class GDScriptWarning { WARN, // STANDALONE_EXPRESSION WARN, // STANDALONE_TERNARY WARN, // INCOMPATIBLE_TERNARY - WARN, // PROPERTY_USED_AS_FUNCTION - WARN, // CONSTANT_USED_AS_FUNCTION - WARN, // FUNCTION_USED_AS_PROPERTY IGNORE, // UNTYPED_DECLARATION // Static typing is optional, we don't want to spam warnings. IGNORE, // INFERRED_DECLARATION // Static typing is optional, we don't want to spam warnings. IGNORE, // UNSAFE_PROPERTY_ACCESS // Too common in untyped scenarios. @@ -140,6 +139,11 @@ class GDScriptWarning { 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. +#ifndef DISABLE_DEPRECATED + WARN, // PROPERTY_USED_AS_FUNCTION + WARN, // CONSTANT_USED_AS_FUNCTION + WARN, // FUNCTION_USED_AS_PROPERTY +#endif }; static_assert((sizeof(default_warning_levels) / sizeof(default_warning_levels[0])) == WARNING_MAX, "Amount of default levels does not match the amount of warnings."); diff --git a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.gd b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.gd deleted file mode 100644 index 292db30bcd49..000000000000 --- a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.gd +++ /dev/null @@ -1,15 +0,0 @@ -@warning_ignore("unused_private_class_variable") -var _unused = 2 - -@warning_ignore("unused_variable") -func test(): - print("test") - var unused = 3 - - @warning_ignore("redundant_await") - print(await regular_func()) - - print("done") - -func regular_func() -> int: - return 0 diff --git a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.out b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.out deleted file mode 100644 index 42292774a00a..000000000000 --- a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.out +++ /dev/null @@ -1,4 +0,0 @@ -GDTEST_OK -test -0 -done diff --git a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_targets.gd b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_targets.gd new file mode 100644 index 000000000000..10eca33647e7 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_targets.gd @@ -0,0 +1,35 @@ +@warning_ignore("confusable_identifier") +class MyClАss: + var my_vАr + +@warning_ignore("narrowing_conversion") +var i: int = f: + get: + return f + +var f: float + +@warning_ignore("narrowing_conversion") +func test_func(_i: int = f): + i = f + +func test(): + @warning_ignore("narrowing_conversion") + if signi(f): # TODO: Allow `@warning_ignore` before `elif`? + i = f + + @warning_ignore("narrowing_conversion") + match signi(f): + 1: + i = f + @warning_ignore("confusable_identifier") + var my_vАr: + var _my_vАr: Variant = my_vАr + + @warning_ignore("narrowing_conversion") + for j in signi(f): + i = f + + @warning_ignore("narrowing_conversion") + while signi(f): + i = f diff --git a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_targets.out b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_targets.out new file mode 100644 index 000000000000..032af0322be1 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_targets.out @@ -0,0 +1,29 @@ +GDTEST_OK +>> WARNING +>> Line: 3 +>> CONFUSABLE_IDENTIFIER +>> The identifier "my_vАr" has misleading characters and might be confused with something else. +>> WARNING +>> Line: 8 +>> NARROWING_CONVERSION +>> Narrowing conversion (float is converted to int and loses precision). +>> WARNING +>> Line: 19 +>> NARROWING_CONVERSION +>> Narrowing conversion (float is converted to int and loses precision). +>> WARNING +>> Line: 24 +>> NARROWING_CONVERSION +>> Narrowing conversion (float is converted to int and loses precision). +>> WARNING +>> Line: 27 +>> CONFUSABLE_IDENTIFIER +>> The identifier "_my_vАr" has misleading characters and might be confused with something else. +>> WARNING +>> Line: 31 +>> NARROWING_CONVERSION +>> Narrowing conversion (float is converted to int and loses precision). +>> WARNING +>> Line: 35 +>> NARROWING_CONVERSION +>> Narrowing conversion (float is converted to int and loses precision). diff --git a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd new file mode 100644 index 000000000000..8a1ab6f406f6 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd @@ -0,0 +1,156 @@ +@warning_ignore("redundant_static_unload") +@static_unload +extends Node + +class A extends Node: + static func static_called_on_instance(): + pass + + @warning_ignore("get_node_default_without_onready") + var get_node_default_without_onready = $Node + +@warning_ignore("unused_private_class_variable") +var _unused_private_class_variable + +@warning_ignore("onready_with_export") +@onready @export var onready_with_export = 1 + +var shadowed_variable +var confusable_local_usage + +@warning_ignore("unused_signal") +signal unused_signal() + +func variant_func() -> Variant: + return null + +func int_func() -> int: + return 1 + +@warning_ignore("unused_parameter") +func test_warnings(unused_private_class_variable): + var t = 1 + + @warning_ignore("unassigned_variable") + var unassigned_variable + print(unassigned_variable) + + var _unassigned_variable_op_assign + @warning_ignore("unassigned_variable_op_assign") + _unassigned_variable_op_assign += t + + @warning_ignore("unused_variable") + var unused_variable + + @warning_ignore("unused_local_constant") + const unused_local_constant = 1 + + @warning_ignore("shadowed_variable") + var shadowed_variable = 1 + print(shadowed_variable) + + @warning_ignore("shadowed_variable_base_class") + var name = "test" + print(name) + + @warning_ignore("shadowed_global_identifier") + var var_to_str = 1 + print(var_to_str) + + @warning_ignore("standalone_expression") + 1 + 2 + + @warning_ignore("standalone_ternary") + 1 if 2 else 3 + + @warning_ignore("incompatible_ternary") + t = 1 if 2 else false + + @warning_ignore("unsafe_property_access") + self.unsafe_property_access = 1 + + var node: Node = null + @warning_ignore("unsafe_method_access") + node.unsafe_method_access() + + @warning_ignore("unsafe_cast") + print(variant_func().x as int) + + var key: Variant = "key" + @warning_ignore("unsafe_call_argument") + set(key, 1) + + variant_func() # No warning (intended?). + @warning_ignore("return_value_discarded") + int_func() + + var a: A = null + @warning_ignore("static_called_on_instance") + a.static_called_on_instance() + + @warning_ignore("redundant_await") + await 1 + + @warning_ignore("assert_always_true") + assert(true) + + assert(false) # No warning (intended). + @warning_ignore("assert_always_false") + assert(false and false) + + @warning_ignore("integer_division") + var _integer_division = 5 / 2 + + @warning_ignore("narrowing_conversion") + var _narrowing_conversion: int = floorf(2.5) + + @warning_ignore("int_as_enum_without_cast") + var _int_as_enum_without_cast: Variant.Type = 1 + + @warning_ignore("int_as_enum_without_cast", "int_as_enum_without_match") + var _int_as_enum_without_match: Variant.Type = 255 + + @warning_ignore("confusable_identifier") + var _cОnfusable_identifier = 1 + + if true: + @warning_ignore("confusable_local_declaration") + var _confusable_local_declaration = 1 + var _confusable_local_declaration = 2 + + @warning_ignore("confusable_local_usage") + print(confusable_local_usage) + @warning_ignore("shadowed_variable") + var confusable_local_usage = 2 + print(confusable_local_usage) + + @warning_ignore("inference_on_variant") + var _inference_on_variant := variant_func() + +func test_unreachable_code(): + return + @warning_ignore("unreachable_code") + print(1) + +func test_unreachable_pattern(): + match 1: + _: + print(0) + @warning_ignore("unreachable_pattern") + 1: + print(1) + +func test_unsafe_void_return_variant() -> void: + return variant_func() # No warning (intended?). + +func test_unsafe_void_return() -> void: + @warning_ignore("unsafe_method_access", "unsafe_void_return") + return variant_func().f() + +@warning_ignore("native_method_override") +func get_class(): + pass + +# We don't want to execute it because of errors, just analyze. +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.out b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.out new file mode 100644 index 000000000000..d73c5eb7cde3 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.out @@ -0,0 +1 @@ +GDTEST_OK diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.out b/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.out index fd88d2395069..c902676a5dea 100644 --- a/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.out +++ b/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.out @@ -2,8 +2,8 @@ GDTEST_OK >> WARNING >> Line: 3 >> UNUSED_PRIVATE_CLASS_VARIABLE ->> The class variable "_a" is declared but never used in the script. +>> The class variable "_a" is declared but never used in the class. >> WARNING >> Line: 7 >> UNUSED_PRIVATE_CLASS_VARIABLE ->> The class variable "_d" is declared but never used in the script. +>> The class variable "_d" is declared but never used in the class. diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.gd b/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.gd new file mode 100644 index 000000000000..d937dfdcfe46 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.gd @@ -0,0 +1,12 @@ +signal s1() +signal s2() +signal s3() +@warning_ignore("unused_signal") +signal s4() + +func no_exec(): + s1.emit() + print(s2) + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.out b/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.out new file mode 100644 index 000000000000..ff570178302f --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.out @@ -0,0 +1,5 @@ +GDTEST_OK +>> WARNING +>> Line: 3 +>> UNUSED_SIGNAL +>> The signal "s3" is declared but never explicitly used in the class. diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.out b/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.out index 26fe23fb7881..497d36120477 100644 --- a/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.out +++ b/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.out @@ -1,2 +1,2 @@ -GDTEST_ANALYZER_ERROR +GDTEST_PARSER_ERROR "@tool" annotation can only be used once. diff --git a/modules/gdscript/tests/scripts/parser/features/class_inheritance_access.gd b/modules/gdscript/tests/scripts/parser/features/class_inheritance_access.gd index eb392672eb58..2e1407237fc5 100644 --- a/modules/gdscript/tests/scripts/parser/features/class_inheritance_access.gd +++ b/modules/gdscript/tests/scripts/parser/features/class_inheritance_access.gd @@ -4,6 +4,7 @@ class Parent: var parent_variable := 2 + @warning_ignore("unused_signal") signal parent_signal var parent_attribute: int: diff --git a/modules/gdscript/tests/scripts/parser/features/lambda_ends_with_new_line.gd b/modules/gdscript/tests/scripts/parser/features/lambda_ends_with_new_line.gd index f16c768f7f22..46b6856d22dd 100644 --- a/modules/gdscript/tests/scripts/parser/features/lambda_ends_with_new_line.gd +++ b/modules/gdscript/tests/scripts/parser/features/lambda_ends_with_new_line.gd @@ -14,6 +14,7 @@ func test(): print(v) print() + @warning_ignore("standalone_ternary") v=func(): print(2) if false else print(3) @warning_ignore("unsafe_cast") (v as Callable).call() diff --git a/modules/gdscript/tests/scripts/parser/features/match.gd b/modules/gdscript/tests/scripts/parser/features/match.gd index 59b5ba242608..a2e93c64fd22 100644 --- a/modules/gdscript/tests/scripts/parser/features/match.gd +++ b/modules/gdscript/tests/scripts/parser/features/match.gd @@ -14,3 +14,6 @@ func test(): print("This won't match") _: print("This will match") + + match 0: + pass diff --git a/modules/gdscript/tests/scripts/parser/features/signal_declaration.gd b/modules/gdscript/tests/scripts/parser/features/signal_declaration.gd index e4d6a72f9077..d02f82d41744 100644 --- a/modules/gdscript/tests/scripts/parser/features/signal_declaration.gd +++ b/modules/gdscript/tests/scripts/parser/features/signal_declaration.gd @@ -1,5 +1,3 @@ -#GDTEST_OK - # No parentheses. signal a @@ -16,5 +14,15 @@ signal d( c, ) +# With type hints. +signal e(a: int, b: Variant, c: Node) + +func no_exec(): + a.emit() + b.emit() + c.emit() + d.emit() + e.emit() + func test(): print("Ok") diff --git a/modules/gdscript/tests/scripts/parser/warnings/incompatible_ternary.out b/modules/gdscript/tests/scripts/parser/warnings/incompatible_ternary.out index 7d1558c6fc40..ff3e8272555b 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/incompatible_ternary.out +++ b/modules/gdscript/tests/scripts/parser/warnings/incompatible_ternary.out @@ -2,4 +2,4 @@ GDTEST_OK >> WARNING >> Line: 8 >> INCOMPATIBLE_TERNARY ->> Values of the ternary conditional are not mutually compatible. +>> Values of the ternary operator are not mutually compatible. diff --git a/modules/gdscript/tests/scripts/parser/warnings/standalone_ternary.gd b/modules/gdscript/tests/scripts/parser/warnings/standalone_ternary.gd new file mode 100644 index 000000000000..9b296e02e193 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/warnings/standalone_ternary.gd @@ -0,0 +1,3 @@ +func test(): + 1 if true else 2 + print(1) if true else print(2) diff --git a/modules/gdscript/tests/scripts/parser/warnings/standalone_ternary.out b/modules/gdscript/tests/scripts/parser/warnings/standalone_ternary.out new file mode 100644 index 000000000000..477449e0e3a4 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/warnings/standalone_ternary.out @@ -0,0 +1,10 @@ +GDTEST_OK +>> WARNING +>> Line: 2 +>> STANDALONE_TERNARY +>> Standalone ternary operator: the return value is being discarded. +>> WARNING +>> Line: 3 +>> STANDALONE_TERNARY +>> Standalone ternary operator: the return value is being discarded. +1 diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info.gd b/modules/gdscript/tests/scripts/runtime/features/member_info.gd index 6fe9647b4d3b..d7485f49e6fb 100644 --- a/modules/gdscript/tests/scripts/runtime/features/member_info.gd +++ b/modules/gdscript/tests/scripts/runtime/features/member_info.gd @@ -56,6 +56,16 @@ signal test_signal_6(a: Resource, b: Array[Resource]) signal test_signal_7(a: TestMemberInfo, b: Array[TestMemberInfo]) signal test_signal_8(a: MyClass, b: Array[MyClass]) +func no_exec(): + test_signal_1.emit() + test_signal_2.emit() + test_signal_3.emit() + test_signal_4.emit() + test_signal_5.emit() + test_signal_6.emit() + test_signal_7.emit() + test_signal_8.emit() + func test(): var script: Script = get_script() for property in script.get_property_list(): diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd index 563c6ce569cc..ee5c1e126788 100644 --- a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd +++ b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd @@ -11,7 +11,9 @@ class A: static func test_static_func_a2(): pass func test_func_a1(): pass func test_func_a2(): pass + @warning_ignore("unused_signal") signal test_signal_a1() + @warning_ignore("unused_signal") signal test_signal_a2() class B extends A: @@ -23,7 +25,9 @@ class B extends A: static func test_static_func_b2(): pass func test_func_b1(): pass func test_func_b2(): pass + @warning_ignore("unused_signal") signal test_signal_b1() + @warning_ignore("unused_signal") signal test_signal_b2() func test():