Repository: godotengine/godot (C++)

Issue:
No parser error for `get_node` used in "not-onready" variable
**Godot version:** 4.0 e1e52b373

**OS/device including version:** Windows 10

**Issue description:**
No parser error for get_node used in "not-onready" variable.

```gdscript
extends Node2D

var node1 = get_node('Sprite2D')
var node2 = $Sprite2D
```

**Steps to reproduce:**
**Minimal reproduction project:**
N/A

GDScript cannot override Godot's native functions
### Godot version

4.0.dev 7791599d5bcfdbae0d2a96e5fc13d9021ec820be

### System information

Linux binogure 5.14.0-2-amd64 #1 SMP Debian 5.14.9-2 (2021-10-03) x86_64 GNU/Linux

### Issue description
I don't know if it is normal, but I cannot override any godot's native functions using GDScript.
It never calls the gdscript function but the godot's core function instead.

### Steps to reproduce

- Create a new project
- Attach this script to the main node:
```gdscript
extends Node

var __data = {}

func _ready():
    set('random_key', 'random value')

func set(key, value):
    print('Setting a new value')
    __data[key] = value
```

The `set` function is never called, and there is no warning/error in godot explaining that is prohibited.

### Minimal reproduction project

_No response_


Discussion: The regression can be confirmed by looking at #46233 reported in 3.2.
I'd like to work on this. Any pointers on which files to investigate to start with?
@angad-k This is the code from 3.2 branch:
https://github.com/godotengine/godot/blob/9047e760d11f0f606db0e40488e5a84f5d42227f/modules/gdscript/gdscript_parser.cpp#L4971-L4983
Reopening for now as #72487 was reverted.

Seems to be related to: https://github.com/godotengine/godot/issues/40288
Shouldn't it be `_set`? I don't recall the engine expecting `set` to be overridable
I expect to override any engine's function if I want to do it. Or at least the editor should tell me that I cannot override such functions and explain why.

Another solution would be, having an empty object which hasn't had any functions but can be extended as a Singleton.
> I expect to override any engine's function if I want to do it

I'm afraid that's not how things work, as it has probably been said in other issues, to achieve that the engine would have to use slow `call` on *absolutely every function that ever faces the script API*, which likely won't happen as it's too much work and downsides to it (unless maybe the engine code and scripts were both in Java?)

I remember that in Godot 3 so far, if you write a function with the same name as an existing function, Godot lets you "shadow" it, although it is not true overriding. For example if you write "get_position" in `Node2D`, GDScript might behave as if you overrode the function (because GDScript uses `call` everywhere), but the *engine* will not use your function when getting the position of the node. Because for this to work, the C++ code *has to expect and handle* the fact the object could have a script attached to it with a function on it.

>  the editor should tell me that I cannot override such functions and explain why.

So there should be a decision wether or not Godot should consider it an error. I personally think it should, but like many edge cases like this one, other people might have decided to exploit it and want it to remain maybe 🤷 

> Another solution would be, having an empty object which hasn't had any functions but can be extended as a Singleton.

I don't understand why you need an empty object or a singleton.
> I don't understand why you need an empty object or a singleton.

It would be great to have an empty class, that you can use as a singleton so you can have any functions you want. That's the bare minimum to me. For example:
```gdscript
extends EmptyObject

var __data = {}

func _ready():
    set('random_key', 'random value')

func set(key, value):
    print('Setting a new value')
    __data[key] = value
```

> It would be great to have an empty class, that you can use as a singleton so you can have any functions you want.

I still don't understand the relation with overriding functions, but you basically describe an utility class with static functions:

`util.gd`
```gdscript
class_name Util
static func foo(a, b) -> int:
    return a + b
```
`bar.gd`
```gdscript
func _ready():
    print(Util.foo(3, 4))
```

And I dont understand the example you give with it... the example sounds like just a dictionary.
```gdscript
var data = {}
data.set("hello", 42)
print(data.hello)
```

And wanting both in the same object can be done with a dictionary inside an autoload.
It has to have some custom code on `set` and `get`, with warnings, exceptions and specific behavior. I simply put the simplest code there.

Here a more complete code
```gdscript
extends Node

var __global = {}

signal key_updated(name)

func _ready():
  reset()

func reset():
  # Load the default locale by default
  set('locale', OS.get_locale().left(2))

  set('soundfx_volume', 0.5)
  set('music_volume', 0.5)

func set(key, value):
  if __global.has(key):
    logger.debug('Overtwriting key %s', [key])

  else:
    logger.debug('Writing key %s', [key])

  __global[key] = value

  emit_signal('key_updated', key)

func get(key, default_value = null):
  return __global[key] if __global.has(key) else default_value
```
And that example code doesn't work? It's basically still a fancy wrapped dictionary. You can put that in an autoload and it should work. Maybe the only difference is, with your code you'd have to call `get` and `set` (or call them something else if Godot prevents you), but if you use `_get` and `_set` you can do it, or even write with `.varname` syntax.
It doesn't because of the `set` `get` functions that belongs to the `Object` class.

