瀏覽代碼

GDScript: Fix `@warning_ignore` annotation issues

Danil Alexeev 1 年之前
父節點
當前提交
ef1909fca3
共有 25 個文件被更改,包括 602 次插入208 次删除
  1. 5 4
      doc/classes/ProjectSettings.xml
  2. 40 57
      modules/gdscript/gdscript_analyzer.cpp
  3. 227 89
      modules/gdscript/gdscript_parser.cpp
  4. 16 7
      modules/gdscript/gdscript_parser.h
  5. 18 18
      modules/gdscript/gdscript_warning.cpp
  6. 12 8
      modules/gdscript/gdscript_warning.h
  7. 0 15
      modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.gd
  8. 0 4
      modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.out
  9. 35 0
      modules/gdscript/tests/scripts/analyzer/features/warning_ignore_targets.gd
  10. 29 0
      modules/gdscript/tests/scripts/analyzer/features/warning_ignore_targets.out
  11. 156 0
      modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd
  12. 1 0
      modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.out
  13. 2 2
      modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.out
  14. 12 0
      modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.gd
  15. 5 0
      modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.out
  16. 1 1
      modules/gdscript/tests/scripts/parser/errors/duplicate_tool.out
  17. 1 0
      modules/gdscript/tests/scripts/parser/features/class_inheritance_access.gd
  18. 1 0
      modules/gdscript/tests/scripts/parser/features/lambda_ends_with_new_line.gd
  19. 3 0
      modules/gdscript/tests/scripts/parser/features/match.gd
  20. 10 2
      modules/gdscript/tests/scripts/parser/features/signal_declaration.gd
  21. 1 1
      modules/gdscript/tests/scripts/parser/warnings/incompatible_ternary.out
  22. 3 0
      modules/gdscript/tests/scripts/parser/warnings/standalone_ternary.gd
  23. 10 0
      modules/gdscript/tests/scripts/parser/warnings/standalone_ternary.out
  24. 10 0
      modules/gdscript/tests/scripts/runtime/features/member_info.gd
  25. 4 0
      modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd

+ 5 - 4
doc/classes/ProjectSettings.xml

@@ -470,11 +470,12 @@
 		<member name="debug/gdscript/warnings/confusable_local_usage" type="int" setter="" getter="" default="1">
 			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.
 		</member>
-		<member name="debug/gdscript/warnings/constant_used_as_function" type="int" setter="" getter="" default="1">
+		<member name="debug/gdscript/warnings/constant_used_as_function" type="int" setter="" getter="" default="1" deprecated="This warning is never produced. Instead, an error is generated if the expression type is known at compile time.">
 			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.
 		</member>
 		<member name="debug/gdscript/warnings/deprecated_keyword" type="int" setter="" getter="" default="1">
 			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.
 		</member>
 		<member name="debug/gdscript/warnings/empty_file" type="int" setter="" getter="" default="1">
 			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 @@
 		<member name="debug/gdscript/warnings/exclude_addons" type="bool" setter="" getter="" default="true">
 			If [code]true[/code], scripts in the [code]res://addons[/code] folder will not generate warnings.
 		</member>
-		<member name="debug/gdscript/warnings/function_used_as_property" type="int" setter="" getter="" default="1">
+		<member name="debug/gdscript/warnings/function_used_as_property" type="int" setter="" getter="" default="1" deprecated="This warning is never produced. When a function is used as a property, a [Callable] is returned.">
 			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.
 		</member>
 		<member name="debug/gdscript/warnings/get_node_default_without_onready" type="int" setter="" getter="" default="2">
@@ -519,7 +520,7 @@
 		<member name="debug/gdscript/warnings/onready_with_export" type="int" setter="" getter="" default="2">
 			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.
 		</member>
-		<member name="debug/gdscript/warnings/property_used_as_function" type="int" setter="" getter="" default="1">
+		<member name="debug/gdscript/warnings/property_used_as_function" type="int" setter="" getter="" default="1" deprecated="This warning is never produced. Instead, an error is generated if the expression type is known at compile time.">
 			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.
 		</member>
 		<member name="debug/gdscript/warnings/redundant_await" type="int" setter="" getter="" default="1">
@@ -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.
 		</member>
 		<member name="debug/gdscript/warnings/unused_signal" type="int" setter="" getter="" default="1">
