summaryrefslogtreecommitdiffstats
path: root/modules/gdscript
diff options
context:
space:
mode:
Diffstat (limited to 'modules/gdscript')
-rw-r--r--modules/gdscript/README.md139
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml10
-rw-r--r--modules/gdscript/editor/gdscript_highlighter.cpp3
-rw-r--r--modules/gdscript/gdscript.cpp151
-rw-r--r--modules/gdscript/gdscript.h9
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp37
-rw-r--r--modules/gdscript/gdscript_analyzer.h3
-rw-r--r--modules/gdscript/gdscript_byte_codegen.h2
-rw-r--r--modules/gdscript/gdscript_compiler.cpp5
-rw-r--r--modules/gdscript/gdscript_editor.cpp213
-rw-r--r--modules/gdscript/gdscript_parser.cpp38
-rw-r--r--modules/gdscript/gdscript_parser.h4
-rw-r--r--modules/gdscript/gdscript_vm.cpp2
-rw-r--r--modules/gdscript/language_server/gdscript_text_document.cpp4
-rw-r--r--modules/gdscript/tests/README.md41
-rw-r--r--modules/gdscript/tests/gdscript_test_runner.cpp2
-rw-r--r--modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg4
-rw-r--r--modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.gd6
-rw-r--r--modules/gdscript/tests/scripts/lsp/class.gd (renamed from modules/gdscript/tests/scripts/lsp/class.notest.gd)0
-rw-r--r--modules/gdscript/tests/scripts/lsp/enums.gd (renamed from modules/gdscript/tests/scripts/lsp/enums.notest.gd)0
-rw-r--r--modules/gdscript/tests/scripts/lsp/indentation.gd (renamed from modules/gdscript/tests/scripts/lsp/indentation.notest.gd)0
-rw-r--r--modules/gdscript/tests/scripts/lsp/lambdas.gd (renamed from modules/gdscript/tests/scripts/lsp/lambdas.notest.gd)0
-rw-r--r--modules/gdscript/tests/scripts/lsp/local_variables.gd (renamed from modules/gdscript/tests/scripts/lsp/local_variables.notest.gd)0
-rw-r--r--modules/gdscript/tests/scripts/lsp/properties.gd (renamed from modules/gdscript/tests/scripts/lsp/properties.notest.gd)0
-rw-r--r--modules/gdscript/tests/scripts/lsp/scopes.gd (renamed from modules/gdscript/tests/scripts/lsp/scopes.notest.gd)0
-rw-r--r--modules/gdscript/tests/scripts/lsp/shadowing_initializer.gd (renamed from modules/gdscript/tests/scripts/lsp/shadowing_initializer.notest.gd)0
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd4
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_invalid.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_too_late.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/features/uid.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/features/uid.out1
-rw-r--r--modules/gdscript/tests/scripts/project.godot2
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd10
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/free_is_callable.out3
-rw-r--r--modules/gdscript/tests/test_completion.h203
-rw-r--r--modules/gdscript/tests/test_gdscript_uid.h115
-rw-r--r--modules/gdscript/tests/test_lsp.h18
40 files changed, 938 insertions, 112 deletions
diff --git a/modules/gdscript/README.md b/modules/gdscript/README.md
new file mode 100644
index 0000000000..30685e672c
--- /dev/null
+++ b/modules/gdscript/README.md
@@ -0,0 +1,139 @@
+# Basic GDScript module architecture
+This provides some basic information in how GDScript is implemented and integrates with the rest of the engine. You can learn more about GDScript in the [documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/gdscript/index.html). It describes the syntax and user facing systems and concepts, and can be used as a reference for what user expectations are.
+
+
+## General design
+
+GDScript is:
+
+1. A [gradually typed](https://en.wikipedia.org/wiki/Gradual_typing) language. Type hints are optional and help with static analysis and performance. However, typed code must easily interoperate with untyped code.
+2. A tightly designed language. Features are added because they are _needed_, and not because they can be added or are interesting to develop.
+3. Primarily an interpreted scripting language: it is compiled to GDScript byte code and interpreted in a GDScript virtual machine. It is meant to be easy to use and develop gameplay in. It is not meant for CPU-intensive algorithms or data processing, and is not optimized for it. For that, [C#](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_basics.html) or [GDExtension](https://docs.godotengine.org/en/stable/tutorials/scripting/gdextension/what_is_gdextension.html) may be used.
+
+
+## Integration into Godot
+
+GDScript is integrated into Godot as a module. Since modules are optional, this means that Godot may be built without GDScript and work perfectly fine without it!
+
+The GDScript module interfaces with Godot's codebase by inheriting from the engine's scripting-related classes. New languages inherit from [`ScriptLanguage`](/core/object/script_language.h), and are registered in Godot's [`ScriptServer`](/core/object/script_language.h). Scripts, referring to a file containing code, are represented in the engine by the `Script` class. Instances of that script, which are used at runtime when actually executing the code, inherit from [`ScriptInstance`](/core/object/script_instance.h).
+
+To access Godot's internal classes, GDScript uses [`ClassDB`](/core/object/class_db.h). `ClassDB` is where Godot registers classes, methods and properties that it wants exposed to its scripting system. This is how GDScript understands that `Node2D` is a class it can use, and that it has a `get_parent()` method.
+
+[Built-in GDScript methods](https://docs.godotengine.org/en/latest/classes/class_@gdscript.html#methods) are defined and exported by [`GDScriptUtilityFunctions`](gdscript_utility_functions.h), whereas [global scope methods](https://docs.godotengine.org/en/latest/classes/class_%2540globalscope.html) are registered in [`Variant::_register_variant_utility_functions()`](/core/variant/variant_utility.cpp).
+
+
+## Compilation
+
+Scripts can be at different stages of compilation. The process isn't entirely linear, but consists of this general order: tokenizing, parsing, analyzing, and finally compiling. This process is the same for scripts in the editor and scripts in an exported game. Scripts are stored as text files in both cases, and the compilation process must happen in full before the bytecode can be passed to the virtual machine and run.
+
+The main class of the GDScript module is the [`GDScript`](gdscript.h) class, which represents a class defined in GDScript. Each `.gd` file is called a _class file_ because it implicitly defines a class in GDScript, and thus results in an associated `GDScript` object. However, GDScript classes may define [_inner classes_](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#inner-classes), and those are also represented by further `GDScript` objects, even though they are not in files of their own.
+
+The `GDScript` class contains all the information related to the corresponding GDScript class: its name and path, its members like variables, functions, symbols, signals, implicit methods like initializers, etc. This is the main class that the compilation step deals with.
+
+A secondary class is `GDScriptInstance`, defined in the same file, containing _runtime_ information for an instance of a `GDScript`, and is more related to the execution of a script by the virtual machine.
+
+
+### Loading source code
+
+This mostly happens by calling `GDScript::load_source_code()` on a `GDScript` object. Parsing only requires a `String`, so it is entirely possible to parse a script without a `GDScript` object!
+
+
+### Tokenizing (see [`GDScriptTokenizer`](gdscript_tokenizer.h))
+
+Tokenizing is the process of converting the source code `String` into a sequence of tokens, which represent language constructs (such as `for` or `if`), identifiers, literals, etc. This happens almost exclusively during the parsing process, which asks for the next token in order to make sense of the source code. The tokenizer is only used outside of the parsing process in very rare exceptions.
+
+
+### Parsing (see [`GDScriptParser`](gdscript_parser.h))
+
+The parser takes a sequence of tokens and builds [the abstract syntax tree (AST)](https://en.wikipedia.org/wiki/Abstract_syntax_tree) of the GDScript program. The AST is used in the analyzing and compilation steps, and the source code `String` and sequence of tokens are discarded. The AST-building process finds syntax errors in a GDScript program and reports them to the user.
+
+The parser class also defines all the possible nodes of the AST as subtypes of `GDScriptParser::Node`, not to be confused with Godot's scene tree `Node`. For example, `GDScriptParser::IfNode` has two children nodes, one for the code in the `if` block, and one for the code in the `else` block. A `GDScriptParser::FunctionNode` contains children nodes for its name, parameters, return type, body, etc. The parser also defines typechecking data structures like `GDScriptParser::Datatype`.
+
+The parser was [intentionally designed](https://godotengine.org/article/gdscript-progress-report-writing-new-parser/#less-lookahead) with a look-ahead of a single token. This means that the parser only has access to the current token and the previous token (or, if you prefer, the current token and the next token). This parsing limitation ensures that GDScript will remain syntactically simple and accessible, and that the parsing process cannot become overly complex.
+
+
+### Analysis and typechecking (see [`GDScriptAnalyzer`](gdscript_analyzer.h))
+
+The analyzer takes in the AST of a program and verifies that "everything checks out". For example, when analyzing a method call with three parameters, it will check whether the function definition also contains three parameters. If the code is typed, it will check that argument and parameter types are compatible.
+
+There are two types of functions in the analyzer: `reduce` functions and `resolve` functions. Their parameters always include the AST node that they are attempting to reduce or resolve.
+- The `reduce` functions work on GDScript expressions, which return values, and thus their main goal is to populate the `GDScriptParser::Datatype` of the underlying AST node. The datatype is then used to typecheck code that depends on this expression, and gives the compiler necessary information to generate appropriate, safe, and optimized bytecode.
+For example, function calls are handled with `reduce_call()`, which must figure out what function is being called and check that the passed arguments match the function's parameters. The type of the underlying `CallNode` will be the return type of the function.
+Another example is `reduce_identifier()`, which does _a lot_ of work: given the string of its `IdentifierNode`, it must figure out what that identifier refers to. It could be a local variable, class name, global or class function, function parameter, class or superclass member, or any number of other things. It has to check many different places to find this information!
+A secondary goal of the `reduce` functions is to perform [constant folding](https://en.wikipedia.org/wiki/Constant_folding): to determine whether an expression is constant, and if it is, compute its _reduced value_ at this time so it does not need to be computed over and over at runtime!
+- The resolve functions work on AST nodes that represent statements, and don't necessarily have values. Their goal is to do work related to program control flow, resolve their child AST nodes, deal with scoping, etc. One of the simplest examples is `resolve_if()`, which reduces the `if` condition, then resolves the `if` body and `else` body if it exists.
+The `resolve_for()` function does more work than simply resolving its code block. With `for i in range(10)`, for example, it must also declare and type the new variable `i` within the scope of its code block, as well as make sure `range(10)` is iterable, among other things.
+To understand classes and inheritance without introducing cyclic dependency problems that would come from immediate full class code analysis, the analyzer often asks only for class _interfaces_: it needs to know what member variables and methods exist as well as their types, but no more.
+This is done through `resolve_class_interface()`, which populates `ClassNode`'s `Datatype` with that information. It first checks for superclass information with `resolve_class_inheritance()`, then populates its member information by calling `resolve_class_member()` on each member. Since this step is only about the class _interface_, methods are resolved with `resolve_function_signature()`, which gets all relevant typing information without resolving the function body!
+The remaining steps of resolution, including member variable initialization code, method code, etc, can happen at a later time.
+
+In fully untyped code, very little static analysis is possible. For example, the analyzer cannot know whether `my_var.some_member` exists when it does not know the type of `my_var`. Therefore, it cannot emit a warning or error because `some_member` _could_ exist - or it could not. The analyzer must trust the programmer. If an error does occur, it will be at runtime.
+However, GDScript is gradually typed, so all of these analyses must work when parts of the code are typed and others untyped. Static analysis in a gradually typed language is a best-effort situation: suppose there is a typed variable `var x : int`, and an untyped `var y = "some string"`. We can obviously tell this isn't going to work, but the analyzer will accept the assignment `x = y` without warnings or errors: it only knows that `y` is untyped and can therefore be anything, including the `int` that `x` expects. It must once again trust the programmer to have written code that works. In this instance, the code will error at runtime.
+In both these cases, the analyzer handles the uncertainty of untyped code by calling `mark_node_unsafe()` on the respective AST node. This means it didn't have enough information to know whether the code was fully safe or necessarily wrong. Lines with unsafe AST nodes are represented by grey line numbers in the GDScript editor. Green line numbers indicate a line of code without any unsafe nodes.
+
+This analysis step is also where dependencies are introduced and that information stored for use later. If class `A` extends class `B` or contains a member with type `B` from some other script file, then the analyzer will attempt to load that second script. If `B` contains references to `A`, then a _cyclic_ dependency is introduced. This is OK in many cases, but impossible to resolve in others.
+
+Clearly, the analyzer is where a lot of the "magic" happens! It determines what constitutes proper code that can actually be compiled, and provides as many safety guarantees as possible with the typing information it is provided with. The more typed the code, the safer and more optimized it will be!
+
+
+#### Cyclic dependencies and member resolution
+
+Cyclic dependencies from inheritance (`A extends B, B extends A`) are not supported in any programming language. Other cyclic dependencies are supported, such as `A extends B` and `B` uses, contains, or preloads, members of type `A`.
+
+To see why cyclic dependencies are complicated, suppose there is one between classes `A <-> B`. Partially through the analysis of `A`, we will need information about `B`, and therefore trigger its analysis. However, the analysis of `B` will eventually need information from `A`, which is incomplete because we never finished analyzing it. This would result in members not being found when they actually exist!
+
+GDScript supports cyclic dependencies due to a few features of the analyzer:
+
+1. Class interface resolution: when analyzing code of class `A` that depends on some other class `B`, we don't need to resolve the _code_ of `B` (its member initializers, function code, etc). We only need to know what members and methods the class has, as well as their types. These are the only things one class can use to work with, or _interface_ with, another. Because of inheritance, a class's interface depends on its superclass as well, so recursive interface resolution is needed. More details can be found in `GDScriptAnalyzer::resolve_class_interface()`.
+2. Out of order member resolution: the analyzer may not even need an entire class interface to be resolved in order to figure out a specific type! For example, if class `A` contains code that references `B.is_alive`, then the analyzer doesn't need to immediately resolve `B`'s entire interface. It may simply check whether `is_alive` exists in `B`, and reduce it for its type information, on-demand.
+A fundamental cyclic dependency problem occurs when the types of two different member variables are mutually dependent. This is commonly checked by a pattern that declares a temporary datatype with `GDScriptParser::DataType resolving_datatype;`, followed by `resolving_datatype.kind = GDScriptParser::DataType::RESOLVING;`. If the analyzer attempts to resolve a member on-demand that is already tagged as resolving, then a cyclic dependency problem has been found and can be reported.
+
+
+### Compiling (see [`GDScriptCompiler`](gdscript_compiler.h))
+
+Compiling is the final step in making a GDScript executable in the [virtual machine](gdscript_vm.h) (VM). The compiler takes a `GDScript` object and an AST, and uses another class, [`GDScriptByteCodeGenerator`](gdscript_byte_codegen.h), to generate bytecode corresponding to the class. In doing this, it creates the objects that the VM understands how to run, like [`GDScriptFunction`](gdscript_function.h), and completes a few extra tasks needed for compilation, such as populating runtime class member information.
+
+Importantly, the compilation process of a class, specifically the `GDScriptCompiler::_compile_class()` method, _cannot_ depend on information obtained by calling `GDScriptCompiler::_compile_class()` on another class, for the same cyclic dependency reasons explained in the previous section.
+Any information that can only be obtained or populated during the compilation step, when `GDScript` objects become available, must be handled before `GDScriptCompiler::_compile_class()` is called. This process is centralized in `GDScriptCompiler::_prepare_compilation()` which works as the compile-time equivalent of `GDScriptAnalyzer::resolve_class_interface()`: it populates a `GDScript`'s "interface" exclusively with information from the analysis step, and without processing other external classes. This information may then be referenced by other classes without introducing problematic cycles.
+
+The more typing information a GDScript has, the more optimized the compiled bytecode can be. For example, if `my_var` is untyped, the bytecode for `my_var.some_member` will need to go through several layers of indirection to figure out the type of `my_var` at runtime, and from there determine how to obtain `some_member`. This varies depending on whether `my_var` is a dictionary, a script, or a native class. If the type of `my_var` was known at compile time, the bytecode can directly call the type-specific method for obtaining a member.
+Similar optimizations are possible for `my_var.some_func()`. With untyped GDScript, the VM will need to resolve `my_var`'s type at runtime, then, depending on the type, use different methods to resolve the function and call it. When the function is fully resolved during static analysis, native function pointers or GDScript function objects can be compiled into the bytecode and directly called by the VM, removing several layers of indirection.
+
+Typed code is safer code and faster code!
+
+
+## Loading scripts
+
+GDScripts can be loaded in a couple of different ways. The main method, used almost everywhere in the engine, is to load scripts through the `ResourceLoader` singleton. In this way, GDScripts are resources like any others: `ResourceLoader::load()` will simply reroute to `ResourceFormatLoaderGDScript::load()`, found in `gdscript.h/cpp`(gdscript.h). This generates a GDScript object which is compiled and ready to use.
+
+The other method is to manually load the source code, then pass it to a parser, then to an analyzer and then to a compiler. The previous approach does this behind the scenes, alongside some smart caching of scripts and other functionalities. It is used in the [GDScript test runner infrastructure](tests/gdscript_test_runner.h).
+
+
+### Full and shallow scripts
+
+The `ResourceFormatLoaderGDScript::load()` method simply calls `GDScriptCache::get_full_script()`. The [`GDScriptCache`](gdscript_cache.h) is, as it sounds, a cache for GDScripts. Its two main methods, `get_shallow_script()` and `get_full_script()`, get and cache, respectively, scripts that have been merely parsed, and scripts which have been statically analyzed and fully compiled. Another internal class, `GDScriptParserRef`, found in the same file, provides even more granularity over the different steps of the parsing process, and is used extensively in the analyzer.
+
+Shallow, or "just parsed" scripts, provide information such as defined classes, class members, and so forth. This is sufficient for many purposes, like obtaining a class interface or checking whether a member exists on a specific class. Full scripts, on the other hand, have been analyzed and compiled and are ready to use.
+
+The distinction between full and shallow scripts is very important, as shallow scripts cannot create cyclic dependency problems, whereas full scripts can. The analyzer, for example, never asks for full scripts. Choosing when to request a shallow vs a full script is an important but subtle decision.
+
+In practice, full scripts are simply scripts where `GDScript::reload()` has been called. This critical function is the primary way in which scripts get compiled in Godot, and essentially does all the compilation steps covered so far in order. Whenever a script is loaded, or updated and reloaded in Godot, it will end up going through `GDScript::reload()`, except in very rare circumstances like the test runner. It is an excellent place to start reading and understanding the GDScript module!
+
+
+## Special types of scripts
+
+Certain types of GDScripts behave slightly differently. For example, autoloads are loaded with `ResourceLoader::load()` during `Main::start()`, very soon after Godot is launched. Many systems aren't initialized at that time, so error reporting is often significantly reduced and may not even show up in the editor.
+
+Tool scripts, declared with the `@tool` annotation on a GDScript file, run in the editor itself as opposed to just when the game is launched. This leads to a significant increase in complexity, as many things that can be changed in the editor may affect a currently executing tool script.
+
+
+## Other
+
+There are many other classes in the GDScript module. Here is a brief overview of some of them:
+
+- Declaration of GDScript warnings in [`GDScriptWarning`](gdscript_warning.h).
+- [`GDScriptFunction`](gdscript_function.h), which represents an executable GDScript function. The relevant file contains both static as well as runtime information.
+- The [virtual machine](gdscript_vm.cpp) is essentially defined as calling `GDScriptFunction::call()`.
+- Editor-related functions can be found in parts of `GDScriptLanguage`, originally declared in [`gdscript.h`](gdscript.h) but defined in [`gdscript_editor.cpp`](gdscript_editor.cpp). Code highlighting can be found in [`GDScriptSyntaxHighlighter`](editor/gdscript_highlighter.h).
+- GDScript decompilation is found in [`gdscript_disassembler.cpp`](gdscript_disassembler.h), defined as `GDScriptFunction::disassemble()`.
+- Documentation generation from GDScript comments in [`GDScriptDocGen`](editor/gdscript_docgen.h) \ No newline at end of file
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml
index 933bfba5ba..b335bf8fae 100644
--- a/modules/gdscript/doc_classes/@GDScript.xml
+++ b/modules/gdscript/doc_classes/@GDScript.xml
@@ -627,7 +627,7 @@
[/codeblock]
[b]Note:[/b] Only the script can have a custom icon. Inner classes are not supported.
[b]Note:[/b] As annotations describe their subject, the [annotation @icon] annotation must be placed before the class definition and inheritance.
- [b]Note:[/b] Unlike other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported).
+ [b]Note:[/b] Unlike most other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported).
</description>
</annotation>
<annotation name="@onready">
@@ -681,6 +681,14 @@
[b]Note:[/b] As annotations describe their subject, the [annotation @tool] annotation must be placed before the class definition and inheritance.
</description>
</annotation>
+ <annotation name="@uid">
+ <return type="void" />
+ <param index="0" name="uid" type="String" />
+ <description>
+ Stores information about UID of this script. This annotation is auto-generated when saving the script and must not be modified manually. Only applies to scripts saved as separate files (i.e. not built-in).
+ [b]Note:[/b] Unlike most other annotations, the argument of the [annotation @uid] annotation must be a string literal (constant expressions are not supported).
+ </description>
+ </annotation>
<annotation name="@warning_ignore" qualifiers="vararg">
<return type="void" />
<param index="0" name="warning" type="String" />
diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp
index 3df07f9794..1f07def21c 100644
--- a/modules/gdscript/editor/gdscript_highlighter.cpp
+++ b/modules/gdscript/editor/gdscript_highlighter.cpp
@@ -35,6 +35,7 @@
#include "core/config/project_settings.h"
#include "editor/editor_settings.h"
+#include "editor/themes/editor_theme_manager.h"
Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_line) {
Dictionary color_map;
@@ -790,7 +791,7 @@ void GDScriptSyntaxHighlighter::_update_cache() {
const String text_edit_color_theme = EDITOR_GET("text_editor/theme/color_theme");
const bool godot_2_theme = text_edit_color_theme == "Godot 2";
- if (godot_2_theme || EditorSettings::get_singleton()->is_dark_theme()) {
+ if (godot_2_theme || EditorThemeManager::is_dark_theme()) {
function_definition_color = Color(0.4, 0.9, 1.0);
global_function_color = Color(0.64, 0.64, 0.96);
node_path_color = Color(0.72, 0.77, 0.49);
diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp
index 1f0830aa17..7b486f2a35 100644
--- a/modules/gdscript/gdscript.cpp
+++ b/modules/gdscript/gdscript.cpp
@@ -55,6 +55,7 @@
#ifdef TOOLS_ENABLED
#include "editor/editor_paths.h"
+#include "editor/editor_settings.h"
#endif
#include <stdint.h>
@@ -1076,6 +1077,36 @@ Ref<GDScript> GDScript::get_base() const {
return base;
}
+String GDScript::get_raw_source_code(const String &p_path, bool *r_error) {
+ Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
+ if (f.is_null()) {
+ if (r_error) {
+ *r_error = true;
+ }
+ return String();
+ }
+ return f->get_as_utf8_string();
+}
+
+Vector2i GDScript::get_uid_lines(const String &p_source) {
+ GDScriptParser parser;
+ parser.parse(p_source, "", false);
+ const GDScriptParser::ClassNode *c = parser.get_tree();
+ if (!c) {
+ return Vector2i(-1, -1);
+ }
+ return c->uid_lines;
+}
+
+String GDScript::create_uid_line(const String &p_uid_str) {
+#ifdef TOOLS_ENABLED
+ if (EDITOR_GET("text_editor/completion/use_single_quotes")) {
+ return vformat(R"(@uid('%s') # %s)", p_uid_str, RTR("Generated automatically, do not modify."));
+ }
+#endif
+ return vformat(R"(@uid("%s") # %s)", p_uid_str, RTR("Generated automatically, do not modify."));
+}
+
bool GDScript::inherits_script(const Ref<Script> &p_script) const {
Ref<GDScript> gd = p_script;
if (gd.is_null()) {
@@ -2320,14 +2351,13 @@ struct GDScriptDepSort {
void GDScriptLanguage::reload_all_scripts() {
#ifdef DEBUG_ENABLED
print_verbose("GDScript: Reloading all scripts");
- List<Ref<GDScript>> scripts;
+ Array scripts;
{
MutexLock lock(this->mutex);
SelfList<GDScript> *elem = script_list.first();
while (elem) {
- // Scripts will reload all subclasses, so only reload root scripts.
- if (elem->self()->is_root_script() && elem->self()->get_path().is_resource_file()) {
+ if (elem->self()->get_path().is_resource_file()) {
print_verbose("GDScript: Found: " + elem->self()->get_path());
scripts.push_back(Ref<GDScript>(elem->self())); //cast to gdscript to avoid being erased by accident
}
@@ -2348,19 +2378,11 @@ void GDScriptLanguage::reload_all_scripts() {
#endif
}
- //as scripts are going to be reloaded, must proceed without locking here
-
- scripts.sort_custom<GDScriptDepSort>(); //update in inheritance dependency order
-
- for (Ref<GDScript> &scr : scripts) {
- print_verbose("GDScript: Reloading: " + scr->get_path());
- scr->load_source_code(scr->get_path());
- scr->reload(true);
- }
+ reload_scripts(scripts, true);
#endif
}
-void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) {
+void GDScriptLanguage::reload_scripts(const Array &p_scripts, bool p_soft_reload) {
#ifdef DEBUG_ENABLED
List<Ref<GDScript>> scripts;
@@ -2386,7 +2408,7 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so
scripts.sort_custom<GDScriptDepSort>(); //update in inheritance dependency order
for (Ref<GDScript> &scr : scripts) {
- bool reload = scr == p_script || to_reload.has(scr->get_base());
+ bool reload = p_scripts.has(scr) || to_reload.has(scr->get_base());
if (!reload) {
continue;
@@ -2409,7 +2431,7 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so
}
}
-//same thing for placeholders
+ //same thing for placeholders
#ifdef TOOLS_ENABLED
while (scr->placeholders.size()) {
@@ -2437,6 +2459,8 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so
for (KeyValue<Ref<GDScript>, HashMap<ObjectID, List<Pair<StringName, Variant>>>> &E : to_reload) {
Ref<GDScript> scr = E.key;
+ print_verbose("GDScript: Reloading: " + scr->get_path());
+ scr->load_source_code(scr->get_path());
scr->reload(p_soft_reload);
//restore state if saved
@@ -2484,6 +2508,12 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so
#endif
}
+void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) {
+ Array scripts;
+ scripts.push_back(p_script);
+ reload_scripts(scripts, p_soft_reload);
+}
+
void GDScriptLanguage::frame() {
calls = 0;
@@ -2594,17 +2624,8 @@ bool GDScriptLanguage::handles_global_class_type(const String &p_type) const {
}
String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_base_type, String *r_icon_path) const {
- Error err;
- Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ, &err);
- if (err) {
- return String();
- }
-
- String source = f->get_as_utf8_string();
-
GDScriptParser parser;
- err = parser.parse(source, p_path, false);
-
+ parser.parse(GDScript::get_raw_source_code(p_path), p_path, false);
const GDScriptParser::ClassNode *c = parser.get_tree();
if (!c) {
return String(); // No class parsed.
@@ -2818,6 +2839,22 @@ String ResourceFormatLoaderGDScript::get_resource_type(const String &p_path) con
return "";
}
+ResourceUID::ID ResourceFormatLoaderGDScript::get_resource_uid(const String &p_path) const {
+ String ext = p_path.get_extension().to_lower();
+
+ if (ext != "gd") {
+ return ResourceUID::INVALID_ID;
+ }
+
+ GDScriptParser parser;
+ parser.parse(GDScript::get_raw_source_code(p_path), p_path, false);
+ const GDScriptParser::ClassNode *c = parser.get_tree();
+ if (!c) {
+ return ResourceUID::INVALID_ID;
+ }
+ return ResourceUID::get_singleton()->text_to_id(c->uid_string);
+}
+
void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types) {
Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_MSG(file.is_null(), "Cannot open file '" + p_path + "'.");
@@ -2842,17 +2879,49 @@ Error ResourceFormatSaverGDScript::save(const Ref<Resource> &p_resource, const S
ERR_FAIL_COND_V(sqscr.is_null(), ERR_INVALID_PARAMETER);
String source = sqscr->get_source_code();
+ ResourceUID::ID uid = ResourceSaver::get_resource_id_for_path(p_path, !p_resource->is_built_in());
{
+ bool source_changed = false;
Error err;
Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err);
ERR_FAIL_COND_V_MSG(err, err, "Cannot save GDScript file '" + p_path + "'.");
- file->store_string(source);
+ if (uid != ResourceUID::INVALID_ID) {
+ GDScriptParser parser;
+ parser.parse(source, "", false);
+ const GDScriptParser::ClassNode *c = parser.get_tree();
+ if (c && ResourceUID::get_singleton()->text_to_id(c->uid_string) != uid) {
+ const Vector2i &uid_idx = c->uid_lines;
+ PackedStringArray lines = source.split("\n");
+
+ if (uid_idx.x > -1) {
+ for (int i = uid_idx.x + 1; i <= uid_idx.y; i++) {
+ // If UID is written across multiple lines, erase extra lines.
+ lines.remove_at(uid_idx.x + 1);
+ }
+ lines.write[uid_idx.x] = GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(uid));
+ } else {
+ lines.insert(0, GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(uid)));
+ }
+ source = String("\n").join(lines);
+ source_changed = true;
+ file->store_string(String("\n").join(lines));
+ } else {
+ file->store_string(source);
+ }
+ }
+
if (file->get_error() != OK && file->get_error() != ERR_FILE_EOF) {
return ERR_CANT_CREATE;
}
+
+ if (source_changed) {
+ sqscr->set_source_code(source);
+ sqscr->reload();
+ sqscr->emit_changed();
+ }
}
if (ScriptServer::is_reload_scripts_on_save_enabled()) {
@@ -2871,3 +2940,33 @@ void ResourceFormatSaverGDScript::get_recognized_extensions(const Ref<Resource>
bool ResourceFormatSaverGDScript::recognize(const Ref<Resource> &p_resource) const {
return Object::cast_to<GDScript>(*p_resource) != nullptr;
}
+
+Error ResourceFormatSaverGDScript::set_uid(const String &p_path, ResourceUID::ID p_uid) {
+ ERR_FAIL_COND_V(p_path.get_extension() != "gd", ERR_INVALID_PARAMETER);
+ ERR_FAIL_COND_V(p_uid == ResourceUID::INVALID_ID, ERR_INVALID_PARAMETER);
+
+ bool error = false;
+ const String &source_code = GDScript::get_raw_source_code(p_path, &error);
+ if (error) {
+ return ERR_CANT_OPEN;
+ }
+
+ Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
+ ERR_FAIL_COND_V(f.is_null(), ERR_CANT_OPEN);
+
+ const Vector2i &uid_idx = GDScript::get_uid_lines(source_code);
+ PackedStringArray lines = source_code.split("\n");
+
+ if (uid_idx.x > -1) {
+ for (int i = uid_idx.x + 1; i <= uid_idx.y; i++) {
+ // If UID is written across multiple lines, erase extra lines.
+ lines.remove_at(uid_idx.x + 1);
+ }
+ lines.write[uid_idx.x] = GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(p_uid));
+ } else {
+ f->store_line(GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(p_uid)));
+ }
+ f->store_string(String("\n").join(lines));
+
+ return OK;
+}
diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h
index 3ff8f757cf..fdfd79f0fc 100644
--- a/modules/gdscript/gdscript.h
+++ b/modules/gdscript/gdscript.h
@@ -262,6 +262,10 @@ public:
bool is_tool() const override { return tool; }
Ref<GDScript> get_base() const;
+ static String get_raw_source_code(const String &p_path, bool *r_error = nullptr);
+ static Vector2i get_uid_lines(const String &p_source);
+ static String create_uid_line(const String &p_uid_str);
+
const HashMap<StringName, MemberInfo> &debug_get_member_indices() const { return member_indices; }
const HashMap<StringName, GDScriptFunction *> &debug_get_member_functions() const; //this is debug only
StringName debug_get_member_by_index(int p_idx) const;
@@ -540,7 +544,7 @@ public:
virtual void get_string_delimiters(List<String> *p_delimiters) const override;
virtual bool is_using_templates() override;
virtual Ref<Script> make_template(const String &p_template, const String &p_class_name, const String &p_base_class_name) const override;
- virtual Vector<ScriptTemplate> get_built_in_templates(StringName p_object) override;
+ virtual Vector<ScriptTemplate> get_built_in_templates(const StringName &p_object) override;
virtual bool validate(const String &p_script, const String &p_path = "", List<String> *r_functions = nullptr, List<ScriptLanguage::ScriptError> *r_errors = nullptr, List<ScriptLanguage::Warning> *r_warnings = nullptr, HashSet<int> *r_safe_lines = nullptr) const override;
virtual Script *create_script() const override;
#ifndef DISABLE_DEPRECATED
@@ -575,6 +579,7 @@ public:
virtual String debug_parse_stack_level_expression(int p_level, const String &p_expression, int p_max_subitems = -1, int p_max_depth = -1) override;
virtual void reload_all_scripts() override;
+ virtual void reload_scripts(const Array &p_scripts, bool p_soft_reload) override;
virtual void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) override;
virtual void frame() override;
@@ -615,6 +620,7 @@ public:
virtual void get_recognized_extensions(List<String> *p_extensions) const;
virtual bool handles_type(const String &p_type) const;
virtual String get_resource_type(const String &p_path) const;
+ virtual ResourceUID::ID get_resource_uid(const String &p_path) const;
virtual void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false);
};
@@ -623,6 +629,7 @@ public:
virtual Error save(const Ref<Resource> &p_resource, const String &p_path, uint32_t p_flags = 0);
virtual void get_recognized_extensions(const Ref<Resource> &p_resource, List<String> *p_extensions) const;
virtual bool recognize(const Ref<Resource> &p_resource) const;
+ virtual Error set_uid(const String &p_path, ResourceUID::ID p_uid);
};
#endif // GDSCRIPT_H
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index b245df15a6..dd9e49fb8d 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -845,7 +845,7 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type
return result;
}
-void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, StringName p_name, const GDScriptParser::Node *p_source) {
+void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, const StringName &p_name, const GDScriptParser::Node *p_source) {
ERR_FAIL_COND(!p_class->has_member(p_name));
resolve_class_member(p_class, p_class->members_indices[p_name], p_source);
}
@@ -4908,8 +4908,19 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo
}
result.builtin_type = p_property.type;
if (p_property.type == Variant::OBJECT) {
- result.kind = GDScriptParser::DataType::NATIVE;
- result.native_type = p_property.class_name == StringName() ? SNAME("Object") : p_property.class_name;
+ if (ScriptServer::is_global_class(p_property.class_name)) {
+ result.kind = GDScriptParser::DataType::SCRIPT;
+ result.script_path = ScriptServer::get_global_class_path(p_property.class_name);
+ result.native_type = ScriptServer::get_global_class_native_base(p_property.class_name);
+
+ Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_property.class_name));
+ if (scr.is_valid()) {
+ result.script_type = scr;
+ }
+ } else {
+ result.kind = GDScriptParser::DataType::NATIVE;
+ result.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name;
+ }
} else {
result.kind = GDScriptParser::DataType::BUILTIN;
result.builtin_type = p_property.type;
@@ -5315,8 +5326,21 @@ GDScriptParser::DataType GDScriptAnalyzer::get_operation_type(Variant::Operator
return result;
}
-// TODO: Add safe/unsafe return variable (for variant cases)
bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion, const GDScriptParser::Node *p_source_node) {
+#ifdef DEBUG_ENABLED
+ if (p_source_node) {
+ if (p_target.kind == GDScriptParser::DataType::ENUM) {
+ if (p_source.kind == GDScriptParser::DataType::BUILTIN && p_source.builtin_type == Variant::INT) {
+ parser->push_warning(p_source_node, GDScriptWarning::INT_AS_ENUM_WITHOUT_CAST);
+ }
+ }
+ }
+#endif
+ return check_type_compatibility(p_target, p_source, p_allow_implicit_conversion, p_source_node);
+}
+
+// TODO: Add safe/unsafe return variable (for variant cases)
+bool GDScriptAnalyzer::check_type_compatibility(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion, const GDScriptParser::Node *p_source_node) {
// These return "true" so it doesn't affect users negatively.
ERR_FAIL_COND_V_MSG(!p_target.is_set(), true, "Parser bug (please report): Trying to check compatibility of unset target type");
ERR_FAIL_COND_V_MSG(!p_source.is_set(), true, "Parser bug (please report): Trying to check compatibility of unset value type");
@@ -5351,11 +5375,6 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ
if (p_target.kind == GDScriptParser::DataType::ENUM) {
if (p_source.kind == GDScriptParser::DataType::BUILTIN && p_source.builtin_type == Variant::INT) {
-#ifdef DEBUG_ENABLED
- if (p_source_node) {
- parser->push_warning(p_source_node, GDScriptWarning::INT_AS_ENUM_WITHOUT_CAST);
- }
-#endif
return true;
}
if (p_source.kind == GDScriptParser::DataType::ENUM) {
diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h
index ec155706df..e398ccfdbb 100644
--- a/modules/gdscript/gdscript_analyzer.h
+++ b/modules/gdscript/gdscript_analyzer.h
@@ -62,7 +62,7 @@ class GDScriptAnalyzer {
void decide_suite_type(GDScriptParser::Node *p_suite, GDScriptParser::Node *p_statement);
void resolve_annotation(GDScriptParser::AnnotationNode *p_annotation);
- void resolve_class_member(GDScriptParser::ClassNode *p_class, StringName p_name, const GDScriptParser::Node *p_source = nullptr);
+ void resolve_class_member(GDScriptParser::ClassNode *p_class, const StringName &p_name, const GDScriptParser::Node *p_source = nullptr);
void resolve_class_member(GDScriptParser::ClassNode *p_class, int p_index, const GDScriptParser::Node *p_source = nullptr);
void resolve_class_interface(GDScriptParser::ClassNode *p_class, const GDScriptParser::Node *p_source = nullptr);
void resolve_class_interface(GDScriptParser::ClassNode *p_class, bool p_recursive);
@@ -147,6 +147,7 @@ public:
Variant make_variable_default_value(GDScriptParser::VariableNode *p_variable);
const HashMap<String, Ref<GDScriptParserRef>> &get_depended_parsers();
+ static bool check_type_compatibility(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false, const GDScriptParser::Node *p_source_node = nullptr);
GDScriptAnalyzer(GDScriptParser *p_parser);
};
diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h
index 9bface6136..f902cb10cc 100644
--- a/modules/gdscript/gdscript_byte_codegen.h
+++ b/modules/gdscript/gdscript_byte_codegen.h
@@ -121,7 +121,7 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator {
RBMap<MethodBind *, int> method_bind_map;
RBMap<GDScriptFunction *, int> lambdas_map;
-#if DEBUG_ENABLED
+#ifdef DEBUG_ENABLED
// Keep method and property names for pointer and validated operations.
// Used when disassembling the bytecode.
Vector<String> operator_names;
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index f6633f8bf6..13ed66710c 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -37,6 +37,7 @@
#include "core/config/engine.h"
#include "core/config/project_settings.h"
+#include "core/core_string_names.h"
bool GDScriptCompiler::_is_class_member_property(CodeGen &codegen, const StringName &p_name) {
if (codegen.function_node && codegen.function_node->is_static) {
@@ -345,7 +346,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
scr = scr->_base;
}
- if (nc && (ClassDB::has_signal(nc->get_name(), identifier) || ClassDB::has_method(nc->get_name(), identifier))) {
+ if (nc && (identifier == CoreStringNames::get_singleton()->_free || ClassDB::has_signal(nc->get_name(), identifier) || ClassDB::has_method(nc->get_name(), identifier))) {
// Get like it was a property.
GDScriptCodeGenerator::Address temp = codegen.add_temporary(); // TODO: Get type here.
GDScriptCodeGenerator::Address self(GDScriptCodeGenerator::Address::SELF);
@@ -1375,7 +1376,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
return GDScriptCodeGenerator::Address();
}
- codegen.script->lambda_info.insert(function, { lambda->captures.size(), lambda->use_self });
+ codegen.script->lambda_info.insert(function, { (int)lambda->captures.size(), lambda->use_self });
gen->write_lambda(result, function, captures, lambda->use_self);
for (int i = 0; i < captures.size(); i++) {
diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp
index b8fd5de4fd..44e104da05 100644
--- a/modules/gdscript/gdscript_editor.cpp
+++ b/modules/gdscript/gdscript_editor.cpp
@@ -104,7 +104,7 @@ Ref<Script> GDScriptLanguage::make_template(const String &p_template, const Stri
return scr;
}
-Vector<ScriptLanguage::ScriptTemplate> GDScriptLanguage::get_built_in_templates(StringName p_object) {
+Vector<ScriptLanguage::ScriptTemplate> GDScriptLanguage::get_built_in_templates(const StringName &p_object) {
Vector<ScriptLanguage::ScriptTemplate> templates;
#ifdef TOOLS_ENABLED
for (int i = 0; i < TEMPLATES_ARRAY_SIZE; i++) {
@@ -537,7 +537,7 @@ struct GDScriptCompletionIdentifier {
// appears. For example, if you are completing code in a class that inherits Node2D, a property found on Node2D
// will have a "better" (lower) location "score" than a property that is found on CanvasItem.
-static int _get_property_location(StringName p_class, StringName p_property) {
+static int _get_property_location(const StringName &p_class, const StringName &p_property) {
if (!ClassDB::has_property(p_class, p_property)) {
return ScriptLanguage::LOCATION_OTHER;
}
@@ -552,7 +552,20 @@ static int _get_property_location(StringName p_class, StringName p_property) {
return depth | ScriptLanguage::LOCATION_PARENT_MASK;
}
-static int _get_constant_location(StringName p_class, StringName p_constant) {
+static int _get_property_location(Ref<Script> p_script, const StringName &p_property) {
+ int depth = 0;
+ Ref<Script> scr = p_script;
+ while (scr.is_valid()) {
+ if (scr->get_member_line(p_property) != -1) {
+ return depth | ScriptLanguage::LOCATION_PARENT_MASK;
+ }
+ depth++;
+ scr = scr->get_base_script();
+ }
+ return depth + _get_property_location(p_script->get_instance_base_type(), p_property);
+}
+
+static int _get_constant_location(const StringName &p_class, const StringName &p_constant) {
if (!ClassDB::has_integer_constant(p_class, p_constant)) {
return ScriptLanguage::LOCATION_OTHER;
}
@@ -567,7 +580,20 @@ static int _get_constant_location(StringName p_class, StringName p_constant) {
return depth | ScriptLanguage::LOCATION_PARENT_MASK;
}
-static int _get_signal_location(StringName p_class, StringName p_signal) {
+static int _get_constant_location(Ref<Script> p_script, const StringName &p_constant) {
+ int depth = 0;
+ Ref<Script> scr = p_script;
+ while (scr.is_valid()) {
+ if (scr->get_member_line(p_constant) != -1) {
+ return depth | ScriptLanguage::LOCATION_PARENT_MASK;
+ }
+ depth++;
+ scr = scr->get_base_script();
+ }
+ return depth + _get_constant_location(p_script->get_instance_base_type(), p_constant);
+}
+
+static int _get_signal_location(const StringName &p_class, const StringName &p_signal) {
if (!ClassDB::has_signal(p_class, p_signal)) {
return ScriptLanguage::LOCATION_OTHER;
}
@@ -582,7 +608,20 @@ static int _get_signal_location(StringName p_class, StringName p_signal) {
return depth | ScriptLanguage::LOCATION_PARENT_MASK;
}
-static int _get_method_location(StringName p_class, StringName p_method) {
+static int _get_signal_location(Ref<Script> p_script, const StringName &p_signal) {
+ int depth = 0;
+ Ref<Script> scr = p_script;
+ while (scr.is_valid()) {
+ if (scr->get_member_line(p_signal) != -1) {
+ return depth | ScriptLanguage::LOCATION_PARENT_MASK;
+ }
+ depth++;
+ scr = scr->get_base_script();
+ }
+ return depth + _get_signal_location(p_script->get_instance_base_type(), p_signal);
+}
+
+static int _get_method_location(const StringName &p_class, const StringName &p_method) {
if (!ClassDB::has_method(p_class, p_method)) {
return ScriptLanguage::LOCATION_OTHER;
}
@@ -597,7 +636,20 @@ static int _get_method_location(StringName p_class, StringName p_method) {
return depth | ScriptLanguage::LOCATION_PARENT_MASK;
}
-static int _get_enum_constant_location(StringName p_class, StringName p_enum_constant) {
+static int _get_method_location(Ref<Script> p_script, const StringName &p_method) {
+ int depth = 0;
+ Ref<Script> scr = p_script;
+ while (scr.is_valid()) {
+ if (scr->get_member_line(p_method) != -1) {
+ return depth | ScriptLanguage::LOCATION_PARENT_MASK;
+ }
+ depth++;
+ scr = scr->get_base_script();
+ }
+ return depth + _get_method_location(p_script->get_instance_base_type(), p_method);
+}
+
+static int _get_enum_constant_location(const StringName &p_class, const StringName &p_enum_constant) {
if (!ClassDB::get_integer_constant_enum(p_class, p_enum_constant)) {
return ScriptLanguage::LOCATION_OTHER;
}
@@ -1083,13 +1135,13 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
List<PropertyInfo> members;
scr->get_script_property_list(&members);
for (const PropertyInfo &E : members) {
- if (E.usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP)) {
+ if (E.usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_INTERNAL)) {
continue;
}
if (E.name.contains("/")) {
continue;
}
- int location = p_recursion_depth + _get_property_location(scr->get_class_name(), E.name);
+ int location = p_recursion_depth + _get_property_location(scr, E.name);
ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, location);
r_result.insert(option.display, option);
}
@@ -1097,7 +1149,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
List<MethodInfo> signals;
scr->get_script_signal_list(&signals);
for (const MethodInfo &E : signals) {
- int location = p_recursion_depth + _get_signal_location(scr->get_class_name(), E.name);
+ int location = p_recursion_depth + _get_signal_location(scr, E.name);
ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location);
r_result.insert(option.display, option);
}
@@ -1105,7 +1157,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
HashMap<StringName, Variant> constants;
scr->get_constants(&constants);
for (const KeyValue<StringName, Variant> &E : constants) {
- int location = p_recursion_depth + _get_constant_location(scr->get_class_name(), E.key);
+ int location = p_recursion_depth + _get_constant_location(scr, E.key);
ScriptLanguage::CodeCompletionOption option(E.key.operator String(), ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, location);
r_result.insert(option.display, option);
}
@@ -1117,7 +1169,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
if (E.name.begins_with("@")) {
continue;
}
- int location = p_recursion_depth + _get_method_location(scr->get_class_name(), E.name);
+ int location = p_recursion_depth + _get_method_location(scr, E.name);
ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION, location);
if (E.arguments.size()) {
option.insert_text += "(";
@@ -1158,7 +1210,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
List<PropertyInfo> pinfo;
ClassDB::get_property_list(type, &pinfo);
for (const PropertyInfo &E : pinfo) {
- if (E.usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP)) {
+ if (E.usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_INTERNAL)) {
continue;
}
if (E.name.contains("/")) {
@@ -1221,7 +1273,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
}
for (const PropertyInfo &E : members) {
- if (E.usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP)) {
+ if (E.usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_INTERNAL)) {
continue;
}
if (!String(E.name).contains("/")) {
@@ -1370,7 +1422,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context
}
}
-static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value) {
+static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value, GDScriptParser::CompletionContext &p_context) {
GDScriptCompletionIdentifier ci;
ci.value = p_value;
ci.type.is_constant = true;
@@ -1392,8 +1444,22 @@ static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value) {
scr = obj->get_script();
}
if (scr.is_valid()) {
- ci.type.script_type = scr;
+ ci.type.script_path = scr->get_path();
+
+ if (scr->get_path().ends_with(".gd")) {
+ Error err;
+ Ref<GDScriptParserRef> parser = GDScriptCache::get_parser(scr->get_path(), GDScriptParserRef::INTERFACE_SOLVED, err);
+ if (err == OK) {
+ ci.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
+ ci.type.class_type = parser->get_parser()->get_tree();
+ ci.type.kind = GDScriptParser::DataType::CLASS;
+ p_context.dependent_parsers.push_back(parser);
+ return ci;
+ }
+ }
+
ci.type.kind = GDScriptParser::DataType::SCRIPT;
+ ci.type.script_type = scr;
ci.type.native_type = scr->get_instance_base_type();
} else {
ci.type.kind = GDScriptParser::DataType::NATIVE;
@@ -1418,8 +1484,19 @@ static GDScriptCompletionIdentifier _type_from_property(const PropertyInfo &p_pr
ci.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
ci.type.builtin_type = p_property.type;
if (p_property.type == Variant::OBJECT) {
- ci.type.kind = GDScriptParser::DataType::NATIVE;
- ci.type.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name;
+ if (ScriptServer::is_global_class(p_property.class_name)) {
+ ci.type.kind = GDScriptParser::DataType::SCRIPT;
+ ci.type.script_path = ScriptServer::get_global_class_path(p_property.class_name);
+ ci.type.native_type = ScriptServer::get_global_class_native_base(p_property.class_name);
+
+ Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_property.class_name));
+ if (scr.is_valid()) {
+ ci.type.script_type = scr;
+ }
+ } else {
+ ci.type.kind = GDScriptParser::DataType::NATIVE;
+ ci.type.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name;
+ }
} else {
ci.type.kind = GDScriptParser::DataType::BUILTIN;
}
@@ -1481,7 +1558,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (p_expression->is_constant) {
// Already has a value, so just use that.
- r_type = _type_from_variant(p_expression->reduced_value);
+ r_type = _type_from_variant(p_expression->reduced_value, p_context);
switch (p_expression->get_datatype().kind) {
case GDScriptParser::DataType::ENUM:
case GDScriptParser::DataType::CLASS:
@@ -1495,7 +1572,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
switch (p_expression->type) {
case GDScriptParser::Node::LITERAL: {
const GDScriptParser::LiteralNode *literal = static_cast<const GDScriptParser::LiteralNode *>(p_expression);
- r_type = _type_from_variant(literal->value);
+ r_type = _type_from_variant(literal->value, p_context);
found = true;
} break;
case GDScriptParser::Node::SELF: {
@@ -1676,7 +1753,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (!which.is_empty()) {
// Try singletons first
if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(which)) {
- r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[which]);
+ r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[which], p_context);
found = true;
} else {
for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {
@@ -1727,7 +1804,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (ce.error == Callable::CallError::CALL_OK && ret.get_type() != Variant::NIL) {
if (ret.get_type() != Variant::OBJECT || ret.operator Object *() != nullptr) {
- r_type = _type_from_variant(ret);
+ r_type = _type_from_variant(ret, p_context);
found = true;
}
}
@@ -1755,7 +1832,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (base.value.get_type() == Variant::DICTIONARY && base.value.operator Dictionary().has(String(subscript->attribute->name))) {
Variant value = base.value.operator Dictionary()[String(subscript->attribute->name)];
- r_type = _type_from_variant(value);
+ r_type = _type_from_variant(value, p_context);
found = true;
break;
}
@@ -1807,7 +1884,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (base.value.in(index.value)) {
Variant value = base.value.get(index.value);
- r_type = _type_from_variant(value);
+ r_type = _type_from_variant(value, p_context);
found = true;
break;
}
@@ -1862,7 +1939,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
bool valid = false;
Variant res = base_val.get(index.value, &valid);
if (valid) {
- r_type = _type_from_variant(res);
+ r_type = _type_from_variant(res, p_context);
r_type.value = Variant();
r_type.type.is_constant = false;
found = true;
@@ -1920,7 +1997,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
found = false;
break;
}
- r_type = _type_from_variant(res);
+ r_type = _type_from_variant(res, p_context);
if (!v1_use_value || !v2_use_value) {
r_type.value = Variant();
r_type.type.is_constant = false;
@@ -2009,6 +2086,21 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
default:
break;
}
+ } else {
+ if (p_context.current_class) {
+ GDScriptCompletionIdentifier base_identifier;
+
+ GDScriptCompletionIdentifier base;
+ base.value = p_context.base;
+ base.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
+ base.type.kind = GDScriptParser::DataType::CLASS;
+ base.type.class_type = p_context.current_class;
+ base.type.is_meta_type = p_context.current_function && p_context.current_function->is_static;
+
+ if (_guess_identifier_type_from_base(p_context, base, p_identifier->name, base_identifier)) {
+ id_type = base_identifier.type;
+ }
+ }
}
while (suite) {
@@ -2062,8 +2154,15 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
if (last_assigned_expression && last_assign_line < p_context.current_line) {
GDScriptParser::CompletionContext c = p_context;
c.current_line = last_assign_line;
- r_type.assigned_expression = last_assigned_expression;
- if (_guess_expression_type(c, last_assigned_expression, r_type)) {
+ GDScriptCompletionIdentifier assigned_type;
+ if (_guess_expression_type(c, last_assigned_expression, assigned_type)) {
+ if (id_type.is_set() && assigned_type.type.is_set() && !GDScriptAnalyzer::check_type_compatibility(id_type, assigned_type.type)) {
+ // The assigned type is incompatible. The annotated type takes priority.
+ r_type.assigned_expression = last_assigned_expression;
+ r_type.type = id_type;
+ } else {
+ r_type = assigned_type;
+ }
return true;
}
}
@@ -2121,20 +2220,6 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
return true;
}
- // Check current class (including inheritance).
- if (p_context.current_class) {
- GDScriptCompletionIdentifier base;
- base.value = p_context.base;
- base.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
- base.type.kind = GDScriptParser::DataType::CLASS;
- base.type.class_type = p_context.current_class;
- base.type.is_meta_type = p_context.current_function && p_context.current_function->is_static;
-
- if (_guess_identifier_type_from_base(p_context, base, p_identifier->name, r_type)) {
- return true;
- }
- }
-
// Check global scripts.
if (ScriptServer::is_global_class(p_identifier->name)) {
String script = ScriptServer::get_global_class_path(p_identifier->name);
@@ -2155,7 +2240,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
} else {
Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_identifier->name));
if (scr.is_valid()) {
- r_type = _type_from_variant(scr);
+ r_type = _type_from_variant(scr, p_context);
r_type.type.is_meta_type = true;
return true;
}
@@ -2165,7 +2250,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
// Check global variables (including autoloads).
if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(p_identifier->name)) {
- r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[p_identifier->name]);
+ r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[p_identifier->name], p_context);
return true;
}
@@ -2218,7 +2303,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
const GDScriptParser::ExpressionNode *init = member.variable->initializer;
if (init->is_constant) {
r_type.value = init->reduced_value;
- r_type = _type_from_variant(init->reduced_value);
+ r_type = _type_from_variant(init->reduced_value, p_context);
return true;
} else if (init->start_line == p_context.current_line) {
return false;
@@ -2245,7 +2330,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
r_type.enumeration = member.m_enum->identifier->name;
return true;
case GDScriptParser::ClassNode::Member::ENUM_VALUE:
- r_type = _type_from_variant(member.enum_value.value);
+ r_type = _type_from_variant(member.enum_value.value, p_context);
return true;
case GDScriptParser::ClassNode::Member::SIGNAL:
r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
@@ -2281,7 +2366,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
HashMap<StringName, Variant> constants;
scr->get_constants(&constants);
if (constants.has(p_identifier)) {
- r_type = _type_from_variant(constants[p_identifier]);
+ r_type = _type_from_variant(constants[p_identifier], p_context);
return true;
}
@@ -2345,7 +2430,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
bool valid = false;
Variant res = tmp.get(p_identifier, &valid);
if (valid) {
- r_type = _type_from_variant(res);
+ r_type = _type_from_variant(res, p_context);
r_type.value = Variant();
r_type.type.is_constant = false;
return true;
@@ -3197,6 +3282,11 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
List<String> opts;
p_owner->get_argument_options("get_node", 0, &opts);
+ bool for_unique_name = false;
+ if (completion_context.node != nullptr && completion_context.node->type == GDScriptParser::Node::GET_NODE && !static_cast<GDScriptParser::GetNodeNode *>(completion_context.node)->use_dollar) {
+ for_unique_name = true;
+ }
+
for (const String &E : opts) {
r_forced = true;
String opt = E.strip_edges();
@@ -3205,6 +3295,14 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
// or handle NodePaths which are valid identifiers and don't need quotes.
opt = opt.unquote();
}
+
+ if (for_unique_name) {
+ if (!opt.begins_with("%")) {
+ continue;
+ }
+ opt = opt.substr(1);
+ }
+
// The path needs quotes if it's not a valid identifier (with an exception
// for "/" as path separator, which also doesn't require quotes).
if (!opt.replace("/", "_").is_valid_identifier()) {
@@ -3216,11 +3314,13 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
options.insert(option.display, option);
}
- // Get autoloads.
- for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {
- String path = "/root/" + E.key;
- ScriptLanguage::CodeCompletionOption option(path.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH);
- options.insert(option.display, option);
+ if (!for_unique_name) {
+ // Get autoloads.
+ for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {
+ String path = "/root/" + E.key;
+ ScriptLanguage::CodeCompletionOption option(path.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH);
+ options.insert(option.display, option);
+ }
}
}
} break;
@@ -3414,6 +3514,12 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
}
if (ClassDB::has_property(class_name, p_symbol, true)) {
+ PropertyInfo prop_info;
+ ClassDB::get_property_info(class_name, p_symbol, &prop_info, true);
+ if (prop_info.usage & PROPERTY_USAGE_INTERNAL) {
+ return ERR_CANT_RESOLVE;
+ }
+
r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY;
r_result.class_name = base_type.native_type;
r_result.class_member = p_symbol;
@@ -3564,7 +3670,8 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
case GDScriptParser::COMPLETION_ASSIGN:
case GDScriptParser::COMPLETION_CALL_ARGUMENTS:
case GDScriptParser::COMPLETION_IDENTIFIER:
- case GDScriptParser::COMPLETION_PROPERTY_METHOD: {
+ case GDScriptParser::COMPLETION_PROPERTY_METHOD:
+ case GDScriptParser::COMPLETION_SUBSCRIPT: {
GDScriptParser::DataType base_type;
if (context.current_class) {
if (context.type != GDScriptParser::COMPLETION_SUPER_METHOD) {
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index a4a12f8bc4..03cf334bed 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -93,6 +93,7 @@ bool GDScriptParser::annotation_exists(const String &p_annotation_name) const {
GDScriptParser::GDScriptParser() {
// Register valid annotations.
if (unlikely(valid_annotations.is_empty())) {
+ register_annotation(MethodInfo("@uid", PropertyInfo(Variant::STRING, "uid")), AnnotationInfo::SCRIPT, &GDScriptParser::uid_annotation);
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);
@@ -520,6 +521,8 @@ void GDScriptParser::parse_program() {
// `@icon` needs to be applied in the parser. See GH-72444.
if (annotation->name == SNAME("@icon")) {
annotation->apply(this, head, nullptr);
+ } else if (annotation->name == SNAME("@uid")) {
+ annotation->apply(this, head, nullptr);
} else {
head->annotations.push_back(annotation);
}
@@ -3834,18 +3837,18 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation)
}
// `@icon`'s argument needs to be resolved in the parser. See GH-72444.
- if (p_annotation->name == SNAME("@icon")) {
+ if (p_annotation->name == SNAME("@icon") || p_annotation->name == SNAME("@uid")) {
ExpressionNode *argument = p_annotation->arguments[0];
if (argument->type != Node::LITERAL) {
- push_error(R"(Argument 1 of annotation "@icon" must be a string literal.)", argument);
+ push_error(vformat(R"(Argument 1 of annotation "%s" must be a string literal.)", p_annotation->name), argument);
return false;
}
Variant value = static_cast<LiteralNode *>(argument)->value;
if (value.get_type() != Variant::STRING) {
- push_error(R"(Argument 1 of annotation "@icon" must be a string literal.)", argument);
+ push_error(vformat(R"(Argument 1 of annotation "%s" must be a string literal.)", p_annotation->name), argument);
return false;
}
@@ -3857,6 +3860,35 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation)
return true;
}
+bool GDScriptParser::uid_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
+ ERR_FAIL_COND_V_MSG(p_target->type != Node::CLASS, false, R"("@uid" annotation can only be applied to classes.)");
+ ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false);
+
+#ifdef DEBUG_ENABLED
+ if (this->_has_uid) {
+ push_error(R"("@uid" annotation can only be used once.)", p_annotation);
+ return false;
+ }
+#endif // DEBUG_ENABLED
+
+ // Assign line range early to allow replacing invalid UIDs.
+ ClassNode *class_node = static_cast<ClassNode *>(p_target);
+ class_node->uid_lines = Vector2i(p_annotation->start_line - 1, p_annotation->end_line - 1); // Lines start from 1, so need to subtract.
+
+ const String &uid_string = p_annotation->resolved_arguments[0];
+#ifdef DEBUG_ENABLED
+ if (ResourceUID::get_singleton()->text_to_id(uid_string) == ResourceUID::INVALID_ID) {
+ push_error(R"(The annotated UID is invalid.)", p_annotation);
+ return false;
+ }
+#endif // DEBUG_ENABLED
+
+ class_node->uid_string = uid_string;
+
+ this->_has_uid = true;
+ return true;
+}
+
bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
#ifdef DEBUG_ENABLED
if (this->_is_tool) {
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index 88b5bdc43f..e058737306 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/modules/gdscript/gdscript_parser.h
@@ -736,6 +736,8 @@ public:
IdentifierNode *identifier = nullptr;
String icon_path;
String simplified_icon_path;
+ String uid_string;
+ Vector2i uid_lines = Vector2i(-1, -1);
Vector<Member> members;
HashMap<StringName, int> members_indices;
ClassNode *outer = nullptr;
@@ -1318,6 +1320,7 @@ private:
friend class GDScriptAnalyzer;
bool _is_tool = false;
+ bool _has_uid = false;
String script_path;
bool for_completion = false;
bool panic_mode = false;
@@ -1473,6 +1476,7 @@ private:
static bool register_annotation(const MethodInfo &p_info, uint32_t p_target_kinds, AnnotationAction p_apply, const Vector<Variant> &p_default_arguments = Vector<Variant>(), bool p_is_vararg = false);
bool validate_annotation_arguments(AnnotationNode *p_annotation);
void clear_unused_annotations();
+ bool uid_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool icon_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool onready_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp
index 7b03ac74d6..1a8c22cc11 100644
--- a/modules/gdscript/gdscript_vm.cpp
+++ b/modules/gdscript/gdscript_vm.cpp
@@ -655,7 +655,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
}
bool exit_ok = false;
bool awaited = false;
- 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 ? (int)p_instance->members.size() : 0 };
#endif
Variant *variant_addresses[ADDR_TYPE_MAX] = { stack, _constants_ptr, p_instance ? p_instance->members.ptrw() : nullptr };
diff --git a/modules/gdscript/language_server/gdscript_text_document.cpp b/modules/gdscript/language_server/gdscript_text_document.cpp
index 0b1371851b..e00b92b752 100644
--- a/modules/gdscript/language_server/gdscript_text_document.cpp
+++ b/modules/gdscript/language_server/gdscript_text_document.cpp
@@ -114,7 +114,7 @@ void GDScriptTextDocument::didSave(const Variant &p_param) {
scr->update_exports();
ScriptEditor::get_singleton()->reload_scripts(true);
ScriptEditor::get_singleton()->update_docs_from_script(scr);
- ScriptEditor::get_singleton()->trigger_live_script_reload();
+ ScriptEditor::get_singleton()->trigger_live_script_reload(scr->get_path());
}
}
@@ -456,7 +456,7 @@ Variant GDScriptTextDocument::declaration(const Dictionary &p_params) {
id = "class_global:" + symbol->native_class + ":" + symbol->name;
break;
}
- call_deferred(SNAME("show_native_symbol_in_editor"), id);
+ callable_mp(this, &GDScriptTextDocument::show_native_symbol_in_editor).call_deferred(id);
} else {
notify_client_show_symbol(symbol);
}
diff --git a/modules/gdscript/tests/README.md b/modules/gdscript/tests/README.md
index 361d586d32..cea251bab5 100644
--- a/modules/gdscript/tests/README.md
+++ b/modules/gdscript/tests/README.md
@@ -6,3 +6,44 @@ and output files.
See the
[Integration tests for GDScript documentation](https://docs.godotengine.org/en/latest/contributing/development/core_and_modules/unit_testing.html#integration-tests-for-gdscript)
for information about creating and running GDScript integration tests.
+
+# GDScript Autocompletion tests
+
+The `script/completion` folder contains test for the GDScript autocompletion.
+
+Each test case consists of at least one `.gd` file, which contains the code, and one `.cfg` file, which contains expected results and configuration. Inside of the GDScript file the character `➡` represents the cursor position, at which autocompletion is invoked.
+
+The config file contains two section:
+
+`[input]` contains keys that configure the test environment. The following keys are possible:
+
+- `cs: boolean = false`: If `true`, the test will be skipped when running a non C# build.
+- `use_single_quotes: boolean = false`: Configures the corresponding editor setting for the test.
+- `scene: String`: Allows to specify a scene which is opened while autocompletion is performed. If this is not set the test runner will search for a `.tscn` file with the same basename as the GDScript file. If that isn't found either, autocompletion will behave as if no scene was opened.
+
+`[output]` specifies the expected results for the test. The following key are supported:
+
+- `include: Array`: An unordered list of suggestions that should be in the result. Each entry is one dictionary with the following keys: `display`, `insert_text`, `kind`, `location`, which correspond to the suggestion struct which is used in the code. The runner only tests against specified keys, so in most cases `display` will suffice.
+- `exclude: Array`: An array of suggestions which should not be in the result. The entries take the same form as for `include`.
+- `call_hint: String`: The expected call hint returned by autocompletion.
+- `forced: boolean`: Whether autocompletion is expected to force opening a completion window.
+
+Tests will only test against entries in `[output]` that were specified.
+
+## Writing autocompletion tests
+
+To avoid failing edge cases a certain behaviour needs to be tested multiple times. Some things that tests should account for:
+
+- All possible types: Test with all possible types that apply to the tested behaviour. (For the last points testing against `SCRIPT` and `CLASS` should suffice. `CLASS` can be obtained through C#, `SCRIPT` through GDScript. Relying on autoloads to be of type `SCRIPT` is not good, since this might change in the future.)
+
+ - `BUILTIN`
+ - `NATIVE`
+ - GDScripts (with `class_name` as well as `preload`ed)
+ - C# (as standin for all other language bindings) (with `class_name` as well as `preload`ed)
+ - Autoloads
+
+- Possible contexts: the completion might be placed in different places of the program. e.g:
+ - initializers of class members
+ - directly inside a suite
+ - assignments inside a suite
+ - as parameter to a call
diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp
index 361ca276bb..4d93a6fc18 100644
--- a/modules/gdscript/tests/gdscript_test_runner.cpp
+++ b/modules/gdscript/tests/gdscript_test_runner.cpp
@@ -266,7 +266,7 @@ bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) {
while (!next.is_empty()) {
if (dir->current_is_dir()) {
- if (next == "." || next == "..") {
+ if (next == "." || next == ".." || next == "completion" || next == "lsp") {
next = dir->get_next();
continue;
}
diff --git a/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg
new file mode 100644
index 0000000000..27e695d245
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg
@@ -0,0 +1,4 @@
+[output]
+include=[
+ {"display": "autoplay"},
+]
diff --git a/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.gd b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.gd
new file mode 100644
index 0000000000..d41bbb970c
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.gd
@@ -0,0 +1,6 @@
+extends Node
+
+var test: AnimationPlayer = $AnimationPlayer
+
+func _ready():
+ test.➡
diff --git a/modules/gdscript/tests/scripts/lsp/class.notest.gd b/modules/gdscript/tests/scripts/lsp/class.gd
index 53d0b14d72..53d0b14d72 100644
--- a/modules/gdscript/tests/scripts/lsp/class.notest.gd
+++ b/modules/gdscript/tests/scripts/lsp/class.gd
diff --git a/modules/gdscript/tests/scripts/lsp/enums.notest.gd b/modules/gdscript/tests/scripts/lsp/enums.gd
index 38b9ec110a..38b9ec110a 100644
--- a/modules/gdscript/tests/scripts/lsp/enums.notest.gd
+++ b/modules/gdscript/tests/scripts/lsp/enums.gd
diff --git a/modules/gdscript/tests/scripts/lsp/indentation.notest.gd b/modules/gdscript/tests/scripts/lsp/indentation.gd
index c25d73a719..c25d73a719 100644
--- a/modules/gdscript/tests/scripts/lsp/indentation.notest.gd
+++ b/modules/gdscript/tests/scripts/lsp/indentation.gd
diff --git a/modules/gdscript/tests/scripts/lsp/lambdas.notest.gd b/modules/gdscript/tests/scripts/lsp/lambdas.gd
index 6f5d468eea..6f5d468eea 100644
--- a/modules/gdscript/tests/scripts/lsp/lambdas.notest.gd
+++ b/modules/gdscript/tests/scripts/lsp/lambdas.gd
diff --git a/modules/gdscript/tests/scripts/lsp/local_variables.notest.gd b/modules/gdscript/tests/scripts/lsp/local_variables.gd
index b6cc46f7da..b6cc46f7da 100644
--- a/modules/gdscript/tests/scripts/lsp/local_variables.notest.gd
+++ b/modules/gdscript/tests/scripts/lsp/local_variables.gd
diff --git a/modules/gdscript/tests/scripts/lsp/properties.notest.gd b/modules/gdscript/tests/scripts/lsp/properties.gd
index 8dfaee2e5b..8dfaee2e5b 100644
--- a/modules/gdscript/tests/scripts/lsp/properties.notest.gd
+++ b/modules/gdscript/tests/scripts/lsp/properties.gd
diff --git a/modules/gdscript/tests/scripts/lsp/scopes.notest.gd b/modules/gdscript/tests/scripts/lsp/scopes.gd
index 20b8fb9bd7..20b8fb9bd7 100644
--- a/modules/gdscript/tests/scripts/lsp/scopes.notest.gd
+++ b/modules/gdscript/tests/scripts/lsp/scopes.gd
diff --git a/modules/gdscript/tests/scripts/lsp/shadowing_initializer.notest.gd b/modules/gdscript/tests/scripts/lsp/shadowing_initializer.gd
index 338000fa0e..338000fa0e 100644
--- a/modules/gdscript/tests/scripts/lsp/shadowing_initializer.notest.gd
+++ b/modules/gdscript/tests/scripts/lsp/shadowing_initializer.gd
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd
new file mode 100644
index 0000000000..4ded8e65db
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd
@@ -0,0 +1,5 @@
+@uid("uid://c4ckv3ryprcn4")
+@uid("uid://c4ckv3ryprcn4")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out
new file mode 100644
index 0000000000..be1061401a
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+"@uid" annotation can only be used once.
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd
new file mode 100644
index 0000000000..114d5b7e98
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd
@@ -0,0 +1,4 @@
+@uid("not a valid uid")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_invalid.out b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.out
new file mode 100644
index 0000000000..83f9f63cbf
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+The annotated UID is invalid.
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd
new file mode 100644
index 0000000000..2b332447b7
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd
@@ -0,0 +1,5 @@
+extends Object
+@uid("uid://c4ckv3ryprcn4")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_too_late.out b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.out
new file mode 100644
index 0000000000..328459923f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Annotation "@uid" must be at the top of the script, before "extends" and "class_name".
diff --git a/modules/gdscript/tests/scripts/parser/features/uid.gd b/modules/gdscript/tests/scripts/parser/features/uid.gd
new file mode 100644
index 0000000000..4070500608
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/uid.gd
@@ -0,0 +1,5 @@
+@uid("uid://c4ckv3ryprcn4")
+extends Object
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/features/uid.out b/modules/gdscript/tests/scripts/parser/features/uid.out
new file mode 100644
index 0000000000..d73c5eb7cd
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/uid.out
@@ -0,0 +1 @@
+GDTEST_OK
diff --git a/modules/gdscript/tests/scripts/project.godot b/modules/gdscript/tests/scripts/project.godot
index 25b49c0abd..c500ef443d 100644
--- a/modules/gdscript/tests/scripts/project.godot
+++ b/modules/gdscript/tests/scripts/project.godot
@@ -3,7 +3,7 @@
; It also helps for opening Godot to edit the scripts, but please don't
; let the editor changes be saved.
-config_version=4
+config_version=5
[application]
diff --git a/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd
new file mode 100644
index 0000000000..b9746a8207
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd
@@ -0,0 +1,10 @@
+func test():
+ var node := Node.new()
+ var callable: Callable = node.free
+ callable.call()
+ print(node)
+
+ node = Node.new()
+ callable = node["free"]
+ callable.call()
+ print(node)
diff --git a/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out
new file mode 100644
index 0000000000..97bfc46d96
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out
@@ -0,0 +1,3 @@
+GDTEST_OK
+<Freed Object>
+<Freed Object>
diff --git a/modules/gdscript/tests/test_completion.h b/modules/gdscript/tests/test_completion.h
new file mode 100644
index 0000000000..fd6b5321e6
--- /dev/null
+++ b/modules/gdscript/tests/test_completion.h
@@ -0,0 +1,203 @@
+/**************************************************************************/
+/* test_completion.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 TEST_COMPLETION_H
+#define TEST_COMPLETION_H
+
+#ifdef TOOLS_ENABLED
+
+#include "core/io/config_file.h"
+#include "core/io/dir_access.h"
+#include "core/io/file_access.h"
+#include "core/object/script_language.h"
+#include "core/variant/dictionary.h"
+#include "core/variant/variant.h"
+#include "gdscript_test_runner.h"
+#include "modules/modules_enabled.gen.h" // For mono.
+#include "scene/resources/packed_scene.h"
+
+#include "../gdscript.h"
+#include "tests/test_macros.h"
+
+#include "editor/editor_settings.h"
+#include "scene/theme/theme_db.h"
+
+namespace GDScriptTests {
+
+static bool match_option(const Dictionary p_expected, const ScriptLanguage::CodeCompletionOption p_got) {
+ if (p_expected.get("display", p_got.display) != p_got.display) {
+ return false;
+ }
+ if (p_expected.get("insert_text", p_got.insert_text) != p_got.insert_text) {
+ return false;
+ }
+ if (p_expected.get("kind", p_got.kind) != Variant(p_got.kind)) {
+ return false;
+ }
+ if (p_expected.get("location", p_got.location) != Variant(p_got.location)) {
+ return false;
+ }
+ return true;
+}
+
+static void to_dict_list(Variant p_variant, List<Dictionary> &p_list) {
+ ERR_FAIL_COND(!p_variant.is_array());
+
+ Array arr = p_variant;
+ for (int i = 0; i < arr.size(); i++) {
+ if (arr[i].get_type() == Variant::DICTIONARY) {
+ p_list.push_back(arr[i]);
+ }
+ }
+}
+
+static void test_directory(const String &p_dir) {
+ Error err = OK;
+ Ref<DirAccess> dir = DirAccess::open(p_dir, &err);
+
+ if (err != OK) {
+ FAIL("Invalid test directory.");
+ return;
+ }
+
+ String path = dir->get_current_dir();
+
+ dir->list_dir_begin();
+ String next = dir->get_next();
+
+ while (!next.is_empty()) {
+ if (dir->current_is_dir()) {
+ if (next == "." || next == "..") {
+ next = dir->get_next();
+ continue;
+ }
+ test_directory(path.path_join(next));
+ } else if (next.ends_with(".gd") && !next.ends_with(".notest.gd")) {
+ Ref<FileAccess> acc = FileAccess::open(path.path_join(next), FileAccess::READ, &err);
+
+ if (err != OK) {
+ next = dir->get_next();
+ continue;
+ }
+
+ String code = acc->get_as_utf8_string();
+ // For ease of reading ➡ (0x27A1) acts as sentinel char instead of 0xFFFF in the files.
+ code = code.replace_first(String::chr(0x27A1), String::chr(0xFFFF));
+ // Require pointer sentinel char in scripts.
+ CHECK(code.find_char(0xFFFF) != -1);
+
+ ConfigFile conf;
+ if (conf.load(path.path_join(next.get_basename() + ".cfg")) != OK) {
+ FAIL("No config file found.");
+ }
+
+#ifndef MODULE_MONO_ENABLED
+ if (conf.get_value("input", "cs", false)) {
+ next = dir->get_next();
+ continue;
+ }
+#endif
+
+ EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", conf.get_value("input", "use_single_quotes", false));
+
+ List<Dictionary> include;
+ to_dict_list(conf.get_value("output", "include", Array()), include);
+
+ List<Dictionary> exclude;
+ to_dict_list(conf.get_value("output", "exclude", Array()), exclude);
+
+ List<ScriptLanguage::CodeCompletionOption> options;
+ String call_hint;
+ bool forced;
+
+ Node *owner = nullptr;
+ if (conf.has_section_key("input", "scene")) {
+ Ref<PackedScene> scene = ResourceLoader::load(conf.get_value("input", "scene"), "PackedScene");
+ if (scene.is_valid()) {
+ owner = scene->instantiate();
+ }
+ } else if (dir->file_exists(next.get_basename() + ".tscn")) {
+ Ref<PackedScene> scene = ResourceLoader::load(path.path_join(next.get_basename() + ".tscn"), "PackedScene");
+ if (scene.is_valid()) {
+ owner = scene->instantiate();
+ }
+ }
+
+ GDScriptLanguage::get_singleton()->complete_code(code, path.path_join(next), owner, &options, forced, call_hint);
+ String contains_excluded;
+ for (ScriptLanguage::CodeCompletionOption &option : options) {
+ for (const Dictionary &E : exclude) {
+ if (match_option(E, option)) {
+ contains_excluded = option.display;
+ break;
+ }
+ }
+ if (!contains_excluded.is_empty()) {
+ break;
+ }
+
+ for (const Dictionary &E : include) {
+ if (match_option(E, option)) {
+ include.erase(E);
+ break;
+ }
+ }
+ }
+ CHECK_MESSAGE(contains_excluded.is_empty(), "Autocompletion suggests illegal option '", contains_excluded, "' for '", path.path_join(next), "'.");
+ CHECK(include.is_empty());
+
+ String expected_call_hint = conf.get_value("output", "call_hint", call_hint);
+ bool expected_forced = conf.get_value("output", "forced", forced);
+
+ CHECK(expected_call_hint == call_hint);
+ CHECK(expected_forced == forced);
+
+ if (owner) {
+ memdelete(owner);
+ }
+ }
+ next = dir->get_next();
+ }
+}
+
+TEST_SUITE("[Modules][GDScript][Completion]") {
+ TEST_CASE("[Editor] Check suggestion list") {
+ // Set all editor settings that code completion relies on.
+ EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", false);
+ init_language("modules/gdscript/tests/scripts");
+
+ test_directory("modules/gdscript/tests/scripts/completion");
+ }
+}
+} // namespace GDScriptTests
+
+#endif
+
+#endif // TEST_COMPLETION_H
diff --git a/modules/gdscript/tests/test_gdscript_uid.h b/modules/gdscript/tests/test_gdscript_uid.h
new file mode 100644
index 0000000000..918fe65890
--- /dev/null
+++ b/modules/gdscript/tests/test_gdscript_uid.h
@@ -0,0 +1,115 @@
+/**************************************************************************/
+/* test_gdscript_uid.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 TEST_GDSCRIPT_UID_H
+#define TEST_GDSCRIPT_UID_H
+
+#ifdef TOOLS_ENABLED
+
+#include "core/io/resource_saver.h"
+#include "core/os/os.h"
+#include "gdscript_test_runner.h"
+
+#include "../gdscript.h"
+#include "tests/test_macros.h"
+
+namespace GDScriptTests {
+
+static HashMap<String, ResourceUID::ID> id_cache;
+
+ResourceUID::ID _resource_saver_get_resource_id_for_path(const String &p_path, bool p_generate) {
+ return ResourceUID::get_singleton()->text_to_id("uid://baba");
+}
+
+static void test_script(const String &p_source, const String &p_target_source) {
+ const String script_path = OS::get_singleton()->get_cache_path().path_join("script.gd");
+
+ Ref<GDScript> script;
+ script.instantiate();
+ script->set_source_code(p_source);
+ ResourceSaver::save(script, script_path);
+
+ Ref<FileAccess> fa = FileAccess::open(script_path, FileAccess::READ);
+ CHECK_EQ(fa->get_as_text(), p_target_source);
+}
+
+TEST_SUITE("[Modules][GDScript][UID]") {
+ TEST_CASE("[ResourceSaver] Adding UID line to script") {
+ init_language("modules/gdscript/tests/scripts");
+ ResourceSaver::set_get_resource_id_for_path(_resource_saver_get_resource_id_for_path);
+
+ const String source = R"(extends Node
+class_name TestClass
+)";
+ const String final_source = R"(@uid("uid://baba") # Generated automatically, do not modify.
+extends Node
+class_name TestClass
+)";
+
+ // Script has no UID, add it.
+ test_script(source, final_source);
+ }
+
+ TEST_CASE("[ResourceSaver] Updating UID line in script") {
+ init_language("modules/gdscript/tests/scripts");
+ ResourceSaver::set_get_resource_id_for_path(_resource_saver_get_resource_id_for_path);
+
+ const String wrong_id_source = R"(
+
+@uid(
+ "uid://dead"
+ ) # G
+extends Node
+class_name TestClass
+)";
+ const String corrected_id_source = R"(
+
+@uid("uid://baba") # Generated automatically, do not modify.
+extends Node
+class_name TestClass
+)";
+ const String correct_id_source = R"(@uid("uid://baba") # G
+extends Node
+class_name TestClass
+)";
+
+ // Script has wrong UID saved. Remove it and add a correct one.
+ // Inserts in the same line, but multiline annotations are flattened.
+ test_script(wrong_id_source, corrected_id_source);
+ // The stored UID is correct, so do not modify it.
+ test_script(correct_id_source, correct_id_source);
+ }
+}
+
+} // namespace GDScriptTests
+
+#endif
+
+#endif // TEST_GDSCRIPT_UID_H
diff --git a/modules/gdscript/tests/test_lsp.h b/modules/gdscript/tests/test_lsp.h
index e57df00e2d..6192272f80 100644
--- a/modules/gdscript/tests/test_lsp.h
+++ b/modules/gdscript/tests/test_lsp.h
@@ -76,7 +76,7 @@ namespace GDScriptTests {
// LSP GDScript test scripts are located inside project of other GDScript tests:
// Cannot reset `ProjectSettings` (singleton) -> Cannot load another workspace and resources in there.
// -> Reuse GDScript test project. LSP specific scripts are then placed inside `lsp` folder.
-// Access via `res://lsp/my_script.notest.gd`.
+// Access via `res://lsp/my_script.gd`.
const String root = "modules/gdscript/tests/scripts/";
/*
@@ -394,7 +394,7 @@ func f():
Ref<GDScriptWorkspace> workspace = GDScriptLanguageProtocol::get_singleton()->get_workspace();
{
- String path = "res://lsp/local_variables.notest.gd";
+ String path = "res://lsp/local_variables.gd";
assert_no_errors_in(path);
String uri = workspace->get_file_uri(path);
Vector<InlineTestData> all_test_data = read_tests(path);
@@ -413,7 +413,7 @@ func f():
}
SUBCASE("Can get correct ranges for indented variables") {
- String path = "res://lsp/indentation.notest.gd";
+ String path = "res://lsp/indentation.gd";
assert_no_errors_in(path);
String uri = workspace->get_file_uri(path);
Vector<InlineTestData> all_test_data = read_tests(path);
@@ -421,7 +421,7 @@ func f():
}
SUBCASE("Can get correct ranges for scopes") {
- String path = "res://lsp/scopes.notest.gd";
+ String path = "res://lsp/scopes.gd";
assert_no_errors_in(path);
String uri = workspace->get_file_uri(path);
Vector<InlineTestData> all_test_data = read_tests(path);
@@ -429,7 +429,7 @@ func f():
}
SUBCASE("Can get correct ranges for lambda") {
- String path = "res://lsp/lambdas.notest.gd";
+ String path = "res://lsp/lambdas.gd";
assert_no_errors_in(path);
String uri = workspace->get_file_uri(path);
Vector<InlineTestData> all_test_data = read_tests(path);
@@ -437,7 +437,7 @@ func f():
}
SUBCASE("Can get correct ranges for inner class") {
- String path = "res://lsp/class.notest.gd";
+ String path = "res://lsp/class.gd";
assert_no_errors_in(path);
String uri = workspace->get_file_uri(path);
Vector<InlineTestData> all_test_data = read_tests(path);
@@ -445,7 +445,7 @@ func f():
}
SUBCASE("Can get correct ranges for inner class") {
- String path = "res://lsp/enums.notest.gd";
+ String path = "res://lsp/enums.gd";
assert_no_errors_in(path);
String uri = workspace->get_file_uri(path);
Vector<InlineTestData> all_test_data = read_tests(path);
@@ -453,7 +453,7 @@ func f():
}
SUBCASE("Can get correct ranges for shadowing & shadowed variables") {
- String path = "res://lsp/shadowing_initializer.notest.gd";
+ String path = "res://lsp/shadowing_initializer.gd";
assert_no_errors_in(path);
String uri = workspace->get_file_uri(path);
Vector<InlineTestData> all_test_data = read_tests(path);
@@ -461,7 +461,7 @@ func f():
}
SUBCASE("Can get correct ranges for properties and getter/setter") {
- String path = "res://lsp/properties.notest.gd";
+ String path = "res://lsp/properties.gd";
assert_no_errors_in(path);
String uri = workspace->get_file_uri(path);
Vector<InlineTestData> all_test_data = read_tests(path);