I understand, but prefixing any function with `_` has a meaning to me. Since there is no `private/public` keywords, a function called `_test` will be private, where a function called `test` will be public.
If you use `_set` and `_get` for Godot it means adding extra code to `get` and `set`, this is one way you can achieve what you want to do here. This is how Godot expects you to "extend" `get` and `set`, basically (not override it, but that's not really what you need it seems?). You can write all the code you wanted in `_get` and `_set`, and Godot will still allow you to call `get` and `set` AND take the `_get` and `_set` into account. 

basically:

```gdscript
func _get(key):
    if key == "foo":
        return 42
```
```
data.get("foo") # gets 42 because Godot will call `_get` internally
data.foo # gets 42 too
```

I understand why you'd expect to do that with `get` and `set` directly so badly in principle (as many other overridable functions existing under a different `_` name), but it isn't how Godot's scripting API works. As of right now, a native function cannot both represent a regular function and a virtual one. This would need some work to have both, but in the meantime your use case can be solved, except for the internal naming, which is relatively minor (i.e code using your class can still be written as if the names were like you wanted). Not saying the issue shouldn't be investigated, but that there is an easy workaround.
Still doesn't solve my naming issue.
This was changed in #54676. It's being reconsidered / further discussed in #54949.
@akien-mga #54949 seems to concern member variables only, not functions, which are the problematic point here.
You are supposed to override only virtual functions and they are all prefixed with `_` (e.g. `_ready()`, `_get_drag_data()` etc.). We should give a clear error when trying to shadow a built-in function, but non-virtual overriding likely won't happen.
> You are supposed to override only virtual functions and they are all prefixed with `_` (e.g. `_ready()`, `_get_drag_data()` etc.). We should give a clear error when trying to shadow a built-in function, but non-virtual overriding likely won't happen.

Thank you for those explanations. However it would be better that behavior is describe somewhere (since it didn't behave that way in Godot 2/3), and also Godot itself should raise an error to warn the user. atm, this is **not** user friendly.
I code like this:
```
class Test
    enum Type{Input,Output}
```
It will report error: `Enum member Input has the same name as a global class`
I think this was valid code, because I can use it like this: `Test.Type.Input`
Another use case:
`set_owner` and `get_owner`, those functions are defined by godot so not usable in GDScript.
Wasted time because:
- There is no warning when we overwrite a godot's native function
- There is no way to `@override` such functions
To repeat what @Zylann was explaining, not all functions in Godot can be overridden. That's the same in any language, some functions are virtual, others are not. In Godot's scripting API there's another level of indirection as some functions might be virtual in C++ but that doesn't transfer to the scripting API. "Virtual" methods are implemented by calling methods via StringName, which is much less efficient than what a C++ compiler can do for you with the virtual function table.

So there's no intention to support overriding *any* method such as `Object.set` or `Node.set_owner`. These methods are used by the engine API internally and making them overridable would lead to terrible performance, and a very simple footgun for users to break the API by changing the way things are expected to behave internally (i.e. if you were to replace `set_owner` and expect it to be called internally, you'd have to make sure that you reimplement exactly what the C++ method does - which might not even be possible).

Now, we should indeed add explicit warnings whenever you shadow such a method unknowingly, as the way it behaves is not intuitive at all (and can still lead to bugs for internal engine calls using `call` or `call_deferred`):

```gdscript
extends Control

var test = 0

func set(property, value):
	print("Setting '%s' to '%s'." % [property, str(value)])
	set(property, value)

func _ready():
	test = 1
	set("test", 2)
	self.set("test", 3)
	$".".set("test", 4)
	call("set", "test", 5)
	$".".call("set", "test", 6)
	call_deferred("set", "test", 7)
	$".".call_deferred("set", "test", 8)
	get_script().set("test", 9)
```

This prints:
```
Setting 'test' to '3'.
Setting 'test' to '5'.
Setting 'test' to '6'.
Setting 'test' to '7'.
Setting 'test' to '8'.
```

IMO it's far from obvious, and not something we should aim to support.

So I would ensure that shadowing an internal function raises a warning, as done in #54949 for variables.
IMO this could even be an error, but given the reactions to #54676, I guess we can start with a warning and let people who still want to play with fire mark the warning as ignored and accept they're using an unsupported feature.
I know it is outstandingly difficult to reimplement, but it is worth reconsidering this stance, at least by adding way more overridable methods, since these "issues" are being reported by the userbase fairly frequently, as it often is limiting, compared to 3.x.
I need too to overwrite **all** functions to buld a spy to verify function interactions.
This was still possible on Godot3 if this will be not allowed anymore than please give me a way to check if a function called or not.
If this still possible than please tell me how to spy a function call without overwriting a function?

I need it! My plugin GdUnit3 provides spy and mock to verify interactions on a instance.

Any update here? 
Hello I don't understand why you want to restrict developers so much, there is no real reason for that.
Whether shadowing or override this is blurring in my eyes. 

Overriding methods/functions is still a valid option in many languages, and was possible in Godot 3.x.
Instead of responding to the users, it is worsened, I would like to refer only to the sorry topic private/protected/public accessors there is stubbornly blocked.
What speaks against it?
It is only important that if you overwrite a function a warning, error (configurable) gets as feedback in the editor to avoid accidental overwrites.

So please don't bring in such regressions! 
This makes me frustrating.

Here is my suggestion
a) allow function overrides
b) show a warning for overwritten functions
c) introduce an annotation `@override` to mark as allowed override and disables the warning
d) an editor setting to configure accidental overrides as a warning or error
e) finally respond to the users and introduce the long requested private/protected accessors
   @private, @protected
   This would also help to disallow overwrites if explicitly requested by the dev
I feel `@override` would not be the right name for this though, the problem is to allow *shadowing core class methods*. `@override` would make it look like it makes it work like a virtual function override, when it doesn't works in all cases, and has limitations. Maybe something like `@overwrite` or `@shadow` or `@scriptonlyoverride`, or whatever retains the fact it's not a typical override like every other language.
Again my question, why is it forbidden to override core class functions?
I want to explicitly overwrite it and not shadow it!

I have written a unit test plugin 'GdUnit3', which provides mock and spy functionality and I'm explicitly instructed to be able to overwrite core functions to carry if and how often they were called.

And again this is a strong limitation compared to Godot3 
Can the speed up really be justified to remove what was existing functionality in 3.0, if so I am curious as to just how much, I am not demanding numbers just curious if any one has numbers.

Would it be possible to allow the over-ride of core function calls at least for debug builds but release builds could still retain the speed to allow the testing in debug builds?

In order to work around this new feature, developers will need to write additional functions to make the call to a core class in order to successfully Spy on what functions were called. This is a waste of developer time by writing additional lines of code, if it could be avoided I would prefer such a solution. 
> Can the speed up really be justified to remove what was existing functionality in 3.0, if so I am curious as to just how much, I am not demanding numbers just curious if any one has numbers.

The GDScript implementations are entirely different between 3.x and 4.0, so it's not possible to compare performance with this change alone.

> Would it be possible to allow the over-ride of core function calls at least for debug builds but release builds could still retain the speed to allow the testing in debug builds?

This would result in bugs that only occur in projects exported in release mode, which are difficult to track down. (Remember the whole dangling Variant thing?)

We should aim to have fewer behavior differences between debug and release, not more 🙂 
Here are some numbers.

Script 1 (overwrites a function)
```GDScript
extends RandomNumberGenerator

func randi_range(a, b):
	pass
```

Script 2 (calls script 1)
```GDScript
extends EditorScript
@tool

func _run() -> void:
	var rng: RandomNumberGenerator = preload("res://FakeRNG.gd").new()
	
	var time = Time.get_ticks_usec()
	for i in 1000000:
		var test = rng.randi_range(0, 100)
	print(Time.get_ticks_usec() - time)
```

Execution time in 3.x `release_debug` build:
**210ms**

Execution time in 4.0 `debug` build:
**140ms**

In 4.0 this executes 33% faster, *despite I used a debug build which comes with severe speed handicap*. If we compared official builds, the difference would be even greater. This kind of optimization would be impossible if functions were overwritable.
`This kind of optimization would be impossible if functions were overwritable.`
As far i understand Godot4 used function ptr to jump to the right method.
Why should this be a problem for overriding?
Its just an another ptr to the overriden method or not?
Overriding *could* be used if ptr calls can't be determined (that is, calling a method on untyped object), but then you'd get inconsistent behavior. It would be confusing.
Worth keeping an eventual solution in the back of the head. `super()` on virtual calls (`_process`, `_ready`...) became mandatory in Godot 4.0 instead of implicit, because it was unexpected behavior to a lot of users (even if it was intentional and documented). One _could argue_ that the same could be said for being able to override built-in functions **_to an extent_**. It is no longer "allowed", and it was not really accounted for, yet some users believe this to be the expected behavior.
Sorry, but any good programming language allows us to override methods to extend existing functionallities.
Now all of a sudden not allowing it anymore will push many devs over the head.
The argumentation to write an equivalent function with underscore that calls the core function is total nonsense.
This just creates useless code noise and forces us to define pseudo names for existing core functions.
A simple example, you want to log if a child node was added.

