From 1bd7b99182f7e8de4d6b2f089fec5db9392ac6b8 Mon Sep 17 00:00:00 2001 From: Danil Alexeev Date: Sat, 1 Nov 2025 16:08:07 +0300 Subject: [PATCH] GDScript: Add `debug/gdscript/warnings/directory_rules` project setting --- doc/classes/ProjectSettings.xml | 99 ++++++++-------- editor/plugins/plugin_config_dialog.cpp | 4 + editor/plugins/plugin_config_dialog.h | 1 + modules/gdscript/gdscript.cpp | 44 ++++--- modules/gdscript/gdscript_editor.cpp | 2 +- modules/gdscript/gdscript_parser.cpp | 109 +++++++++++++++--- modules/gdscript/gdscript_parser.h | 30 ++++- modules/gdscript/gdscript_warning.cpp | 8 +- modules/gdscript/gdscript_warning.h | 8 +- .../gdscript/tests/gdscript_test_runner.cpp | 13 ++- 10 files changed, 222 insertions(+), 96 deletions(-) diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index e37f099cf38..f8f43411436 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -530,151 +530,154 @@ If the [code]--log-file <file>[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url] is used, log rotation is always disabled. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an [code]assert[/code] call always evaluates to [code]false[/code]. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when an [code]assert[/code] call always evaluates to [code]false[/code]. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an [code]assert[/code] call always evaluates to [code]true[/code]. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when an [code]assert[/code] call always evaluates to [code]true[/code]. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a local variable captured by a lambda is reassigned, since this does not modify the outer local variable. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a local variable captured by a lambda is reassigned, since this does not modify the outer local variable. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an identifier contains characters that can be confused with something else, like when mixing different alphabets. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when an identifier contains characters that can be confused with something else, like when mixing different alphabets. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an identifier declared in the nested block has the same name as an identifier declared below in the parent block. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when an identifier declared in the nested block has the same name as an identifier declared below in the parent block. - 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 [b]Warn[/b] or [b]Error[/b], 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 deprecated keywords are used. + When set to [b]Warn[/b] or [b]Error[/b], 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. + + The rules for including or excluding scripts when generating warnings, as a dictionary. Each rule is an entry consisting of a directory path (key) and a decision (value). When trying to generate a warning, the GDScript parser chooses the most specific rule, i.e. the most nested directory containing the script. If the decision is [b]Exclude[/b], warnings are not generated for this script. If the decision is [b]Include[/b] or the script doesn't satisfy any of the rules, the warning configuration specified in the Project Settings is applied. + It is recommended to include your own addons/libraries, either project-specific or actively being developed at the moment. Third-party or project-agnostic addons/libraries should be excluded, as they may be incompatible with the project's warning configuration. + [b]Note:[/b] It is not recommended to remove or change the rule for [code]"res://addons"[/code] as the project's warning configuration may break third-party addons. Instead, consider including individual addons, if necessary. + [b]Note:[/b] The editor does not check whether the specified paths are existing directories. It also does not automatically update these paths when directories are moved. + - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an empty file is parsed. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when an empty file is parsed. If [code]true[/code], enables specific GDScript warnings (see [code]debug/gdscript/warnings/*[/code] settings). If [code]false[/code], disables all GDScript warnings. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a variable has an enum type but no explicit default value, but only if the enum does not contain [code]0[/code] as a valid value. - - - If [code]true[/code], scripts in the [code]res://addons[/code] folder will not generate warnings. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a variable has an enum type but no explicit default value, but only if the enum does not contain [code]0[/code] as a valid value. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when [method Node.get_node] (or the shorthand [code]$[/code]) is used as default value of a class variable without the [code]@onready[/code] annotation. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when [method Node.get_node] (or the shorthand [code]$[/code]) is used as default value of a class variable without the [code]@onready[/code] annotation. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a ternary operator may emit values with incompatible types. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a ternary operator may emit values with incompatible types. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a static inferred type uses a [Variant] as initial value, which makes the static type to also be Variant. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a static inferred type uses a [Variant] as initial value, which makes the static type to also be Variant. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a variable, constant, or parameter has an implicitly inferred static type. In GDScript, type inference is performed by declaring a variable with [code]:=[/code] instead of [code]=[/code] and leaving out the type specifier. For example, [code]var x := 1[/code] will [i]infer[/i] the [int] type, while [code]var x: int = 1[/code] explicitly declares the variable as [int]. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a variable, constant, or parameter has an implicitly inferred static type. In GDScript, type inference is performed by declaring a variable with [code]:=[/code] instead of [code]=[/code] and leaving out the type specifier. For example, [code]var x := 1[/code] will [i]infer[/i] the [int] type, while [code]var x: int = 1[/code] explicitly declares the variable as [int]. [b]Note:[/b] This warning is recommended [i]in addition[/i] to [member debug/gdscript/warnings/untyped_declaration] if you want to always specify the type explicitly. Having [code]INFERRED_DECLARATION[/code] warning level higher than [code]UNTYPED_DECLARATION[/code] warning level makes little sense and is not recommended. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when trying to use an integer as an enum without an explicit cast. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when trying to use an integer as an enum without an explicit cast. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when trying to use an integer as an enum when there is no matching enum member for that numeric value. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when trying to use an integer as an enum when there is no matching enum member for that numeric value. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when dividing an integer by another integer (the decimal part will be discarded). + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when dividing an integer by another integer (the decimal part will be discarded). - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when calling a coroutine without [code]await[/code]. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when calling a coroutine without [code]await[/code]. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when the base class script has the [code]@tool[/code] annotation, but the current class script does not have it. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when the base class script has the [code]@tool[/code] annotation, but the current class script does not have it. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when passing a floating-point value to a function that expects an integer (it will be converted and lose precision). + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when passing a floating-point value to a function that expects an integer (it will be converted and lose precision). - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a method in the script overrides a native method, because it may not behave as expected. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a method in the script overrides a native method, because it may not behave as expected. - 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 [b]Warn[/b] or [b]Error[/b], 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 a function that is not a coroutine is called with await. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a function that is not a coroutine is called with await. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when the [code]@static_unload[/code] annotation is used in a script without any static variables. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when the [code]@static_unload[/code] annotation is used in a script without any static variables. When enabled, using a property, enum, or function that was renamed since Godot 3 will produce a hint if an error occurs. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when calling a function without using its return value (by assigning it to a variable or using it as a function argument). These return values are sometimes used to indicate possible errors using the [enum Error] enum. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when calling a function without using its return value (by assigning it to a variable or using it as a function argument). These return values are sometimes used to indicate possible errors using the [enum Error] enum. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when defining a local or member variable, signal, or enum that would have the same name as a built-in function or global class name, thus shadowing it. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when defining a local or member variable, signal, or enum that would have the same name as a built-in function or global class name, thus shadowing it. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a local variable or local constant shadows a member declared in the current class. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a local variable or local constant shadows a member declared in the current class. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a local variable or local constant shadows a member declared in a base class. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a local variable or local constant shadows a member declared in a base class. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when calling an expression that may have no effect on the surrounding code, such as writing [code]2 + 2[/code] as a statement. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when calling an expression that may have no effect on the surrounding code, such as writing [code]2 + 2[/code] as a statement. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when calling a ternary expression that may have no effect on the surrounding code, such as writing [code]42 if active else 0[/code] as a statement. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when calling a ternary expression that may have no effect on the surrounding code, such as writing [code]42 if active else 0[/code] as a statement. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when calling a static method from an instance of a class instead of from the class directly. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when calling a static method from an instance of a class instead of from the class directly. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when using a variable that wasn't previously assigned. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when using a variable that wasn't previously assigned. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when assigning a variable using an assignment operator like [code]+=[/code] if the variable wasn't previously assigned. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when assigning a variable using an assignment operator like [code]+=[/code] if the variable wasn't previously assigned. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when unreachable code is detected (such as after a [code]return[/code] statement that will always be executed). + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when unreachable code is detected (such as after a [code]return[/code] statement that will always be executed). - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when an unreachable [code]match[/code] pattern is detected. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when an unreachable [code]match[/code] pattern is detected. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when using an expression whose type may not be compatible with the function parameter expected. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when using an expression whose type may not be compatible with the function parameter expected. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a [Variant] value is cast to a non-Variant. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a [Variant] value is cast to a non-Variant. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when calling a method whose presence is not guaranteed at compile-time in the class. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when calling a method whose presence is not guaranteed at compile-time in the class. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when accessing a property whose presence is not guaranteed at compile-time in the class. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when accessing a property whose presence is not guaranteed at compile-time in the class. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when returning a call from a [code]void[/code] function when such call cannot be guaranteed to be also [code]void[/code]. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when returning a call from a [code]void[/code] function when such call cannot be guaranteed to be also [code]void[/code]. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a variable or parameter has no static type, or if a function has no static return type. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a variable or parameter has no static type, or if a function has no static return type. [b]Note:[/b] This warning is recommended together with [member EditorSettings.text_editor/completion/add_type_hints] to help achieve type safety. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a local constant is never used. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a local constant is never used. - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a function parameter is never used. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a function parameter is never used. - 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 [b]Warn[/b] or [b]Error[/b], 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 explicitly used in the class. + When set to [b]Warn[/b] or [b]Error[/b], 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. + When set to [b]Warn[/b] or [b]Error[/b], produces a warning or an error respectively when a local variable is unused. Message to be displayed before the backtrace when the engine crashes. By default, this message is only used in exported projects due to the editor-only override applied to this setting. diff --git a/editor/plugins/plugin_config_dialog.cpp b/editor/plugins/plugin_config_dialog.cpp index c16225b2126..7aefbd974f5 100644 --- a/editor/plugins/plugin_config_dialog.cpp +++ b/editor/plugins/plugin_config_dialog.cpp @@ -131,6 +131,9 @@ void PluginConfigDialog::_on_required_text_changed() { if ((!script_edit->get_text().get_extension().is_empty() && script_edit->get_text().get_extension() != ext) || script_edit->get_text().ends_with(".")) { validation_panel->set_message(MSG_ID_SCRIPT, vformat(TTR("Script extension must match chosen language extension (.%s)."), ext), EditorValidationPanel::MSG_ERROR); } + if (language->get_name() == "GDScript") { + validation_panel->set_message(MSG_ID_ENABLE_WARNINGS, TTR("Consider enabling GDScript warnings for this plugin by adding an entry for it to the project setting Debug > GDScript > Warnings > Directory Rules."), EditorValidationPanel::MSG_INFO); + } } String PluginConfigDialog::_get_subfolder() { @@ -317,6 +320,7 @@ PluginConfigDialog::PluginConfigDialog() { validation_panel->add_line(MSG_ID_SCRIPT, TTR("Script extension is valid.")); validation_panel->add_line(MSG_ID_SUBFOLDER, TTR("Subfolder name is valid.")); validation_panel->add_line(MSG_ID_ACTIVE, ""); + validation_panel->add_line(MSG_ID_ENABLE_WARNINGS, ""); validation_panel->set_update_callback(callable_mp(this, &PluginConfigDialog::_on_required_text_changed)); validation_panel->set_accept_button(get_ok_button()); diff --git a/editor/plugins/plugin_config_dialog.h b/editor/plugins/plugin_config_dialog.h index fed7293cb69..1e1218fe445 100644 --- a/editor/plugins/plugin_config_dialog.h +++ b/editor/plugins/plugin_config_dialog.h @@ -47,6 +47,7 @@ class PluginConfigDialog : public ConfirmationDialog { MSG_ID_SUBFOLDER, MSG_ID_SCRIPT, MSG_ID_ACTIVE, + MSG_ID_ENABLE_WARNINGS, }; LineEdit *name_edit = nullptr; diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index d8664bb7966..9ebcb2b9602 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -2282,11 +2282,18 @@ void GDScriptLanguage::init() { GDExtensionManager::get_singleton()->connect("extension_loaded", callable_mp(this, &GDScriptLanguage::_extension_loaded)); GDExtensionManager::get_singleton()->connect("extension_unloading", callable_mp(this, &GDScriptLanguage::_extension_unloading)); } -#endif +#endif // TOOLS_ENABLED + +#ifdef DEBUG_ENABLED + GDScriptParser::update_project_settings(); + if (!ProjectSettings::get_singleton()->is_connected("settings_changed", callable_mp_static(&GDScriptParser::update_project_settings))) { + ProjectSettings::get_singleton()->connect("settings_changed", callable_mp_static(&GDScriptParser::update_project_settings)); + } +#endif // DEBUG_ENABLED #ifdef TESTS_ENABLED GDScriptTests::GDScriptTestRunner::handle_cmdline(); -#endif +#endif // TESTS_ENABLED } #ifdef TOOLS_ENABLED @@ -2950,7 +2957,7 @@ GDScriptLanguage::GDScriptLanguage() { profiling = false; profile_native_calls = false; script_frame_time = 0; -#endif +#endif // DEBUG_ENABLED _debug_max_call_stack = GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "debug/settings/gdscript/max_call_stack", PROPERTY_HINT_RANGE, "512," + itos(GDScriptFunction::MAX_CALL_DEPTH - 1) + ",1"), 1024); track_call_stack = GLOBAL_DEF_RST("debug/settings/gdscript/always_track_call_stacks", false); @@ -2961,20 +2968,29 @@ GDScriptLanguage::GDScriptLanguage() { track_locals = track_locals || EngineDebugger::is_active(); GLOBAL_DEF("debug/gdscript/warnings/enable", true); - GLOBAL_DEF("debug/gdscript/warnings/exclude_addons", true); - GLOBAL_DEF("debug/gdscript/warnings/renamed_in_godot_4_hint", true); + + GLOBAL_DEF(PropertyInfo(Variant::DICTIONARY, + "debug/gdscript/warnings/directory_rules", + PROPERTY_HINT_TYPE_STRING, + vformat("%d/%d:;%d/%d:Exclude,Include", Variant::STRING, PROPERTY_HINT_DIR, Variant::INT, PROPERTY_HINT_ENUM)), + Dictionary({ { "res://addons", GDScriptParser::WarningDirectoryRule::DECISION_EXCLUDE } })); + for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) { - GDScriptWarning::Code code = (GDScriptWarning::Code)i; - Variant default_enabled = GDScriptWarning::get_default_value(code); - String path = GDScriptWarning::get_settings_path_from_code(code); - GLOBAL_DEF(GDScriptWarning::get_property_info(code), default_enabled); - } + const GDScriptWarning::Code code = (GDScriptWarning::Code)i; + const Variant default_value = GDScriptWarning::get_default_value(code); + GLOBAL_DEF(GDScriptWarning::get_property_info(code), default_value); #ifndef DISABLE_DEPRECATED - ProjectSettings::get_singleton()->set_as_internal("debug/gdscript/warnings/property_used_as_function", true); - ProjectSettings::get_singleton()->set_as_internal("debug/gdscript/warnings/constant_used_as_function", true); - ProjectSettings::get_singleton()->set_as_internal("debug/gdscript/warnings/function_used_as_property", true); -#endif + if (i >= GDScriptWarning::FIRST_DEPRECATED_WARNING) { + const String setting_path = GDScriptWarning::get_setting_path_from_code(code); + ProjectSettings::get_singleton()->set_as_internal(setting_path, true); + } +#endif // DISABLE_DEPRECATED + } + + // TODO: This setting has nothing to do with warnings. It should be moved at the next compatibility breakage, + // if the setting is still relevant at that time. + GLOBAL_DEF("debug/gdscript/warnings/renamed_in_godot_4_hint", true); #endif // DEBUG_ENABLED } diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 38f710ae1a8..8b4ae0b65f1 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -1010,7 +1010,7 @@ static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_a if (warning_code >= GDScriptWarning::FIRST_DEPRECATED_WARNING) { break; // Don't suggest deprecated warnings as they are never produced. } -#endif +#endif // DISABLE_DEPRECATED ScriptLanguage::CodeCompletionOption warning(GDScriptWarning::get_name_from_code((GDScriptWarning::Code)warning_code).to_lower(), ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); warning.insert_text = warning.display.quote(p_quote_style); r_result.insert(warning.display, warning); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index ca64ba73862..06056ca940d 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -68,9 +68,15 @@ Variant::Type GDScriptParser::get_builtin_type(const StringName &p_type) { return Variant::VARIANT_MAX; } +#ifdef DEBUG_ENABLED +bool GDScriptParser::is_project_ignoring_warnings = false; +GDScriptWarning::WarnLevel GDScriptParser::warning_levels[GDScriptWarning::WARNING_MAX]; +LocalVector GDScriptParser::warning_directory_rules; +#endif // DEBUG_ENABLED + #ifdef TOOLS_ENABLED HashMap GDScriptParser::theme_color_names; -#endif +#endif // TOOLS_ENABLED HashMap GDScriptParser::valid_annotations; @@ -89,6 +95,53 @@ bool GDScriptParser::annotation_exists(const String &p_annotation_name) const { return valid_annotations.has(p_annotation_name); } +#ifdef DEBUG_ENABLED +void GDScriptParser::update_project_settings() { + is_project_ignoring_warnings = !GLOBAL_GET("debug/gdscript/warnings/enable").booleanize(); + + for (int i = 0; i < GDScriptWarning::WARNING_MAX; i++) { + const String setting_path = GDScriptWarning::get_setting_path_from_code((GDScriptWarning::Code)i); + warning_levels[i] = (GDScriptWarning::WarnLevel)(int)GLOBAL_GET(setting_path); + } + +#ifndef DISABLE_DEPRECATED + // We do not use `GLOBAL_GET`, since we check without taking overrides into account. We leave the migration of non-trivial configurations to the user. + if (unlikely(ProjectSettings::get_singleton()->has_setting("debug/gdscript/warnings/exclude_addons"))) { + const bool is_excluding_addons = ProjectSettings::get_singleton()->get_setting("debug/gdscript/warnings/exclude_addons", true).booleanize(); + ProjectSettings::get_singleton()->clear("debug/gdscript/warnings/exclude_addons"); + + Dictionary rules = ProjectSettings::get_singleton()->get_setting("debug/gdscript/warnings/directory_rules"); + rules["res://addons"] = is_excluding_addons ? WarningDirectoryRule::DECISION_EXCLUDE : WarningDirectoryRule::DECISION_INCLUDE; + ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/directory_rules", rules); + } +#endif // DISABLE_DEPRECATED + + warning_directory_rules.clear(); + + const Dictionary rules = GLOBAL_GET("debug/gdscript/warnings/directory_rules"); + for (const KeyValue &kv : rules) { + String dir = kv.key.operator String().simplify_path(); + ERR_CONTINUE_MSG(!dir.begins_with("res://"), R"(Paths in the project setting "debug/gdscript/warnings/directory_rules" keys must start with the "res://" prefix.)"); + if (!dir.ends_with("/")) { + dir += '/'; + } + + const int decision = kv.value; + ERR_CONTINUE(decision < 0 || decision >= WarningDirectoryRule::DECISION_MAX); + + warning_directory_rules.push_back({ dir, (WarningDirectoryRule::Decision)decision }); + } + + struct RuleSort { + bool operator()(const WarningDirectoryRule &p_a, const WarningDirectoryRule &p_b) const { + return p_a.directory_path.count("/") > p_b.directory_path.count("/"); + } + }; + + warning_directory_rules.sort_custom(); +} +#endif // DEBUG_ENABLED + GDScriptParser::GDScriptParser() { // Register valid annotations. if (unlikely(valid_annotations.is_empty())) { @@ -137,11 +190,10 @@ GDScriptParser::GDScriptParser() { } #ifdef DEBUG_ENABLED - is_ignoring_warnings = !(bool)GLOBAL_GET("debug/gdscript/warnings/enable"); for (int i = 0; i < GDScriptWarning::WARNING_MAX; i++) { warning_ignore_start_lines[i] = INT_MAX; } -#endif +#endif // DEBUG_ENABLED #ifdef TOOLS_ENABLED if (unlikely(theme_color_names.is_empty())) { @@ -161,7 +213,7 @@ GDScriptParser::GDScriptParser() { theme_color_names.insert("a", "axis_w_color"); theme_color_names.insert("a8", "axis_w_color"); } -#endif +#endif // TOOLS_ENABLED } GDScriptParser::~GDScriptParser() { @@ -195,13 +247,11 @@ void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_ ERR_FAIL_NULL(p_source); ERR_FAIL_INDEX(p_code, GDScriptWarning::WARNING_MAX); - if (is_ignoring_warnings) { + if (is_project_ignoring_warnings || is_script_ignoring_warnings) { return; } - if (GLOBAL_GET_CACHED(bool, "debug/gdscript/warnings/exclude_addons") && script_path.begins_with("res://addons/")) { - return; - } - GDScriptWarning::WarnLevel warn_level = (GDScriptWarning::WarnLevel)(int)GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(p_code)); + + const GDScriptWarning::WarnLevel warn_level = warning_levels[p_code]; if (warn_level == GDScriptWarning::IGNORE) { return; } @@ -251,6 +301,24 @@ void GDScriptParser::apply_pending_warnings() { pending_warnings.clear(); } + +void GDScriptParser::evaluate_warning_directory_rules_for_script_path() { + is_script_ignoring_warnings = false; + for (const WarningDirectoryRule &rule : warning_directory_rules) { + if (script_path.begins_with(rule.directory_path)) { + switch (rule.decision) { + case WarningDirectoryRule::DECISION_EXCLUDE: + is_script_ignoring_warnings = true; + return; // Stop checking rules. + case WarningDirectoryRule::DECISION_INCLUDE: + is_script_ignoring_warnings = false; + return; // Stop checking rules. + case WarningDirectoryRule::DECISION_MAX: + return; // Unreachable. + } + } + } +} #endif // DEBUG_ENABLED void GDScriptParser::override_completion_context(const Node *p_for_node, CompletionType p_type, Node *p_node, int p_argument) { @@ -391,9 +459,14 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ text_tokenizer->set_source_code(source); tokenizer = text_tokenizer; - tokenizer->set_cursor_position(cursor_line, cursor_column); + script_path = p_script_path.simplify_path(); + +#ifdef DEBUG_ENABLED + evaluate_warning_directory_rules_for_script_path(); +#endif // DEBUG_ENABLED + current = tokenizer->scan(); // Avoid error or newline as the first token. // The latter can mess with the parser when opening files filled exclusively with comments and newlines. @@ -414,7 +487,7 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ nd->end_line = 1; push_warning(nd, GDScriptWarning::EMPTY_FILE); } -#endif +#endif // DEBUG_ENABLED push_multiline(false); // Keep one for the whole parsing. parse_program(); @@ -422,7 +495,7 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ #ifdef TOOLS_ENABLED comment_data = tokenizer->get_comments(); -#endif +#endif // TOOLS_ENABLED memdelete(text_tokenizer); tokenizer = nullptr; @@ -431,7 +504,7 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ if (multiline_stack.size() > 0) { ERR_PRINT("Parser bug: Imbalanced multiline stack."); } -#endif +#endif // DEBUG_ENABLED if (errors.is_empty()) { return OK; @@ -450,7 +523,13 @@ Error GDScriptParser::parse_binary(const Vector &p_binary, const String } tokenizer = buffer_tokenizer; + script_path = p_script_path.simplify_path(); + +#ifdef DEBUG_ENABLED + evaluate_warning_directory_rules_for_script_path(); +#endif // DEBUG_ENABLED + current = tokenizer->scan(); // Avoid error or newline as the first token. // The latter can mess with the parser when opening files filled exclusively with comments and newlines. @@ -4997,10 +5076,6 @@ bool GDScriptParser::export_group_annotations(AnnotationNode *p_annotation, Node bool GDScriptParser::warning_ignore_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { #ifdef 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_code = GDScriptWarning::get_code_from_name(String(warning_name).to_upper()); diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 8a8ab5af458..2427481a151 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1349,6 +1349,19 @@ private: List errors; #ifdef DEBUG_ENABLED +public: + struct WarningDirectoryRule { + enum Decision { + DECISION_EXCLUDE, + DECISION_INCLUDE, + DECISION_MAX, + }; + + String directory_path; // With a trailing slash. + Decision decision = DECISION_EXCLUDE; + }; + +private: struct PendingWarning { const Node *source = nullptr; GDScriptWarning::Code code = GDScriptWarning::WARNING_MAX; @@ -1356,13 +1369,17 @@ private: Vector symbols; }; - bool is_ignoring_warnings = false; + static bool is_project_ignoring_warnings; + static GDScriptWarning::WarnLevel warning_levels[GDScriptWarning::WARNING_MAX]; + static LocalVector warning_directory_rules; + List warnings; List pending_warnings; + bool is_script_ignoring_warnings = false; HashSet warning_ignored_lines[GDScriptWarning::WARNING_MAX]; int warning_ignore_start_lines[GDScriptWarning::WARNING_MAX]; HashSet unsafe_lines; -#endif +#endif // DEBUG_ENABLED GDScriptTokenizer *tokenizer = nullptr; GDScriptTokenizer::Token previous; @@ -1473,6 +1490,7 @@ private: } void clear(); + void push_error(const String &p_message, const Node *p_origin = nullptr); #ifdef DEBUG_ENABLED void push_warning(const Node *p_source, GDScriptWarning::Code p_code, const Vector &p_symbols); @@ -1481,7 +1499,9 @@ private: push_warning(p_source, p_code, Vector{ p_symbols... }); } void apply_pending_warnings(); -#endif + void evaluate_warning_directory_rules_for_script_path(); +#endif // DEBUG_ENABLED + // Setting p_force to false will prevent the completion context from being update if a context was already set before. // This should only be done when we push context before we consumed any tokens for the corresponding structure. // See parse_precedence for an example. @@ -1615,11 +1635,13 @@ public: // TODO: Keep track of deps. return List(); } + #ifdef DEBUG_ENABLED + static void update_project_settings(); const List &get_warnings() const { return warnings; } const HashSet &get_unsafe_lines() const { return unsafe_lines; } int get_last_line_number() const { return current.end_line; } -#endif +#endif // DEBUG_ENABLED #ifdef TOOLS_ENABLED static HashMap theme_color_names; diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index 6e21cac2baa..9761f4c25db 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -170,7 +170,7 @@ String GDScriptWarning::get_message() const { case CONSTANT_USED_AS_FUNCTION: // There is already an error. case FUNCTION_USED_AS_PROPERTY: // This is valid, returns `Callable`. break; -#endif +#endif // DISABLE_DEPRECATED case WARNING_MAX: break; // Can't happen, but silences warning. } @@ -185,7 +185,7 @@ int GDScriptWarning::get_default_value(Code p_code) { } PropertyInfo GDScriptWarning::get_property_info(Code p_code) { - return PropertyInfo(Variant::INT, get_settings_path_from_code(p_code), PROPERTY_HINT_ENUM, "Ignore,Warn,Error"); + return PropertyInfo(Variant::INT, get_setting_path_from_code(p_code), PROPERTY_HINT_ENUM, "Ignore,Warn,Error"); } String GDScriptWarning::get_name() const { @@ -245,7 +245,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "PROPERTY_USED_AS_FUNCTION", "CONSTANT_USED_AS_FUNCTION", "FUNCTION_USED_AS_PROPERTY", -#endif +#endif // DISABLE_DEPRECATED }; static_assert(std_size(names) == WARNING_MAX, "Amount of warning types don't match the amount of warning names."); @@ -253,7 +253,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) { return names[(int)p_code]; } -String GDScriptWarning::get_settings_path_from_code(Code p_code) { +String GDScriptWarning::get_setting_path_from_code(Code p_code) { return "debug/gdscript/warnings/" + get_name_from_code(p_code).to_lower(); } diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index 5ce1b6fff4e..e4f22c85ba6 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -94,13 +94,13 @@ public: 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 +#endif // DISABLE_DEPRECATED WARNING_MAX, }; #ifndef DISABLE_DEPRECATED static constexpr int FIRST_DEPRECATED_WARNING = PROPERTY_USED_AS_FUNCTION; -#endif +#endif // DISABLE_DEPRECATED constexpr static WarnLevel default_warning_levels[] = { WARN, // UNASSIGNED_VARIABLE @@ -152,7 +152,7 @@ public: WARN, // PROPERTY_USED_AS_FUNCTION WARN, // CONSTANT_USED_AS_FUNCTION WARN, // FUNCTION_USED_AS_PROPERTY -#endif +#endif // DISABLE_DEPRECATED }; static_assert(std_size(default_warning_levels) == WARNING_MAX, "Amount of default levels does not match the amount of warnings."); @@ -166,7 +166,7 @@ public: static int get_default_value(Code p_code); static PropertyInfo get_property_info(Code p_code); static String get_name_from_code(Code p_code); - static String get_settings_path_from_code(Code p_code); + static String get_setting_path_from_code(Code p_code); static Code get_code_from_name(const String &p_name); }; diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp index 59faf819aea..07861790f25 100644 --- a/modules/gdscript/tests/gdscript_test_runner.cpp +++ b/modules/gdscript/tests/gdscript_test_runner.cpp @@ -145,6 +145,7 @@ GDScriptTestRunner::GDScriptTestRunner(const String &p_source_dir, bool p_init_l if (do_init_languages) { init_language(p_source_dir); } + #ifdef DEBUG_ENABLED // Set all warning levels to "Warn" in order to test them properly, even the ones that default to error. ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/enable", true); @@ -153,12 +154,16 @@ GDScriptTestRunner::GDScriptTestRunner(const String &p_source_dir, bool p_init_l // TODO: Add ability for test scripts to specify which warnings to enable/disable for testing. continue; } - String warning_setting = GDScriptWarning::get_settings_path_from_code((GDScriptWarning::Code)i); - ProjectSettings::get_singleton()->set_setting(warning_setting, (int)GDScriptWarning::WARN); + const String setting_path = GDScriptWarning::get_setting_path_from_code((GDScriptWarning::Code)i); + ProjectSettings::get_singleton()->set_setting(setting_path, (int)GDScriptWarning::WARN); } -#endif - // Enable printing to show results + // Force the call, since the language is initialized **before** applying project settings + // and the `settings_changed` signal is emitted with `call_deferred()`. + GDScriptParser::update_project_settings(); +#endif // DEBUG_ENABLED + + // Enable printing to show results. CoreGlobals::print_line_enabled = true; CoreGlobals::print_error_enabled = true; }