-			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.
 		</member>
 		<member name="debug/gdscript/warnings/unused_variable" type="int" setter="" getter="" default="1">
 			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a local variable is unused.

+ 40 - 57
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<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.
@@ -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<GDScriptParserRef> 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<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);
 			}
@@ -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<GDScriptParser::ClassNode *>(p_node), true)) {
 				resolve_class_interface(static_cast<GDScriptParser::ClassNode *>(p_node), true);
 				resolve_class_body(static_cast<GDScriptParser::ClassNode *>(p_node), true);
@@ -1584,13 +1590,6 @@ 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;
 	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<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;
 
@@ -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<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);
 		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;
 	}

+ 227 - 89
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<PROPERTY_USAGE_GROUP>, varray(""));
 		register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_SUBGROUP>, 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<String> &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<GDScriptWarning>::Element *before = nullptr;
-	for (List<GDScriptWarning>::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<GDScriptWarning>::Element *before = nullptr;
+		for (List<GDScriptWarning>::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<ClassNode>();
+	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<AnnotationNode *> 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<GDScriptParser::LiteralNode *>(expression)->value.get_type() == Variant::STRING) {
-							// Allow strings as multiline comments.
-							break;
+						// Allow strings as multiline comments.
+						if (static_cast<GDScriptParser::LiteralNode *>(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>();
+	MatchNode *match_node = alloc_node<MatchNode>();
 
-	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<AnnotationNode *> 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<IdentifierNode *>(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<m_class *>(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<MatchBranchNode *>(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
 }
 

+ 16 - 7
modules/gdscript/gdscript_parser.h

@@ -338,9 +338,6 @@ public:
 		int leftmost_column = 0, rightmost_column = 0;
 		Node *next = nullptr;
 		List<AnnotationNode *> annotations;
-#ifdef DEBUG_ENABLED
-		Vector<GDScriptWarning::Code> ignored_warnings;
-#endif
 
 		DataType datatype;
 
@@ -900,9 +897,10 @@ public:
 
 		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 @@ public:
 		MemberDocData doc_data;
 #endif // TOOLS_ENABLED
 
+		int usages = 0;
+
 		SignalNode() {
 			type = SIGNAL;
 		}
@@ -1334,9 +1334,17 @@ private:
 	List<ParserError> errors;
 
 #ifdef DEBUG_ENABLED
+	struct PendingWarning {
+		const Node *source = nullptr;
+		GDScriptWarning::Code code = GDScriptWarning::WARNING_MAX;
+		bool treated_as_error = false;
+		Vector<String> symbols;
+	};
+
 	bool is_ignoring_warnings = false;
 	List<GDScriptWarning> warnings;
-	HashSet<GDScriptWarning::Code> ignored_warnings;
+	List<PendingWarning> pending_warnings;
+	HashSet<int> warning_ignored_lines[GDScriptWarning::WARNING_MAX];
 	HashSet<int> unsafe_lines;
 #endif
 
@@ -1368,7 +1376,7 @@ private:
 			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 @@ private:
 	void push_warning(const Node *p_source, GDScriptWarning::Code p_code, const Symbols &...p_symbols) {
 		push_warning(p_source, p_code, Vector<String>{ 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);

+ 18 - 18
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.");

+ 12 - 8
modules/gdscript/gdscript_warning.h

@@ -50,9 +50,9 @@ public:
 		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 @@ public:
 		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 @@ public:
 		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 @@ public:
 		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 @@ public:
 		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.");

+ 0 - 15
modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.gd

@@ -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

+ 0 - 4
modules/gdscript/tests/scripts/analyzer/features/warning_ignore_annotation.out

@@ -1,4 +0,0 @@
-GDTEST_OK
-test
-0
-done

+ 35 - 0
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

+ 29 - 0
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).

+ 156 - 0
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

+ 1 - 0
modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.out

@@ -0,0 +1 @@
+GDTEST_OK

+ 2 - 2
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.

+ 12 - 0
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

+ 5 - 0
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.

+ 1 - 1
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.

+ 1 - 0
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:

+ 1 - 0
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()

+ 3 - 0
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

+ 10 - 2
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")

+ 1 - 1
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.

+ 3 - 0
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)

+ 10 - 0
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

+ 10 - 0
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():

+ 4 - 0
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():