Godot 3:
```gdscript
func add_child(node: Node, force_readable_name: bool = false) -> void:
	LOG.debug("node added %s" % [node])
	.add_child(node, force_readable_name)
```

for Godot4 it would look like this
```gdscript
func _add_child(node: Node, force_readable_name: bool = false, internal := INTERNAL_MODE_DISABLED) -> void:
	LOG.debug("node added %s" % [node])
	super.add_child(node, force_readable_name, internal)
```
now we have two functions with almost the same name `_add_child` and `add_child`.
Furthermore it leads to confusion when providing such an API, should i call `_add_child` or `add_child`.



> Sorry, but any good programming language allows us to override methods to extend existing functionallities.

No they don't. Unless you say that C++, C#, Rust or any compiled language are not good. They require you to explicitly mark overrideable functions as virutal and most of their functions aren't virtual.
ok `good programming language` was probably said a little too harshly ;) 

to get ahead here and solve my own problem.
How can I determine if and how often and with which arguments a function was called without overwriting it?

Due to the new limitation I can not realize a spy or mock on core classes anymore

Best regards
Just chiming in to say in my issue #69765 overriding would be really nice for plugin development. I wanted to have a `RandomTimer` class ([see here for Godot 3 implementation](https://github.com/firebelley/godot-addons/blob/master/node_library/scripts/random_timer.gd)) with the same methods as the build in `Timer` so that it can be used in the same way. I don't really want to tell users "This is a timer, but make sure to call start_random instead of start, otherwise it won't work as expected". Lots of room for error there.
3.x:

