summaryrefslogtreecommitdiffstats
path: root/modules/gdscript
diff options
context:
space:
mode:
Diffstat (limited to 'modules/gdscript')
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml31
-rw-r--r--modules/gdscript/doc_classes/GDScript.xml3
-rw-r--r--modules/gdscript/editor/gdscript_docgen.cpp272
-rw-r--r--modules/gdscript/editor/gdscript_docgen.h42
-rw-r--r--modules/gdscript/editor/gdscript_translation_parser_plugin.cpp8
-rw-r--r--modules/gdscript/editor/script_templates/RichTextEffect/default.gd17
-rw-r--r--modules/gdscript/gdscript.cpp304
-rw-r--r--modules/gdscript/gdscript.h24
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp139
-rw-r--r--modules/gdscript/gdscript_analyzer.h1
-rw-r--r--modules/gdscript/gdscript_byte_codegen.cpp119
-rw-r--r--modules/gdscript/gdscript_byte_codegen.h4
-rw-r--r--modules/gdscript/gdscript_cache.cpp10
-rw-r--r--modules/gdscript/gdscript_cache.h3
-rw-r--r--modules/gdscript/gdscript_codegen.h2
-rw-r--r--modules/gdscript/gdscript_compiler.cpp315
-rw-r--r--modules/gdscript/gdscript_compiler.h3
-rw-r--r--modules/gdscript/gdscript_disassembler.cpp4
-rw-r--r--modules/gdscript/gdscript_editor.cpp102
-rw-r--r--modules/gdscript/gdscript_function.h3
-rw-r--r--modules/gdscript/gdscript_parser.cpp466
-rw-r--r--modules/gdscript/gdscript_parser.h23
-rw-r--r--modules/gdscript/gdscript_tokenizer.cpp2
-rw-r--r--modules/gdscript/gdscript_vm.cpp21
-rw-r--r--modules/gdscript/gdscript_warning.cpp217
-rw-r--r--modules/gdscript/gdscript_warning.h56
-rw-r--r--modules/gdscript/language_server/gdscript_extend_parser.cpp16
-rw-r--r--modules/gdscript/language_server/gdscript_language_protocol.cpp3
-rw-r--r--modules/gdscript/language_server/gdscript_workspace.cpp40
-rw-r--r--modules/gdscript/language_server/godot_lsp.h14
-rw-r--r--modules/gdscript/tests/gdscript_test_runner.cpp26
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_1.gd9
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_1.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_2.gd12
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_2.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/extend_variable.gd9
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/extend_variable.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/prints_base_type_not_found.gd6
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/prints_base_type_not_found.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.gd5
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.gd9
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/allow_void_function_to_return_void.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/assert_literal_false.gd6
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/assert_literal_false.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/auto_inferred_type_dont_error.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/lambda_unused_arg.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/shadowning.out8
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.gd10
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.out9
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/annotation_extra_comma.gd4
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/annotation_extra_comma.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/duplicate_icon.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/duplicate_icon.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/duplicate_tool.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/duplicate_tool.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.gd6
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/features/annotations.gd48
-rw-r--r--modules/gdscript/tests/scripts/parser/features/annotations.out13
-rw-r--r--modules/gdscript/tests/scripts/parser/features/constants.out16
-rw-r--r--modules/gdscript/tests/scripts/parser/features/match_bind_unused.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/features/reserved_keywords_as_attribute.gd36
-rw-r--r--modules/gdscript/tests/scripts/parser/features/reserved_keywords_as_attribute.out1
-rw-r--r--modules/gdscript/tests/scripts/parser/features/static_typing.out10
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/return_value_discarded.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/shadowed_constant.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/shadowed_global_identifier.out4
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_class.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_function.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/unassigned_variable_op_assign.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/unreachable_code_after_return.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/unused_argument.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/unused_variable.out2
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/assign_operator.gd31
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/assign_operator.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/getter_with_freed_object.gd15
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/getter_with_freed_object.out2
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_constructor.gd13
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_constructor.out4
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables.gd56
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables.out16
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables_load.gd10
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables_load.out2
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables_other.gd11
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables_other.out2
93 files changed, 1912 insertions, 838 deletions
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml
index 85ff5080f0..5a28246555 100644
--- a/modules/gdscript/doc_classes/@GDScript.xml
+++ b/modules/gdscript/doc_classes/@GDScript.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="@GDScript" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
- Built-in GDScript functions.
+ Built-in GDScript constants, functions, and annotations.
</brief_description>
<description>
A list of GDScript-specific utility functions and annotations accessible from any script.
@@ -139,7 +139,7 @@
print(is_instance_of(a, MyClass))
print(is_instance_of(a, MyClass.InnerClass))
[/codeblock]
- [b]Note:[/b] If [param value] and/or [param type] are freed objects (see [method @GlobalScope.is_instance_valid]), or [param type] is not one of the above options, this method will raise an runtime error.
+ [b]Note:[/b] If [param value] and/or [param type] are freed objects (see [method @GlobalScope.is_instance_valid]), or [param type] is not one of the above options, this method will raise a runtime error.
See also [method @GlobalScope.typeof], [method type_exists], [method Array.is_same_typed] (and other [Array] methods).
</description>
</method>
@@ -170,6 +170,7 @@
[b]Important:[/b] The path must be absolute. A relative path will always return [code]null[/code].
This function is a simplified version of [method ResourceLoader.load], which can be used for more advanced scenarios.
[b]Note:[/b] Files have to be imported into the engine first to load them using this function. If you want to load [Image]s at run-time, you may use [method Image.load]. If you want to import audio files, you can use the snippet described in [member AudioStreamMP3.data].
+ [b]Note:[/b] If [member ProjectSettings.editor/export/convert_text_resources_to_binary] is [code]true[/code], [method @GDScript.load] will not be able to read converted files in an exported project. If you rely on run-time loading of files present within the PCK, set [member ProjectSettings.editor/export/convert_text_resources_to_binary] to [code]false[/code].
</description>
</method>
<method name="preload">
@@ -227,8 +228,8 @@
To iterate over an [Array] backwards, use:
[codeblock]
var array = [3, 6, 9]
- for i in range(array.size(), 0, -1):
- print(array[i - 1])
+ for i in range(array.size() - 1, -1, -1):
+ print(array[i])
[/codeblock]
Output:
[codeblock]
@@ -273,7 +274,7 @@
[b]Warning:[/b] Numeric infinity is only a concept with floating-point numbers, and has no equivalent for integers. Dividing an integer number by [code]0[/code] will not result in [constant INF] and will result in a run-time error instead.
</constant>
<constant name="NAN" value="nan">
- "Not a Number", an invalid floating-point value. [constant NAN] has special properties, including that it is not equal to itself ([code]NAN == NAN[/code] returns [code]false[/code]). It is output by some invalid operations, such as dividing floating-point [code]0.0[/code] by [code]0.0[/code].
+ "Not a Number", an invalid floating-point value. [constant NAN] has special properties, including that [code]!=[/code] always returns [code]true[/code], while other comparison operators always return [code]false[/code]. This is true even when comparing with itself ([code]NAN == NAN[/code] returns [code]false[/code] and [code]NAN != NAN[/code] returns [code]true[/code]). It is returned by some invalid operations, such as dividing floating-point [code]0.0[/code] by [code]0.0[/code].
[b]Warning:[/b] "Not a Number" is only a concept with floating-point numbers, and has no equivalent for integers. Dividing an integer [code]0[/code] by [code]0[/code] will not result in [constant NAN] and will result in a run-time error instead.
</constant>
</constants>
@@ -457,6 +458,16 @@
[/codeblock]
</description>
</annotation>
+ <annotation name="@export_flags_avoidance">
+ <return type="void" />
+ <description>
+ Export an integer property as a bit flag field for navigation avoidance layers. The widget in the Inspector dock will use the layer names defined in [member ProjectSettings.layer_names/avoidance/layer_1].
+ See also [constant PROPERTY_HINT_LAYERS_AVOIDANCE].
+ [codeblock]
+ @export_flags_avoidance var avoidance_layers: int
+ [/codeblock]
+ </description>
+ </annotation>
<annotation name="@export_global_dir">
<return type="void" />
<description>
@@ -486,7 +497,7 @@
<param index="1" name="prefix" type="String" default="&quot;&quot;" />
<description>
Define a new group for the following exported properties. This helps to organize properties in the Inspector dock. Groups can be added with an optional [param prefix], which would make group to only consider properties that have this prefix. The grouping will break on the first property that doesn't have a prefix. The prefix is also removed from the property's name in the Inspector dock.
- If no [param prefix] is provided, the every following property is added to the group. The group ends when then next group or category is defined. You can also force end a group by using this annotation with empty strings for parameters, [code]@export_group("", "")[/code].
+ If no [param prefix] is provided, then every following property will be added to the group. The group ends when then next group or category is defined. You can also force end a group by using this annotation with empty strings for parameters, [code]@export_group("", "")[/code].
Groups cannot be nested, use [annotation @export_subgroup] to add subgroups within groups.
See also [constant PROPERTY_USAGE_GROUP].
[codeblock]
@@ -609,7 +620,7 @@
<param index="3" name="transfer_channel" type="int" default="0" />
<description>
Mark the following method for remote procedure calls. See [url=$DOCS_URL/tutorials/networking/high_level_multiplayer.html]High-level multiplayer[/url].
- The order of [code]mode[/code], [code]sync[/code] and [code]transfer_mode[/code] does not matter and all arguments can be omitted, but [code]transfer_channel[/code] always has to be the last argument. The accepted values for [code]mode[/code] are [code]"any_peer"[/code] or [code]"authority"[/code], for [code]sync[/code] are [code]"call_remote"[/code] or [code]"call_local"[/code] and for [code]transfer_mode[/code] are [code]"unreliable"[/code], [code]"unreliable_ordered"[/code] or [code]"reliable"[/code].
+ The order of [param mode], [param sync] and [param transfer_mode] does not matter and all arguments can be omitted, but [param transfer_channel] always has to be the last argument. The accepted values for [param mode] are [code]"any_peer"[/code] or [code]"authority"[/code], for [param sync] are [code]"call_remote"[/code] or [code]"call_local"[/code] and for [param transfer_mode] are [code]"unreliable"[/code], [code]"unreliable_ordered"[/code] or [code]"reliable"[/code].
[codeblock]
@rpc
func fn(): pass
@@ -622,6 +633,12 @@
[/codeblock]
</description>
</annotation>
+ <annotation name="@static_unload">
+ <return type="void" />
+ <description>
+ Make a script with static variables to not persist after all references are lost. If the script is loaded again the static variables will revert to their default values.
+ </description>
+ </annotation>
<annotation name="@tool">
<return type="void" />
<description>
diff --git a/modules/gdscript/doc_classes/GDScript.xml b/modules/gdscript/doc_classes/GDScript.xml
index 556f747eb3..f383eed480 100644
--- a/modules/gdscript/doc_classes/GDScript.xml
+++ b/modules/gdscript/doc_classes/GDScript.xml
@@ -5,7 +5,8 @@
</brief_description>
<description>
A script implemented in the GDScript programming language. The script extends the functionality of all objects that instantiate it.
- [method new] creates a new instance of the script. [method Object.set_script] extends an existing object, if that object's class matches one of the script's base classes.
+ Calling [method new] creates a new instance of the script. [method Object.set_script] extends an existing object, if that object's class matches one of the script's base classes.
+ If you are looking for GDScript's built-in functions, see [@GDScript] instead.
</description>
<tutorials>
<link title="GDScript documentation index">$DOCS_URL/tutorials/scripting/gdscript/index.html</link>
diff --git a/modules/gdscript/editor/gdscript_docgen.cpp b/modules/gdscript/editor/gdscript_docgen.cpp
new file mode 100644
index 0000000000..ce64d79747
--- /dev/null
+++ b/modules/gdscript/editor/gdscript_docgen.cpp
@@ -0,0 +1,272 @@
+/**************************************************************************/
+/* gdscript_docgen.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "gdscript_docgen.h"
+#include "../gdscript.h"
+
+using GDP = GDScriptParser;
+using GDType = GDP::DataType;
+
+static String _get_script_path(const String &p_path) {
+ return vformat(R"("%s")", p_path.get_slice("://", 1));
+}
+
+static String _get_class_name(const GDP::ClassNode &p_class) {
+ const GDP::ClassNode *curr_class = &p_class;
+ if (!curr_class->identifier) { // All inner classes have a identifier, so this is the outer class
+ return _get_script_path(curr_class->fqcn);
+ }
+
+ String full_name = curr_class->identifier->name;
+ while (curr_class->outer) {
+ curr_class = curr_class->outer;
+ if (!curr_class->identifier) { // All inner classes have a identifier, so this is the outer class
+ return vformat("%s.%s", _get_script_path(curr_class->fqcn), full_name);
+ }
+ full_name = vformat("%s.%s", curr_class->identifier->name, full_name);
+ }
+ return full_name;
+}
+
+static PropertyInfo _property_info_from_datatype(const GDType &p_type) {
+ PropertyInfo pi;
+ pi.type = p_type.builtin_type;
+ if (p_type.kind == GDType::CLASS) {
+ pi.class_name = _get_class_name(*p_type.class_type);
+ } else if (p_type.kind == GDType::ENUM && p_type.enum_type != StringName()) {
+ pi.type = Variant::INT; // Only int types are recognized as enums by the EditorHelp
+ pi.usage |= PROPERTY_USAGE_CLASS_IS_ENUM;
+ // Replace :: from enum's use of fully qualified class names with regular .
+ pi.class_name = String(p_type.native_type).replace("::", ".");
+ } else if (p_type.kind == GDType::NATIVE) {
+ pi.class_name = p_type.native_type;
+ }
+ return pi;
+}
+
+void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_class) {
+ p_script->_clear_doc();
+
+ DocData::ClassDoc &doc = p_script->doc;
+
+ doc.script_path = _get_script_path(p_script->get_script_path());
+ if (p_script->name.is_empty()) {
+ doc.name = doc.script_path;
+ } else {
+ doc.name = p_script->name;
+ }
+
+ if (p_script->_owner) {
+ doc.name = p_script->_owner->doc.name + "." + doc.name;
+ doc.script_path = doc.script_path + "." + doc.name;
+ }
+
+ doc.is_script_doc = true;
+
+ if (p_script->base.is_valid() && p_script->base->is_valid()) {
+ if (!p_script->base->doc.name.is_empty()) {
+ doc.inherits = p_script->base->doc.name;
+ } else {
+ doc.inherits = p_script->base->get_instance_base_type();
+ }
+ } else if (p_script->native.is_valid()) {
+ doc.inherits = p_script->native->get_name();
+ }
+
+ doc.brief_description = p_class->doc_brief_description;
+ doc.description = p_class->doc_description;
+ for (const Pair<String, String> &p : p_class->doc_tutorials) {
+ DocData::TutorialDoc td;
+ td.title = p.first;
+ td.link = p.second;
+ doc.tutorials.append(td);
+ }
+
+ for (const GDP::ClassNode::Member &member : p_class->members) {
+ switch (member.type) {
+ case GDP::ClassNode::Member::CLASS: {
+ const GDP::ClassNode *inner_class = member.m_class;
+ const StringName &class_name = inner_class->identifier->name;
+
+ p_script->member_lines[class_name] = inner_class->start_line;
+
+ // Recursively generate inner class docs
+ // Needs inner GDScripts to exist: previously generated in GDScriptCompiler::make_scripts()
+ GDScriptDocGen::generate_docs(*p_script->subclasses[class_name], inner_class);
+ } break;
+
+ case GDP::ClassNode::Member::CONSTANT: {
+ const GDP::ConstantNode *m_const = member.constant;
+ const StringName &const_name = member.constant->identifier->name;
+
+ p_script->member_lines[const_name] = m_const->start_line;
+
+ DocData::ConstantDoc const_doc;
+ DocData::constant_doc_from_variant(const_doc, const_name, m_const->initializer->reduced_value, m_const->doc_description);
+ doc.constants.push_back(const_doc);
+ } break;
+
+ case GDP::ClassNode::Member::FUNCTION: {
+ const GDP::FunctionNode *m_func = member.function;
+ const StringName &func_name = m_func->identifier->name;
+
+ p_script->member_lines[func_name] = m_func->start_line;
+
+ MethodInfo mi;
+ mi.name = func_name;
+
+ if (m_func->return_type) {
+ mi.return_val = _property_info_from_datatype(m_func->return_type->get_datatype());
+ }
+ for (const GDScriptParser::ParameterNode *p : m_func->parameters) {
+ PropertyInfo pi = _property_info_from_datatype(p->get_datatype());
+ pi.name = p->identifier->name;
+ mi.arguments.push_back(pi);
+ }
+
+ DocData::MethodDoc method_doc;
+ DocData::method_doc_from_methodinfo(method_doc, mi, m_func->doc_description);
+ doc.methods.push_back(method_doc);
+ } break;
+
+ case GDP::ClassNode::Member::SIGNAL: {
+ const GDP::SignalNode *m_signal = member.signal;
+ const StringName &signal_name = m_signal->identifier->name;
+
+ p_script->member_lines[signal_name] = m_signal->start_line;
+
+ MethodInfo mi;
+ mi.name = signal_name;
+ for (const GDScriptParser::ParameterNode *p : m_signal->parameters) {
+ PropertyInfo pi = _property_info_from_datatype(p->get_datatype());
+ pi.name = p->identifier->name;
+ mi.arguments.push_back(pi);
+ }
+
+ DocData::MethodDoc signal_doc;
+ DocData::signal_doc_from_methodinfo(signal_doc, mi, m_signal->doc_description);
+ doc.signals.push_back(signal_doc);
+ } break;
+
+ case GDP::ClassNode::Member::VARIABLE: {
+ const GDP::VariableNode *m_var = member.variable;
+ const StringName &var_name = m_var->identifier->name;
+
+ p_script->member_lines[var_name] = m_var->start_line;
+
+ DocData::PropertyDoc prop_doc;
+
+ prop_doc.name = var_name;
+ prop_doc.description = m_var->doc_description;
+
+ GDType dt = m_var->get_datatype();
+ switch (dt.kind) {
+ case GDType::CLASS:
+ prop_doc.type = _get_class_name(*dt.class_type);
+ break;
+ case GDType::VARIANT:
+ prop_doc.type = "Variant";
+ break;
+ case GDType::ENUM:
+ prop_doc.type = Variant::get_type_name(dt.builtin_type);
+ // Replace :: from enum's use of fully qualified class names with regular .
+ prop_doc.enumeration = String(dt.native_type).replace("::", ".");
+ break;
+ case GDType::NATIVE:;
+ prop_doc.type = dt.native_type;
+ break;
+ case GDType::BUILTIN:
+ prop_doc.type = Variant::get_type_name(dt.builtin_type);
+ break;
+ default:
+ // SCRIPT: can be preload()'d and perhaps used as types directly?
+ // RESOLVING & UNRESOLVED should never happen since docgen requires analyzing w/o errors
+ break;
+ }
+
+ if (m_var->property == GDP::VariableNode::PROP_SETGET) {
+ if (m_var->setter_pointer != nullptr) {
+ prop_doc.setter = m_var->setter_pointer->name;
+ }
+ if (m_var->getter_pointer != nullptr) {
+ prop_doc.getter = m_var->getter_pointer->name;
+ }
+ }
+
+ if (m_var->initializer && m_var->initializer->is_constant) {
+ prop_doc.default_value = m_var->initializer->reduced_value.get_construct_string().replace("\n", "");
+ }
+
+ prop_doc.overridden = false;
+
+ doc.properties.push_back(prop_doc);
+ } break;
+
+ case GDP::ClassNode::Member::ENUM: {
+ const GDP::EnumNode *m_enum = member.m_enum;
+ StringName name = m_enum->identifier->name;
+
+ p_script->member_lines[name] = m_enum->start_line;
+
+ for (const GDP::EnumNode::Value &val : m_enum->values) {
+ DocData::ConstantDoc const_doc;
+ const_doc.name = val.identifier->name;
+ const_doc.value = String(Variant(val.value));
+ const_doc.is_value_valid = true;
+ const_doc.description = val.doc_description;
+ const_doc.enumeration = name;
+
+ doc.enums[const_doc.name] = const_doc.description;
+ doc.constants.push_back(const_doc);
+ }
+
+ } break;
+
+ case GDP::ClassNode::Member::ENUM_VALUE: {
+ const GDP::EnumNode::Value &m_enum_val = member.enum_value;
+ const StringName &name = m_enum_val.identifier->name;
+
+ p_script->member_lines[name] = m_enum_val.identifier->start_line;
+
+ DocData::ConstantDoc constant_doc;
+ constant_doc.enumeration = "@unnamed_enums";
+ DocData::constant_doc_from_variant(constant_doc, name, m_enum_val.value, m_enum_val.doc_description);
+ doc.constants.push_back(constant_doc);
+ } break;
+ case GDP::ClassNode::Member::GROUP:
+ case GDP::ClassNode::Member::UNDEFINED:
+ default:
+ break;
+ }
+ }
+
+ // Add doc to the outer-most class.
+ p_script->_add_doc(doc);
+}
diff --git a/modules/gdscript/editor/gdscript_docgen.h b/modules/gdscript/editor/gdscript_docgen.h
new file mode 100644
index 0000000000..bb3647196a
--- /dev/null
+++ b/modules/gdscript/editor/gdscript_docgen.h
@@ -0,0 +1,42 @@
+/**************************************************************************/
+/* gdscript_docgen.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef GDSCRIPT_DOCGEN_H
+#define GDSCRIPT_DOCGEN_H
+
+#include "../gdscript_parser.h"
+#include "core/doc_data.h"
+
+class GDScriptDocGen {
+public:
+ static void generate_docs(GDScript *p_script, const GDScriptParser::ClassNode *p_class);
+};
+
+#endif // GDSCRIPT_DOCGEN_H
diff --git a/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp b/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp
index 00d50d1737..3458eb43b5 100644
--- a/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp
+++ b/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp
@@ -311,6 +311,14 @@ void GDScriptEditorTranslationParserPlugin::_extract_from_call(GDScriptParser::C
}
}
}
+
+ if (p_call->callee && p_call->callee->type == GDScriptParser::Node::SUBSCRIPT) {
+ GDScriptParser::SubscriptNode *subscript_node = static_cast<GDScriptParser::SubscriptNode *>(p_call->callee);
+ if (subscript_node->base && subscript_node->base->type == GDScriptParser::Node::CALL) {
+ GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(subscript_node->base);
+ _extract_from_call(call_node);
+ }
+ }
}
void GDScriptEditorTranslationParserPlugin::_extract_fd_literals(GDScriptParser::ExpressionNode *p_expression) {
diff --git a/modules/gdscript/editor/script_templates/RichTextEffect/default.gd b/modules/gdscript/editor/script_templates/RichTextEffect/default.gd
new file mode 100644
index 0000000000..c79eeb91ec
--- /dev/null
+++ b/modules/gdscript/editor/script_templates/RichTextEffect/default.gd
@@ -0,0 +1,17 @@
+# meta-description: Base template for rich text effects
+
+@tool
+class_name _CLASS_
+extends _BASE_
+
+
+# To use this effect:
+# - Enable BBCode on a RichTextLabel.
+# - Register this effect on the label.
+# - Use [_CLASS_ param=2.0]hello[/_CLASS_] in text.
+var bbcode := "_CLASS_"
+
+
+func _process_custom_fx(char_fx: CharFXTransform) -> bool:
+ var param: float = char_fx.env.get("param", 1.0)
+ return true
diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp
index 2646c1ad15..d0790aba25 100644
--- a/modules/gdscript/gdscript.cpp
+++ b/modules/gdscript/gdscript.cpp
@@ -52,6 +52,7 @@
#ifdef TOOLS_ENABLED
#include "editor/editor_paths.h"
+#include "editor/gdscript_docgen.h"
#endif
///////////////////////////
@@ -340,12 +341,11 @@ void GDScript::_get_script_property_list(List<PropertyInfo> *r_list, bool p_incl
r_list->push_back(E);
}
- props.clear();
-
if (!p_include_base) {
break;
}
+ props.clear();
sptr = sptr->_base;
}
}
@@ -461,9 +461,9 @@ void GDScript::_update_exports_values(HashMap<StringName, Variant> &values, List
}
void GDScript::_add_doc(const DocData::ClassDoc &p_inner_class) {
- if (_owner) {
+ if (_owner) { // Only the top-level class stores doc info
_owner->_add_doc(p_inner_class);
- } else {
+ } else { // Remove old docs, add new
for (int i = 0; i < docs.size(); i++) {
if (docs[i].name == p_inner_class.name) {
docs.remove_at(i);
@@ -478,167 +478,6 @@ void GDScript::_clear_doc() {
docs.clear();
doc = DocData::ClassDoc();
}
-
-void GDScript::_update_doc() {
- _clear_doc();
-
- doc.script_path = vformat(R"("%s")", get_script_path().get_slice("://", 1));
- if (!name.is_empty()) {
- doc.name = name;
- } else {
- doc.name = doc.script_path;
- }
-
- if (_owner) {
- doc.name = _owner->doc.name + "." + doc.name;
- doc.script_path = doc.script_path + "." + doc.name;
- }
-
- doc.is_script_doc = true;
-
- if (base.is_valid() && base->is_valid()) {
- if (!base->doc.name.is_empty()) {
- doc.inherits = base->doc.name;
- } else {
- doc.inherits = base->get_instance_base_type();
- }
- } else if (native.is_valid()) {
- doc.inherits = native->get_name();
- }
-
- doc.brief_description = doc_brief_description;
- doc.description = doc_description;
- doc.tutorials = doc_tutorials;
-
- for (const KeyValue<String, DocData::EnumDoc> &E : doc_enums) {
- if (!E.value.description.is_empty()) {
- doc.enums[E.key] = E.value.description;
- }
- }
-
- List<MethodInfo> methods;
- _get_script_method_list(&methods, false);
- for (int i = 0; i < methods.size(); i++) {
- // Ignore internal methods.
- if (methods[i].name[0] == '@') {
- continue;
- }
-
- DocData::MethodDoc method_doc;
- const String &class_name = methods[i].name;
- if (member_functions.has(class_name)) {
- GDScriptFunction *fn = member_functions[class_name];
-
- // Change class name if return type is script reference.
- GDScriptDataType return_type = fn->get_return_type();
- if (return_type.kind == GDScriptDataType::GDSCRIPT) {
- methods[i].return_val.class_name = _get_gdscript_reference_class_name(Object::cast_to<GDScript>(return_type.script_type));
- }
-
- // Change class name if argument is script reference.
- for (int j = 0; j < fn->get_argument_count(); j++) {
- GDScriptDataType arg_type = fn->get_argument_type(j);
- if (arg_type.kind == GDScriptDataType::GDSCRIPT) {
- methods[i].arguments[j].class_name = _get_gdscript_reference_class_name(Object::cast_to<GDScript>(arg_type.script_type));
- }
- }
- }
- if (doc_functions.has(methods[i].name)) {
- DocData::method_doc_from_methodinfo(method_doc, methods[i], doc_functions[methods[i].name]);
- } else {
- DocData::method_doc_from_methodinfo(method_doc, methods[i], String());
- }
- doc.methods.push_back(method_doc);
- }
-
- List<PropertyInfo> props;
- _get_script_property_list(&props, false);
- for (int i = 0; i < props.size(); i++) {
- if (props[i].usage & PROPERTY_USAGE_CATEGORY || props[i].usage & PROPERTY_USAGE_GROUP || props[i].usage & PROPERTY_USAGE_SUBGROUP) {
- continue;
- }
- ScriptMemberInfo scr_member_info;
- scr_member_info.propinfo = props[i];
- scr_member_info.propinfo.usage |= PROPERTY_USAGE_NIL_IS_VARIANT;
- if (member_indices.has(props[i].name)) {
- const MemberInfo &mi = member_indices[props[i].name];
- scr_member_info.setter = mi.setter;
- scr_member_info.getter = mi.getter;
- if (mi.data_type.kind == GDScriptDataType::GDSCRIPT) {
- scr_member_info.propinfo.class_name = _get_gdscript_reference_class_name(
- Object::cast_to<GDScript>(mi.data_type.script_type));
- }
- }
- if (member_default_values.has(props[i].name)) {
- scr_member_info.has_default_value = true;
- scr_member_info.default_value = member_default_values[props[i].name];
- }
- if (doc_variables.has(props[i].name)) {
- scr_member_info.doc_string = doc_variables[props[i].name];
- }
-
- DocData::PropertyDoc prop_doc;
- DocData::property_doc_from_scriptmemberinfo(prop_doc, scr_member_info);
- doc.properties.push_back(prop_doc);
- }
-
- List<MethodInfo> signals;
- _get_script_signal_list(&signals, false);
- for (int i = 0; i < signals.size(); i++) {
- DocData::MethodDoc signal_doc;
- if (doc_signals.has(signals[i].name)) {
- DocData::signal_doc_from_methodinfo(signal_doc, signals[i], doc_signals[signals[i].name]);
- } else {
- DocData::signal_doc_from_methodinfo(signal_doc, signals[i], String());
- }
- doc.signals.push_back(signal_doc);
- }
-
- for (const KeyValue<StringName, Variant> &E : constants) {
- if (subclasses.has(E.key)) {
- continue;
- }
-
- // Enums.
- bool is_enum = false;
- if (E.value.get_type() == Variant::DICTIONARY) {
- if (doc_enums.has(E.key)) {
- is_enum = true;
- for (int i = 0; i < doc_enums[E.key].values.size(); i++) {
- doc_enums[E.key].values.write[i].enumeration = E.key;
- doc.constants.push_back(doc_enums[E.key].values[i]);
- }
- }
- }
- if (!is_enum && doc_enums.has("@unnamed_enums")) {
- for (int i = 0; i < doc_enums["@unnamed_enums"].values.size(); i++) {
- if (E.key == doc_enums["@unnamed_enums"].values[i].name) {
- is_enum = true;
- DocData::ConstantDoc constant_doc;
- constant_doc.enumeration = "@unnamed_enums";
- DocData::constant_doc_from_variant(constant_doc, E.key, E.value, doc_enums["@unnamed_enums"].values[i].description);
- doc.constants.push_back(constant_doc);
- break;
- }
- }
- }
- if (!is_enum) {
- DocData::ConstantDoc constant_doc;
- String const_description;
- if (doc_constants.has(E.key)) {
- const_description = doc_constants[E.key];
- }
- DocData::constant_doc_from_variant(constant_doc, E.key, E.value, const_description);
- doc.constants.push_back(constant_doc);
- }
- }
-
- for (KeyValue<StringName, Ref<GDScript>> &E : subclasses) {
- E.value->_update_doc();
- }
-
- _add_doc(doc);
-}
#endif
bool GDScript::_update_exports(bool *r_err, bool p_recursive_call, PlaceHolderScriptInstance *p_instance_to_update) {
@@ -812,6 +651,49 @@ String GDScript::_get_debug_path() const {
}
}
+Error GDScript::_static_init() {
+ if (static_initializer) {
+ Callable::CallError call_err;
+ static_initializer->call(nullptr, nullptr, 0, call_err);
+ if (call_err.error != Callable::CallError::CALL_OK) {
+ return ERR_CANT_CREATE;
+ }
+ }
+ Error err = OK;
+ for (KeyValue<StringName, Ref<GDScript>> &inner : subclasses) {
+ err = inner.value->_static_init();
+ if (err) {
+ break;
+ }
+ }
+ return err;
+}
+
+#ifdef TOOLS_ENABLED
+
+void GDScript::_save_old_static_data() {
+ old_static_variables_indices = static_variables_indices;
+ old_static_variables = static_variables;
+ for (KeyValue<StringName, Ref<GDScript>> &inner : subclasses) {
+ inner.value->_save_old_static_data();
+ }
+}
+
+void GDScript::_restore_old_static_data() {
+ for (KeyValue<StringName, MemberInfo> &E : old_static_variables_indices) {
+ if (static_variables_indices.has(E.key)) {
+ static_variables.write[static_variables_indices[E.key].index] = old_static_variables[E.value.index];
+ }
+ }
+ old_static_variables_indices.clear();
+ old_static_variables.clear();
+ for (KeyValue<StringName, Ref<GDScript>> &inner : subclasses) {
+ inner.value->_restore_old_static_data();
+ }
+}
+
+#endif
+
Error GDScript::reload(bool p_keep_state) {
if (reloading) {
return OK;
@@ -857,6 +739,14 @@ Error GDScript::reload(bool p_keep_state) {
}
}
+ bool can_run = ScriptServer::is_scripting_enabled() || is_tool();
+
+#ifdef TOOLS_ENABLED
+ if (p_keep_state && can_run && is_valid()) {
+ _save_old_static_data();
+ }
+#endif
+
valid = false;
GDScriptParser parser;
Error err = parser.parse(source, path, false);
@@ -887,7 +777,7 @@ Error GDScript::reload(bool p_keep_state) {
return ERR_PARSE_ERROR;
}
- bool can_run = ScriptServer::is_scripting_enabled() || parser.is_tool();
+ can_run = ScriptServer::is_scripting_enabled() || parser.is_tool();
GDScriptCompiler compiler;
err = compiler.compile(&parser, this, p_keep_state);
@@ -905,6 +795,13 @@ Error GDScript::reload(bool p_keep_state) {
return err;
}
}
+
+#ifdef TOOLS_ENABLED
+ // Done after compilation because it needs the GDScript object's inner class GDScript objects,
+ // which are made by calling make_scripts() within compiler.compile() above.
+ GDScriptDocGen::generate_docs(this, parser.get_tree());
+#endif
+
#ifdef DEBUG_ENABLED
for (const GDScriptWarning &warning : parser.get_warnings()) {
if (EngineDebugger::is_active()) {
@@ -914,6 +811,19 @@ Error GDScript::reload(bool p_keep_state) {
}
#endif
+ if (can_run) {
+ err = _static_init();
+ if (err) {
+ return err;
+ }
+ }
+
+#ifdef TOOLS_ENABLED
+ if (can_run && p_keep_state) {
+ _restore_old_static_data();
+ }
+#endif
+
reloading = false;
return OK;
}
@@ -942,6 +852,10 @@ const Variant GDScript::get_rpc_config() const {
return rpc_config;
}
+void GDScript::unload_static() const {
+ GDScriptCache::remove_script(fully_qualified_name);
+}
+
Variant GDScript::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
GDScript *top = this;
while (top) {
@@ -978,6 +892,19 @@ bool GDScript::_get(const StringName &p_name, Variant &r_ret) const {
return true;
}
}
+
+ {
+ HashMap<StringName, MemberInfo>::ConstIterator E = static_variables_indices.find(p_name);
+ if (E) {
+ if (E->value.getter) {
+ Callable::CallError ce;
+ r_ret = const_cast<GDScript *>(this)->callp(E->value.getter, nullptr, 0, ce);
+ return true;
+ }
+ r_ret = static_variables[E->value.index];
+ return true;
+ }
+ }
top = top->_base;
}
@@ -995,7 +922,32 @@ bool GDScript::_set(const StringName &p_name, const Variant &p_value) {
set_source_code(p_value);
reload();
} else {
- return false;
+ const GDScript *top = this;
+ while (top) {
+ HashMap<StringName, MemberInfo>::ConstIterator E = static_variables_indices.find(p_name);
+ if (E) {
+ const GDScript::MemberInfo *member = &E->value;
+ Variant value = p_value;
+ if (member->data_type.has_type && !member->data_type.is_type(value)) {
+ const Variant *args = &p_value;
+ Callable::CallError err;
+ Variant::construct(member->data_type.builtin_type, value, &args, 1, err);
+ if (err.error != Callable::CallError::CALL_OK || !member->data_type.is_type(value)) {
+ return false;
+ }
+ }
+ if (member->setter) {
+ const Variant *args = &value;
+ Callable::CallError err;
+ callp(member->setter, &args, 1, err);
+ return err.error == Callable::CallError::CALL_OK;
+ } else {
+ static_variables.write[member->index] = value;
+ return true;
+ }
+ }
+ top = top->_base;
+ }
}
return true;
@@ -1266,7 +1218,6 @@ void GDScript::_get_script_signal_list(List<MethodInfo> *r_list, bool p_include_
else if (base_cache.is_valid()) {
base_cache->get_script_signal_list(r_list);
}
-
#endif
}
@@ -1448,6 +1399,13 @@ void GDScript::clear(GDScript::ClearData *p_clear_data) {
E.value.data_type.script_type_ref = Ref<Script>();
}
+ for (KeyValue<StringName, GDScript::MemberInfo> &E : static_variables_indices) {
+ clear_data->scripts.insert(E.value.data_type.script_type_ref);
+ E.value.data_type.script_type_ref = Ref<Script>();
+ }
+ static_variables.clear();
+ static_variables_indices.clear();
+
if (implicit_initializer) {
clear_data->functions.insert(implicit_initializer);
implicit_initializer = nullptr;
@@ -1458,6 +1416,11 @@ void GDScript::clear(GDScript::ClearData *p_clear_data) {
implicit_ready = nullptr;
}
+ if (static_initializer) {
+ clear_data->functions.insert(static_initializer);
+ static_initializer = nullptr;
+ }
+
_save_orphaned_subclasses(clear_data);
#ifdef TOOLS_ENABLED
@@ -1512,10 +1475,6 @@ GDScript::~GDScript() {
GDScriptLanguage::get_singleton()->script_list.remove(&script_list);
}
-
- if (GDScriptCache::singleton) { // Cache may have been already destroyed at engine shutdown.
- GDScriptCache::remove_script(get_path());
- }
}
//////////////////////////////
@@ -2546,6 +2505,7 @@ GDScriptLanguage::GDScriptLanguage() {
ERR_FAIL_COND(singleton);
singleton = this;
strings._init = StaticCString::create("_init");
+ strings._static_init = StaticCString::create("_static_init");
strings._notification = StaticCString::create("_notification");
strings._set = StaticCString::create("_set");
strings._get = StaticCString::create("_get");
diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h
index 0117ed40ab..60bd9eef53 100644
--- a/modules/gdscript/gdscript.h
+++ b/modules/gdscript/gdscript.h
@@ -83,6 +83,7 @@ class GDScript : public Script {
friend class GDScriptFunction;
friend class GDScriptAnalyzer;
friend class GDScriptCompiler;
+ friend class GDScriptDocGen;
friend class GDScriptLanguage;
friend struct GDScriptUtilityFunctionsDefinitions;
@@ -93,6 +94,8 @@ class GDScript : public Script {
HashSet<StringName> members; //members are just indices to the instantiated script.
HashMap<StringName, Variant> constants;
+ HashMap<StringName, MemberInfo> static_variables_indices;
+ Vector<Variant> static_variables;
HashMap<StringName, GDScriptFunction *> member_functions;
HashMap<StringName, MemberInfo> member_indices; //members are just indices to the instantiated script.
HashMap<StringName, Ref<GDScript>> subclasses;
@@ -101,6 +104,12 @@ class GDScript : public Script {
#ifdef TOOLS_ENABLED
+ // For static data storage during hot-reloading.
+ HashMap<StringName, MemberInfo> old_static_variables_indices;
+ Vector<Variant> old_static_variables;
+ void _save_old_static_data();
+ void _restore_old_static_data();
+
HashMap<StringName, int> member_lines;
HashMap<StringName, Variant> member_default_values;
List<PropertyInfo> members_cache;
@@ -113,16 +122,7 @@ class GDScript : public Script {
DocData::ClassDoc doc;
Vector<DocData::ClassDoc> docs;
- String doc_brief_description;
- String doc_description;
- Vector<DocData::TutorialDoc> doc_tutorials;
- HashMap<String, String> doc_functions;
- HashMap<String, String> doc_variables;
- HashMap<String, String> doc_constants;
- HashMap<String, String> doc_signals;
- HashMap<String, DocData::EnumDoc> doc_enums;
void _clear_doc();
- void _update_doc();
void _add_doc(const DocData::ClassDoc &p_inner_class);
#endif
@@ -131,6 +131,9 @@ class GDScript : public Script {
GDScriptFunction *implicit_initializer = nullptr;
GDScriptFunction *initializer = nullptr; //direct pointer to new , faster to locate
GDScriptFunction *implicit_ready = nullptr;
+ GDScriptFunction *static_initializer = nullptr;
+
+ Error _static_init();
int subclass_count = 0;
RBSet<Object *> instances;
@@ -276,6 +279,8 @@ public:
virtual const Variant get_rpc_config() const override;
+ void unload_static() const;
+
#ifdef TOOLS_ENABLED
virtual bool is_placeholder_fallback_enabled() const override { return placeholder_fallback_enabled; }
#endif
@@ -447,6 +452,7 @@ public:
struct {
StringName _init;
+ StringName _static_init;
StringName _notification;
StringName _set;
StringName _get;
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index a2cab25ce8..96bd8aafad 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -479,7 +479,24 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c
}
if (look_class->has_member(name)) {
resolve_class_member(look_class, name, id);
- base = look_class->get_member(name).get_datatype();
+ GDScriptParser::ClassNode::Member member = look_class->get_member(name);
+ GDScriptParser::DataType member_datatype = member.get_datatype();
+
+ switch (member.type) {
+ case GDScriptParser::ClassNode::Member::CLASS:
+ break; // OK.
+ case GDScriptParser::ClassNode::Member::CONSTANT:
+ if (member_datatype.kind != GDScriptParser::DataType::SCRIPT && member_datatype.kind != GDScriptParser::DataType::CLASS) {
+ push_error(vformat(R"(Constant "%s" is not a preloaded script or class.)", name), id);
+ return ERR_PARSE_ERROR;
+ }
+ break;
+ default:
+ push_error(vformat(R"(Cannot use %s "%s" in extends chain.)", member.get_type_name(), name), id);
+ return ERR_PARSE_ERROR;
+ }
+
+ base = member_datatype;
found = true;
break;
}
@@ -506,6 +523,9 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c
if (!id_type.is_set()) {
push_error(vformat(R"(Could not find nested type "%s".)", id->name), id);
return ERR_PARSE_ERROR;
+ } else if (id_type.kind != GDScriptParser::DataType::SCRIPT && id_type.kind != GDScriptParser::DataType::CLASS) {
+ push_error(vformat(R"(Identifier "%s" is not a preloaded script or class.)", id->name), id);
+ return ERR_PARSE_ERROR;
}
base = id_type;
@@ -859,6 +879,8 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
#endif
switch (member.type) {
case GDScriptParser::ClassNode::Member::VARIABLE: {
+ bool previous_static_context = static_context;
+ static_context = member.variable->is_static;
check_class_member_name_conflict(p_class, member.variable->identifier->name, member.variable);
member.variable->set_datatype(resolving_datatype);
resolve_variable(member.variable, false);
@@ -870,6 +892,7 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
E->apply(parser, member.variable);
}
}
+ static_context = previous_static_context;
#ifdef DEBUG_ENABLED
if (member.variable->exported && member.variable->onready) {
parser->push_warning(member.variable, GDScriptWarning::ONREADY_WITH_EXPORT);
@@ -877,7 +900,7 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
if (member.variable->initializer) {
// Check if it is call to get_node() on self (using shorthand $ or not), so we can check if @onready is needed.
// This could be improved by traversing the expression fully and checking the presence of get_node at any level.
- if (!member.variable->onready && member.variable->initializer && (member.variable->initializer->type == GDScriptParser::Node::GET_NODE || member.variable->initializer->type == GDScriptParser::Node::CALL || member.variable->initializer->type == GDScriptParser::Node::CAST)) {
+ if (!member.variable->is_static && !member.variable->onready && member.variable->initializer && (member.variable->initializer->type == GDScriptParser::Node::GET_NODE || member.variable->initializer->type == GDScriptParser::Node::CALL || member.variable->initializer->type == GDScriptParser::Node::CAST)) {
GDScriptParser::Node *expr = member.variable->initializer;
if (expr->type == GDScriptParser::Node::CAST) {
expr = static_cast<GDScriptParser::CastNode *>(expr)->operand;
@@ -1062,6 +1085,10 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas
p_source = p_class;
}
+#ifdef DEBUG_ENABLED
+ 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;
@@ -1104,7 +1131,29 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas
for (int i = 0; i < p_class->members.size(); i++) {
resolve_class_member(p_class, i);
+
+#ifdef DEBUG_ENABLED
+ if (!has_static_data) {
+ GDScriptParser::ClassNode::Member member = p_class->members[i];
+ if (member.type == GDScriptParser::ClassNode::Member::CLASS) {
+ has_static_data = member.m_class->has_static_data;
+ }
+ }
+#endif
+ }
+
+#ifdef DEBUG_ENABLED
+ if (!has_static_data && p_class->annotated_static_unload) {
+ GDScriptParser::Node *static_unload = nullptr;
+ for (GDScriptParser::AnnotationNode *node : p_class->annotations) {
+ if (node->name == "@static_unload") {
+ static_unload = node;
+ break;
+ }
+ }
+ parser->push_warning(static_unload ? static_unload : p_class, GDScriptWarning::REDUNDANT_STATIC_UNLOAD);
}
+#endif
}
}
@@ -1287,10 +1336,11 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
push_error(vformat(R"(Getter with type "%s" cannot be used along with setter of type "%s".)", getter_function->datatype.to_string(), setter_function->parameters[0]->datatype.to_string()), member.variable);
}
}
+ }
+
#ifdef DEBUG_ENABLED
- parser->ignored_warnings = previously_ignored_warnings;
+ parser->ignored_warnings = previously_ignored_warnings;
#endif // DEBUG_ENABLED
- }
}
}
@@ -1479,6 +1529,8 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
GDScriptParser::FunctionNode *previous_function = parser->current_function;
parser->current_function = p_function;
+ bool previous_static_context = static_context;
+ static_context = p_function->is_static;
GDScriptParser::DataType prev_datatype = p_function->get_datatype();
@@ -1494,7 +1546,11 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
resolve_parameter(p_function->parameters[i]);
#ifdef DEBUG_ENABLED
if (p_function->parameters[i]->usages == 0 && !String(p_function->parameters[i]->identifier->name).begins_with("_")) {
- parser->push_warning(p_function->parameters[i]->identifier, GDScriptWarning::UNUSED_PARAMETER, function_name, p_function->parameters[i]->identifier->name);
+ String visible_name = function_name;
+ if (function_name == StringName()) {
+ visible_name = p_is_lambda ? "<anonymous lambda>" : "<unknown function>";
+ }
+ parser->push_warning(p_function->parameters[i]->identifier, GDScriptWarning::UNUSED_PARAMETER, visible_name, p_function->parameters[i]->identifier->name);
}
is_shadowing(p_function->parameters[i]->identifier, "function parameter");
#endif // DEBUG_ENABLED
@@ -1522,6 +1578,18 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
push_error("Constructor cannot have an explicit return type.", p_function->return_type);
}
}
+ } else if (!p_is_lambda && function_name == GDScriptLanguage::get_singleton()->strings._static_init) {
+ // Static constructor.
+ GDScriptParser::DataType return_type;
+ return_type.kind = GDScriptParser::DataType::BUILTIN;
+ return_type.builtin_type = Variant::NIL;
+ p_function->set_datatype(return_type);
+ if (p_function->return_type) {
+ GDScriptParser::DataType declared_return = resolve_datatype(p_function->return_type);
+ if (declared_return.kind != GDScriptParser::DataType::BUILTIN || declared_return.builtin_type != Variant::NIL) {
+ push_error("Static constructor cannot have an explicit return type.", p_function->return_type);
+ }
+ }
} else {
if (p_function->return_type != nullptr) {
p_function->set_datatype(type_from_metatype(resolve_datatype(p_function->return_type)));
@@ -1605,6 +1673,7 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
parser->ignored_warnings = previously_ignored_warnings;
#endif
parser->current_function = previous_function;
+ static_context = previous_static_context;
}
void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_function, bool p_is_lambda) {
@@ -2007,7 +2076,7 @@ void GDScriptAnalyzer::resolve_assert(GDScriptParser::AssertNode *p_assert) {
if (p_assert->condition->is_constant) {
if (p_assert->condition->reduced_value.booleanize()) {
parser->push_warning(p_assert->condition, GDScriptWarning::ASSERT_ALWAYS_TRUE);
- } else {
+ } else if (!(p_assert->condition->type == GDScriptParser::Node::LITERAL && static_cast<GDScriptParser::LiteralNode *>(p_assert->condition)->value.get_type() == Variant::BOOL)) {
parser->push_warning(p_assert->condition, GDScriptWarning::ASSERT_ALWAYS_FALSE);
}
}
@@ -3030,13 +3099,17 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
base_type.is_meta_type = false;
}
- if (is_self && parser->current_function != nullptr && parser->current_function->is_static && !is_static) {
- // Get the parent function above any lambda.
- GDScriptParser::FunctionNode *parent_function = parser->current_function;
- while (parent_function->source_lambda) {
- parent_function = parent_function->source_lambda->parent_function;
+ if (is_self && static_context && !is_static) {
+ if (parser->current_function) {
+ // Get the parent function above any lambda.
+ GDScriptParser::FunctionNode *parent_function = parser->current_function;
+ while (parent_function->source_lambda) {
+ parent_function = parent_function->source_lambda->parent_function;
+ }
+ push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parent_function->identifier->name), p_call);
+ } else {
+ push_error(vformat(R"*(Cannot call non-static function "%s()" for static variable initializer.)*", p_call->function_name), p_call);
}
- push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parent_function->identifier->name), p_call);
} else if (!is_self && base_type.is_meta_type && !is_static) {
base_type.is_meta_type = false; // For `to_string()`.
push_error(vformat(R"*(Cannot call non-static function "%s()" on the class "%s" directly. Make an instance instead.)*", p_call->function_name, base_type.to_string()), p_call);
@@ -3053,7 +3126,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
parser->push_warning(p_call, GDScriptWarning::RETURN_VALUE_DISCARDED, p_call->function_name);
}
- if (is_static && !base_type.is_meta_type && !(is_self && parser->current_function != nullptr && parser->current_function->is_static)) {
+ if (is_static && !is_constructor && !base_type.is_meta_type && !(is_self && static_context)) {
String caller_type = String(base_type.native_type);
if (caller_type.is_empty()) {
@@ -3105,7 +3178,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
String base_name = is_self && !p_call->is_super ? "self" : base_type.to_string();
#ifdef SUGGEST_GODOT4_RENAMES
String rename_hint = String();
- if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GD4_HINT)).booleanize()) {
+ if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GODOT_4_HINT)).booleanize()) {
const char *renamed_function_name = check_for_renamed_identifier(p_call->function_name, p_call->type);
if (renamed_function_name) {
rename_hint = " " + vformat(R"(Did you mean to use "%s"?)", String(renamed_function_name) + "()");
@@ -3306,7 +3379,7 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod
} else if (base.is_hard_type()) {
#ifdef SUGGEST_GODOT4_RENAMES
String rename_hint = String();
- if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GD4_HINT)).booleanize()) {
+ if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GODOT_4_HINT)).booleanize()) {
const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type);
if (renamed_identifier_name) {
rename_hint = " " + vformat(R"(Did you mean to use "%s"?)", renamed_identifier_name);
@@ -3346,7 +3419,7 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod
if (base.is_hard_type()) {
#ifdef SUGGEST_GODOT4_RENAMES
String rename_hint = String();
- if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GD4_HINT)).booleanize()) {
+ if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GODOT_4_HINT)).booleanize()) {
const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type);
if (renamed_identifier_name) {
rename_hint = " " + vformat(R"(Did you mean to use "%s"?)", renamed_identifier_name);
@@ -3408,9 +3481,9 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod
}
case GDScriptParser::ClassNode::Member::VARIABLE: {
- if (is_base && !base.is_meta_type) {
+ if (is_base && (!base.is_meta_type || member.variable->is_static)) {
p_identifier->set_datatype(member.get_datatype());
- p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_VARIABLE;
+ p_identifier->source = member.variable->is_static ? GDScriptParser::IdentifierNode::STATIC_VARIABLE : GDScriptParser::IdentifierNode::MEMBER_VARIABLE;
p_identifier->variable_source = member.variable;
member.variable->usages += 1;
return;
@@ -3552,6 +3625,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
mark_lambda_use_self();
p_identifier->variable_source->usages++;
[[fallthrough]];
+ case GDScriptParser::IdentifierNode::STATIC_VARIABLE:
case GDScriptParser::IdentifierNode::LOCAL_VARIABLE:
p_identifier->set_datatype(p_identifier->variable_source->get_datatype());
found_source = true;
@@ -3582,13 +3656,17 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
if (found_source) {
bool source_is_variable = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_VARIABLE || p_identifier->source == GDScriptParser::IdentifierNode::INHERITED_VARIABLE;
bool source_is_signal = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_SIGNAL;
- if ((source_is_variable || source_is_signal) && parser->current_function && parser->current_function->is_static) {
- // Get the parent function above any lambda.
- GDScriptParser::FunctionNode *parent_function = parser->current_function;
- while (parent_function->source_lambda) {
- parent_function = parent_function->source_lambda->parent_function;
+ if ((source_is_variable || source_is_signal) && static_context) {
+ if (parser->current_function) {
+ // Get the parent function above any lambda.
+ GDScriptParser::FunctionNode *parent_function = parser->current_function;
+ while (parent_function->source_lambda) {
+ parent_function = parent_function->source_lambda->parent_function;
+ }
+ push_error(vformat(R"*(Cannot access %s "%s" from the static function "%s()".)*", source_is_signal ? "signal" : "instance variable", p_identifier->name, parent_function->identifier->name), p_identifier);
+ } else {
+ push_error(vformat(R"*(Cannot access %s "%s" for a static variable initializer.)*", source_is_signal ? "signal" : "instance variable", p_identifier->name), p_identifier);
}
- push_error(vformat(R"*(Cannot access %s "%s" from the static function "%s()".)*", source_is_signal ? "signal" : "instance variable", p_identifier->name, parent_function->identifier->name), p_identifier);
}
if (!lambda_stack.is_empty()) {
@@ -3730,7 +3808,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
} else {
#ifdef SUGGEST_GODOT4_RENAMES
String rename_hint = String();
- if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GD4_HINT)).booleanize()) {
+ if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GODOT_4_HINT)).booleanize()) {
const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type);
if (renamed_identifier_name) {
rename_hint = " " + vformat(R"(Did you mean to use "%s"?)", renamed_identifier_name);
@@ -4546,7 +4624,7 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo
result.set_container_element_type(elem_type);
} else if (p_property.type == Variant::INT) {
// Check if it's enum.
- if ((p_property.usage & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)) && p_property.class_name != StringName()) {
+ if ((p_property.usage & PROPERTY_USAGE_CLASS_IS_ENUM) && p_property.class_name != StringName()) {
if (CoreConstants::is_global_enum(p_property.class_name)) {
result = make_global_enum_type(p_property.class_name, StringName(), false);
result.is_constant = false;
@@ -4558,6 +4636,7 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo
}
}
}
+ // PROPERTY_USAGE_CLASS_IS_BITFIELD: BitField[T] isn't supported (yet?), use plain int.
}
}
return result;
@@ -4758,9 +4837,11 @@ void GDScriptAnalyzer::validate_call_arg(const List<GDScriptParser::DataType> &p
}
GDScriptParser::DataType arg_type = p_call->arguments[i]->get_datatype();
- if ((arg_type.is_variant() || !arg_type.is_hard_type()) && !(par_type.is_hard_type() && par_type.is_variant())) {
- // Argument can be anything, so this is unsafe.
- mark_node_unsafe(p_call->arguments[i]);
+ if (arg_type.is_variant() || !arg_type.is_hard_type()) {
+ // Argument can be anything, so this is unsafe (unless the parameter is a hard variant).
+ if (!(par_type.is_hard_type() && par_type.is_variant())) {
+ mark_node_unsafe(p_call->arguments[i]);
+ }
} else if (par_type.is_hard_type() && !is_type_compatible(par_type, arg_type, true)) {
// Supertypes are acceptable for dynamic compliance, but it's unsafe.
mark_node_unsafe(p_call);
diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h
index 5902035bcd..0c7bf4125b 100644
--- a/modules/gdscript/gdscript_analyzer.h
+++ b/modules/gdscript/gdscript_analyzer.h
@@ -43,6 +43,7 @@ class GDScriptAnalyzer {
const GDScriptParser::EnumNode *current_enum = nullptr;
List<GDScriptParser::LambdaNode *> lambda_stack;
+ bool static_context = false;
// Tests for detecting invalid overloading of script members
static _FORCE_INLINE_ bool has_member_name_conflict_in_script_class(const StringName &p_name, const GDScriptParser::ClassNode *p_current_class_node, const GDScriptParser::Node *p_member);
diff --git a/modules/gdscript/gdscript_byte_codegen.cpp b/modules/gdscript/gdscript_byte_codegen.cpp
index d6f21d297a..1414075ba8 100644
--- a/modules/gdscript/gdscript_byte_codegen.cpp
+++ b/modules/gdscript/gdscript_byte_codegen.cpp
@@ -69,56 +69,52 @@ uint32_t GDScriptByteCodeGenerator::add_or_get_name(const StringName &p_name) {
uint32_t GDScriptByteCodeGenerator::add_temporary(const GDScriptDataType &p_type) {
Variant::Type temp_type = Variant::NIL;
- if (p_type.has_type) {
- if (p_type.kind == GDScriptDataType::BUILTIN) {
- switch (p_type.builtin_type) {
- case Variant::NIL:
- case Variant::BOOL:
- case Variant::INT:
- case Variant::FLOAT:
- case Variant::STRING:
- case Variant::VECTOR2:
- case Variant::VECTOR2I:
- case Variant::RECT2:
- case Variant::RECT2I:
- case Variant::VECTOR3:
- case Variant::VECTOR3I:
- case Variant::TRANSFORM2D:
- case Variant::VECTOR4:
- case Variant::VECTOR4I:
- case Variant::PLANE:
- case Variant::QUATERNION:
- case Variant::AABB:
- case Variant::BASIS:
- case Variant::TRANSFORM3D:
- case Variant::PROJECTION:
- case Variant::COLOR:
- case Variant::STRING_NAME:
- case Variant::NODE_PATH:
- case Variant::RID:
- case Variant::OBJECT:
- case Variant::CALLABLE:
- case Variant::SIGNAL:
- case Variant::DICTIONARY:
- case Variant::ARRAY:
- temp_type = p_type.builtin_type;
- break;
- case Variant::PACKED_BYTE_ARRAY:
- case Variant::PACKED_INT32_ARRAY:
- case Variant::PACKED_INT64_ARRAY:
- case Variant::PACKED_FLOAT32_ARRAY:
- case Variant::PACKED_FLOAT64_ARRAY:
- case Variant::PACKED_STRING_ARRAY:
- case Variant::PACKED_VECTOR2_ARRAY:
- case Variant::PACKED_VECTOR3_ARRAY:
- case Variant::PACKED_COLOR_ARRAY:
- case Variant::VARIANT_MAX:
- // Packed arrays are reference counted, so we don't use the pool for them.
- temp_type = Variant::NIL;
- break;
- }
- } else {
- temp_type = Variant::OBJECT;
+ if (p_type.has_type && p_type.kind == GDScriptDataType::BUILTIN) {
+ switch (p_type.builtin_type) {
+ case Variant::NIL:
+ case Variant::BOOL:
+ case Variant::INT:
+ case Variant::FLOAT:
+ case Variant::STRING:
+ case Variant::VECTOR2:
+ case Variant::VECTOR2I:
+ case Variant::RECT2:
+ case Variant::RECT2I:
+ case Variant::VECTOR3:
+ case Variant::VECTOR3I:
+ case Variant::TRANSFORM2D:
+ case Variant::VECTOR4:
+ case Variant::VECTOR4I:
+ case Variant::PLANE:
+ case Variant::QUATERNION:
+ case Variant::AABB:
+ case Variant::BASIS:
+ case Variant::TRANSFORM3D:
+ case Variant::PROJECTION:
+ case Variant::COLOR:
+ case Variant::STRING_NAME:
+ case Variant::NODE_PATH:
+ case Variant::RID:
+ case Variant::CALLABLE:
+ case Variant::SIGNAL:
+ temp_type = p_type.builtin_type;
+ break;
+ case Variant::OBJECT:
+ case Variant::DICTIONARY:
+ case Variant::ARRAY:
+ case Variant::PACKED_BYTE_ARRAY:
+ case Variant::PACKED_INT32_ARRAY:
+ case Variant::PACKED_INT64_ARRAY:
+ case Variant::PACKED_FLOAT32_ARRAY:
+ case Variant::PACKED_FLOAT64_ARRAY:
+ case Variant::PACKED_STRING_ARRAY:
+ case Variant::PACKED_VECTOR2_ARRAY:
+ case Variant::PACKED_VECTOR3_ARRAY:
+ case Variant::PACKED_COLOR_ARRAY:
+ case Variant::VARIANT_MAX:
+ // Arrays, dictionaries, and objects are reference counted, so we don't use the pool for them.
+ temp_type = Variant::NIL;
+ break;
}
}
@@ -143,10 +139,12 @@ void GDScriptByteCodeGenerator::pop_temporary() {
ERR_FAIL_COND(used_temporaries.is_empty());
int slot_idx = used_temporaries.back()->get();
const StackSlot &slot = temporaries[slot_idx];
- if (slot.type == Variant::OBJECT) {
+ if (slot.type == Variant::NIL) {
// Avoid keeping in the stack long-lived references to objects,
// which may prevent RefCounted objects from being freed.
- write_assign_false(Address(Address::TEMPORARY, slot_idx));
+ // However, the cleanup will be performed an the end of the
+ // statement, to allow object references to survive chaining.
+ temporaries_pending_clear.push_back(slot_idx);
}
temporaries_pool[slot.type].push_back(slot_idx);
used_temporaries.pop_back();
@@ -1756,6 +1754,23 @@ void GDScriptByteCodeGenerator::end_block() {
pop_stack_identifiers();
}
+void GDScriptByteCodeGenerator::clean_temporaries() {
+ List<int>::Element *E = temporaries_pending_clear.front();
+ while (E) {
+ // The temporary may have been re-used as something else than an object
+ // since it was added to the list. In that case, there's no need to clear it.
+ int slot_idx = E->get();
+ const StackSlot &slot = temporaries[slot_idx];
+ if (slot.type == Variant::NIL) {
+ write_assign_false(Address(Address::TEMPORARY, slot_idx));
+ }
+
+ List<int>::Element *next = E->next();
+ E->erase();
+ E = next;
+ }
+}
+
GDScriptByteCodeGenerator::~GDScriptByteCodeGenerator() {
if (!ended && function != nullptr) {
memdelete(function);
diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h
index dc05de9fc6..fc684e4d8f 100644
--- a/modules/gdscript/gdscript_byte_codegen.h
+++ b/modules/gdscript/gdscript_byte_codegen.h
@@ -88,6 +88,7 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator {
Vector<StackSlot> locals;
Vector<StackSlot> temporaries;
List<int> used_temporaries;
+ List<int> temporaries_pending_clear;
RBMap<Variant::Type, List<int>> temporaries_pool;
List<GDScriptFunction::StackDebug> stack_debug;
@@ -365,6 +366,8 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator {
return p_address.address | (GDScriptFunction::ADDR_TYPE_MEMBER << GDScriptFunction::ADDR_BITS);
case Address::CONSTANT:
return p_address.address | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS);
+ case Address::STATIC_VARIABLE:
+ return p_address.address | (GDScriptFunction::ADDR_TYPE_STATIC_VAR << GDScriptFunction::ADDR_BITS);
case Address::LOCAL_VARIABLE:
case Address::FUNCTION_PARAMETER:
return p_address.address | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS);
@@ -463,6 +466,7 @@ public:
virtual uint32_t add_or_get_name(const StringName &p_name) override;
virtual uint32_t add_temporary(const GDScriptDataType &p_type) override;
virtual void pop_temporary() override;
+ virtual void clean_temporaries() override;
virtual void start_parameters() override;
virtual void end_parameters() override;
diff --git a/modules/gdscript/gdscript_cache.cpp b/modules/gdscript/gdscript_cache.cpp
index a009e8e0a8..126fccbbf0 100644
--- a/modules/gdscript/gdscript_cache.cpp
+++ b/modules/gdscript/gdscript_cache.cpp
@@ -342,6 +342,16 @@ Error GDScriptCache::finish_compiling(const String &p_owner) {
return err;
}
+void GDScriptCache::add_static_script(Ref<GDScript> p_script) {
+ ERR_FAIL_COND_MSG(p_script.is_null(), "Trying to cache empty script as static.");
+ ERR_FAIL_COND_MSG(!p_script->is_valid(), "Trying to cache non-compiled script as static.");
+ singleton->static_gdscript_cache[p_script->get_fully_qualified_name()] = p_script;
+}
+
+void GDScriptCache::remove_static_script(const String &p_fqcn) {
+ singleton->static_gdscript_cache.erase(p_fqcn);
+}
+
Ref<PackedScene> GDScriptCache::get_packed_scene(const String &p_path, Error &r_error, const String &p_owner) {
MutexLock lock(singleton->mutex);
diff --git a/modules/gdscript/gdscript_cache.h b/modules/gdscript/gdscript_cache.h
index c7f40f6e82..28266a1c0b 100644
--- a/modules/gdscript/gdscript_cache.h
+++ b/modules/gdscript/gdscript_cache.h
@@ -78,6 +78,7 @@ class GDScriptCache {
HashMap<String, GDScriptParserRef *> parser_map;
HashMap<String, Ref<GDScript>> shallow_gdscript_cache;
HashMap<String, Ref<GDScript>> full_gdscript_cache;
+ HashMap<String, Ref<GDScript>> static_gdscript_cache;
HashMap<String, HashSet<String>> dependencies;
HashMap<String, Ref<PackedScene>> packed_scene_cache;
HashMap<String, HashSet<String>> packed_scene_dependencies;
@@ -101,6 +102,8 @@ public:
static Ref<GDScript> get_full_script(const String &p_path, Error &r_error, const String &p_owner = String(), bool p_update_from_disk = false);
static Ref<GDScript> get_cached_script(const String &p_path);
static Error finish_compiling(const String &p_owner);
+ static void add_static_script(Ref<GDScript> p_script);
+ static void remove_static_script(const String &p_fqcn);
static Ref<PackedScene> get_packed_scene(const String &p_path, Error &r_error, const String &p_owner = "");
static void clear_unreferenced_packed_scenes();
diff --git a/modules/gdscript/gdscript_codegen.h b/modules/gdscript/gdscript_codegen.h
index 7847ab28c7..dbc2466393 100644
--- a/modules/gdscript/gdscript_codegen.h
+++ b/modules/gdscript/gdscript_codegen.h
@@ -44,6 +44,7 @@ public:
CLASS,
MEMBER,
CONSTANT,
+ STATIC_VARIABLE,
LOCAL_VARIABLE,
FUNCTION_PARAMETER,
TEMPORARY,
@@ -72,6 +73,7 @@ public:
virtual uint32_t add_or_get_name(const StringName &p_name) = 0;
virtual uint32_t add_temporary(const GDScriptDataType &p_type) = 0;
virtual void pop_temporary() = 0;
+ virtual void clean_temporaries() = 0;
virtual void start_parameters() = 0;
virtual void end_parameters() = 0;
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index 5413eadf60..327e24ef11 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -254,13 +254,29 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
gen->write_call_self(temp, codegen.script->member_indices[identifier].getter, args);
return temp;
} else {
- // No getter or inside getter: direct member access.,
+ // No getter or inside getter: direct member access.
int idx = codegen.script->member_indices[identifier].index;
return GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::MEMBER, idx, codegen.script->get_member_type(identifier));
}
}
}
+ // Try static variables.
+ if (codegen.script->static_variables_indices.has(identifier)) {
+ if (codegen.script->static_variables_indices[identifier].getter != StringName() && codegen.script->static_variables_indices[identifier].getter != codegen.function_name) {
+ // Perform getter.
+ GDScriptCodeGenerator::Address temp = codegen.add_temporary(codegen.script->static_variables_indices[identifier].data_type);
+ GDScriptCodeGenerator::Address class_addr(GDScriptCodeGenerator::Address::CLASS);
+ Vector<GDScriptCodeGenerator::Address> args; // No argument needed.
+ gen->write_call(temp, class_addr, codegen.script->static_variables_indices[identifier].getter, args);
+ return temp;
+ } else {
+ // No getter or inside getter: direct variable access.
+ int idx = codegen.script->static_variables_indices[identifier].index;
+ return GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::STATIC_VARIABLE, idx, codegen.script->static_variables_indices[identifier].data_type);
+ }
+ }
+
// Try class constants.
{
GDScript *owner = codegen.script;
@@ -563,7 +579,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
// Not exact arguments, but still can use method bind call.
gen->write_call_method_bind(result, self, method, arguments);
}
- } else if ((codegen.function_node && codegen.function_node->is_static) || call->function_name == "new") {
+ } else if (codegen.is_static || (codegen.function_node && codegen.function_node->is_static) || call->function_name == "new") {
GDScriptCodeGenerator::Address self;
self.mode = GDScriptCodeGenerator::Address::CLASS;
if (is_awaited) {
@@ -909,6 +925,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
bool is_member_property = false;
bool member_property_has_setter = false;
bool member_property_is_in_setter = false;
+ bool is_static = false;
StringName member_property_setter_function;
List<const GDScriptParser::SubscriptNode *> chain;
@@ -925,14 +942,16 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
StringName var_name = identifier->name;
if (_is_class_member_property(codegen, var_name)) {
assign_class_member_property = var_name;
- } else if (!_is_local_or_parameter(codegen, var_name) && codegen.script->member_indices.has(var_name)) {
+ } else if (!_is_local_or_parameter(codegen, var_name) && (codegen.script->member_indices.has(var_name) || codegen.script->static_variables_indices.has(var_name))) {
is_member_property = true;
- member_property_setter_function = codegen.script->member_indices[var_name].setter;
+ is_static = codegen.script->static_variables_indices.has(var_name);
+ const GDScript::MemberInfo &minfo = is_static ? codegen.script->static_variables_indices[var_name] : codegen.script->member_indices[var_name];
+ member_property_setter_function = minfo.setter;
member_property_has_setter = member_property_setter_function != StringName();
member_property_is_in_setter = member_property_has_setter && member_property_setter_function == codegen.function_name;
- target_member_property.mode = GDScriptCodeGenerator::Address::MEMBER;
- target_member_property.address = codegen.script->member_indices[var_name].index;
- target_member_property.type = codegen.script->member_indices[var_name].data_type;
+ target_member_property.mode = is_static ? GDScriptCodeGenerator::Address::STATIC_VARIABLE : GDScriptCodeGenerator::Address::MEMBER;
+ target_member_property.address = minfo.index;
+ target_member_property.type = minfo.data_type;
}
}
break;
@@ -1085,7 +1104,8 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
if (member_property_has_setter && !member_property_is_in_setter) {
Vector<GDScriptCodeGenerator::Address> args;
args.push_back(assigned);
- gen->write_call(GDScriptCodeGenerator::Address(), GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::SELF), member_property_setter_function, args);
+ GDScriptCodeGenerator::Address self = is_static ? GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::CLASS) : GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::SELF);
+ gen->write_call(GDScriptCodeGenerator::Address(), self, member_property_setter_function, args);
} else {
gen->write_assign(target_member_property, assigned);
}
@@ -1134,16 +1154,19 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
bool is_member = false;
bool has_setter = false;
bool is_in_setter = false;
+ bool is_static = false;
StringName setter_function;
StringName var_name = static_cast<const GDScriptParser::IdentifierNode *>(assignment->assignee)->name;
- if (!_is_local_or_parameter(codegen, var_name) && codegen.script->member_indices.has(var_name)) {
+ if (!_is_local_or_parameter(codegen, var_name) && (codegen.script->member_indices.has(var_name) || codegen.script->static_variables_indices.has(var_name))) {
is_member = true;
- setter_function = codegen.script->member_indices[var_name].setter;
+ is_static = codegen.script->static_variables_indices.has(var_name);
+ GDScript::MemberInfo &minfo = is_static ? codegen.script->static_variables_indices[var_name] : codegen.script->member_indices[var_name];
+ setter_function = minfo.setter;
has_setter = setter_function != StringName();
is_in_setter = has_setter && setter_function == codegen.function_name;
- member.mode = GDScriptCodeGenerator::Address::MEMBER;
- member.address = codegen.script->member_indices[var_name].index;
- member.type = codegen.script->member_indices[var_name].data_type;
+ member.mode = is_static ? GDScriptCodeGenerator::Address::STATIC_VARIABLE : GDScriptCodeGenerator::Address::MEMBER;
+ member.address = minfo.index;
+ member.type = minfo.data_type;
}
GDScriptCodeGenerator::Address target;
@@ -1165,18 +1188,8 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
bool has_operation = assignment->operation != GDScriptParser::AssignmentNode::OP_NONE;
if (has_operation) {
// Perform operation.
- GDScriptCodeGenerator::Address og_value = _parse_expression(codegen, r_error, assignment->assignee);
-
- if (!has_setter && !assignment->use_conversion_assign) {
- // If there's nothing special about the assignment, perform the assignment as part of the operator
- gen->write_binary_operator(target, assignment->variant_op, og_value, assigned_value);
- if (assigned_value.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
- gen->pop_temporary(); // Pop assigned value if not done before.
- }
- return GDScriptCodeGenerator::Address();
- }
-
GDScriptCodeGenerator::Address op_result = codegen.add_temporary(_gdtype_from_datatype(assignment->get_datatype(), codegen.script));
+ GDScriptCodeGenerator::Address og_value = _parse_expression(codegen, r_error, assignment->assignee);
gen->write_binary_operator(op_result, assignment->variant_op, og_value, assigned_value);
to_assign = op_result;
@@ -1665,6 +1678,7 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Sui
Error err = OK;
GDScriptCodeGenerator *gen = codegen.generator;
+ gen->clean_temporaries();
codegen.start_block();
if (p_add_locals) {
@@ -1967,6 +1981,8 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Sui
}
} break;
}
+
+ gen->clean_temporaries();
}
codegen.end_block();
@@ -2008,6 +2024,7 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
}
codegen.function_name = func_name;
+ codegen.is_static = is_static;
codegen.generator->write_start(p_script, func_name, is_static, rpc_config, return_type);
int optional_parameters = 0;
@@ -2031,7 +2048,7 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
bool is_implicit_ready = !p_func && p_for_ready;
if (!p_for_lambda && is_implicit_initializer) {
- // Initialize the default values for type variables before anything.
+ // Initialize the default values for typed variables before anything.
// This avoids crashes if they are accessed with validated calls before being properly initialized.
// It may happen with out-of-order access or with `@onready` variables.
for (const GDScriptParser::ClassNode::Member &member : p_class->members) {
@@ -2040,6 +2057,10 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
}
const GDScriptParser::VariableNode *field = member.variable;
+ if (field->is_static) {
+ continue;
+ }
+
GDScriptDataType field_type = _gdtype_from_datatype(field->get_datatype(), codegen.script);
GDScriptCodeGenerator::Address dst_address(GDScriptCodeGenerator::Address::MEMBER, codegen.script->member_indices[field->identifier->name].index, field_type);
@@ -2063,6 +2084,10 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
continue;
}
const GDScriptParser::VariableNode *field = p_class->members[i].variable;
+ if (field->is_static) {
+ continue;
+ }
+
if (field->onready != is_implicit_ready) {
// Only initialize in @implicit_ready.
continue;
@@ -2152,12 +2177,6 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
if (p_func) {
codegen.generator->set_initial_line(p_func->start_line);
-#ifdef TOOLS_ENABLED
- if (!p_for_lambda) {
- p_script->member_lines[func_name] = p_func->start_line;
- p_script->doc_functions[func_name] = p_func->doc_description;
- }
-#endif
} else {
codegen.generator->set_initial_line(0);
}
@@ -2196,6 +2215,135 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
return gd_function;
}
+GDScriptFunction *GDScriptCompiler::_make_static_initializer(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class) {
+ r_error = OK;
+ CodeGen codegen;
+ codegen.generator = memnew(GDScriptByteCodeGenerator);
+
+ codegen.class_node = p_class;
+ codegen.script = p_script;
+
+ StringName func_name = SNAME("@static_initializer");
+ bool is_static = true;
+ Variant rpc_config;
+ GDScriptDataType return_type;
+ return_type.has_type = true;
+ return_type.kind = GDScriptDataType::BUILTIN;
+ return_type.builtin_type = Variant::NIL;
+
+ codegen.function_name = func_name;
+ codegen.is_static = is_static;
+ codegen.generator->write_start(p_script, func_name, is_static, rpc_config, return_type);
+
+ GDScriptCodeGenerator::Address class_addr(GDScriptCodeGenerator::Address::CLASS);
+
+ // Initialize the default values for typed variables before anything.
+ // This avoids crashes if they are accessed with validated calls before being properly initialized.
+ // It may happen with out-of-order access or with `@onready` variables.
+ for (const GDScriptParser::ClassNode::Member &member : p_class->members) {
+ if (member.type != GDScriptParser::ClassNode::Member::VARIABLE) {
+ continue;
+ }
+
+ const GDScriptParser::VariableNode *field = member.variable;
+ if (!field->is_static) {
+ continue;
+ }
+
+ GDScriptDataType field_type = _gdtype_from_datatype(field->get_datatype(), codegen.script);
+
+ if (field_type.has_type) {
+ codegen.generator->write_newline(field->start_line);
+
+ if (field_type.has_container_element_type()) {
+ GDScriptCodeGenerator::Address temp = codegen.add_temporary(field_type);
+ codegen.generator->write_construct_typed_array(temp, field_type.get_container_element_type(), Vector<GDScriptCodeGenerator::Address>());
+ codegen.generator->write_set_named(class_addr, field->identifier->name, temp);
+ codegen.generator->pop_temporary();
+
+ } else if (field_type.kind == GDScriptDataType::BUILTIN) {
+ GDScriptCodeGenerator::Address temp = codegen.add_temporary(field_type);
+ codegen.generator->write_construct(temp, field_type.builtin_type, Vector<GDScriptCodeGenerator::Address>());
+ codegen.generator->write_set_named(class_addr, field->identifier->name, temp);
+ codegen.generator->pop_temporary();
+ }
+ // The `else` branch is for objects, in such case we leave it as `null`.
+ }
+ }
+
+ for (int i = 0; i < p_class->members.size(); i++) {
+ // Initialize static fields.
+ if (p_class->members[i].type != GDScriptParser::ClassNode::Member::VARIABLE) {
+ continue;
+ }
+ const GDScriptParser::VariableNode *field = p_class->members[i].variable;
+ if (!field->is_static) {
+ continue;
+ }
+
+ GDScriptDataType field_type = _gdtype_from_datatype(field->get_datatype(), codegen.script);
+
+ if (field->initializer) {
+ // Emit proper line change.
+ codegen.generator->write_newline(field->initializer->start_line);
+
+ GDScriptCodeGenerator::Address src_address = _parse_expression(codegen, r_error, field->initializer, false, true);
+ if (r_error) {
+ memdelete(codegen.generator);
+ return nullptr;
+ }
+
+ GDScriptCodeGenerator::Address temp = codegen.add_temporary(field_type);
+ if (field->use_conversion_assign) {
+ codegen.generator->write_assign_with_conversion(temp, src_address);
+ } else {
+ codegen.generator->write_assign(temp, src_address);
+ }
+ if (src_address.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
+ codegen.generator->pop_temporary();
+ }
+
+ codegen.generator->write_set_named(class_addr, field->identifier->name, temp);
+ codegen.generator->pop_temporary();
+ }
+ }
+
+ if (p_script->has_method(GDScriptLanguage::get_singleton()->strings._static_init)) {
+ codegen.generator->write_newline(p_class->start_line);
+ codegen.generator->write_call(GDScriptCodeGenerator::Address(), class_addr, GDScriptLanguage::get_singleton()->strings._static_init, Vector<GDScriptCodeGenerator::Address>());
+ }
+
+#ifdef DEBUG_ENABLED
+ if (EngineDebugger::is_active()) {
+ String signature;
+ // Path.
+ if (!p_script->get_script_path().is_empty()) {
+ signature += p_script->get_script_path();
+ }
+ // Location.
+ signature += "::0";
+
+ // Function and class.
+
+ if (p_class->identifier) {
+ signature += "::" + String(p_class->identifier->name) + "." + String(func_name);
+ } else {
+ signature += "::" + String(func_name);
+ }
+
+ codegen.generator->set_signature(signature);
+ }
+#endif
+
+ codegen.generator->set_initial_line(p_class->start_line);
+
+ GDScriptFunction *gd_function = codegen.generator->write_end();
+
+ memdelete(codegen.generator);
+
+ return gd_function;
+}
+
Error GDScriptCompiler::_parse_setter_getter(GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::VariableNode *p_variable, bool p_is_setter) {
Error err = OK;
@@ -2226,23 +2374,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
parsing_classes.insert(p_script);
p_script->clearing = true;
-#ifdef TOOLS_ENABLED
- p_script->doc_functions.clear();
- p_script->doc_variables.clear();
- p_script->doc_constants.clear();
- p_script->doc_enums.clear();
- p_script->doc_signals.clear();
- p_script->doc_tutorials.clear();
-
- p_script->doc_brief_description = p_class->doc_brief_description;
- p_script->doc_description = p_class->doc_description;
- for (int i = 0; i < p_class->doc_tutorials.size(); i++) {
- DocData::TutorialDoc td;
- td.title = p_class->doc_tutorials[i].first;
- td.link = p_class->doc_tutorials[i].second;
- p_script->doc_tutorials.append(td);
- }
-#endif
p_script->native = Ref<GDScriptNativeClass>();
p_script->base = Ref<GDScript>();
@@ -2266,19 +2397,28 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
}
member_functions.clear();
+ p_script->static_variables.clear();
+
if (p_script->implicit_initializer) {
memdelete(p_script->implicit_initializer);
}
if (p_script->implicit_ready) {
memdelete(p_script->implicit_ready);
}
+ if (p_script->static_initializer) {
+ memdelete(p_script->static_initializer);
+ }
+
p_script->member_functions.clear();
p_script->member_indices.clear();
p_script->member_info.clear();
+ p_script->static_variables_indices.clear();
+ p_script->static_variables.clear();
p_script->_signals.clear();
p_script->initializer = nullptr;
p_script->implicit_initializer = nullptr;
p_script->implicit_ready = nullptr;
+ p_script->static_initializer = nullptr;
p_script->clearing = false;
@@ -2386,13 +2526,15 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
} else {
prop_info.usage = PROPERTY_USAGE_SCRIPT_VARIABLE;
}
-#ifdef TOOLS_ENABLED
- p_script->doc_variables[name] = variable->doc_description;
-#endif
- p_script->member_info[name] = prop_info;
- p_script->member_indices[name] = minfo;
- p_script->members.insert(name);
+ if (variable->is_static) {
+ minfo.index = p_script->static_variables_indices.size();
+ p_script->static_variables_indices[name] = minfo;
+ } else {
+ p_script->member_info[name] = prop_info;
+ p_script->member_indices[name] = minfo;
+ p_script->members.insert(name);
+ }
#ifdef TOOLS_ENABLED
if (variable->initializer != nullptr && variable->initializer->is_constant) {
@@ -2401,7 +2543,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
} else {
p_script->member_default_values.erase(name);
}
- p_script->member_lines[name] = variable->start_line;
#endif
} break;
@@ -2410,12 +2551,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
StringName name = constant->identifier->name;
p_script->constants.insert(name, constant->initializer->reduced_value);
-#ifdef TOOLS_ENABLED
- p_script->member_lines[name] = constant->start_line;
- if (!constant->doc_description.is_empty()) {
- p_script->doc_constants[name] = constant->doc_description;
- }
-#endif
} break;
case GDScriptParser::ClassNode::Member::ENUM_VALUE: {
@@ -2423,18 +2558,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
StringName name = enum_value.identifier->name;
p_script->constants.insert(name, enum_value.value);
-#ifdef TOOLS_ENABLED
- p_script->member_lines[name] = enum_value.identifier->start_line;
- if (!p_script->doc_enums.has("@unnamed_enums")) {
- p_script->doc_enums["@unnamed_enums"] = DocData::EnumDoc();
- p_script->doc_enums["@unnamed_enums"].name = "@unnamed_enums";
- }
- DocData::ConstantDoc const_doc;
- const_doc.name = enum_value.identifier->name;
- const_doc.value = Variant(enum_value.value).operator String(); // TODO-DOC: enum value currently is int.
- const_doc.description = enum_value.doc_description;
- p_script->doc_enums["@unnamed_enums"].values.push_back(const_doc);
-#endif
} break;
case GDScriptParser::ClassNode::Member::SIGNAL: {
@@ -2447,11 +2570,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
parameters_names.write[j] = signal->parameters[j]->identifier->name;
}
p_script->_signals[name] = parameters_names;
-#ifdef TOOLS_ENABLED
- if (!signal->doc_description.is_empty()) {
- p_script->doc_signals[name] = signal->doc_description;
- }
-#endif
} break;
case GDScriptParser::ClassNode::Member::ENUM: {
@@ -2459,19 +2577,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
StringName name = enum_n->identifier->name;
p_script->constants.insert(name, enum_n->dictionary);
-#ifdef TOOLS_ENABLED
- p_script->member_lines[name] = enum_n->start_line;
- p_script->doc_enums[name] = DocData::EnumDoc();
- p_script->doc_enums[name].name = name;
- p_script->doc_enums[name].description = enum_n->doc_description;
- for (int j = 0; j < enum_n->values.size(); j++) {
- DocData::ConstantDoc const_doc;
- const_doc.name = enum_n->values[j].identifier->name;
- const_doc.value = Variant(enum_n->values[j].value).operator String();
- const_doc.description = enum_n->values[j].doc_description;
- p_script->doc_enums[name].values.push_back(const_doc);
- }
-#endif
} break;
case GDScriptParser::ClassNode::Member::GROUP: {
@@ -2497,6 +2602,8 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
}
}
+ p_script->static_variables.resize(p_script->static_variables_indices.size());
+
parsed_classes.insert(p_script);
parsing_classes.erase(p_script);
@@ -2519,9 +2626,6 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
}
}
-#ifdef TOOLS_ENABLED
- p_script->member_lines[name] = inner_class->start_line;
-#endif
p_script->constants.insert(name, subclass); //once parsed, goes to the list of constants
}
@@ -2576,6 +2680,15 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
}
}
+ if (p_class->has_static_data) {
+ Error err = OK;
+ GDScriptFunction *func = _make_static_initializer(err, p_script, p_class);
+ p_script->static_initializer = func;
+ if (err) {
+ return err;
+ }
+ }
+
#ifdef DEBUG_ENABLED
//validate instances if keeping state
@@ -2614,7 +2727,7 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
//well, tough luck, not gonna do anything here
}
}
-#endif
+#endif // TOOLS_ENABLED
} else {
GDScriptInstance *gi = static_cast<GDScriptInstance *>(si);
gi->reload_members();
@@ -2623,7 +2736,9 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
E = N;
}
}
-#endif
+#endif //DEBUG_ENABLED
+
+ has_static_data = p_class->has_static_data;
for (int i = 0; i < p_class->members.size(); i++) {
if (p_class->members[i].type != GDScriptParser::ClassNode::Member::CLASS) {
@@ -2637,6 +2752,8 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
if (err) {
return err;
}
+
+ has_static_data = has_static_data || inner_class->has_static_data;
}
p_script->_init_rpc_methods_properties();
@@ -2723,9 +2840,9 @@ Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_scri
return err;
}
-#ifdef TOOLS_ENABLED
- p_script->_update_doc();
-#endif
+ if (has_static_data && !root->annotated_static_unload) {
+ GDScriptCache::add_static_script(p_script);
+ }
return GDScriptCache::finish_compiling(main_script->get_path());
}
diff --git a/modules/gdscript/gdscript_compiler.h b/modules/gdscript/gdscript_compiler.h
index 5328c17c73..2d15d461fb 100644
--- a/modules/gdscript/gdscript_compiler.h
+++ b/modules/gdscript/gdscript_compiler.h
@@ -52,6 +52,7 @@ class GDScriptCompiler {
HashMap<StringName, GDScriptCodeGenerator::Address> parameters;
HashMap<StringName, GDScriptCodeGenerator::Address> locals;
List<HashMap<StringName, GDScriptCodeGenerator::Address>> locals_stack;
+ bool is_static = false;
GDScriptCodeGenerator::Address add_local(const StringName &p_name, const GDScriptDataType &p_type) {
uint32_t addr = generator->add_local(p_name, p_type);
@@ -130,6 +131,7 @@ class GDScriptCompiler {
void _add_locals_in_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block);
Error _parse_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block, bool p_add_locals = true);
GDScriptFunction *_parse_function(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::FunctionNode *p_func, bool p_for_ready = false, bool p_for_lambda = false);
+ GDScriptFunction *_make_static_initializer(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class);
Error _parse_setter_getter(GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::VariableNode *p_variable, bool p_is_setter);
Error _populate_class_members(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state);
Error _compile_class(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state);
@@ -138,6 +140,7 @@ class GDScriptCompiler {
StringName source;
String error;
GDScriptParser::ExpressionNode *awaited_node = nullptr;
+ bool has_static_data = false;
public:
static void convert_to_initializer_type(Variant &p_variant, const GDScriptParser::VariableNode *p_node);
diff --git a/modules/gdscript/gdscript_disassembler.cpp b/modules/gdscript/gdscript_disassembler.cpp
index 0acc03be3d..45ad8792d9 100644
--- a/modules/gdscript/gdscript_disassembler.cpp
+++ b/modules/gdscript/gdscript_disassembler.cpp
@@ -851,7 +851,7 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const {
}
text += ")";
- incr = 3 + captures_count;
+ incr = 4 + captures_count;
} break;
case OPCODE_CREATE_SELF_LAMBDA: {
int instr_var_args = _code_ptr[++ip];
@@ -871,7 +871,7 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const {
}
text += ")";
- incr = 3 + captures_count;
+ incr = 4 + captures_count;
} break;
case OPCODE_JUMP: {
text += "jump ";
diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp
index f3a86522ae..09af51656c 100644
--- a/modules/gdscript/gdscript_editor.cpp
+++ b/modules/gdscript/gdscript_editor.cpp
@@ -54,6 +54,7 @@ void GDScriptLanguage::get_string_delimiters(List<String> *p_delimiters) const {
p_delimiters->push_back("\" \"");
p_delimiters->push_back("' '");
p_delimiters->push_back("\"\"\" \"\"\"");
+ p_delimiters->push_back("''' '''");
}
bool GDScriptLanguage::is_using_templates() {
@@ -73,9 +74,11 @@ Ref<Script> GDScriptLanguage::make_template(const String &p_template, const Stri
.replace(": String", "")
.replace(": Array[String]", "")
.replace(": float", "")
+ .replace(": CharFXTransform", "")
.replace(":=", "=")
.replace(" -> String", "")
.replace(" -> int", "")
+ .replace(" -> bool", "")
.replace(" -> void", "");
}
@@ -578,29 +581,34 @@ static int _get_enum_constant_location(StringName p_class, StringName p_enum_con
// END LOCATION METHODS
-static String _get_visual_datatype(const PropertyInfo &p_info, bool p_is_arg = true) {
- if (p_info.usage & (PROPERTY_USAGE_CLASS_IS_ENUM | PROPERTY_USAGE_CLASS_IS_BITFIELD)) {
- String enum_name = p_info.class_name;
- if (!enum_name.contains(".")) {
- return enum_name;
+static String _trim_parent_class(const String &p_class, const String &p_base_class) {
+ if (p_base_class.is_empty()) {
+ return p_class;
+ }
+ Vector<String> names = p_class.split(".", false, 1);
+ if (names.size() == 2) {
+ String first = names[0];
+ String rest = names[1];
+ if (ClassDB::class_exists(p_base_class) && ClassDB::class_exists(first) && ClassDB::is_parent_class(p_base_class, first)) {
+ return rest;
}
- return enum_name.get_slice(".", 1);
}
+ return p_class;
+}
- String n = p_info.name;
- int idx = n.find(":");
- if (idx != -1) {
- return n.substr(idx + 1, n.length());
- }
+static String _get_visual_datatype(const PropertyInfo &p_info, bool p_is_arg, const String &p_base_class = "") {
+ String class_name = p_info.class_name;
+ bool is_enum = p_info.type == Variant::INT && p_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM;
+ // PROPERTY_USAGE_CLASS_IS_BITFIELD: BitField[T] isn't supported (yet?), use plain int.
- if (p_info.type == Variant::OBJECT) {
- if (p_info.hint == PROPERTY_HINT_RESOURCE_TYPE) {
- return p_info.hint_string;
- } else {
- return p_info.class_name.operator String();
+ if ((p_info.type == Variant::OBJECT || is_enum) && !class_name.is_empty()) {
+ if (is_enum && CoreConstants::is_global_enum(p_info.class_name)) {
+ return class_name;
}
- }
- if (p_info.type == Variant::NIL) {
+ return _trim_parent_class(class_name, p_base_class);
+ } else if (p_info.type == Variant::ARRAY && p_info.hint == PROPERTY_HINT_ARRAY_TYPE && !p_info.hint_string.is_empty()) {
+ return "Array[" + _trim_parent_class(p_info.hint_string, p_base_class) + "]";
+ } else if (p_info.type == Variant::NIL) {
if (p_is_arg || (p_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT)) {
return "Variant";
} else {
@@ -898,19 +906,20 @@ static void _list_available_types(bool p_inherit_only, GDScriptParser::Completio
}
}
-static void _find_identifiers_in_suite(const GDScriptParser::SuiteNode *p_suite, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result) {
+static void _find_identifiers_in_suite(const GDScriptParser::SuiteNode *p_suite, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, int p_recursion_depth = 0) {
for (int i = 0; i < p_suite->locals.size(); i++) {
ScriptLanguage::CodeCompletionOption option;
+ int location = p_recursion_depth == 0 ? ScriptLanguage::LOCATION_LOCAL : (p_recursion_depth | ScriptLanguage::LOCATION_PARENT_MASK);
if (p_suite->locals[i].type == GDScriptParser::SuiteNode::Local::CONSTANT) {
- option = ScriptLanguage::CodeCompletionOption(p_suite->locals[i].name, ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, ScriptLanguage::LOCATION_LOCAL);
+ option = ScriptLanguage::CodeCompletionOption(p_suite->locals[i].name, ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, location);
option.default_value = p_suite->locals[i].constant->initializer->reduced_value;
} else {
- option = ScriptLanguage::CodeCompletionOption(p_suite->locals[i].name, ScriptLanguage::CODE_COMPLETION_KIND_VARIABLE, ScriptLanguage::LOCATION_LOCAL);
+ option = ScriptLanguage::CodeCompletionOption(p_suite->locals[i].name, ScriptLanguage::CODE_COMPLETION_KIND_VARIABLE, location);
}
r_result.insert(option.display, option);
}
if (p_suite->parent_block) {
- _find_identifiers_in_suite(p_suite->parent_block, r_result);
+ _find_identifiers_in_suite(p_suite->parent_block, r_result, p_recursion_depth + 1);
}
}
@@ -925,7 +934,7 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class,
int classes_processed = 0;
while (clss) {
for (int i = 0; i < clss->members.size(); i++) {
- const int location = (classes_processed + p_recursion_depth) | ScriptLanguage::LOCATION_PARENT_MASK;
+ const int location = p_recursion_depth == 0 ? classes_processed : (p_recursion_depth | ScriptLanguage::LOCATION_PARENT_MASK);
const GDScriptParser::ClassNode::Member &member = clss->members[i];
ScriptLanguage::CodeCompletionOption option;
switch (member.type) {
@@ -1017,7 +1026,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
while (!base_type.has_no_type()) {
switch (base_type.kind) {
case GDScriptParser::DataType::CLASS: {
- _find_identifiers_in_class(base_type.class_type, p_only_functions, base_type.is_meta_type, false, r_result, p_recursion_depth + 1);
+ _find_identifiers_in_class(base_type.class_type, p_only_functions, base_type.is_meta_type, false, r_result, p_recursion_depth);
// This already finds all parent identifiers, so we are done.
base_type = GDScriptParser::DataType();
} break;
@@ -1197,7 +1206,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context
}
if (p_context.current_class) {
- _find_identifiers_in_class(p_context.current_class, p_only_functions, (!p_context.current_function || p_context.current_function->is_static), false, r_result, p_recursion_depth + 1);
+ _find_identifiers_in_class(p_context.current_class, p_only_functions, (!p_context.current_function || p_context.current_function->is_static), false, r_result, p_recursion_depth);
}
List<StringName> functions;
@@ -2984,6 +2993,15 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
List<MethodInfo> virtual_methods;
ClassDB::get_virtual_methods(class_name, &virtual_methods);
+
+ {
+ // Not truly a virtual method, but can also be "overridden".
+ MethodInfo static_init("_static_init");
+ static_init.return_val.type = Variant::NIL;
+ static_init.flags |= METHOD_FLAG_STATIC | METHOD_FLAG_VIRTUAL;
+ virtual_methods.push_back(static_init);
+ }
+
for (const MethodInfo &mi : virtual_methods) {
String method_hint = mi.name;
if (method_hint.contains(":")) {
@@ -3001,26 +3019,14 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
arg = arg.substr(0, arg.find(":"));
}
method_hint += arg;
- if (use_type_hint && mi.arguments[i].type != Variant::NIL) {
- method_hint += ": ";
- if (mi.arguments[i].type == Variant::OBJECT && mi.arguments[i].class_name != StringName()) {
- method_hint += mi.arguments[i].class_name.operator String();
- } else {
- method_hint += Variant::get_type_name(mi.arguments[i].type);
- }
+ if (use_type_hint) {
+ method_hint += ": " + _get_visual_datatype(mi.arguments[i], true, class_name);
}
}
}
method_hint += ")";
- if (use_type_hint && (mi.return_val.type != Variant::NIL || !(mi.return_val.usage & PROPERTY_USAGE_NIL_IS_VARIANT))) {
- method_hint += " -> ";
- if (mi.return_val.type == Variant::NIL) {
- method_hint += "void";
- } else if (mi.return_val.type == Variant::OBJECT && mi.return_val.class_name != StringName()) {
- method_hint += mi.return_val.class_name.operator String();
- } else {
- method_hint += Variant::get_type_name(mi.return_val.type);
- }
+ if (use_type_hint) {
+ method_hint += " -> " + _get_visual_datatype(mi.return_val, false, class_name);
}
method_hint += ":";
@@ -3093,12 +3099,7 @@ String GDScriptLanguage::_get_indentation() const {
if (use_space_indentation) {
int indent_size = EDITOR_GET("text_editor/behavior/indent/size");
-
- String space_indent = "";
- for (int i = 0; i < indent_size; i++) {
- space_indent += " ";
- }
- return space_indent;
+ return String(" ").repeat(indent_size);
}
}
#endif
@@ -3145,12 +3146,7 @@ void GDScriptLanguage::auto_indent_code(String &p_code, int p_from_line, int p_t
}
if (i >= p_from_line) {
- l = "";
- for (int j = 0; j < indent_stack.size(); j++) {
- l += indent;
- }
- l += st;
-
+ l = indent.repeat(indent_stack.size()) + st;
} else if (i > p_to_line) {
break;
}
diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h
index 1a5e9eef53..390e562e6f 100644
--- a/modules/gdscript/gdscript_function.h
+++ b/modules/gdscript/gdscript_function.h
@@ -409,7 +409,8 @@ public:
ADDR_TYPE_STACK = 0,
ADDR_TYPE_CONSTANT = 1,
ADDR_TYPE_MEMBER = 2,
- ADDR_TYPE_MAX = 3,
+ ADDR_TYPE_STATIC_VAR = 3,
+ ADDR_TYPE_MAX = 4,
};
enum FixedAddresses {
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index e2a37ab6e9..d90503c658 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -81,6 +81,8 @@ GDScriptParser::GDScriptParser() {
// TODO: Should this be static?
register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation);
register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation);
+ register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);
+
register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation);
// Export annotations.
register_annotation(MethodInfo("@export"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NONE, Variant::NIL>);
@@ -102,6 +104,7 @@ GDScriptParser::GDScriptParser() {
register_annotation(MethodInfo("@export_flags_3d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_RENDER, Variant::INT>);
register_annotation(MethodInfo("@export_flags_3d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_PHYSICS, Variant::INT>);
register_annotation(MethodInfo("@export_flags_3d_navigation"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_NAVIGATION, Variant::INT>);
+ register_annotation(MethodInfo("@export_flags_avoidance"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_AVOIDANCE, Variant::INT>);
// Export grouping annotations.
register_annotation(MethodInfo("@export_category", PropertyInfo(Variant::STRING, "name")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_CATEGORY>);
register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_GROUP>, varray(""));
@@ -623,7 +626,7 @@ bool GDScriptParser::has_class(const GDScriptParser::ClassNode *p_class) const {
return false;
}
-GDScriptParser::ClassNode *GDScriptParser::parse_class() {
+GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) {
ClassNode *n_class = alloc_node<ClassNode>();
ClassNode *previous_class = current_class;
@@ -724,7 +727,7 @@ void GDScriptParser::parse_extends() {
}
template <class T>
-void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(), AnnotationInfo::TargetKind p_target, const String &p_member_kind) {
+void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static) {
advance();
#ifdef TOOLS_ENABLED
@@ -749,7 +752,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)()
#endif // TOOLS_ENABLED
}
- T *member = (this->*p_parse_function)();
+ T *member = (this->*p_parse_function)(p_is_static);
if (member == nullptr) {
return;
}
@@ -761,7 +764,11 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)()
#ifdef TOOLS_ENABLED
// Consume doc comments.
class_doc_line = MIN(class_doc_line, doc_comment_line - 1);
- if (has_comment(doc_comment_line)) {
+
+ // Check whether current line has a doc comment
+ if (has_comment(previous.start_line, true)) {
+ member->doc_description = get_doc_comment(previous.start_line, true);
+ } else if (has_comment(doc_comment_line, true)) {
if constexpr (std::is_same_v<T, ClassNode>) {
get_class_doc_comment(doc_comment_line, member->doc_brief_description, member->doc_description, member->doc_tutorials, true);
} else {
@@ -799,10 +806,15 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)()
void GDScriptParser::parse_class_body(bool p_is_multiline) {
bool class_end = false;
+ bool next_is_static = false;
while (!class_end && !is_at_end()) {
- switch (current.type) {
+ GDScriptTokenizer::Token token = current;
+ switch (token.type) {
case GDScriptTokenizer::Token::VAR:
- parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable");
+ parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", next_is_static);
+ if (next_is_static) {
+ current_class->has_static_data = true;
+ }
break;
case GDScriptTokenizer::Token::CONST:
parse_class_member(&GDScriptParser::parse_constant, AnnotationInfo::CONSTANT, "constant");
@@ -810,9 +822,8 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
case GDScriptTokenizer::Token::SIGNAL:
parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal");
break;
- case GDScriptTokenizer::Token::STATIC:
case GDScriptTokenizer::Token::FUNC:
- parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function");
+ parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", next_is_static);
break;
case GDScriptTokenizer::Token::CLASS:
parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class");
@@ -820,6 +831,13 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
case GDScriptTokenizer::Token::ENUM:
parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum");
break;
+ case GDScriptTokenizer::Token::STATIC: {
+ advance();
+ next_is_static = true;
+ if (!check(GDScriptTokenizer::Token::FUNC) && !check(GDScriptTokenizer::Token::VAR)) {
+ push_error(R"(Expected "func" or "var" after "static".)");
+ }
+ } break;
case GDScriptTokenizer::Token::ANNOTATION: {
advance();
@@ -866,6 +884,9 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
advance();
break;
}
+ if (token.type != GDScriptTokenizer::Token::STATIC) {
+ next_is_static = false;
+ }
if (panic_mode) {
synchronize();
}
@@ -875,11 +896,11 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
}
}
-GDScriptParser::VariableNode *GDScriptParser::parse_variable() {
- return parse_variable(true);
+GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static) {
+ return parse_variable(p_is_static, true);
}
-GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_allow_property) {
+GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, bool p_allow_property) {
VariableNode *variable = alloc_node<VariableNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) {
@@ -889,6 +910,7 @@ GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_allow_proper
variable->identifier = parse_identifier();
variable->export_info.name = variable->identifier->name;
+ variable->is_static = p_is_static;
if (match(GDScriptTokenizer::Token::COLON)) {
if (check(GDScriptTokenizer::Token::NEWLINE)) {
@@ -1032,6 +1054,7 @@ void GDScriptParser::parse_property_setter(VariableNode *p_variable) {
complete_extents(identifier);
identifier->name = "@" + p_variable->identifier->name + "_setter";
function->identifier = identifier;
+ function->is_static = p_variable->is_static;
consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected "(" after "set".)");
@@ -1083,6 +1106,7 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) {
complete_extents(identifier);
identifier->name = "@" + p_variable->identifier->name + "_getter";
function->identifier = identifier;
+ function->is_static = p_variable->is_static;
FunctionNode *previous_function = current_function;
current_function = function;
@@ -1107,7 +1131,7 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) {
}
}
-GDScriptParser::ConstantNode *GDScriptParser::parse_constant() {
+GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_static) {
ConstantNode *constant = alloc_node<ConstantNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) {
@@ -1174,7 +1198,7 @@ GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() {
return parameter;
}
-GDScriptParser::SignalNode *GDScriptParser::parse_signal() {
+GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_static) {
SignalNode *signal = alloc_node<SignalNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) {
@@ -1219,7 +1243,7 @@ GDScriptParser::SignalNode *GDScriptParser::parse_signal() {
return signal;
}
-GDScriptParser::EnumNode *GDScriptParser::parse_enum() {
+GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) {
EnumNode *enum_node = alloc_node<EnumNode>();
bool named = false;
@@ -1343,7 +1367,7 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
default_used = true;
} else {
if (default_used) {
- push_error("Cannot have a mandatory parameters after optional parameters.");
+ push_error("Cannot have mandatory parameters after optional parameters.");
continue;
}
}
@@ -1368,23 +1392,23 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
}
}
+ if (!p_function->source_lambda && p_function->identifier && p_function->identifier->name == GDScriptLanguage::get_singleton()->strings._static_init) {
+ if (!p_function->is_static) {
+ push_error(R"(Static constructor must be declared static.)");
+ }
+ if (p_function->parameters.size() != 0) {
+ push_error(R"(Static constructor cannot have parameters.)");
+ }
+ current_class->has_static_data = true;
+ }
+
// TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens.
consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after %s declaration.)", p_type));
}
-GDScriptParser::FunctionNode *GDScriptParser::parse_function() {
+GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) {
FunctionNode *function = alloc_node<FunctionNode>();
- bool _static = false;
- if (previous.type == GDScriptTokenizer::Token::STATIC) {
- // TODO: Improve message if user uses "static" with "var" or "const"
- if (!consume(GDScriptTokenizer::Token::FUNC, R"(Expected "func" after "static".)")) {
- complete_extents(function);
- return nullptr;
- }
- _static = true;
- }
-
make_completion_context(COMPLETION_OVERRIDE_METHOD, function);
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected function name after "func".)")) {
@@ -1396,7 +1420,7 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function() {
current_function = function;
function->identifier = parse_identifier();
- function->is_static = _static;
+ function->is_static = p_is_static;
SuiteNode *body = alloc_node<SuiteNode>();
SuiteNode *previous_suite = current_suite;
@@ -1439,27 +1463,32 @@ GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_vali
valid = false;
}
- if (match(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) {
+ if (check(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) {
+ push_multiline(true);
+ advance();
// Arguments.
push_completion_call(annotation);
make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, 0, true);
- if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) {
- push_multiline(true);
- int argument_index = 0;
- do {
- make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, argument_index, true);
- set_last_completion_call_arg(argument_index++);
- ExpressionNode *argument = parse_expression(false);
- if (argument == nullptr) {
- valid = false;
- continue;
- }
- annotation->arguments.push_back(argument);
- } while (match(GDScriptTokenizer::Token::COMMA));
- pop_multiline();
+ int argument_index = 0;
+ do {
+ if (check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE)) {
+ // Allow for trailing comma.
+ break;
+ }
- consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after annotation arguments.)*");
- }
+ make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, argument_index, true);
+ set_last_completion_call_arg(argument_index++);
+ ExpressionNode *argument = parse_expression(false);
+ if (argument == nullptr) {
+ push_error("Expected expression as the annotation argument.");
+ valid = false;
+ continue;
+ }
+ annotation->arguments.push_back(argument);
+ } while (match(GDScriptTokenizer::Token::COMMA) && !is_at_end());
+
+ pop_multiline();
+ consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after annotation arguments.)*");
pop_completion_call();
}
complete_extents(annotation);
@@ -1475,7 +1504,7 @@ GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_vali
void GDScriptParser::clear_unused_annotations() {
for (const AnnotationNode *annotation : annotation_stack) {
- push_error(vformat(R"(Annotation "%s" does not precedes a valid target, so it will have no effect.)", annotation->name), annotation);
+ push_error(vformat(R"(Annotation "%s" does not precede a valid target, so it will have no effect.)", annotation->name), annotation);
}
annotation_stack.clear();
@@ -1603,11 +1632,11 @@ GDScriptParser::Node *GDScriptParser::parse_statement() {
break;
case GDScriptTokenizer::Token::VAR:
advance();
- result = parse_variable();
+ result = parse_variable(false, false);
break;
case GDScriptTokenizer::Token::CONST:
advance();
- result = parse_constant();
+ result = parse_constant(false);
break;
case GDScriptTokenizer::Token::IF:
advance();
@@ -1637,7 +1666,7 @@ GDScriptParser::Node *GDScriptParser::parse_statement() {
advance();
ReturnNode *n_return = alloc_node<ReturnNode>();
if (!is_statement_end()) {
- if (current_function && current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._init) {
+ if (current_function && (current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._init || current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._static_init)) {
push_error(R"(Constructor cannot return a value.)");
}
n_return->return_value = parse_expression(false);
@@ -1817,7 +1846,7 @@ GDScriptParser::ForNode *GDScriptParser::parse_for() {
n_for->list = parse_expression(false);
if (!n_for->list) {
- push_error(R"(Expected a list or range after "in".)");
+ push_error(R"(Expected iterable after "in".)");
}
consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "for" condition.)");
@@ -2820,6 +2849,9 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_attribute(ExpressionNode *
attribute->base = p_previous_operand;
+ if (current.is_node_name()) {
+ current.type = GDScriptTokenizer::Token::IDENTIFIER;
+ }
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier after "." for attribute access.)")) {
complete_extents(attribute);
return attribute;
@@ -3261,35 +3293,125 @@ GDScriptParser::TypeNode *GDScriptParser::parse_type(bool p_allow_void) {
}
#ifdef TOOLS_ENABLED
-static bool _in_codeblock(String p_line, bool p_already_in, int *r_block_begins = nullptr) {
- int start_block = p_line.rfind("[codeblock]");
- int end_block = p_line.rfind("[/codeblock]");
-
- if (start_block != -1 && r_block_begins) {
- *r_block_begins = start_block;
+enum DocLineState {
+ DOC_LINE_NORMAL,
+ DOC_LINE_IN_CODE,
+ DOC_LINE_IN_CODEBLOCK,
+};
+
+static String _process_doc_line(const String &p_line, const String &p_text, const String &p_space_prefix, DocLineState &r_state) {
+ String line = p_line;
+ if (r_state == DOC_LINE_NORMAL) {
+ line = line.strip_edges(true, false);
+ } else {
+ line = line.trim_prefix(p_space_prefix);
}
- if (p_already_in) {
- if (end_block == -1) {
- return true;
- } else if (start_block == -1) {
- return false;
+ String line_join;
+ if (!p_text.is_empty()) {
+ if (r_state == DOC_LINE_NORMAL) {
+ if (p_text.ends_with("[/codeblock]")) {
+ line_join = "\n";
+ } else if (!p_text.ends_with("[br]")) {
+ line_join = " ";
+ }
} else {
- return start_block > end_block;
+ line_join = "\n";
}
- } else {
- if (start_block == -1) {
- return false;
- } else if (end_block == -1) {
- return true;
- } else {
- return start_block > end_block;
+ }
+
+ String result;
+ int from = 0;
+ int buffer_start = 0;
+ const int len = line.length();
+ bool process = true;
+ while (process) {
+ switch (r_state) {
+ case DOC_LINE_NORMAL: {
+ int lb_pos = line.find_char('[', from);
+ if (lb_pos < 0) {
+ process = false;
+ break;
+ }
+ int rb_pos = line.find_char(']', lb_pos + 1);
+ if (rb_pos < 0) {
+ process = false;
+ break;
+ }
+
+ from = rb_pos + 1;
+
+ String tag = line.substr(lb_pos + 1, rb_pos - lb_pos - 1);
+ if (tag == "code") {
+ r_state = DOC_LINE_IN_CODE;
+ } else if (tag == "codeblock") {
+ if (lb_pos == 0) {
+ line_join = "\n";
+ } else {
+ result += line.substr(buffer_start, lb_pos - buffer_start) + '\n';
+ }
+ result += "[codeblock]";
+ if (from < len) {
+ result += '\n';
+ }
+
+ r_state = DOC_LINE_IN_CODEBLOCK;
+ buffer_start = from;
+ }
+ } break;
+ case DOC_LINE_IN_CODE: {
+ int pos = line.find("[/code]", from);
+ if (pos < 0) {
+ process = false;
+ break;
+ }
+
+ from = pos + 7;
+
+ r_state = DOC_LINE_NORMAL;
+ } break;
+ case DOC_LINE_IN_CODEBLOCK: {
+ int pos = line.find("[/codeblock]", from);
+ if (pos < 0) {
+ process = false;
+ break;
+ }
+
+ from = pos + 12;
+
+ if (pos == 0) {
+ line_join = "\n";
+ } else {
+ result += line.substr(buffer_start, pos - buffer_start) + '\n';
+ }
+ result += "[/codeblock]";
+ if (from < len) {
+ result += '\n';
+ }
+
+ r_state = DOC_LINE_NORMAL;
+ buffer_start = from;
+ } break;
}
}
+
+ result += line.substr(buffer_start);
+ if (r_state == DOC_LINE_NORMAL) {
+ result = result.strip_edges(false, true);
+ }
+
+ return line_join + result;
}
-bool GDScriptParser::has_comment(int p_line) {
- return tokenizer.get_comments().has(p_line);
+bool GDScriptParser::has_comment(int p_line, bool p_must_be_doc) {
+ bool has_comment = tokenizer.get_comments().has(p_line);
+ // If there are no comments or if we don't care whether the comment
+ // is a docstring, we have our result.
+ if (!p_must_be_doc || !has_comment) {
+ return has_comment;
+ }
+
+ return tokenizer.get_comments()[p_line].comment.begins_with("##");
}
String GDScriptParser::get_doc_comment(int p_line, bool p_single_line) {
@@ -3306,7 +3428,7 @@ String GDScriptParser::get_doc_comment(int p_line, bool p_single_line) {
String doc;
int line = p_line;
- bool in_codeblock = false;
+ DocLineState state = DOC_LINE_NORMAL;
while (comments.has(line - 1)) {
if (!comments[line - 1].new_line || !comments[line - 1].comment.begins_with("##")) {
@@ -3315,29 +3437,24 @@ String GDScriptParser::get_doc_comment(int p_line, bool p_single_line) {
line--;
}
- int codeblock_begins = 0;
+ String space_prefix;
+ if (comments.has(line) && comments[line].comment.begins_with("##")) {
+ int i = 2;
+ for (; i < comments[line].comment.length(); i++) {
+ if (comments[line].comment[i] != ' ') {
+ break;
+ }
+ }
+ space_prefix = String(" ").repeat(i - 2);
+ }
+
while (comments.has(line)) {
if (!comments[line].new_line || !comments[line].comment.begins_with("##")) {
break;
}
- String doc_line = comments[line].comment.trim_prefix("##");
-
- in_codeblock = _in_codeblock(doc_line, in_codeblock, &codeblock_begins);
- if (in_codeblock) {
- int i = 0;
- for (; i < codeblock_begins; i++) {
- if (doc_line[i] != ' ') {
- break;
- }
- }
- doc_line = doc_line.substr(i);
- } else {
- doc_line = doc_line.strip_edges();
- }
- String line_join = (in_codeblock) ? "\n" : " ";
-
- doc = (doc.is_empty()) ? doc_line : doc + line_join + doc_line;
+ String doc_line = comments[line].comment.trim_prefix("##");
+ doc += _process_doc_line(doc_line, doc, space_prefix, state);
line++;
}
@@ -3352,7 +3469,7 @@ void GDScriptParser::get_class_doc_comment(int p_line, String &p_brief, String &
ERR_FAIL_COND(!p_brief.is_empty() || !p_desc.is_empty() || p_tutorials.size() != 0);
int line = p_line;
- bool in_codeblock = false;
+ DocLineState state = DOC_LINE_NORMAL;
enum Mode {
BRIEF,
DESC,
@@ -3370,96 +3487,87 @@ void GDScriptParser::get_class_doc_comment(int p_line, String &p_brief, String &
}
}
- int codeblock_begins = 0;
+ String space_prefix;
+ if (comments.has(line) && comments[line].comment.begins_with("##")) {
+ int i = 2;
+ for (; i < comments[line].comment.length(); i++) {
+ if (comments[line].comment[i] != ' ') {
+ break;
+ }
+ }
+ space_prefix = String(" ").repeat(i - 2);
+ }
+
while (comments.has(line)) {
if (!comments[line].new_line || !comments[line].comment.begins_with("##")) {
break;
}
- String title, link; // For tutorials.
String doc_line = comments[line++].comment.trim_prefix("##");
- String stripped_line = doc_line.strip_edges();
-
- // Set the read mode.
- if (stripped_line.is_empty() && mode == BRIEF && !p_brief.is_empty()) {
- mode = DESC;
- continue;
-
- } else if (stripped_line.begins_with("@tutorial")) {
- int begin_scan = String("@tutorial").length();
- if (begin_scan >= stripped_line.length()) {
- continue; // invalid syntax.
- }
-
- if (stripped_line[begin_scan] == ':') { // No title.
- // Syntax: ## @tutorial: https://godotengine.org/ // The title argument is optional.
- title = "";
- link = stripped_line.trim_prefix("@tutorial:").strip_edges();
-
- } else {
- /* Syntax:
- * @tutorial ( The Title Here ) : https://the.url/
- * ^ open ^ close ^ colon ^ url
- */
- int open_bracket_pos = begin_scan, close_bracket_pos = 0;
- while (open_bracket_pos < stripped_line.length() && (stripped_line[open_bracket_pos] == ' ' || stripped_line[open_bracket_pos] == '\t')) {
- open_bracket_pos++;
- }
- if (open_bracket_pos == stripped_line.length() || stripped_line[open_bracket_pos++] != '(') {
- continue; // invalid syntax.
- }
- close_bracket_pos = open_bracket_pos;
- while (close_bracket_pos < stripped_line.length() && stripped_line[close_bracket_pos] != ')') {
- close_bracket_pos++;
- }
- if (close_bracket_pos == stripped_line.length()) {
- continue; // invalid syntax.
- }
+ String title, link; // For tutorials.
- int colon_pos = close_bracket_pos + 1;
- while (colon_pos < stripped_line.length() && (stripped_line[colon_pos] == ' ' || stripped_line[colon_pos] == '\t')) {
- colon_pos++;
+ if (state == DOC_LINE_NORMAL) {
+ // Set the read mode.
+ String stripped_line = doc_line.strip_edges();
+ if (stripped_line.is_empty()) {
+ if (mode == BRIEF && !p_brief.is_empty()) {
+ mode = DESC;
}
- if (colon_pos == stripped_line.length() || stripped_line[colon_pos++] != ':') {
- continue; // invalid syntax.
+ continue;
+ } else if (stripped_line.begins_with("@tutorial")) {
+ int begin_scan = String("@tutorial").length();
+ if (begin_scan >= stripped_line.length()) {
+ continue; // Invalid syntax.
}
- title = stripped_line.substr(open_bracket_pos, close_bracket_pos - open_bracket_pos).strip_edges();
- link = stripped_line.substr(colon_pos).strip_edges();
- }
-
- mode = TUTORIALS;
- in_codeblock = false;
- } else if (stripped_line.is_empty()) {
- continue;
- } else {
- // Tutorial docs are single line, we need a @tag after it.
- if (mode == TUTORIALS) {
- mode = DONE;
- }
+ if (stripped_line[begin_scan] == ':') { // No title.
+ // Syntax: ## @tutorial: https://godotengine.org/ // The title argument is optional.
+ title = "";
+ link = stripped_line.trim_prefix("@tutorial:").strip_edges();
+ } else {
+ /* Syntax:
+ * @tutorial ( The Title Here ) : https://the.url/
+ * ^ open ^ close ^ colon ^ url
+ */
+ int open_bracket_pos = begin_scan, close_bracket_pos = 0;
+ while (open_bracket_pos < stripped_line.length() && (stripped_line[open_bracket_pos] == ' ' || stripped_line[open_bracket_pos] == '\t')) {
+ open_bracket_pos++;
+ }
+ if (open_bracket_pos == stripped_line.length() || stripped_line[open_bracket_pos++] != '(') {
+ continue; // Invalid syntax.
+ }
+ close_bracket_pos = open_bracket_pos;
+ while (close_bracket_pos < stripped_line.length() && stripped_line[close_bracket_pos] != ')') {
+ close_bracket_pos++;
+ }
+ if (close_bracket_pos == stripped_line.length()) {
+ continue; // Invalid syntax.
+ }
- in_codeblock = _in_codeblock(doc_line, in_codeblock, &codeblock_begins);
- }
+ int colon_pos = close_bracket_pos + 1;
+ while (colon_pos < stripped_line.length() && (stripped_line[colon_pos] == ' ' || stripped_line[colon_pos] == '\t')) {
+ colon_pos++;
+ }
+ if (colon_pos == stripped_line.length() || stripped_line[colon_pos++] != ':') {
+ continue; // Invalid syntax.
+ }
- if (in_codeblock) {
- int i = 0;
- for (; i < codeblock_begins; i++) {
- if (doc_line[i] != ' ') {
- break;
+ title = stripped_line.substr(open_bracket_pos, close_bracket_pos - open_bracket_pos).strip_edges();
+ link = stripped_line.substr(colon_pos).strip_edges();
}
+
+ mode = TUTORIALS;
+ } else if (mode == TUTORIALS) { // Tutorial docs are single line, we need a @tag after it.
+ mode = DONE;
}
- doc_line = doc_line.substr(i);
- } else {
- doc_line = stripped_line;
}
- String line_join = (in_codeblock) ? "\n" : " ";
switch (mode) {
case BRIEF:
- p_brief = (p_brief.length() == 0) ? doc_line : p_brief + line_join + doc_line;
+ p_brief += _process_doc_line(doc_line, p_brief, space_prefix, state);
break;
case DESC:
- p_desc = (p_desc.length() == 0) ? doc_line : p_desc + line_join + doc_line;
+ p_desc += _process_doc_line(doc_line, p_desc, space_prefix, state);
break;
case TUTORIALS:
p_tutorials.append(Pair<String, String>(title, link));
@@ -3468,6 +3576,7 @@ void GDScriptParser::get_class_doc_comment(int p_line, String &p_brief, String &
break;
}
}
+
if (current_class->members.size() > 0) {
const ClassNode::Member &m = current_class->members[0];
int first_member_line = m.get_line();
@@ -3678,6 +3787,12 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation)
}
bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_node) {
+#ifdef DEBUG_ENABLED
+ if (this->_is_tool) {
+ push_error(R"("@tool" annotation can only be used once.)", p_annotation);
+ return false;
+ }
+#endif // DEBUG_ENABLED
this->_is_tool = true;
return true;
}
@@ -3686,6 +3801,16 @@ bool GDScriptParser::icon_annotation(const AnnotationNode *p_annotation, Node *p
ERR_FAIL_COND_V_MSG(p_node->type != Node::CLASS, false, R"("@icon" annotation can only be applied to classes.)");
ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false);
ClassNode *p_class = static_cast<ClassNode *>(p_node);
+#ifdef DEBUG_ENABLED
+ if (!p_class->icon_path.is_empty()) {
+ push_error(R"("@icon" annotation can only be used once.)", p_annotation);
+ return false;
+ }
+ if (String(p_annotation->resolved_arguments[0]).is_empty()) {
+ push_error(R"("@icon" annotation argument must contain the path to the icon.)", p_annotation->arguments[0]);
+ return false;
+ }
+#endif // DEBUG_ENABLED
p_class->icon_path = p_annotation->resolved_arguments[0];
return true;
}
@@ -3856,7 +3981,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node
variable->export_info.hint = PROPERTY_HINT_NODE_TYPE;
variable->export_info.hint_string = export_type.to_string();
} else {
- push_error(R"(Export type can only be built-in, a resource, a node or an enum.)", variable);
+ push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", variable);
return false;
}
@@ -3901,8 +4026,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node
variable->export_info.hint_string = enum_hint_string;
} break;
default:
- // TODO: Allow custom user resources.
- push_error(R"(Export type can only be built-in, a resource, or an enum.)", variable);
+ push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", variable);
break;
}
@@ -4067,6 +4191,17 @@ bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_
return true;
}
+bool GDScriptParser::static_unload_annotation(const AnnotationNode *p_annotation, Node *p_target) {
+ ERR_FAIL_COND_V_MSG(p_target->type != Node::CLASS, false, vformat(R"("%s" annotation can only be applied to classes.)", p_annotation->name));
+ ClassNode *p_class = static_cast<ClassNode *>(p_target);
+ if (p_class->annotated_static_unload) {
+ push_error(vformat(R"("%s" annotation can only be used once per script.)", p_annotation->name), p_annotation);
+ return false;
+ }
+ p_class->annotated_static_unload = true;
+ return true;
+}
+
GDScriptParser::DataType GDScriptParser::SuiteNode::Local::get_datatype() const {
switch (type) {
case CONSTANT:
@@ -4121,16 +4256,13 @@ String GDScriptParser::DataType::to_string() const {
}
return native_type.operator String();
case CLASS:
- if (is_meta_type) {
- return GDScript::get_class_static();
- }
if (class_type->identifier != nullptr) {
return class_type->identifier->name.operator String();
}
return class_type->fqcn;
case SCRIPT: {
if (is_meta_type) {
- return script_type->get_class_name().operator String();
+ return script_type != nullptr ? script_type->get_class_name().operator String() : "";
}
String name = script_type != nullptr ? script_type->get_name() : "";
if (!name.is_empty()) {
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index 29841ab060..8f0265510f 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/modules/gdscript/gdscript_parser.h
@@ -709,6 +709,8 @@ public:
ClassNode *outer = nullptr;
bool extends_used = false;
bool onready_used = false;
+ bool has_static_data = false;
+ bool annotated_static_unload = false;
String extends_path;
Vector<IdentifierNode *> extends; // List for indexing: extends A.B.C
DataType base_type;
@@ -847,6 +849,7 @@ public:
LOCAL_BIND, // Pattern bind.
MEMBER_SIGNAL,
MEMBER_VARIABLE,
+ STATIC_VARIABLE,
MEMBER_CONSTANT,
INHERITED_VARIABLE,
};
@@ -1202,6 +1205,7 @@ public:
bool onready = false;
PropertyInfo export_info;
int assignments = 0;
+ bool is_static = false;
#ifdef TOOLS_ENABLED
String doc_description;
#endif // TOOLS_ENABLED
@@ -1405,16 +1409,16 @@ private:
// Main blocks.
void parse_program();
- ClassNode *parse_class();
+ ClassNode *parse_class(bool p_is_static);
void parse_class_name();
void parse_extends();
void parse_class_body(bool p_is_multiline);
template <class T>
- void parse_class_member(T *(GDScriptParser::*p_parse_function)(), AnnotationInfo::TargetKind p_target, const String &p_member_kind);
- SignalNode *parse_signal();
- EnumNode *parse_enum();
+ void parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static = false);
+ SignalNode *parse_signal(bool p_is_static);
+ EnumNode *parse_enum(bool p_is_static);
ParameterNode *parse_parameter();
- FunctionNode *parse_function();
+ FunctionNode *parse_function(bool p_is_static);
void parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type);
SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false);
// Annotations
@@ -1431,14 +1435,15 @@ private:
bool export_group_annotations(const AnnotationNode *p_annotation, Node *p_target);
bool warning_annotations(const AnnotationNode *p_annotation, Node *p_target);
bool rpc_annotation(const AnnotationNode *p_annotation, Node *p_target);
+ bool static_unload_annotation(const AnnotationNode *p_annotation, Node *p_target);
// Statements.
Node *parse_statement();
- VariableNode *parse_variable();
- VariableNode *parse_variable(bool p_allow_property);
+ VariableNode *parse_variable(bool p_is_static);
+ VariableNode *parse_variable(bool p_is_static, bool p_allow_property);
VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent);
void parse_property_getter(VariableNode *p_variable);
void parse_property_setter(VariableNode *p_variable);
- ConstantNode *parse_constant();
+ ConstantNode *parse_constant(bool p_is_static);
AssertNode *parse_assert();
BreakNode *parse_break();
ContinueNode *parse_continue();
@@ -1480,7 +1485,7 @@ private:
#ifdef TOOLS_ENABLED
// Doc comments.
int class_doc_line = 0x7FFFFFFF;
- bool has_comment(int p_line);
+ bool has_comment(int p_line, bool p_must_be_doc = false);
String get_doc_comment(int p_line, bool p_single_line = false);
void get_class_doc_comment(int p_line, String &p_brief, String &p_desc, Vector<Pair<String, String>> &p_tutorials, bool p_inner_class);
#endif // TOOLS_ENABLED
diff --git a/modules/gdscript/gdscript_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp
index d586380c41..a45a73a8d5 100644
--- a/modules/gdscript/gdscript_tokenizer.cpp
+++ b/modules/gdscript/gdscript_tokenizer.cpp
@@ -1099,7 +1099,7 @@ void GDScriptTokenizer::check_indent() {
_advance();
}
- if (mixed) {
+ if (mixed && !(line_continuation || multiline_mode)) {
Token error = make_error("Mixed use of tabs and spaces for indentation.");
error.start_line = line;
error.start_column = 1;
diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp
index 83d2ed6010..bebf34cbb3 100644
--- a/modules/gdscript/gdscript_vm.cpp
+++ b/modules/gdscript/gdscript_vm.cpp
@@ -680,10 +680,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
bool awaited = false;
#endif
#ifdef DEBUG_ENABLED
- int variant_address_limits[ADDR_TYPE_MAX] = { _stack_size, _constant_count, p_instance ? p_instance->members.size() : 0 };
+ int variant_address_limits[ADDR_TYPE_MAX] = { _stack_size, _constant_count, p_instance ? p_instance->members.size() : 0, script->static_variables.size() };
#endif
- Variant *variant_addresses[ADDR_TYPE_MAX] = { stack, _constants_ptr, p_instance ? p_instance->members.ptrw() : nullptr };
+ Variant *variant_addresses[ADDR_TYPE_MAX] = { stack, _constants_ptr, p_instance ? p_instance->members.ptrw() : nullptr, script->static_variables.ptrw() };
#ifdef DEBUG_ENABLED
OPCODE_WHILE(ip < _code_size) {
@@ -774,7 +774,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
bool result = false;
if (value->get_type() == Variant::ARRAY) {
Array *array = VariantInternal::get_array(value);
- result = array->get_typed_builtin() == ((uint32_t)builtin_type) && array->get_typed_class_name() == native_type && array->get_typed_script() == *script_type && array->get_typed_class_name() == native_type;
+ result = array->get_typed_builtin() == ((uint32_t)builtin_type) && array->get_typed_class_name() == native_type && array->get_typed_script() == *script_type;
}
*dst = result;
@@ -1252,7 +1252,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
Array *array = VariantInternal::get_array(src);
- if (array->get_typed_builtin() != ((uint32_t)builtin_type) || array->get_typed_class_name() != native_type || array->get_typed_script() != *script_type || array->get_typed_class_name() != native_type) {
+ if (array->get_typed_builtin() != ((uint32_t)builtin_type) || array->get_typed_class_name() != native_type || array->get_typed_script() != *script_type) {
#ifdef DEBUG_ENABLED
err_text = vformat(R"(Trying to assign an array of type "%s" to a variable of type "Array[%s]".)",
_get_var_type(src), _get_element_type(builtin_type, native_type, *script_type));
@@ -1651,10 +1651,6 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
bool was_freed = false;
Object *obj = ret->get_validated_object_with_check(was_freed);
- if (was_freed) {
- err_text = "Got a freed object as a result of the call.";
- OPCODE_BREAK;
- }
if (obj && obj->is_class_ptr(GDScriptFunctionState::get_class_ptr_static())) {
err_text = R"(Trying to call an async function without "await".)";
OPCODE_BREAK;
@@ -2583,7 +2579,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
Array *array = VariantInternal::get_array(r);
- if (array->get_typed_builtin() != ((uint32_t)builtin_type) || array->get_typed_class_name() != native_type || array->get_typed_script() != *script_type || array->get_typed_class_name() != native_type) {
+ if (array->get_typed_builtin() != ((uint32_t)builtin_type) || array->get_typed_class_name() != native_type || array->get_typed_script() != *script_type) {
#ifdef DEBUG_ENABLED
err_text = vformat(R"(Trying to return an array of type "%s" where expected return type is "Array[%s]".)",
_get_var_type(r), _get_element_type(builtin_type, native_type, *script_type));
@@ -3572,8 +3568,9 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
//error
// function, file, line, error, explanation
String err_file;
- if (p_instance && ObjectDB::get_instance(p_instance->owner_id) != nullptr && p_instance->script->is_valid() && !p_instance->script->path.is_empty()) {
- err_file = p_instance->script->path;
+ bool instance_valid_with_script = p_instance && ObjectDB::get_instance(p_instance->owner_id) != nullptr && p_instance->script->is_valid();
+ if (instance_valid_with_script && !get_script()->path.is_empty()) {
+ err_file = get_script()->path;
} else if (script) {
err_file = script->path;
}
@@ -3581,7 +3578,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
err_file = "<built-in>";
}
String err_func = name;
- if (p_instance && ObjectDB::get_instance(p_instance->owner_id) != nullptr && p_instance->script->is_valid() && !p_instance->script->name.is_empty()) {
+ if (instance_valid_with_script && !p_instance->script->name.is_empty()) {
err_func = p_instance->script->name + "." + err_func;
}
int err_line = line;
diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp
index ef59a07f1a..8de78d2b9a 100644
--- a/modules/gdscript/gdscript_warning.cpp
+++ b/modules/gdscript/gdscript_warning.cpp
@@ -38,153 +38,115 @@ String GDScriptWarning::get_message() const {
#define CHECK_SYMBOLS(m_amount) ERR_FAIL_COND_V(symbols.size() < m_amount, String());
switch (code) {
- case UNASSIGNED_VARIABLE_OP_ASSIGN: {
+ case UNASSIGNED_VARIABLE:
CHECK_SYMBOLS(1);
- return "Using assignment with operation but the variable '" + symbols[0] + "' was not previously assigned a value.";
- } break;
- case UNASSIGNED_VARIABLE: {
+ return vformat(R"(The variable "%s" was used but never assigned a value.)", symbols[0]);
+ case UNASSIGNED_VARIABLE_OP_ASSIGN:
CHECK_SYMBOLS(1);
- return "The variable '" + symbols[0] + "' was used but never assigned a value.";
- } break;
- case UNUSED_VARIABLE: {
+ return vformat(R"(Using assignment with operation but the variable "%s" was not previously assigned a value.)", symbols[0]);
+ case UNUSED_VARIABLE:
CHECK_SYMBOLS(1);
- return "The local variable '" + symbols[0] + "' is declared but never used in the block. If this is intended, prefix it with an underscore: '_" + symbols[0] + "'";
- } break;
- case UNUSED_LOCAL_CONSTANT: {
+ return vformat(R"(The local variable "%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_LOCAL_CONSTANT:
CHECK_SYMBOLS(1);
- return "The local constant '" + symbols[0] + "' is declared but never used in the block. If this is intended, prefix it with an underscore: '_" + symbols[0] + "'";
- } break;
- case SHADOWED_VARIABLE: {
+ 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]);
+ 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]);
+ 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]);
- } break;
- case SHADOWED_VARIABLE_BASE_CLASS: {
+ case SHADOWED_VARIABLE_BASE_CLASS:
CHECK_SYMBOLS(4);
return vformat(R"(The local %s "%s" is shadowing an already-declared %s at the base class "%s".)", symbols[0], symbols[1], symbols[2], symbols[3]);
- } break;
- case UNUSED_PRIVATE_CLASS_VARIABLE: {
- CHECK_SYMBOLS(1);
- return "The class variable '" + symbols[0] + "' is declared but never used in the script.";
- } break;
- case UNUSED_PARAMETER: {
- CHECK_SYMBOLS(2);
- return "The parameter '" + symbols[1] + "' is never used in the function '" + symbols[0] + "'. If this is intended, prefix it with an underscore: '_" + symbols[1] + "'";
- } break;
- case UNREACHABLE_CODE: {
+ case SHADOWED_GLOBAL_IDENTIFIER:
+ CHECK_SYMBOLS(3);
+ return vformat(R"(The %s "%s" has the same name as a %s.)", symbols[0], symbols[1], symbols[2]);
+ case UNREACHABLE_CODE:
CHECK_SYMBOLS(1);
- return "Unreachable code (statement after return) in function '" + symbols[0] + "()'.";
- } break;
- case UNREACHABLE_PATTERN: {
+ return vformat(R"*(Unreachable code (statement after return) in function "%s()".)*", symbols[0]);
+ case UNREACHABLE_PATTERN:
return "Unreachable pattern (pattern after wildcard or bind).";
- } break;
- case STANDALONE_EXPRESSION: {
+ case STANDALONE_EXPRESSION:
return "Standalone expression (the line has no effect).";
- } break;
- case NARROWING_CONVERSION: {
- return "Narrowing conversion (float is converted to int and loses precision).";
- } break;
- case INCOMPATIBLE_TERNARY: {
+ case STANDALONE_TERNARY:
+ return "Standalone ternary conditional operator: the return value is being discarded.";
+ case INCOMPATIBLE_TERNARY:
return "Values of the ternary conditional are not mutually compatible.";
- } break;
- case UNUSED_SIGNAL: {
- CHECK_SYMBOLS(1);
- return "The signal '" + symbols[0] + "' is declared but never emitted.";
- } break;
- case RETURN_VALUE_DISCARDED: {
- CHECK_SYMBOLS(1);
- return "The function '" + symbols[0] + "()' returns a value that will be discarded if not used.";
- } break;
- case PROPERTY_USED_AS_FUNCTION: {
+ case PROPERTY_USED_AS_FUNCTION:
CHECK_SYMBOLS(2);
- return "The method '" + symbols[0] + "()' was not found in base '" + symbols[1] + "' but there's a property with the same name. Did you mean to access it?";
- } break;
- case CONSTANT_USED_AS_FUNCTION: {
+ 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 "The method '" + symbols[0] + "()' was not found in base '" + symbols[1] + "' but there's a constant with the same name. Did you mean to access it?";
- } break;
- case FUNCTION_USED_AS_PROPERTY: {
+ 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 "The property '" + symbols[0] + "' was not found in base '" + symbols[1] + "' but there's a method with the same name. Did you mean to call it?";
- } break;
- case INTEGER_DIVISION: {
- return "Integer division, decimal part will be discarded.";
- } break;
- case UNSAFE_PROPERTY_ACCESS: {
+ 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]);
+ case UNSAFE_PROPERTY_ACCESS:
CHECK_SYMBOLS(2);
- return "The property '" + symbols[0] + "' is not present on the inferred type '" + symbols[1] + "' (but may be present on a subtype).";
- } break;
- case UNSAFE_METHOD_ACCESS: {
+ return vformat(R"(The property "%s" is not present on the inferred type "%s" (but may be present on a subtype).)", symbols[0], symbols[1]);
+ case UNSAFE_METHOD_ACCESS:
CHECK_SYMBOLS(2);
- return "The method '" + symbols[0] + "' is not present on the inferred type '" + symbols[1] + "' (but may be present on a subtype).";
- } break;
- case UNSAFE_CAST: {
+ return vformat(R"*(The method "%s()" is not present on the inferred type "%s" (but may be present on a subtype).)*", symbols[0], symbols[1]);
+ case UNSAFE_CAST:
CHECK_SYMBOLS(1);
- return "The value is cast to '" + symbols[0] + "' but has an unknown type.";
- } break;
- case UNSAFE_CALL_ARGUMENT: {
+ return vformat(R"(The value is cast to "%s" but has an unknown type.)", symbols[0]);
+ case UNSAFE_CALL_ARGUMENT:
CHECK_SYMBOLS(4);
- return "The argument '" + symbols[0] + "' of the function '" + symbols[1] + "' requires a the subtype '" + symbols[2] + "' but the supertype '" + symbols[3] + "' was provided";
- } break;
- case UNSAFE_VOID_RETURN: {
+ return vformat(R"*(The argument %s of the function "%s()" requires a the subtype "%s" but the supertype "%s" was provided.)*", symbols[0], symbols[1], symbols[2], symbols[3]);
+ case UNSAFE_VOID_RETURN:
CHECK_SYMBOLS(2);
- return "The method '" + symbols[0] + "()' returns 'void' but it's trying to return a call to '" + symbols[1] + "()' that can't be ensured to also be 'void'.";
- } break;
- case DEPRECATED_KEYWORD: {
+ return vformat(R"*(The method "%s()" returns "void" but it's trying to return a call to "%s()" that can't be ensured to also be "void".)*", symbols[0], symbols[1]);
+ case RETURN_VALUE_DISCARDED:
+ CHECK_SYMBOLS(1);
+ return vformat(R"*(The function "%s()" returns a value that will be discarded if not used.)*", symbols[0]);
+ case STATIC_CALLED_ON_INSTANCE:
CHECK_SYMBOLS(2);
- return "The '" + symbols[0] + "' keyword is deprecated and will be removed in a future release, please replace its uses by '" + symbols[1] + "'.";
- } break;
- case STANDALONE_TERNARY: {
- return "Standalone ternary conditional operator: the return value is being discarded.";
- }
- case ASSERT_ALWAYS_TRUE: {
+ return vformat(R"*(The function "%s()" is a static function but was called from an instance. Instead, it should be directly called from the type: "%s.%s()".)*", symbols[0], symbols[1], symbols[0]);
+ case REDUNDANT_STATIC_UNLOAD:
+ return R"(The "@static_unload" annotation is redundant because the file does not have a class with static variables.)";
+ case REDUNDANT_AWAIT:
+ return R"("await" keyword not needed in this case, because the expression isn't a coroutine nor a signal.)";
+ case ASSERT_ALWAYS_TRUE:
return "Assert statement is redundant because the expression is always true.";
- }
- case ASSERT_ALWAYS_FALSE: {
+ case ASSERT_ALWAYS_FALSE:
return "Assert statement will raise an error because the expression is always false.";
- }
- case REDUNDANT_AWAIT: {
- return R"("await" keyword not needed in this case, because the expression isn't a coroutine nor a signal.)";
- }
- case EMPTY_FILE: {
- return "Empty script file.";
- }
- case SHADOWED_GLOBAL_IDENTIFIER: {
- CHECK_SYMBOLS(3);
- return vformat(R"(The %s '%s' has the same name as a %s.)", symbols[0], symbols[1], symbols[2]);
- }
- case INT_AS_ENUM_WITHOUT_CAST: {
+ case INTEGER_DIVISION:
+ return "Integer division, decimal part will be discarded.";
+ case NARROWING_CONVERSION:
+ return "Narrowing conversion (float is converted to int and loses precision).";
+ case INT_AS_ENUM_WITHOUT_CAST:
return "Integer used when an enum value is expected. If this is intended cast the integer to the enum type.";
- }
- case INT_AS_ENUM_WITHOUT_MATCH: {
+ case INT_AS_ENUM_WITHOUT_MATCH:
CHECK_SYMBOLS(3);
return vformat(R"(Cannot %s %s as Enum "%s": no enum member has matching value.)", symbols[0], symbols[1], symbols[2]);
- } break;
- case STATIC_CALLED_ON_INSTANCE: {
+ case EMPTY_FILE:
+ return "Empty script file.";
+ case DEPRECATED_KEYWORD:
CHECK_SYMBOLS(2);
- return vformat(R"(The function '%s()' is a static function but was called from an instance. Instead, it should be directly called from the type: '%s.%s()'.)", symbols[0], symbols[1], symbols[0]);
- }
- case CONFUSABLE_IDENTIFIER: {
+ return vformat(R"(The "%s" keyword is deprecated and will be removed in a future release, please replace its uses by "%s".)", symbols[0], symbols[1]);
+ case RENAMED_IN_GODOT_4_HINT:
+ break; // Renamed identifier hint is taken care of by the GDScriptAnalyzer. No message needed here.
+ case CONFUSABLE_IDENTIFIER:
CHECK_SYMBOLS(1);
return vformat(R"(The identifier "%s" has misleading characters and might be confused with something else.)", symbols[0]);
- }
- case RENAMED_IN_GD4_HINT: {
- break; // Renamed identifier hint is taken care of by the GDScriptAnalyzer. No message needed here.
- }
- case INFERENCE_ON_VARIANT: {
+ case INFERENCE_ON_VARIANT:
CHECK_SYMBOLS(1);
return vformat("The %s type is being inferred from a Variant value, so it will be typed as Variant.", symbols[0]);
- }
- case NATIVE_METHOD_OVERRIDE: {
+ case NATIVE_METHOD_OVERRIDE:
CHECK_SYMBOLS(2);
- return vformat(R"(The method "%s" overrides a method from native class "%s". This won't be called by the engine and may not work as expected.)", symbols[0], symbols[1]);
- }
- case GET_NODE_DEFAULT_WITHOUT_ONREADY: {
+ return vformat(R"*(The method "%s()" overrides a method from native class "%s". This won't be called by the engine and may not work as expected.)*", symbols[0], symbols[1]);
+ case GET_NODE_DEFAULT_WITHOUT_ONREADY:
CHECK_SYMBOLS(1);
return vformat(R"*(The default value is using "%s" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this.)*", symbols[0]);
- }
- case ONREADY_WITH_EXPORT: {
- return R"(The "@onready" annotation will make the default value to be set after the "@export" takes effect and will override it.)";
- }
+ case ONREADY_WITH_EXPORT:
+ return R"("@onready" will set the default value after "@export" takes effect and will override it.)";
case WARNING_MAX:
break; // Can't happen, but silences warning
}
@@ -200,7 +162,7 @@ int GDScriptWarning::get_default_value(Code p_code) {
PropertyInfo GDScriptWarning::get_property_info(Code p_code) {
// Making this a separate function in case a warning needs different PropertyInfo in the future.
- if (p_code == Code::RENAMED_IN_GD4_HINT) {
+ if (p_code == Code::RENAMED_IN_GODOT_4_HINT) {
return PropertyInfo(Variant::BOOL, get_settings_path_from_code(p_code));
}
return PropertyInfo(Variant::INT, get_settings_path_from_code(p_code), PROPERTY_HINT_ENUM, "Ignore,Warn,Error");
@@ -218,38 +180,39 @@ String GDScriptWarning::get_name_from_code(Code p_code) {
"UNASSIGNED_VARIABLE_OP_ASSIGN",
"UNUSED_VARIABLE",
"UNUSED_LOCAL_CONSTANT",
- "SHADOWED_VARIABLE",
- "SHADOWED_VARIABLE_BASE_CLASS",
"UNUSED_PRIVATE_CLASS_VARIABLE",
"UNUSED_PARAMETER",
+ "UNUSED_SIGNAL",
+ "SHADOWED_VARIABLE",
+ "SHADOWED_VARIABLE_BASE_CLASS",
+ "SHADOWED_GLOBAL_IDENTIFIER",
"UNREACHABLE_CODE",
"UNREACHABLE_PATTERN",
"STANDALONE_EXPRESSION",
- "NARROWING_CONVERSION",
+ "STANDALONE_TERNARY",
"INCOMPATIBLE_TERNARY",
- "UNUSED_SIGNAL",
- "RETURN_VALUE_DISCARDED",
"PROPERTY_USED_AS_FUNCTION",
"CONSTANT_USED_AS_FUNCTION",
"FUNCTION_USED_AS_PROPERTY",
- "INTEGER_DIVISION",
"UNSAFE_PROPERTY_ACCESS",
"UNSAFE_METHOD_ACCESS",
"UNSAFE_CAST",
"UNSAFE_CALL_ARGUMENT",
"UNSAFE_VOID_RETURN",
- "DEPRECATED_KEYWORD",
- "STANDALONE_TERNARY",
+ "RETURN_VALUE_DISCARDED",
+ "STATIC_CALLED_ON_INSTANCE",
+ "REDUNDANT_STATIC_UNLOAD",
+ "REDUNDANT_AWAIT",
"ASSERT_ALWAYS_TRUE",
"ASSERT_ALWAYS_FALSE",
- "REDUNDANT_AWAIT",
- "EMPTY_FILE",
- "SHADOWED_GLOBAL_IDENTIFIER",
+ "INTEGER_DIVISION",
+ "NARROWING_CONVERSION",
"INT_AS_ENUM_WITHOUT_CAST",
"INT_AS_ENUM_WITHOUT_MATCH",
- "STATIC_CALLED_ON_INSTANCE",
- "CONFUSABLE_IDENTIFIER",
+ "EMPTY_FILE",
+ "DEPRECATED_KEYWORD",
"RENAMED_IN_GODOT_4_HINT",
+ "CONFUSABLE_IDENTIFIER",
"INFERENCE_ON_VARIANT",
"NATIVE_METHOD_OVERRIDE",
"GET_NODE_DEFAULT_WITHOUT_ONREADY",
diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h
index f0123c518c..ae6207fcdc 100644
--- a/modules/gdscript/gdscript_warning.h
+++ b/modules/gdscript/gdscript_warning.h
@@ -50,38 +50,39 @@ 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.
- 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.
UNUSED_PRIVATE_CLASS_VARIABLE, // Class variable is declared private ("_" prefix) but never used in the file.
UNUSED_PARAMETER, // Function parameter is never used.
+ UNUSED_SIGNAL, // Signal is defined but never emitted.
+ 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.
UNREACHABLE_CODE, // Code after a return statement.
UNREACHABLE_PATTERN, // Pattern in a match statement after a catch all pattern (wildcard or bind).
STANDALONE_EXPRESSION, // Expression not assigned to a variable.
- NARROWING_CONVERSION, // Float value into an integer slot, precision is lost.
+ STANDALONE_TERNARY, // Return value of ternary expression is discarded.
INCOMPATIBLE_TERNARY, // Possible values of a ternary if are not mutually compatible.
- UNUSED_SIGNAL, // Signal is defined but never emitted.
- RETURN_VALUE_DISCARDED, // Function call returns something but the value isn't used.
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.
- INTEGER_DIVISION, // Integer divide by integer, decimal part is discarded.
UNSAFE_PROPERTY_ACCESS, // Property not found in the detected type (but can be in subtypes).
UNSAFE_METHOD_ACCESS, // Function not found in the detected type (but can be in subtypes).
UNSAFE_CAST, // Cast used in an unknown type.
- UNSAFE_CALL_ARGUMENT, // Function call argument is of a supertype of the require argument.
+ UNSAFE_CALL_ARGUMENT, // Function call argument is of a supertype of the required type.
UNSAFE_VOID_RETURN, // Function returns void but returned a call to a function that can't be type checked.
- DEPRECATED_KEYWORD, // The keyword is deprecated and should be replaced.
- STANDALONE_TERNARY, // Return value of ternary expression is discarded.
+ RETURN_VALUE_DISCARDED, // Function call returns something but the value isn't used.
+ STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself.
+ REDUNDANT_STATIC_UNLOAD, // The `@static_unload` annotation is used but the class does not have static data.
+ REDUNDANT_AWAIT, // await is used but expression is synchronous (not a signal nor a coroutine).
ASSERT_ALWAYS_TRUE, // Expression for assert argument is always true.
ASSERT_ALWAYS_FALSE, // Expression for assert argument is always false.
- REDUNDANT_AWAIT, // await is used but expression is synchronous (not a signal nor a coroutine).
- EMPTY_FILE, // A script file is empty.
- SHADOWED_GLOBAL_IDENTIFIER, // A global class or function has the same name as variable.
+ INTEGER_DIVISION, // Integer divide by integer, decimal part is discarded.
+ NARROWING_CONVERSION, // Float value into an integer slot, precision is lost.
INT_AS_ENUM_WITHOUT_CAST, // An integer value was used as an enum value without casting.
INT_AS_ENUM_WITHOUT_MATCH, // An integer value was used as an enum value without matching enum member.
- STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself.
+ EMPTY_FILE, // A script file is empty.
+ DEPRECATED_KEYWORD, // The keyword is deprecated and should be replaced.
+ RENAMED_IN_GODOT_4_HINT, // A variable or function that could not be found has been renamed in Godot 4.
CONFUSABLE_IDENTIFIER, // The identifier contains misleading characters that can be confused. E.g. "usеr" (has Cyrillic "е" instead of Latin "e").
- RENAMED_IN_GD4_HINT, // A variable or function that could not be found has been renamed in Godot 4
INFERENCE_ON_VARIANT, // The declaration uses type inference but the value is typed as Variant.
NATIVE_METHOD_OVERRIDE, // The script method overrides a native one, this may not work as intended.
GET_NODE_DEFAULT_WITHOUT_ONREADY, // A class variable uses `get_node()` (or the `$` notation) as its default value, but does not use the @onready annotation.
@@ -94,38 +95,39 @@ public:
WARN, // UNASSIGNED_VARIABLE_OP_ASSIGN
WARN, // UNUSED_VARIABLE
WARN, // UNUSED_LOCAL_CONSTANT
- WARN, // SHADOWED_VARIABLE
- WARN, // SHADOWED_VARIABLE_BASE_CLASS
WARN, // UNUSED_PRIVATE_CLASS_VARIABLE
WARN, // UNUSED_PARAMETER
+ WARN, // UNUSED_SIGNAL
+ WARN, // SHADOWED_VARIABLE
+ WARN, // SHADOWED_VARIABLE_BASE_CLASS
+ WARN, // SHADOWED_GLOBAL_IDENTIFIER
WARN, // UNREACHABLE_CODE
WARN, // UNREACHABLE_PATTERN
WARN, // STANDALONE_EXPRESSION
- WARN, // NARROWING_CONVERSION
+ WARN, // STANDALONE_TERNARY
WARN, // INCOMPATIBLE_TERNARY
- WARN, // UNUSED_SIGNAL
- IGNORE, // RETURN_VALUE_DISCARDED // Too spammy by default on common cases (connect, Tween, etc.).
WARN, // PROPERTY_USED_AS_FUNCTION
WARN, // CONSTANT_USED_AS_FUNCTION
WARN, // FUNCTION_USED_AS_PROPERTY
- WARN, // INTEGER_DIVISION
IGNORE, // UNSAFE_PROPERTY_ACCESS // Too common in untyped scenarios.
IGNORE, // UNSAFE_METHOD_ACCESS // Too common in untyped scenarios.
IGNORE, // UNSAFE_CAST // Too common in untyped scenarios.
IGNORE, // UNSAFE_CALL_ARGUMENT // Too common in untyped scenarios.
WARN, // UNSAFE_VOID_RETURN
- WARN, // DEPRECATED_KEYWORD
- WARN, // STANDALONE_TERNARY
+ IGNORE, // RETURN_VALUE_DISCARDED // Too spammy by default on common cases (connect, Tween, etc.).
+ WARN, // STATIC_CALLED_ON_INSTANCE
+ WARN, // REDUNDANT_STATIC_UNLOAD
+ WARN, // REDUNDANT_AWAIT
WARN, // ASSERT_ALWAYS_TRUE
WARN, // ASSERT_ALWAYS_FALSE
- WARN, // REDUNDANT_AWAIT
- WARN, // EMPTY_FILE
- WARN, // SHADOWED_GLOBAL_IDENTIFIER
+ WARN, // INTEGER_DIVISION
+ WARN, // NARROWING_CONVERSION
WARN, // INT_AS_ENUM_WITHOUT_CAST
WARN, // INT_AS_ENUM_WITHOUT_MATCH
- WARN, // STATIC_CALLED_ON_INSTANCE
+ WARN, // EMPTY_FILE
+ WARN, // DEPRECATED_KEYWORD
+ WARN, // RENAMED_IN_GODOT_4_HINT
WARN, // CONFUSABLE_IDENTIFIER
- WARN, // RENAMED_IN_GD4_HINT
ERROR, // INFERENCE_ON_VARIANT // Most likely done by accident, usually inference is trying for a particular type.
ERROR, // NATIVE_METHOD_OVERRIDE // May not work as expected.
ERROR, // GET_NODE_DEFAULT_WITHOUT_ONREADY // May not work as expected.
diff --git a/modules/gdscript/language_server/gdscript_extend_parser.cpp b/modules/gdscript/language_server/gdscript_extend_parser.cpp
index 2ed444c7ad..3a5a54e275 100644
--- a/modules/gdscript/language_server/gdscript_extend_parser.cpp
+++ b/modules/gdscript/language_server/gdscript_extend_parser.cpp
@@ -337,7 +337,7 @@ void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionN
symbol.kind = lsp::SymbolKind::Variable;
symbol.name = parameter->identifier->name;
symbol.range.start.line = LINE_NUMBER_TO_INDEX(parameter->start_line);
- symbol.range.start.character = LINE_NUMBER_TO_INDEX(parameter->start_line);
+ symbol.range.start.character = LINE_NUMBER_TO_INDEX(parameter->start_column);
symbol.range.end.line = LINE_NUMBER_TO_INDEX(parameter->end_line);
symbol.range.end.character = LINE_NUMBER_TO_INDEX(parameter->end_column);
symbol.uri = uri;
@@ -400,6 +400,20 @@ void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionN
}
} break;
+ case GDScriptParser::TypeNode::VARIABLE: {
+ GDScriptParser::VariableNode *variable_node = (GDScriptParser::VariableNode *)(node);
+ lsp::DocumentSymbol symbol;
+ symbol.kind = lsp::SymbolKind::Variable;
+ symbol.name = variable_node->identifier->name;
+ symbol.range.start.line = LINE_NUMBER_TO_INDEX(variable_node->start_line);
+ symbol.range.start.character = LINE_NUMBER_TO_INDEX(variable_node->start_column);
+ symbol.range.end.line = LINE_NUMBER_TO_INDEX(variable_node->end_line);
+ symbol.range.end.character = LINE_NUMBER_TO_INDEX(variable_node->end_column);
+ symbol.uri = uri;
+ symbol.script_path = path;
+ r_symbol.children.push_back(symbol);
+ } break;
+
default:
continue;
}
diff --git a/modules/gdscript/language_server/gdscript_language_protocol.cpp b/modules/gdscript/language_server/gdscript_language_protocol.cpp
index acd75f039a..112db4df3a 100644
--- a/modules/gdscript/language_server/gdscript_language_protocol.cpp
+++ b/modules/gdscript/language_server/gdscript_language_protocol.cpp
@@ -46,7 +46,7 @@ Error GDScriptLanguageProtocol::LSPeer::handle_data() {
while (true) {
if (req_pos >= LSP_MAX_BUFFER_SIZE) {
req_pos = 0;
- ERR_FAIL_COND_V_MSG(true, ERR_OUT_OF_MEMORY, "Response header too big");
+ ERR_FAIL_V_MSG(ERR_OUT_OF_MEMORY, "Response header too big");
}
Error err = connection->get_partial_data(&req_buf[req_pos], 1, read);
if (err != OK) {
@@ -237,6 +237,7 @@ void GDScriptLanguageProtocol::poll() {
HashMap<int, Ref<LSPeer>>::Iterator E = clients.begin();
while (E != clients.end()) {
Ref<LSPeer> peer = E->value;
+ peer->connection->poll();
StreamPeerTCP::Status status = peer->connection->get_status();
if (status == StreamPeerTCP::STATUS_NONE || status == StreamPeerTCP::STATUS_ERROR) {
on_client_disconnected(E->key);
diff --git a/modules/gdscript/language_server/gdscript_workspace.cpp b/modules/gdscript/language_server/gdscript_workspace.cpp
index b90c452346..7fc2962341 100644
--- a/modules/gdscript/language_server/gdscript_workspace.cpp
+++ b/modules/gdscript/language_server/gdscript_workspace.cpp
@@ -185,15 +185,27 @@ const lsp::DocumentSymbol *GDScriptWorkspace::get_local_symbol(const ExtendGDScr
const lsp::DocumentSymbol *class_symbol = &p_parser->get_symbols();
for (int i = 0; i < class_symbol->children.size(); ++i) {
- if (class_symbol->children[i].kind == lsp::SymbolKind::Function || class_symbol->children[i].kind == lsp::SymbolKind::Class) {
- const lsp::DocumentSymbol *function_symbol = &class_symbol->children[i];
+ int kind = class_symbol->children[i].kind;
+ switch (kind) {
+ case lsp::SymbolKind::Function:
+ case lsp::SymbolKind::Method:
+ case lsp::SymbolKind::Class: {
+ const lsp::DocumentSymbol *function_symbol = &class_symbol->children[i];
+
+ for (int l = 0; l < function_symbol->children.size(); ++l) {
+ const lsp::DocumentSymbol *local = &function_symbol->children[l];
+ if (!local->detail.is_empty() && local->name == p_symbol_identifier) {
+ return local;
+ }
+ }
+ } break;
- for (int l = 0; l < function_symbol->children.size(); ++l) {
- const lsp::DocumentSymbol *local = &function_symbol->children[l];
- if (!local->detail.is_empty() && local->name == p_symbol_identifier) {
- return local;
+ case lsp::SymbolKind::Variable: {
+ const lsp::DocumentSymbol *variable_symbol = &class_symbol->children[i];
+ if (variable_symbol->name == p_symbol_identifier) {
+ return variable_symbol;
}
- }
+ } break;
}
}
@@ -650,8 +662,18 @@ const lsp::DocumentSymbol *GDScriptWorkspace::resolve_symbol(const lsp::TextDocu
if (const ExtendGDScriptParser *target_parser = get_parse_result(target_script_path)) {
symbol = target_parser->get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(ret.location));
- if (symbol && symbol->kind == lsp::SymbolKind::Function && symbol->name != symbol_identifier) {
- symbol = get_parameter_symbol(symbol, symbol_identifier);
+ if (symbol) {
+ switch (symbol->kind) {
+ case lsp::SymbolKind::Function: {
+ if (symbol->name != symbol_identifier) {
+ symbol = get_parameter_symbol(symbol, symbol_identifier);
+ }
+ } break;
+
+ case lsp::SymbolKind::Variable: {
+ symbol = get_local_symbol(parser, symbol_identifier);
+ } break;
+ }
}
}
diff --git a/modules/gdscript/language_server/godot_lsp.h b/modules/gdscript/language_server/godot_lsp.h
index 8a033204da..b9a54cf818 100644
--- a/modules/gdscript/language_server/godot_lsp.h
+++ b/modules/gdscript/language_server/godot_lsp.h
@@ -293,16 +293,6 @@ struct WorkspaceEdit {
}
_FORCE_INLINE_ void add_change(const String &uri, const int &line, const int &start_character, const int &end_character, const String &new_text) {
- if (HashMap<String, Vector<TextEdit>>::Iterator E = changes.find(uri)) {
- Vector<TextEdit> edit_list = E->value;
- for (int i = 0; i < edit_list.size(); ++i) {
- TextEdit edit = edit_list[i];
- if (edit.range.start.character == start_character) {
- return;
- }
- }
- }
-
TextEdit new_edit;
new_edit.newText = new_text;
new_edit.range.start.line = line;
@@ -1015,7 +1005,9 @@ struct CompletionItem {
if (commitCharacters.size()) {
dict["commitCharacters"] = commitCharacters;
}
- dict["command"] = command.to_json();
+ if (!command.command.is_empty()) {
+ dict["command"] = command.to_json();
+ }
}
return dict;
}
diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp
index 57405aa1ce..b8448d16c2 100644
--- a/modules/gdscript/tests/gdscript_test_runner.cpp
+++ b/modules/gdscript/tests/gdscript_test_runner.cpp
@@ -350,13 +350,13 @@ void GDScriptTestRunner::handle_cmdline() {
for (List<String>::Element *E = cmdline_args.front(); E; E = E->next()) {
String &cmd = E->get();
if (cmd == "--gdscript-generate-tests") {
- if (E->next() == nullptr) {
- ERR_PRINT("Needed a path for the test files.");
- exit(-1);
+ String path;
+ if (E->next()) {
+ path = E->next()->get();
+ } else {
+ path = "modules/gdscript/tests/scripts";
}
- const String &path = E->next()->get();
-
GDScriptTestRunner runner(path, false, cmdline_args.find("--print-filenames") != nullptr);
bool completed = runner.generate_outputs();
@@ -566,6 +566,14 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) {
ERR_FAIL_V_MSG(result, "\nCould not find test function on: '" + source_file + "'");
}
+ // Setup output handlers.
+ ErrorHandlerData error_data(&result, this);
+
+ _print_handler.userdata = &result;
+ _error_handler.userdata = &error_data;
+ add_print_handler(&_print_handler);
+ add_error_handler(&_error_handler);
+
script->reload();
// Create object instance for test.
@@ -577,14 +585,6 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) {
obj->set_script(script);
GDScriptInstance *instance = static_cast<GDScriptInstance *>(obj->get_script_instance());
- // Setup output handlers.
- ErrorHandlerData error_data(&result, this);
-
- _print_handler.userdata = &result;
- _error_handler.userdata = &error_data;
- add_print_handler(&_print_handler);
- add_error_handler(&_error_handler);
-
// Call test function.
Callable::CallError call_err;
instance->callp(GDScriptTestRunner::test_function_name, nullptr, 0, call_err);
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_1.gd b/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_1.gd
new file mode 100644
index 0000000000..72af099158
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_1.gd
@@ -0,0 +1,9 @@
+# GH-75870
+
+const A = 1
+
+class B extends A:
+ pass
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_1.out b/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_1.out
new file mode 100644
index 0000000000..65d629a35b
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_1.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Constant "A" is not a preloaded script or class.
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_2.gd b/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_2.gd
new file mode 100644
index 0000000000..fe334f8cb7
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_2.gd
@@ -0,0 +1,12 @@
+# GH-75870
+
+class A:
+ const X = 1
+
+const Y = A.X # A.X is now resolved.
+
+class B extends A.X:
+ pass
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_2.out b/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_2.out
new file mode 100644
index 0000000000..951cfb1ea4
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/extend_non_class_constant_2.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Identifier "X" is not a preloaded script or class.
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/extend_variable.gd b/modules/gdscript/tests/scripts/analyzer/errors/extend_variable.gd
new file mode 100644
index 0000000000..6574d4cf31
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/extend_variable.gd
@@ -0,0 +1,9 @@
+# GH-75870
+
+var A = 1
+
+class B extends A:
+ pass
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/extend_variable.out b/modules/gdscript/tests/scripts/analyzer/errors/extend_variable.out
new file mode 100644
index 0000000000..7b39af6979
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/extend_variable.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Cannot use variable "A" in extends chain.
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/prints_base_type_not_found.gd b/modules/gdscript/tests/scripts/analyzer/errors/prints_base_type_not_found.gd
new file mode 100644
index 0000000000..e56ae7b11d
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/prints_base_type_not_found.gd
@@ -0,0 +1,6 @@
+class InnerClass:
+ pass
+
+func test():
+ var x : InnerClass.DoesNotExist
+ print("FAIL")
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/prints_base_type_not_found.out b/modules/gdscript/tests/scripts/analyzer/errors/prints_base_type_not_found.out
new file mode 100644
index 0000000000..29c75ae3c0
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/prints_base_type_not_found.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Could not find type "DoesNotExist" under base "InnerClass".
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.gd
new file mode 100644
index 0000000000..3dac751ba9
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.gd
@@ -0,0 +1,5 @@
+static func _static_init() -> int:
+ print("static init")
+
+func test():
+ print("done")
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.out b/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.out
new file mode 100644
index 0000000000..42294b7afb
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Static constructor cannot have an explicit return type.
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.gd
new file mode 100644
index 0000000000..1ae814ea34
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.gd
@@ -0,0 +1,9 @@
+@static_unload
+
+func non_static():
+ return "non static"
+
+static var static_var = non_static()
+
+func test():
+ print("does not run")
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out
new file mode 100644
index 0000000000..f1e9ec34f2
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Cannot call non-static function "non_static()" for static variable initializer.
diff --git a/modules/gdscript/tests/scripts/analyzer/features/allow_void_function_to_return_void.out b/modules/gdscript/tests/scripts/analyzer/features/allow_void_function_to_return_void.out
index 7c0416371f..f49d93620f 100644
--- a/modules/gdscript/tests/scripts/analyzer/features/allow_void_function_to_return_void.out
+++ b/modules/gdscript/tests/scripts/analyzer/features/allow_void_function_to_return_void.out
@@ -2,7 +2,7 @@ GDTEST_OK
>> WARNING
>> Line: 20
>> UNSAFE_VOID_RETURN
->> The method 'return_side_effect()' returns 'void' but it's trying to return a call to 'side_effect()' that can't be ensured to also be 'void'.
+>> The method "return_side_effect()" returns "void" but it's trying to return a call to "side_effect()" that can't be ensured to also be "void".
hello
effect
effect
diff --git a/modules/gdscript/tests/scripts/analyzer/features/assert_literal_false.gd b/modules/gdscript/tests/scripts/analyzer/features/assert_literal_false.gd
new file mode 100644
index 0000000000..d6c3cfc50e
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/assert_literal_false.gd
@@ -0,0 +1,6 @@
+func test():
+ var never: Variant = false
+ if never:
+ assert(false)
+ assert(false, 'message')
+ print('ok')
diff --git a/modules/gdscript/tests/scripts/analyzer/features/assert_literal_false.out b/modules/gdscript/tests/scripts/analyzer/features/assert_literal_false.out
new file mode 100644
index 0000000000..1b47ed10dc
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/assert_literal_false.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+ok
diff --git a/modules/gdscript/tests/scripts/analyzer/features/auto_inferred_type_dont_error.out b/modules/gdscript/tests/scripts/analyzer/features/auto_inferred_type_dont_error.out
index 481016138a..22cd5d3a17 100644
--- a/modules/gdscript/tests/scripts/analyzer/features/auto_inferred_type_dont_error.out
+++ b/modules/gdscript/tests/scripts/analyzer/features/auto_inferred_type_dont_error.out
@@ -2,5 +2,5 @@ GDTEST_OK
>> WARNING
>> Line: 6
>> UNSAFE_METHOD_ACCESS
->> The method 'free' is not present on the inferred type 'Variant' (but may be present on a subtype).
+>> The method "free()" is not present on the inferred type "Variant" (but may be present on a subtype).
Ok
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/lambda_unused_arg.out b/modules/gdscript/tests/scripts/analyzer/warnings/lambda_unused_arg.out
index 32e230fc80..fe6083c329 100644
--- a/modules/gdscript/tests/scripts/analyzer/warnings/lambda_unused_arg.out
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/lambda_unused_arg.out
@@ -2,4 +2,4 @@ GDTEST_OK
>> WARNING
>> Line: 2
>> UNUSED_PARAMETER
->> The parameter 'unused' is never used in the function ''. If this is intended, prefix it with an underscore: '_unused'
+>> The parameter "unused" is never used in the function "<anonymous lambda>()". If this is intended, prefix it with an underscore: "_unused".
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out
index ff184f9f04..f861d52f2b 100644
--- a/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/onready_with_export.out
@@ -2,5 +2,5 @@ GDTEST_OK
>> WARNING
>> Line: 3
>> ONREADY_WITH_EXPORT
->> The "@onready" annotation will make the default value to be set after the "@export" takes effect and will override it.
+>> "@onready" will set the default value after "@export" takes effect and will override it.
warn
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out
index 793faa05d4..c76950179d 100644
--- a/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/overriding_native_method.out
@@ -2,5 +2,5 @@ GDTEST_OK
>> WARNING
>> Line: 4
>> NATIVE_METHOD_OVERRIDE
->> The method "get" overrides a method from native class "Object". This won't be called by the engine and may not work as expected.
+>> The method "get()" overrides a method from native class "Object". This won't be called by the engine and may not work as expected.
warn
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/shadowning.out b/modules/gdscript/tests/scripts/analyzer/warnings/shadowning.out
index 9d0e567534..8467697a96 100644
--- a/modules/gdscript/tests/scripts/analyzer/warnings/shadowning.out
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/shadowning.out
@@ -2,19 +2,19 @@ GDTEST_OK
>> WARNING
>> Line: 5
>> SHADOWED_GLOBAL_IDENTIFIER
->> The variable 'Array' has the same name as a built-in type.
+>> The variable "Array" has the same name as a built-in type.
>> WARNING
>> Line: 6
>> SHADOWED_GLOBAL_IDENTIFIER
->> The variable 'Node' has the same name as a global class.
+>> The variable "Node" has the same name as a global class.
>> WARNING
>> Line: 7
>> SHADOWED_GLOBAL_IDENTIFIER
->> The variable 'is_same' has the same name as a built-in function.
+>> The variable "is_same" has the same name as a built-in function.
>> WARNING
>> Line: 8
>> SHADOWED_GLOBAL_IDENTIFIER
->> The variable 'sqrt' has the same name as a built-in function.
+>> The variable "sqrt" has the same name as a built-in function.
>> WARNING
>> Line: 9
>> SHADOWED_VARIABLE
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.gd b/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.gd
new file mode 100644
index 0000000000..5ca8ceffdd
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.gd
@@ -0,0 +1,10 @@
+# GH-72135
+
+var _a
+@warning_ignore("unused_private_class_variable")
+var _b
+@warning_ignore("unused_private_class_variable") var _c
+var _d
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.out b/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.out
new file mode 100644
index 0000000000..fd88d23950
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/unused_private_class_variable.out
@@ -0,0 +1,9 @@
+GDTEST_OK
+>> WARNING
+>> Line: 3
+>> UNUSED_PRIVATE_CLASS_VARIABLE
+>> The class variable "_a" is declared but never used in the script.
+>> WARNING
+>> Line: 7
+>> UNUSED_PRIVATE_CLASS_VARIABLE
+>> The class variable "_d" is declared but never used in the script.
diff --git a/modules/gdscript/tests/scripts/parser/errors/annotation_extra_comma.gd b/modules/gdscript/tests/scripts/parser/errors/annotation_extra_comma.gd
new file mode 100644
index 0000000000..271a831732
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/annotation_extra_comma.gd
@@ -0,0 +1,4 @@
+@export_enum("A",, "B", "C") var a
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/annotation_extra_comma.out b/modules/gdscript/tests/scripts/parser/errors/annotation_extra_comma.out
new file mode 100644
index 0000000000..70eee5b39f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/annotation_extra_comma.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Expected expression as the annotation argument.
diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_icon.gd b/modules/gdscript/tests/scripts/parser/errors/duplicate_icon.gd
new file mode 100644
index 0000000000..7500e406f6
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/duplicate_icon.gd
@@ -0,0 +1,5 @@
+@icon("res://1.png")
+@icon("res://1.png")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_icon.out b/modules/gdscript/tests/scripts/parser/errors/duplicate_icon.out
new file mode 100644
index 0000000000..d6cbc95d10
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/duplicate_icon.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+"@icon" annotation can only be used once.
diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.gd b/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.gd
new file mode 100644
index 0000000000..3a2f7118f9
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.gd
@@ -0,0 +1,5 @@
+@tool
+@tool
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.out b/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.out
new file mode 100644
index 0000000000..26fe23fb78
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/duplicate_tool.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+"@tool" annotation can only be used once.
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.gd b/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.gd
new file mode 100644
index 0000000000..cbfa1f314f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.gd
@@ -0,0 +1,5 @@
+func _static_init():
+ print("static init")
+
+func test():
+ print("done")
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.out b/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.out
new file mode 100644
index 0000000000..b2b8711e96
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Static constructor must be declared static.
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.gd b/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.gd
new file mode 100644
index 0000000000..711243f822
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.gd
@@ -0,0 +1,6 @@
+static func _static_init():
+ print("static init")
+ return true
+
+func test():
+ print("done")
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.out b/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.out
new file mode 100644
index 0000000000..a034850e86
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Constructor cannot return a value.
diff --git a/modules/gdscript/tests/scripts/parser/features/annotations.gd b/modules/gdscript/tests/scripts/parser/features/annotations.gd
new file mode 100644
index 0000000000..13c89a0a09
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/annotations.gd
@@ -0,0 +1,48 @@
+extends Node
+
+@export_enum("A", "B", "C") var a0
+@export_enum("A", "B", "C",) var a1
+
+@export_enum(
+ "A",
+ "B",
+ "C"
+) var a2
+
+@export_enum(
+ "A",
+ "B",
+ "C",
+) var a3
+
+@export
+var a4: int
+
+@export()
+var a5: int
+
+@export() var a6: int
+@warning_ignore("onready_with_export") @onready @export var a7: int
+@warning_ignore("onready_with_export") @onready() @export() var a8: int
+
+@warning_ignore("onready_with_export")
+@onready
+@export
+var a9: int
+
+@warning_ignore("onready_with_export")
+@onready()
+@export()
+var a10: int
+
+@warning_ignore("onready_with_export")
+@onready()
+@export()
+
+var a11: int
+
+
+func test():
+ for property in get_property_list():
+ if property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
+ print(property)
diff --git a/modules/gdscript/tests/scripts/parser/features/annotations.out b/modules/gdscript/tests/scripts/parser/features/annotations.out
new file mode 100644
index 0000000000..3af0436c53
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/annotations.out
@@ -0,0 +1,13 @@
+GDTEST_OK
+{ "name": "a0", "class_name": &"", "type": 2, "hint": 2, "hint_string": "A,B,C", "usage": 4102 }
+{ "name": "a1", "class_name": &"", "type": 2, "hint": 2, "hint_string": "A,B,C", "usage": 4102 }
+{ "name": "a2", "class_name": &"", "type": 2, "hint": 2, "hint_string": "A,B,C", "usage": 4102 }
+{ "name": "a3", "class_name": &"", "type": 2, "hint": 2, "hint_string": "A,B,C", "usage": 4102 }
+{ "name": "a4", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 }
+{ "name": "a5", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 }
+{ "name": "a6", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 }
+{ "name": "a7", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 }
+{ "name": "a8", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 }
+{ "name": "a9", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 }
+{ "name": "a10", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 }
+{ "name": "a11", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 }
diff --git a/modules/gdscript/tests/scripts/parser/features/constants.out b/modules/gdscript/tests/scripts/parser/features/constants.out
index 6093e4a6ca..7ec33470d3 100644
--- a/modules/gdscript/tests/scripts/parser/features/constants.out
+++ b/modules/gdscript/tests/scripts/parser/features/constants.out
@@ -2,32 +2,32 @@ GDTEST_OK
>> WARNING
>> Line: 2
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_TEST' is declared but never used in the block. If this is intended, prefix it with an underscore: '__TEST'
+>> The local constant "_TEST" is declared but never used in the block. If this is intended, prefix it with an underscore: "__TEST".
>> WARNING
>> Line: 3
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_STRING' is declared but never used in the block. If this is intended, prefix it with an underscore: '__STRING'
+>> The local constant "_STRING" is declared but never used in the block. If this is intended, prefix it with an underscore: "__STRING".
>> WARNING
>> Line: 4
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_VECTOR' is declared but never used in the block. If this is intended, prefix it with an underscore: '__VECTOR'
+>> The local constant "_VECTOR" is declared but never used in the block. If this is intended, prefix it with an underscore: "__VECTOR".
>> WARNING
>> Line: 5
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_ARRAY' is declared but never used in the block. If this is intended, prefix it with an underscore: '__ARRAY'
+>> The local constant "_ARRAY" is declared but never used in the block. If this is intended, prefix it with an underscore: "__ARRAY".
>> WARNING
>> Line: 6
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_DICTIONARY' is declared but never used in the block. If this is intended, prefix it with an underscore: '__DICTIONARY'
+>> The local constant "_DICTIONARY" is declared but never used in the block. If this is intended, prefix it with an underscore: "__DICTIONARY".
>> WARNING
>> Line: 9
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_HELLO' is declared but never used in the block. If this is intended, prefix it with an underscore: '__HELLO'
+>> The local constant "_HELLO" is declared but never used in the block. If this is intended, prefix it with an underscore: "__HELLO".
>> WARNING
>> Line: 10
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_INFINITY' is declared but never used in the block. If this is intended, prefix it with an underscore: '__INFINITY'
+>> The local constant "_INFINITY" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INFINITY".
>> WARNING
>> Line: 11
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_NOT_A_NUMBER' is declared but never used in the block. If this is intended, prefix it with an underscore: '__NOT_A_NUMBER'
+>> The local constant "_NOT_A_NUMBER" is declared but never used in the block. If this is intended, prefix it with an underscore: "__NOT_A_NUMBER".
diff --git a/modules/gdscript/tests/scripts/parser/features/match_bind_unused.out b/modules/gdscript/tests/scripts/parser/features/match_bind_unused.out
index 057c1b11e5..44d29cb82d 100644
--- a/modules/gdscript/tests/scripts/parser/features/match_bind_unused.out
+++ b/modules/gdscript/tests/scripts/parser/features/match_bind_unused.out
@@ -2,5 +2,5 @@ GDTEST_OK
>> WARNING
>> Line: 9
>> UNUSED_VARIABLE
->> The local variable 'value' is declared but never used in the block. If this is intended, prefix it with an underscore: '_value'
+>> The local variable "value" is declared but never used in the block. If this is intended, prefix it with an underscore: "_value".
value
diff --git a/modules/gdscript/tests/scripts/parser/features/reserved_keywords_as_attribute.gd b/modules/gdscript/tests/scripts/parser/features/reserved_keywords_as_attribute.gd
new file mode 100644
index 0000000000..87f9479812
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/reserved_keywords_as_attribute.gd
@@ -0,0 +1,36 @@
+var dict = {}
+
+func test():
+ dict.if = 1
+ dict.elif = 1
+ dict.else = 1
+ dict.for = 1
+ dict.while = 1
+ dict.match = 1
+ dict.break = 1
+ dict.continue = 1
+ dict.pass = 1
+ dict.return = 1
+ dict.class = 1
+ dict.class_name = 1
+ dict.extends = 1
+ dict.is = 1
+ dict.in = 1
+ dict.as = 1
+ dict.self = 1
+ dict.signal = 1
+ dict.func = 1
+ dict.static = 1
+ dict.const = 1
+ dict.enum = 1
+ dict.var = 1
+ dict.breakpoint = 1
+ dict.preload = 1
+ dict.await = 1
+ dict.yield = 1
+ dict.assert = 1
+ dict.void = 1
+ dict.PI = 1
+ dict.TAU = 1
+ dict.INF = 1
+ dict.NAN = 1
diff --git a/modules/gdscript/tests/scripts/parser/features/reserved_keywords_as_attribute.out b/modules/gdscript/tests/scripts/parser/features/reserved_keywords_as_attribute.out
new file mode 100644
index 0000000000..d73c5eb7cd
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/reserved_keywords_as_attribute.out
@@ -0,0 +1 @@
+GDTEST_OK
diff --git a/modules/gdscript/tests/scripts/parser/features/static_typing.out b/modules/gdscript/tests/scripts/parser/features/static_typing.out
index 207d90fef1..40a8f97416 100644
--- a/modules/gdscript/tests/scripts/parser/features/static_typing.out
+++ b/modules/gdscript/tests/scripts/parser/features/static_typing.out
@@ -2,20 +2,20 @@ GDTEST_OK
>> WARNING
>> Line: 11
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_INTEGER' is declared but never used in the block. If this is intended, prefix it with an underscore: '__INTEGER'
+>> The local constant "_INTEGER" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER".
>> WARNING
>> Line: 12
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_INTEGER_REDUNDANT_TYPED' is declared but never used in the block. If this is intended, prefix it with an underscore: '__INTEGER_REDUNDANT_TYPED'
+>> The local constant "_INTEGER_REDUNDANT_TYPED" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER_REDUNDANT_TYPED".
>> WARNING
>> Line: 13
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_INTEGER_REDUNDANT_TYPED2' is declared but never used in the block. If this is intended, prefix it with an underscore: '__INTEGER_REDUNDANT_TYPED2'
+>> The local constant "_INTEGER_REDUNDANT_TYPED2" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER_REDUNDANT_TYPED2".
>> WARNING
>> Line: 14
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_INTEGER_REDUNDANT_INFERRED' is declared but never used in the block. If this is intended, prefix it with an underscore: '__INTEGER_REDUNDANT_INFERRED'
+>> The local constant "_INTEGER_REDUNDANT_INFERRED" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER_REDUNDANT_INFERRED".
>> WARNING
>> Line: 15
>> UNUSED_LOCAL_CONSTANT
->> The local constant '_INTEGER_REDUNDANT_INFERRED2' is declared but never used in the block. If this is intended, prefix it with an underscore: '__INTEGER_REDUNDANT_INFERRED2'
+>> The local constant "_INTEGER_REDUNDANT_INFERRED2" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER_REDUNDANT_INFERRED2".
diff --git a/modules/gdscript/tests/scripts/parser/warnings/return_value_discarded.out b/modules/gdscript/tests/scripts/parser/warnings/return_value_discarded.out
index e89bb9226f..f2db4e9307 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/return_value_discarded.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/return_value_discarded.out
@@ -2,4 +2,4 @@ GDTEST_OK
>> WARNING
>> Line: 6
>> RETURN_VALUE_DISCARDED
->> The function 'i_return_int()' returns a value that will be discarded if not used.
+>> The function "i_return_int()" returns a value that will be discarded if not used.
diff --git a/modules/gdscript/tests/scripts/parser/warnings/shadowed_constant.out b/modules/gdscript/tests/scripts/parser/warnings/shadowed_constant.out
index 9c9417e11d..75fa01f928 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/shadowed_constant.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/shadowed_constant.out
@@ -2,7 +2,7 @@ GDTEST_OK
>> WARNING
>> Line: 8
>> UNUSED_LOCAL_CONSTANT
->> The local constant 'TEST' is declared but never used in the block. If this is intended, prefix it with an underscore: '_TEST'
+>> The local constant "TEST" is declared but never used in the block. If this is intended, prefix it with an underscore: "_TEST".
>> WARNING
>> Line: 8
>> SHADOWED_VARIABLE
diff --git a/modules/gdscript/tests/scripts/parser/warnings/shadowed_global_identifier.out b/modules/gdscript/tests/scripts/parser/warnings/shadowed_global_identifier.out
index c613140eb8..75a02c5d3c 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/shadowed_global_identifier.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/shadowed_global_identifier.out
@@ -2,8 +2,8 @@ GDTEST_OK
>> WARNING
>> Line: 2
>> UNUSED_VARIABLE
->> The local variable 'abs' is declared but never used in the block. If this is intended, prefix it with an underscore: '_abs'
+>> The local variable "abs" is declared but never used in the block. If this is intended, prefix it with an underscore: "_abs".
>> WARNING
>> Line: 2
>> SHADOWED_GLOBAL_IDENTIFIER
->> The variable 'abs' has the same name as a built-in function.
+>> The variable "abs" has the same name as a built-in function.
diff --git a/modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_class.out b/modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_class.out
index 82e467b368..aab27e78e2 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_class.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_class.out
@@ -2,7 +2,7 @@ GDTEST_OK
>> WARNING
>> Line: 8
>> UNUSED_VARIABLE
->> The local variable 'foo' is declared but never used in the block. If this is intended, prefix it with an underscore: '_foo'
+>> The local variable "foo" is declared but never used in the block. If this is intended, prefix it with an underscore: "_foo".
>> WARNING
>> Line: 8
>> SHADOWED_VARIABLE
diff --git a/modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_function.out b/modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_function.out
index 26ce0465b1..e3cd358126 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_function.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/shadowed_variable_function.out
@@ -2,7 +2,7 @@ GDTEST_OK
>> WARNING
>> Line: 2
>> UNUSED_VARIABLE
->> The local variable 'test' is declared but never used in the block. If this is intended, prefix it with an underscore: '_test'
+>> The local variable "test" is declared but never used in the block. If this is intended, prefix it with an underscore: "_test".
>> WARNING
>> Line: 2
>> SHADOWED_VARIABLE
diff --git a/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.out b/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.out
index 3933a35178..77994ce9ba 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/static_called_on_instance.out
@@ -2,6 +2,6 @@ GDTEST_OK
>> WARNING
>> Line: 11
>> STATIC_CALLED_ON_INSTANCE
->> The function 'num_uint64()' is a static function but was called from an instance. Instead, it should be directly called from the type: 'String.num_uint64()'.
+>> The function "num_uint64()" is a static function but was called from an instance. Instead, it should be directly called from the type: "String.num_uint64()".
8589934592
8589934592
diff --git a/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out b/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out
index cf14502e9a..10f89be132 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out
@@ -2,4 +2,4 @@ GDTEST_OK
>> WARNING
>> Line: 2
>> UNASSIGNED_VARIABLE
->> The variable '__' was used but never assigned a value.
+>> The variable "__" was used but never assigned a value.
diff --git a/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable_op_assign.out b/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable_op_assign.out
index ba55a4e0f8..4fc91487f2 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable_op_assign.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable_op_assign.out
@@ -2,4 +2,4 @@ GDTEST_OK
>> WARNING
>> Line: 4
>> UNASSIGNED_VARIABLE_OP_ASSIGN
->> Using assignment with operation but the variable '__' was not previously assigned a value.
+>> Using assignment with operation but the variable "__" was not previously assigned a value.
diff --git a/modules/gdscript/tests/scripts/parser/warnings/unreachable_code_after_return.out b/modules/gdscript/tests/scripts/parser/warnings/unreachable_code_after_return.out
index 9316abd5eb..f67dbdcd03 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/unreachable_code_after_return.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/unreachable_code_after_return.out
@@ -2,4 +2,4 @@ GDTEST_OK
>> WARNING
>> Line: 7
>> UNREACHABLE_CODE
->> Unreachable code (statement after return) in function 'test()'.
+>> Unreachable code (statement after return) in function "test()".
diff --git a/modules/gdscript/tests/scripts/parser/warnings/unused_argument.out b/modules/gdscript/tests/scripts/parser/warnings/unused_argument.out
index 92f3308f85..3a03406f92 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/unused_argument.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/unused_argument.out
@@ -2,4 +2,4 @@ GDTEST_OK
>> WARNING
>> Line: 2
>> UNUSED_PARAMETER
->> The parameter 'p_arg2' is never used in the function 'function_with_unused_argument'. If this is intended, prefix it with an underscore: '_p_arg2'
+>> The parameter "p_arg2" is never used in the function "function_with_unused_argument()". If this is intended, prefix it with an underscore: "_p_arg2".
diff --git a/modules/gdscript/tests/scripts/parser/warnings/unused_variable.out b/modules/gdscript/tests/scripts/parser/warnings/unused_variable.out
index 270e0e69c0..b9b3968473 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/unused_variable.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/unused_variable.out
@@ -2,4 +2,4 @@ GDTEST_OK
>> WARNING
>> Line: 2
>> UNUSED_VARIABLE
->> The local variable 'unused' is declared but never used in the block. If this is intended, prefix it with an underscore: '_unused'
+>> The local variable "unused" is declared but never used in the block. If this is intended, prefix it with an underscore: "_unused".
diff --git a/modules/gdscript/tests/scripts/runtime/features/assign_operator.gd b/modules/gdscript/tests/scripts/runtime/features/assign_operator.gd
new file mode 100644
index 0000000000..2a9fe851ef
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/assign_operator.gd
@@ -0,0 +1,31 @@
+# https://github.com/godotengine/godot/issues/75832
+
+@warning_ignore("narrowing_conversion")
+func test():
+ var hf := 2.0
+ var sf = 2.0
+
+ var i := 2
+ i *= hf
+ i *= sf
+ i *= 2.0
+ print(i)
+ var v2 := Vector2i(1, 2)
+ v2 *= hf
+ v2 *= sf
+ v2 *= 2.0
+ print(v2)
+ var v3 := Vector3i(1, 2, 3)
+ v3 *= hf
+ v3 *= sf
+ v3 *= 2.0
+ print(v3)
+ var v4 := Vector4i(1, 2, 3, 4)
+ v4 *= hf
+ v4 *= sf
+ v4 *= 2.0
+ print(v4)
+
+ var arr := [1, 2, 3]
+ arr += [4, 5]
+ print(arr)
diff --git a/modules/gdscript/tests/scripts/runtime/features/assign_operator.out b/modules/gdscript/tests/scripts/runtime/features/assign_operator.out
new file mode 100644
index 0000000000..bfcfc1dff5
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/assign_operator.out
@@ -0,0 +1,6 @@
+GDTEST_OK
+16
+(8, 16)
+(8, 16, 24)
+(8, 16, 24, 32)
+[1, 2, 3, 4, 5]
diff --git a/modules/gdscript/tests/scripts/runtime/features/getter_with_freed_object.gd b/modules/gdscript/tests/scripts/runtime/features/getter_with_freed_object.gd
new file mode 100644
index 0000000000..a2d09bf7d3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/getter_with_freed_object.gd
@@ -0,0 +1,15 @@
+# https://github.com/godotengine/godot/issues/68184
+
+var node: Node:
+ get:
+ return node
+ set(n):
+ node = n
+
+
+func test():
+ node = Node.new()
+ node.free()
+
+ if !is_instance_valid(node):
+ print("It is freed")
diff --git a/modules/gdscript/tests/scripts/runtime/features/getter_with_freed_object.out b/modules/gdscript/tests/scripts/runtime/features/getter_with_freed_object.out
new file mode 100644
index 0000000000..b380f593d9
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/getter_with_freed_object.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+It is freed
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_constructor.gd b/modules/gdscript/tests/scripts/runtime/features/static_constructor.gd
new file mode 100644
index 0000000000..e08f77df12
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_constructor.gd
@@ -0,0 +1,13 @@
+@static_unload
+
+static var foo = "bar"
+
+static func _static_init():
+ print("static init called")
+ prints("foo is", foo)
+
+func test():
+ var _lambda = func _static_init():
+ print("lambda does not conflict with static constructor")
+
+ print("done")
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_constructor.out b/modules/gdscript/tests/scripts/runtime/features/static_constructor.out
new file mode 100644
index 0000000000..7f72a0ac2c
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_constructor.out
@@ -0,0 +1,4 @@
+GDTEST_OK
+static init called
+foo is bar
+done
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables.gd b/modules/gdscript/tests/scripts/runtime/features/static_variables.gd
new file mode 100644
index 0000000000..e193312381
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables.gd
@@ -0,0 +1,56 @@
+@static_unload
+
+static var perm := 0
+
+static var prop := "Hello!":
+ get: return prop + " suffix"
+ set(value): prop = "prefix " + str(value)
+
+static func get_data():
+ return "data"
+
+static var data = get_data()
+
+class Inner:
+ static var prop := "inner"
+ static func _static_init() -> void:
+ prints("Inner._static_init", prop)
+
+ class InnerInner:
+ static var prop := "inner inner"
+ static func _static_init() -> void:
+ prints("InnerInner._static_init", prop)
+
+func test():
+ prints("data:", data)
+
+ prints("perm:", perm)
+ prints("prop:", prop)
+
+ perm = 1
+ prop = "World!"
+
+ prints("perm:", perm)
+ prints("prop:", prop)
+
+ print("other.perm:", StaticVariablesOther.perm)
+ print("other.prop:", StaticVariablesOther.prop)
+
+ StaticVariablesOther.perm = 2
+ StaticVariablesOther.prop = "foo"
+
+ print("other.perm:", StaticVariablesOther.perm)
+ print("other.prop:", StaticVariablesOther.prop)
+
+ @warning_ignore("unsafe_method_access")
+ var path = get_script().get_path().get_base_dir()
+ var other = load(path + "/static_variables_load.gd")
+
+ print("load.perm:", other.perm)
+ print("load.prop:", other.prop)
+
+ other.perm = 3
+ other.prop = "bar"
+
+ print("load.perm:", other.perm)
+ print("load.prop:", other.prop)
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables.out b/modules/gdscript/tests/scripts/runtime/features/static_variables.out
new file mode 100644
index 0000000000..d2491aef5e
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables.out
@@ -0,0 +1,16 @@
+GDTEST_OK
+Inner._static_init inner
+InnerInner._static_init inner inner
+data: data
+perm: 0
+prop: prefix Hello! suffix
+perm: 1
+prop: prefix World! suffix
+other.perm:0
+other.prop:prefix Hello! suffix
+other.perm:2
+other.prop:prefix foo suffix
+load.perm:0
+load.prop:prefix Hello! suffix
+load.perm:3
+load.prop:prefix bar suffix
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables_load.gd b/modules/gdscript/tests/scripts/runtime/features/static_variables_load.gd
new file mode 100644
index 0000000000..8913b02756
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables_load.gd
@@ -0,0 +1,10 @@
+@static_unload
+
+static var perm := 0
+
+static var prop := "Hello!":
+ get: return prop + " suffix"
+ set(value): prop = "prefix " + str(value)
+
+func test():
+ print("ok")
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables_load.out b/modules/gdscript/tests/scripts/runtime/features/static_variables_load.out
new file mode 100644
index 0000000000..1b47ed10dc
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables_load.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+ok
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables_other.gd b/modules/gdscript/tests/scripts/runtime/features/static_variables_other.gd
new file mode 100644
index 0000000000..7a3b0acca6
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables_other.gd
@@ -0,0 +1,11 @@
+@static_unload
+class_name StaticVariablesOther
+
+static var perm := 0
+
+static var prop := "Hello!":
+ get: return prop + " suffix"
+ set(value): prop = "prefix " + str(value)
+
+func test():
+ print("ok")
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables_other.out b/modules/gdscript/tests/scripts/runtime/features/static_variables_other.out
new file mode 100644
index 0000000000..1b47ed10dc
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables_other.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+ok