![](https://user-images.githubusercontent.com/47700418/209368611-1861e422-c4e1-4a4e-ae22-b1e57b5ba4c6.png)

4.0-dev:

![](https://user-images.githubusercontent.com/47700418/209369109-b11ce7e0-9f47-4986-a641-c8384a3407ee.png)

In both 3.x and 4.0-dev you can't really override non-virtual methods (to have C++ call them), this only works for GDScript.

I'm not sure if the behavior in 4.0 should be the same as in 3.x, or if we should completely disallow overriding non-virtual native functions. But if we choose the first option, then we should add a warning and/or make another indicator (blue arrow).

Given that `call("native_func_name")` and `native_func_name.call()` call the GDScript method, can we classify this as a bug?

I'm a bit confused by this debate. There seems to be many comments about to how difficult it is to allow overriding (or shadowing) methods of built-in classes. Since this was a capability in 3.5.1 it suggests it's difficult only because of engine changes made for "GDScript 2.0". That fact alone means it's a regression. Call it a bug or a feature, but it's a regression.

So here are a few serious questions.
1. If method overriding/shadowing is allowed for custom methods in custom classes, why not built-in classes? What's so special about built-in classes?
2. If built-in classes are so special why not make them all FINAL so they can't be extended?
3. Is this just a case of "trying to protect the programmer"? If so, from what?
4. Is GDScript intended to be an object-oriented-like language? Method overrides are a core feature of OOP languages.

EDIT: If this was an intentional change why is it implemented as a silent failure? At the very minimum there should be an error raised during editing that states built-in class methods are FINAL and cannot be overridden/shadowed.

Opinion: We are talking about a language feature that previously existed, is expected to exist, and is widely used.  If it is truly difficult to support method override/shadow in 4.0, maybe the solution needs to be revisited?

In the old days issues like this were referred to as "trying to fix the solution rather than solving the problem". 

@tx350z It didn't exist in 3.x. Unless the method is registered in a way that allows extending it with scripting, which few are, the engine cannot call into it, it has no idea that you've overridden the method. What might have worked is scripts calling methods of other scripts directly. But it wasn't really overriding the method, it was indeed just shadowing it for the scripting side of things, while the engine was not aware that you might've overridden it.

So it was a bad idea to do it, even if it could be perceived as working in some practical scenarios.
> If method overriding/shadowing is allowed for custom methods in custom classes, why not built-in classes? What's so special about built-in classes?

All methods in custom classes are *virtual* by default.

> If built-in classes are so special why not make them all FINAL so they can't be extended?

They are not special. You can extend them and override their *virtual* methods. Every single script extends some class, so making them non-extendable would make the engine literally unusable.

> Is this just a case of "trying to protect the programmer"? If so, from what?

No, it was done for performance. Calling methods in GDScript 2.0 is **much** faster thanks to that.

> Is GDScript intended to be an object-oriented-like language? Method overrides are a core feature of OOP languages.

And GDScript does support overriding *virtual* methods like every OOP language. The behavior in 3.x was a convenient bug that went against the OOP, because it allowed shadowing methods that aren't *virtual*.
The methods that you can override are marked as such in the documentation:
![image](https://user-images.githubusercontent.com/2223172/210354110-9544c1dd-0ed3-47a6-8001-cae264ba69e1.png)

> If this was an intentional change why is it implemented as a silent failure?

Because it's bugged. It's the only reason why this issue is still open.
So the ability to shadow built-in class methods in 3.x was a bug that is now fixed in 4.0?
If my vote counts, I'd like the bug back please.
@tx350z It didn't actually work in 3.x. Bringing it back, if it even was sensible, would only create an illusion that you can override built-in methods when in fact you cannot. And shadowing is not a feature of any language, it's a bug or a code smell at the very least.
> @tx350z It didn't actually work in 3.x. [snipped]

This seems to work in 3.5.1

main.gd
```
extends Node2D

var cfg:ExtConfigFile = ExtConfigFile.new();

func _ready():
	cfg.set_value("my_section","my_value",1);
	print("Unsaved? %s"%[cfg.is_unsaved()]);
	cfg.save("res://junk.ini");
	print("Unsaved? %s"%[cfg.is_unsaved()]);
	cfg.load("res://junk.ini");
	print("Unsaved? %s"%[cfg.is_unsaved()]);
	cfg.set_value("my_section","my_value2",2);
	print("Unsaved? %s"%[cfg.is_unsaved()]);
	cfg.erase_section_key("my_section","my_value");
	cfg.save("res://junk.ini")
	return;

```

ext_config_file.gd
```
class_name ExtConfigFile
extends ConfigFile

var _is_unsaved:bool = false;

## Returns true if there are unsaved changes.
func is_unsaved()->bool:
	return _is_unsaved;

####
## The methods below shadow ConfigFile methods.
####

## Shadows ConfigFile.load()
## Adds logic to set saved state and and emit changed signal.
func load(path:String)->int:
	print("ExtConfigFile.load()");
	var err = .load(path);
	_is_unsaved = true;
	return err;

## Shadows ConfigFile.load_encrypted()
## Adds logic to set saved state and and emit changed signal.
func load_encrypted(path:String, key:PoolByteArray)->int:
	print("ExtConfigFile.load_encrypted()");
	var err = .load_encrypted(path,key);
	_is_unsaved = true;
	return err;

## Shadows ConfigFile.load_encrypted_pass()
## Adds logic to set saved state and and emit changed signal.
func load_encrypted_pass(path:String, password:String)->int:
	print("ExtConfigFile.load_encrypted_pass()");
	var err = .load_encrypted_pass(path,password);
	_is_unsaved = true;
	return err;

## Shadows ConfigFile.save()
## Adds logic to set saved state and and emit changed signal.
func save(path:String)->int:
	print("ExtConfigFile.save()");
	var err = .save(path);
	if err == OK:
		_is_unsaved = false;
	return err;

## Shadows ConfigFile.save_encrypted()
## Adds logic to set saved state and and emit changed signal.
func save_encrypted(path:String,key:PoolByteArray)->int:
	print("ExtConfigFile.save_encrypted()");
	var err = .save_encrypted(path,key);
	if err == OK:
		_is_unsaved = false;
	return err;

## Shadows ConfigFile.save_encrypted_pass()
## Adds logic to set saved state and and emit changed signal.
func save_encrypted_pass(path:String,password:String)->int:
	print("ExtConfigFile.save_encrypted_pass()");
	var err = .save_encrypted_pass(path, password);
	if err == OK:
		_is_unsaved = false;
	return err;

## Shadows ConfigFile.set_value()
## Adds logic to set saved state and and emit changed signal.
func set_value(section:String, key:String, new_value)->void:
	print("ExtConfigFile.set_value()");
	# check for a value change
	var old_value;
	if .has_section(section) and has_section_key(section,key):
		old_value = .get_value(section,key);
	
	if new_value != old_value:
		.set_value(section,key,new_value);
		_is_unsaved = true;
	return;

## Shadows ConfigFile.erase_section()
## Adds logic to set saved state and and emit changed signal.
func erase_section(section:String)->void:
	print("ExtConfigFile.erase_section()");
	var old_value:Dictionary;
	if .has_section(section):
		# Loop through keys in the section
		for key in .get_section_keys(section):
			old_value[key] = .get_value(section,key);
	else:
		return;
	.erase_section(section);
	_is_unsaved = true;
	return;

## Shadows ConfigFile.erase_section_key()
## Adds logic to set saved state and and emit changed signal.
func erase_section_key(section:String,key:String)->void:
	print("ExtConfigFile.erase_section_key()");
	var old_value = null;
	if .has_section(section) && .has_section_key(section,key):
		old_value = .get_value(section,key);
	else:
		return;
	.erase_section_key(section,key);
	_is_unsaved = true;
	return;

## Shadows ConfigFile.parse()
## Adds logic to set saved state and and emit changed signal.
func parse(data:String,auto_save:bool=false)->int:
	print("ExtConfigFile.parse()");
	var err:int;
	err = .parse(data);
	_is_unsaved = true; # Assume values were changed even if the parse failed
	return err;

```

junk.ini after run.

```
[my_section]

my_value2=2

```

![ext_config_file-Console-Output](https://user-images.githubusercontent.com/956567/210372701-d5390c96-9acb-486c-b4d4-a2135319884d.png)

> @tx350z It didn't actually work in 3.x. Bringing it back, if it even was sensible, would only create an illusion that you can override built-in methods when in fact you cannot. And shadowing is not a feature of any language, it's a bug or a code smell at the very least.

Thats not true!
I still using override built-in methods on Godot 3.5.1 as build a mock/spy  by fuction doubling and calling than the original implementation.
In 3.x you can "override" non-virtual methods, but only for GDScript, not C++. That is, if some method is called directly, the C++ original will be called, not your GDScript override (this does not apply to calling via `Object.call`). This is especially noticeable when trying to override a property setter or getter.
@tx350z Yes, this will work for scripting, but it won't work if the engine needs to call the same method that you are trying to override. That's why we make a distinction between overriding and shadowing. Shadowing is a potential bug and code smell, we don't recommend getting into this situation, so there is little reason to support it. And apparently, avoiding it improves the execution speed of GDScript VM. Overriding of non-virtual methods is not available at all.
I disagree with the claim "there is little reason to support it". I can't think of a single use case where I want the engine to call my custom functionality. So I'll depart this discussion.

For my needs I'm now building an editor plugin that automates code generation of wrapper classes around built-in classes so I can extend functionality as needed. The result will no doubt be slower than having built-in support but it is what it is.

P.S. My hope was 4.0 would support polymorphism but that's not likely to happen if simple method overrides will never be supported.
I think we have two options:

1. Prohibit "overriding" non-virtual methods (this should produce an error).
2. Allow non-virtual methods to be "overridden" (as in 3.x), but generate a warning about it. And also we can change the method override indicator (blue arrow) for this case.
> I can't think of a single use case where I want the engine to call my custom functionality.

Whether you generally want to or not, if it was true overriding then the expectation from a programmer would be that the engine would call the override and not the original. Of course, not every public/exposed method is called by the engine, but some do, and consistency is expected. Allowing just shadowing wouldn't be very OOP of us, would it?

And overrides in general _are_ allowed, but we have strict rules that the method should be declared as virtual. It's just not all built-in methods are, for one reason or another (already explained in the discussion above). This is all completely within the OOP principles.
> Whether you generally want to or not, if it was true overriding then the expectation from a programmer would be that the engine would call the override and not the original. Of course, not every public/exposed method is called by the engine, but some do, and consistency is expected. Allowing just shadowing wouldn't be very OOP of us, would it?
> 
> And overrides in general _are_ allowed, but we have strict rules that the method should be declared as virtual. It's just not all built-in methods are, for one reason or another (already explained in the discussion above). This is all completely within the OOP principles.

Four Pillars of OOP:
1 Inheritance: child classes inherit data and behaviors from the parent class **with the ability to extend or replace parent behaviors**
2 Encapsulation: containing information in an object, **exposing only selected information**
3 **Abstraction: only exposing high-level public methods for accessing an object**
4 **Polymorphism: functions bearing the same name, but each may have different behaviors**
Reopening for now as #72487 was reverted.



Fix (patch):
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index e429759e933c..73cf400f04d6 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -405,9 +405,15 @@
 		<member name="debug/gdscript/warnings/function_used_as_property" type="int" setter="" getter="" default="1">
 			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when using a function as if it is a property.
 		</member>
+		<member name="debug/gdscript/warnings/get_node_default_without_onready" type="int" setter="" getter="" default="2">
+			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when [method Node.get_node] (or the shorthand [code]$[/code]) is used as default value of a class variable without the [code]@onready[/code] annotation.
+		</member>
 		<member name="debug/gdscript/warnings/incompatible_ternary" type="int" setter="" getter="" default="1">
 			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a ternary operator may emit values with incompatible types.
 		</member>
+		<member name="debug/gdscript/warnings/inference_on_variant" type="int" setter="" getter="" default="2">
+			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a static inferred type uses a [Variant] as initial value, which makes the static type to also be Variant.
+		</member>
 		<member name="debug/gdscript/warnings/int_as_enum_without_cast" type="int" setter="" getter="" default="1">
 			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when trying to use an integer as an enum without an explicit cast.
 		</member>
@@ -420,6 +426,12 @@
 		<member name="debug/gdscript/warnings/narrowing_conversion" type="int" setter="" getter="" default="1">
 			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when passing a floating-point value to a function that expects an integer (it will be converted and lose precision).
 		</member>
+		<member name="debug/gdscript/warnings/native_method_override" type="int" setter="" getter="" default="2">
+			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a method in the script overrides a native method, because it may not behave as expected.
+		</member>
+		<member name="debug/gdscript/warnings/onready_with_export" type="int" setter="" getter="" default="2">
+			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when the [code]@onready[/code] annotation is used together with the [code]@export[/code] annotation, since it may not behave as expected.
+		</member>
 		<member name="debug/gdscript/warnings/property_used_as_function" type="int" setter="" getter="" default="1">
 			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when using a property as if it is a function.
 		</member>
@@ -477,7 +489,7 @@
 		<member name="debug/gdscript/warnings/unsafe_property_access" type="int" setter="" getter="" default="0">
 			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when accessing a property whose presence is not guaranteed at compile-time in the class.
 		</member>
-		<member name="debug/gdscript/warnings/unsafe_void_return" type="int" setter="" getter="" default="0">
+		<member name="debug/gdscript/warnings/unsafe_void_return" type="int" setter="" getter="" default="1">
 			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when returning a call from a [code]void[/code] function when such call cannot be guaranteed to be also [code]void[/code].
 		</member>
 		<member name="debug/gdscript/warnings/unused_local_constant" type="int" setter="" getter="" default="1">
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index cdb4f29854bc..8a723cab9c75 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -138,13 +138,25 @@ static GDScriptParser::DataType make_enum_type(const StringName &p_enum_name, co
 }
 
 static GDScriptParser::DataType make_native_enum_type(const StringName &p_enum_name, const StringName &p_native_class, const bool p_meta = true) {
-	GDScriptParser::DataType type = make_enum_type(p_enum_name, p_native_class, p_meta);
+	// Find out which base class declared the enum, so the name is always the same even when coming from other contexts.
+	StringName native_base = p_native_class;
+	while (true && native_base != StringName()) {
+		if (ClassDB::has_enum(native_base, p_enum_name, true)) {
+			break;
+		}
+		native_base = ClassDB::get_parent_class_nocheck(native_base);
+	}
+
+	GDScriptParser::DataType type = make_enum_type(p_enum_name, native_base, p_meta);
+	if (p_meta) {
+		type.builtin_type = Variant::NIL; // Native enum types are not Dictionaries
+	}
 
 	List<StringName> enum_values;
-	ClassDB::get_enum_constants(p_native_class, p_enum_name, &enum_values);
+	ClassDB::get_enum_constants(native_base, p_enum_name, &enum_values, true);
 
 	for (const StringName &E : enum_values) {
-		type.enum_values[E] = ClassDB::get_integer_constant(p_native_class, E);
+		type.enum_values[E] = ClassDB::get_integer_constant(native_base, E);
 	}
 
 	return type;
@@ -782,6 +794,22 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
 	resolving_datatype.kind = GDScriptParser::DataType::RESOLVING;
 
 	{
+#ifdef DEBUG_ENABLED
+		HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings;
+		GDScriptParser::Node *member_node = member.get_source_node();
+		if (member_node && member_node->type != GDScriptParser::Node::ANNOTATION) {
+			// Apply @warning_ignore annotations before resolving member.
+			for (GDScriptParser::AnnotationNode *&E : member_node->annotations) {
+				if (E->name == SNAME("@warning_ignore")) {
+					resolve_annotation(E);
+					E->apply(parser, member.variable);
+				}
+			}
+			for (GDScriptWarning::Code ignored_warning : member_node->ignored_warnings) {
+				parser->ignored_warnings.insert(ignored_warning);
+			}
+		}
+#endif
 		switch (member.type) {
 			case GDScriptParser::ClassNode::Member::VARIABLE: {
 				check_class_member_name_conflict(p_class, member.variable->identifier->name, member.variable);
@@ -790,9 +818,48 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
 
 				// Apply annotations.
 				for (GDScriptParser::AnnotationNode *&E : member.variable->annotations) {
-					resolve_annotation(E);
-					E->apply(parser, member.variable);
+					if (E->name != SNAME("@warning_ignore")) {
+						resolve_annotation(E);
+						E->apply(parser, member.variable);
+					}
+				}
+#ifdef DEBUG_ENABLED
+				if (member.variable->exported && member.variable->onready) {
+					parser->push_warning(member.variable, GDScriptWarning::ONREADY_WITH_EXPORT);
 				}
+				if (member.variable->initializer) {
+					// Check if it is call to get_node() on self (using shorthand $ or not), so we can check if @onready is needed.
+					// This could be improved by traversing the expression fully and checking the presence of get_node at any level.
+					if (!member.variable->onready && member.variable->initializer && (member.variable->initializer->type == GDScriptParser::Node::GET_NODE || member.variable->initializer->type == GDScriptParser::Node::CALL || member.variable->initializer->type == GDScriptParser::Node::CAST)) {
+						GDScriptParser::Node *expr = member.variable->initializer;
+						if (expr->type == GDScriptParser::Node::CAST) {
+							expr = static_cast<GDScriptParser::CastNode *>(expr)->operand;
+						}
+						bool is_get_node = expr->type == GDScriptParser::Node::GET_NODE;
+						bool is_using_shorthand = is_get_node;
+						if (!is_get_node && expr->type == GDScriptParser::Node::CALL) {
+							is_using_shorthand = false;
+							GDScriptParser::CallNode *call = static_cast<GDScriptParser::CallNode *>(expr);
+							if (call->function_name == SNAME("get_node")) {
+								switch (call->get_callee_type()) {
+									case GDScriptParser::Node::IDENTIFIER: {
+										is_get_node = true;
+									} break;
+									case GDScriptParser::Node::SUBSCRIPT: {
+										GDScriptParser::SubscriptNode *subscript = static_cast<GDScriptParser::SubscriptNode *>(call->callee);
+										is_get_node = subscript->is_attribute && subscript->base->type == GDScriptParser::Node::SELF;
+									} break;
+									default:
+										break;
+								}
+							}
+						}
+						if (is_get_node) {
+							parser->push_warning(member.variable, GDScriptWarning::GET_NODE_DEFAULT_WITHOUT_ONREADY, is_using_shorthand ? "$" : "get_node()");
+						}
+					}
+				}
+#endif
 			} break;
 			case GDScriptParser::ClassNode::Member::CONSTANT: {
 				check_class_member_name_conflict(p_class, member.constant->identifier->name, member.constant);
@@ -878,6 +945,10 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
 				}
 			} break;
 			case GDScriptParser::ClassNode::Member::FUNCTION:
+				for (GDScriptParser::AnnotationNode *&E : member.function->annotations) {
+					resolve_annotation(E);
+					E->apply(parser, member.function);
+				}
 				resolve_function_signature(member.function, p_source);
 				break;
 			case GDScriptParser::ClassNode::Member::ENUM_VALUE: {
@@ -931,6 +1002,9 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
 				ERR_PRINT("Trying to resolve undefined member.");
 				break;
 		}
+#ifdef DEBUG_ENABLED
+		parser->ignored_warnings = previously_ignored_warnings;
+#endif
 	}
 
 	parser->current_class = previous_class;
@@ -1059,19 +1133,7 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
 				resolve_annotation(E);
 				E->apply(parser, member.function);
 			}
-
-#ifdef DEBUG_ENABLED
-			HashSet<uint32_t> previously_ignored = parser->ignored_warning_codes;
-			for (uint32_t ignored_warning : member.function->ignored_warnings) {
-				parser->ignored_warning_codes.insert(ignored_warning);
-			}
-#endif // DEBUG_ENABLED
-
 			resolve_function_body(member.function);
-
-#ifdef DEBUG_ENABLED
-			parser->ignored_warning_codes = previously_ignored;
-#endif // DEBUG_ENABLED
 		} else if (member.type == GDScriptParser::ClassNode::Member::VARIABLE && member.variable->property != GDScriptParser::VariableNode::PROP_NONE) {
 			if (member.variable->property == GDScriptParser::VariableNode::PROP_INLINE) {
 				if (member.variable->getter != nullptr) {
@@ -1102,9 +1164,9 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
 		GDScriptParser::ClassNode::Member member = p_class->members[i];
 		if (member.type == GDScriptParser::ClassNode::Member::VARIABLE) {
 #ifdef DEBUG_ENABLED
-			HashSet<uint32_t> previously_ignored = parser->ignored_warning_codes;
-			for (uint32_t ignored_warning : member.function->ignored_warnings) {
-				parser->ignored_warning_codes.insert(ignored_warning);
+			HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings;
+			for (GDScriptWarning::Code ignored_warning : member.variable->ignored_warnings) {
+				parser->ignored_warnings.insert(ignored_warning);
 			}
 			if (member.variable->usages == 0 && String(member.variable->identifier->name).begins_with("_")) {
 				parser->push_warning(member.variable->identifier, GDScriptWarning::UNUSED_PRIVATE_CLASS_VARIABLE, member.variable->identifier->name);
@@ -1179,7 +1241,7 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
 					}
 				}
 #ifdef DEBUG_ENABLED
-				parser->ignored_warning_codes = previously_ignored;
+				parser->ignored_warnings = previously_ignored_warnings;
 #endif // DEBUG_ENABLED
 			}
 		}
@@ -1289,6 +1351,11 @@ void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node, bool p_is_root
 void GDScriptAnalyzer::resolve_annotation(GDScriptParser::AnnotationNode *p_annotation) {
 	ERR_FAIL_COND_MSG(!parser->valid_annotations.has(p_annotation->name), vformat(R"(Annotation "%s" not found to validate.)", p_annotation->name));
 
+	if (p_annotation->is_resolved) {
+		return;
+	}
+	p_annotation->is_resolved = true;
+
 	const MethodInfo &annotation_info = parser->valid_annotations[p_annotation->name].info;
 
 	const List<PropertyInfo>::Element *E = annotation_info.arguments.front();
@@ -1355,6 +1422,13 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
 	}
 	p_function->resolved_signature = true;
 
+#ifdef DEBUG_ENABLED
+	HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings;
+	for (GDScriptWarning::Code ignored_warning : p_function->ignored_warnings) {
+		parser->ignored_warnings.insert(ignored_warning);
+	}
+#endif
+
 	GDScriptParser::FunctionNode *previous_function = parser->current_function;
 	parser->current_function = p_function;
 
@@ -1421,7 +1495,8 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
 		int default_par_count = 0;
 		bool is_static = false;
 		bool is_vararg = false;
-		if (!p_is_lambda && get_function_signature(p_function, false, base_type, function_name, parent_return_type, parameters_types, default_par_count, is_static, is_vararg)) {
+		StringName native_base;
+		if (!p_is_lambda && get_function_signature(p_function, false, base_type, function_name, parent_return_type, parameters_types, default_par_count, is_static, is_vararg, &native_base)) {
 			bool valid = p_function->is_static == is_static;
 			valid = valid && parent_return_type == p_function->get_datatype();
 
@@ -1447,8 +1522,8 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
 						parameter = "Variant";
 					}
 					parent_signature += parameter;
-					if (j == parameters_types.size() - default_par_count) {
-						parent_signature += " = default";
+					if (j >= parameters_types.size() - default_par_count) {
+						parent_signature += " = <default>";
 					}
 
 					j++;
@@ -1464,6 +1539,11 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
 
 				push_error(vformat(R"(The function signature doesn't match the parent. Parent signature is "%s".)", parent_signature), p_function);
 			}
+#ifdef DEBUG_ENABLED
+			if (native_base != StringName()) {
+				parser->push_warning(p_function, GDScriptWarning::NATIVE_METHOD_OVERRIDE, function_name, native_base);
+			}
+#endif
 		}
 #endif // TOOLS_ENABLED
 	}
@@ -1472,6 +1552,9 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
 		p_function->set_datatype(prev_datatype);
 	}
 
+#ifdef DEBUG_ENABLED
+	parser->ignored_warnings = previously_ignored_warnings;
+#endif
 	parser->current_function = previous_function;
 }
 
@@ -1481,6 +1564,13 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun
 	}
 	p_function->resolved_body = true;
 
+#ifdef DEBUG_ENABLED
+	HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings;
+	for (GDScriptWarning::Code ignored_warning : p_function->ignored_warnings) {
+		parser->ignored_warnings.insert(ignored_warning);
+	}
+#endif
+
 	GDScriptParser::FunctionNode *previous_function = parser->current_function;
 	parser->current_function = p_function;
 
@@ -1498,6 +1588,9 @@ void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_fun
 		}
 	}
 
+#ifdef DEBUG_ENABLED
+	parser->ignored_warnings = previously_ignored_warnings;
+#endif
 	parser->current_function = previous_function;
 }
 
@@ -1538,16 +1631,16 @@ void GDScriptAnalyzer::resolve_suite(GDScriptParser::SuiteNode *p_suite) {
 		}
 
 #ifdef DEBUG_ENABLED
-		HashSet<uint32_t> previously_ignored = parser->ignored_warning_codes;
-		for (uint32_t ignored_warning : stmt->ignored_warnings) {
-			parser->ignored_warning_codes.insert(ignored_warning);
+		HashSet<GDScriptWarning::Code> previously_ignored_warnings = parser->ignored_warnings;
+		for (GDScriptWarning::Code ignored_warning : stmt->ignored_warnings) {
+			parser->ignored_warnings.insert(ignored_warning);
 		}
 #endif // DEBUG_ENABLED
 
 		resolve_node(stmt);
 
 #ifdef DEBUG_ENABLED
-		parser->ignored_warning_codes = previously_ignored;
+		parser->ignored_warnings = previously_ignored_warnings;
 #endif // DEBUG_ENABLED
 
 		decide_suite_type(p_suite, stmt);
@@ -1599,6 +1692,11 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi
 			} else if (initializer_type.kind == GDScriptParser::DataType::BUILTIN && initializer_type.builtin_type == Variant::NIL && !is_constant) {
 				push_error(vformat(R"(Cannot infer the type of "%s" %s because the value is "null".)", p_assignable->identifier->name, p_kind), p_assignable->initializer);
 			}
+#ifdef DEBUG_ENABLED
+			if (initializer_type.is_hard_type() && initializer_type.is_variant()) {
+				parser->push_warning(p_assignable, GDScriptWarning::INFERENCE_ON_VARIANT, p_kind);
+			}
+#endif
 		} else {
 			if (!initializer_type.is_set()) {
 				push_error(vformat(R"(Could not resolve type for %s "%s".)", p_kind, p_assignable->identifier->name), p_assignable->initializer);
@@ -2931,7 +3029,11 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
 
 		// Enums do not have functions other than the built-in dictionary ones.
 		if (base_type.kind == GDScriptParser::DataType::ENUM && base_type.is_meta_type) {
-			push_error(vformat(R"*(Enums only have Dictionary built-in methods. Function "%s()" does not exist for enum "%s".)*", p_call->function_name, base_type.enum_type), p_call->callee);
+			if (base_type.builtin_type == Variant::DICTIONARY) {
+				push_error(vformat(R"*(Enums only have Dictionary built-in methods. Function "%s()" does not exist for enum "%s".)*", p_call->function_name, base_type.enum_type), p_call->callee);
+			} else {
+				push_error(vformat(R"*(The native enum "%s" does not behave like Dictionary and does not have methods of its own.)*", base_type.enum_type), p_call->callee);
+			}
 		} else if (!p_call->is_super && callee_type != GDScriptParser::Node::NONE) { // Check if the name exists as something else.
 			GDScriptParser::IdentifierNode *callee_id;
 			if (callee_type == GDScriptParser::Node::IDENTIFIER) {
@@ -4310,15 +4412,27 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo
 			}
 			elem_type.is_constant = false;
 			result.set_container_element_type(elem_type);
+		} else if (p_property.type == Variant::INT) {
+			// Check if it's enum.
+			if (p_property.class_name != StringName()) {
+				Vector<String> names = String(p_property.class_name).split(".");
+				if (names.size() == 2) {
+					result = make_native_enum_type(names[1], names[0], false);
+					result.is_constant = false;
+				}
+			}
 		}
 	}
 	return result;
 }
 
-bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType p_base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg) {
+bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType p_base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg, StringName *r_native_class) {
 	r_static = false;
 	r_vararg = false;
 	r_default_arg_count = 0;
+	if (r_native_class) {
+		*r_native_class = StringName();
+	}
 	StringName function_name = p_function;
 
 	bool was_enum = false;
@@ -4453,6 +4567,12 @@ bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bo
 		if (valid && Engine::get_singleton()->has_singleton(base_native)) {
 			r_static = true;
 		}
+#ifdef DEBUG_ENABLED
+		MethodBind *native_method = ClassDB::get_method(base_native, function_name);
+		if (native_method && r_native_class) {
+			*r_native_class = native_method->get_instance_class();
+		}
+#endif
 		return valid;
 	}
 
diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h
index b51564fb0a23..ab2d2b3c6c56 100644
--- a/modules/gdscript/gdscript_analyzer.h
+++ b/modules/gdscript/gdscript_analyzer.h
@@ -117,7 +117,7 @@ class GDScriptAnalyzer {
 	static GDScriptParser::DataType type_from_metatype(const GDScriptParser::DataType &p_meta_type);
 	GDScriptParser::DataType type_from_property(const PropertyInfo &p_property, bool p_is_arg = false) const;
 	GDScriptParser::DataType make_global_class_meta_type(const StringName &p_class_name, const GDScriptParser::Node *p_source);
-	bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg);
+	bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg, StringName *r_native_class = nullptr);
 	bool function_signature_from_info(const MethodInfo &p_info, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg);
 	void validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call);
 	void validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call);
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index ed2dce471fd8..64b5b59858d4 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -158,14 +158,10 @@ void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_
 		return;
 	}
 
-	if (ignored_warning_codes.has(p_code)) {
+	if (ignored_warnings.has(p_code)) {
 		return;
 	}
 
-	String warn_name = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)p_code).to_lower();
-	if (ignored_warnings.has(warn_name)) {
-		return;
-	}
 	int warn_level = (int)GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(p_code));
 	if (!warn_level) {
 		return;
@@ -180,7 +176,7 @@ void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_
 	warning.rightmost_column = p_source->rightmost_column;
 
 	if (warn_level == GDScriptWarning::WarnLevel::ERROR || bool(GLOBAL_GET("debug/gdscript/warnings/treat_warnings_as_errors"))) {
-		push_error(warning.get_message(), p_source);
+		push_error(warning.get_message() + String(" (Warning treated as error.)"), p_source);
 		return;
 	}
 
@@ -3548,7 +3544,11 @@ const GDScriptParser::SuiteNode::Local &GDScriptParser::SuiteNode::get_local(con
 	return empty;
 }
 
-bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_target) const {
+bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_target) {
+	if (is_applied) {
+		return true;
+	}
+	is_applied = true;
 	return (p_this->*(p_this->valid_annotations[name].apply))(this, p_target);
 }
 
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index 07dac25ec5f1..7255d21a6e3c 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/modules/gdscript/gdscript_parser.h
@@ -297,7 +297,9 @@ class GDScriptParser {
 		int leftmost_column = 0, rightmost_column = 0;
 		Node *next = nullptr;
 		List<AnnotationNode *> annotations;
-		Vector<uint32_t> ignored_warnings;
+#ifdef DEBUG_ENABLED
+		Vector<GDScriptWarning::Code> ignored_warnings;
+#endif
 
 		DataType datatype;
 
@@ -329,8 +331,10 @@ class GDScriptParser {
 
 		AnnotationInfo *info = nullptr;
 		PropertyInfo export_info;
+		bool is_resolved = false;
+		bool is_applied = false;
 
-		bool apply(GDScriptParser *p_this, Node *p_target) const;
+		bool apply(GDScriptParser *p_this, Node *p_target);
 		bool applies_to(uint32_t p_target_kinds) const;
 
 		AnnotationNode() {
@@ -1263,8 +1267,7 @@ class GDScriptParser {
 #ifdef DEBUG_ENABLED
 	bool is_ignoring_warnings = false;
 	List<GDScriptWarning> warnings;
-	HashSet<String> ignored_warnings;
-	HashSet<uint32_t> ignored_warning_codes;
+	HashSet<GDScriptWarning::Code> ignored_warnings;
 	HashSet<int> unsafe_lines;
 #endif
 
diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp
index 9436146bedaa..ef59a07f1a8f 100644
--- a/modules/gdscript/gdscript_warning.cpp
+++ b/modules/gdscript/gdscript_warning.cpp
@@ -170,6 +170,21 @@ String GDScriptWarning::get_message() const {
 		case RENAMED_IN_GD4_HINT: {
 			break; // Renamed identifier hint is taken care of by the GDScriptAnalyzer. No message needed here.
 		}
+		case INFERENCE_ON_VARIANT: {
+			CHECK_SYMBOLS(1);
+			return vformat("The %s type is being inferred from a Variant value, so it will be typed as Variant.", symbols[0]);
+		}
+		case NATIVE_METHOD_OVERRIDE: {
+			CHECK_SYMBOLS(2);
+			return vformat(R"(The method "%s" overrides a method from native class "%s". This won't be called by the engine and may not work as expected.)", symbols[0], symbols[1]);
+		}
+		case GET_NODE_DEFAULT_WITHOUT_ONREADY: {
+			CHECK_SYMBOLS(1);
+			return vformat(R"*(The default value is using "%s" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this.)*", symbols[0]);
+		}
+		case ONREADY_WITH_EXPORT: {
+			return R"(The "@onready" annotation will make the default value to be set after the "@export" takes effect and will override it.)";
+		}
 		case WARNING_MAX:
 			break; // Can't happen, but silences warning
 	}
@@ -179,14 +194,8 @@ String GDScriptWarning::get_message() const {
 }
 
 int GDScriptWarning::get_default_value(Code p_code) {
-	if (get_name_from_code(p_code).to_lower().begins_with("unsafe_")) {
-		return WarnLevel::IGNORE;
-	}
-	// Too spammy by default on common cases (connect, Tween, etc.).
-	if (p_code == RETURN_VALUE_DISCARDED) {
-		return WarnLevel::IGNORE;
-	}
-	return WarnLevel::WARN;
+	ERR_FAIL_INDEX_V_MSG(p_code, WARNING_MAX, WarnLevel::IGNORE, "Getting default value of invalid warning code.");
+	return default_warning_levels[p_code];
 }
 
 PropertyInfo GDScriptWarning::get_property_info(Code p_code) {
@@ -240,7 +249,11 @@ String GDScriptWarning::get_name_from_code(Code p_code) {
 		"INT_AS_ENUM_WITHOUT_MATCH",
 		"STATIC_CALLED_ON_INSTANCE",
 		"CONFUSABLE_IDENTIFIER",
-		"RENAMED_IN_GODOT_4_HINT"
+		"RENAMED_IN_GODOT_4_HINT",
+		"INFERENCE_ON_VARIANT",
+		"NATIVE_METHOD_OVERRIDE",
+		"GET_NODE_DEFAULT_WITHOUT_ONREADY",
+		"ONREADY_WITH_EXPORT",
 	};
 
 	static_assert((sizeof(names) / sizeof(*names)) == WARNING_MAX, "Amount of warning types don't match the amount of warning names.");
diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h
index fa2907cdaebe..f0123c518c06 100644
--- a/modules/gdscript/gdscript_warning.h
+++ b/modules/gdscript/gdscript_warning.h
@@ -82,9 +82,58 @@ class GDScriptWarning {
 		STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself.
 		CONFUSABLE_IDENTIFIER, // The identifier contains misleading characters that can be confused. E.g. "usеr" (has Cyrillic "е" instead of Latin "e").
 		RENAMED_IN_GD4_HINT, // A variable or function that could not be found has been renamed in Godot 4
+		INFERENCE_ON_VARIANT, // The declaration uses type inference but the value is typed as Variant.
+		NATIVE_METHOD_OVERRIDE, // The script method overrides a native one, this may not work as intended.
+		GET_NODE_DEFAULT_WITHOUT_ONREADY, // A class variable uses `get_node()` (or the `$` notation) as its default value, but does not use the @onready annotation.
+		ONREADY_WITH_EXPORT, // The `@onready` annotation will set the value after `@export` which is likely not intended.
 		WARNING_MAX,
 	};
 
+	constexpr static WarnLevel default_warning_levels[] = {
+		WARN, // UNASSIGNED_VARIABLE
+		WARN, // UNASSIGNED_VARIABLE_OP_ASSIGN
+		WARN, // UNUSED_VARIABLE
+		WARN, // UNUSED_LOCAL_CONSTANT
+		WARN, // SHADOWED_VARIABLE
+		WARN, // SHADOWED_VARIABLE_BASE_CLASS
+		WARN, // UNUSED_PRIVATE_CLASS_VARIABLE
+		WARN, // UNUSED_PARAMETER
+		WARN, // UNREACHABLE_CODE
+		WARN, // UNREACHABLE_PATTERN
+		WARN, // STANDALONE_EXPRESSION
+		WARN, // NARROWING_CONVERSION
+		WARN, // INCOMPATIBLE_TERNARY
+		WARN, // UNUSED_SIGNAL
+		IGNORE, // RETURN_VALUE_DISCARDED // Too spammy by default on common cases (connect, Tween, etc.).
+		WARN, // PROPERTY_USED_AS_FUNCTION
+		WARN, // CONSTANT_USED_AS_FUNCTION
+		WARN, // FUNCTION_USED_AS_PROPERTY
+		WARN, // INTEGER_DIVISION
+		IGNORE, // UNSAFE_PROPERTY_ACCESS // Too common in untyped scenarios.
+		IGNORE, // UNSAFE_METHOD_ACCESS // Too common in untyped scenarios.
+		IGNORE, // UNSAFE_CAST // Too common in untyped scenarios.
+		IGNORE, // UNSAFE_CALL_ARGUMENT // Too common in untyped scenarios.
+		WARN, // UNSAFE_VOID_RETURN
+		WARN, // DEPRECATED_KEYWORD
+		WARN, // STANDALONE_TERNARY
+		WARN, // ASSERT_ALWAYS_TRUE
+		WARN, // ASSERT_ALWAYS_FALSE
+		WARN, // REDUNDANT_AWAIT
+		WARN, // EMPTY_FILE
+		WARN, // SHADOWED_GLOBAL_IDENTIFIER
+		WARN, // INT_AS_ENUM_WITHOUT_CAST
+		WARN, // INT_AS_ENUM_WITHOUT_MATCH
+		WARN, // STATIC_CALLED_ON_INSTANCE
+		WARN, // CONFUSABLE_IDENTIFIER
+		WARN, // RENAMED_IN_GD4_HINT
+		ERROR, // INFERENCE_ON_VARIANT // Most likely done by accident, usually inference is trying for a particular type.
+		ERROR, // NATIVE_METHOD_OVERRIDE // May not work as expected.
+		ERROR, // GET_NODE_DEFAULT_WITHOUT_ONREADY // May not work as expected.
+		ERROR, // ONREADY_WITH_EXPORT // May not work as expected.
+	};
+
+	static_assert((sizeof(default_warning_levels) / sizeof(default_warning_levels[0])) == WARNING_MAX, "Amount of default levels does not match the amount of warnings.");
+
 	Code code = WARNING_MAX;
 	int start_line = -1, end_line = -1;
 	int leftmost_column = -1, rightmost_column = -1;