Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Magical type deduction deduces type of function result stored in variable not declared with static type, and enables autocompletion on direct members, but not indirect members (members of members) #78003

Closed
hsandt opened this issue Jun 8, 2023 · 2 comments · Fixed by #84264

Comments

@hsandt
Copy link
Contributor

hsandt commented Jun 8, 2023

Godot version

v4.0.2.stable.official [7a0977c]

System information

Linux Ubuntu 22.04 with Unity desktop

Issue description

I found a super edge case for autocompletion breaking, where autocompletion will work on a variable not declared with static type, but only for 1st-degree members. I don't know if the bug if that the parser goes too far (it shouldn't even try to guess non-static variable type), or not far enough (if it does, it might as well autocomplete the 2nd member).

I suppose that the "magical" unsollicited type deduction on non-static-type variables has been added because it's convenient in every day use like doing math computation or trivial calls without needing a := instead of = every single time, but it causes inconsistent behavior when dealing with 2nd-degree members; and may confuse the user as autocompletion works on the 1st level so they don't immediately realize that they forgot to static type with :=.

So it'd be better if either autocompletion was working all along, or not at all (then user could immediately spot they forgot to static type).

Take a custom class:

# my_class.gd
class_name MyClass
extends Node2D

var custom_instance: MyClass2
var native_instance: Node

and

# my_class2.gd
class_name MyClass2
extends Node2D

var custom_field: int

and a script that uses the class, defining some methods, typed or not:

# class_user.gd
extends Node

@export var my_instance: MyClass

func _get_instance_untyped():
	return my_instance

func _get_instance() -> MyClass:
	return my_instance

func _get_array_of_instances_untyped():
	return [my_instance]

func _get_array_of_instances() -> Array[MyClass]:
	return [my_instance]

Now we add a final method to this script to test autocompletion.

Note that behavior slightly differs between single vars and arrays: it is the same with single vars whether you use a typed function result or not (and I don't understand how; maybe untyped function is simpler to magic-parse so they can deduce the return type anyway). Whereas a function returning an array but without explicit type will not autocomplete at all when using the result (unless you locally

# class_user.gd
func _ready():
	var local_instance_ref = _get_instance()
	# Even though local_instance_ref is not static typed, parser exploits info from _get_instance
	# ("magical type deduction"). However, autocompletion breaks on indirect member custom_field, UNEXPECTED
	print(local_instance_ref.custom_instance.custom_field)

	var local_instance_ref_untyped = _get_instance_untyped()
	# Even though local_instance_ref is not static typed AND _get_instance_untyped is not typed,
	# parser exploits info from _get_instance_untyped ("magical type deduction"), and I have no idea how
	# Anyway, however, autocompletion breaks only on indirect member custom_field, UNEXPECTED
	print(local_instance_ref_untyped.custom_instance.custom_field) # autocompletion FAILS for custom_field

	# Storing array in local var without explicit type makes the first autocompletion work,
	# but not 2nd degree -> UNEXPECTED (In real project, I forgot a colon here)
	var array_of_instances6 = _get_array_of_instances()
	for instance in array_of_instances6:
		print(instance.get_parent().get_parent()) # autocompletion WORKS for native types, but...
		print(instance.custom_instance.custom_field) # autocompletion FAILS on dot after custom_instance
		print(instance.native_instance.get_parent()) # autocompletion FAILS on dot after native_instance

In addition, adding code using the members above in _ready, such as

	print(my_instance.custom_instance)
	print(my_instance.native_instance)

will hint the parser and enable autocompletion on indirect members. So when reproducing the bug, you must avoid adding extra code that use the custom members to avoid hiding the bug.

The MRP has even more cases, I just put the unexpected ones (the one where the magical type deduction stops working after 2 members) here.

Workaround

The bug occurs where user forgot to statically type, so fortunately a simple := generally does the trick. If the function does not return a static type, you can even explicitly declare type with : MyType =.

However, fixing the bug by either not completing at all, or completing all the way down, would help the user avoid being lost.

Steps to reproduce

  1. You can copy the code above to create the 3 scripts, or download the MRP
  2. Place the caret inside class_user.gd > _ready in each line/block marked UNEXPECTED, after the first dot, and press Ctrl+Space to try to autocomplete: it works.
  3. Repeat after the second (and last) dot: this time, it fails (you cannot see custom_field, get_parent, etc.)

Minimal reproduction project

v4.0.2 - Autocompletion doesnt work for 2nd degree member (member of custom class).zip

@Calinou
Copy link
Member

Calinou commented Jun 8, 2023

PS: The backtick syntax to use for highlighting GDScript code is gdscript, not gd. I edited your comment accordingly 🙂

@HolonProduction
Copy link
Member

So I did some chasing through the debugger and found the reason for this. When using static typing the types are handled by the parser. If the parser encounters another type it parses this other type recursively and therefore the other static types get discovered and parsed recursively. Static types can therefore be in indirect members.

What you call magical type interference is done by the analyzer. The analyzer does not seem to recursively analyze other types it encounters (at least not in all cases since your print statements successfully triggers analyzing the types). Therefore "magic" interference only works for stuff in the current class. But since those magically interfered types never get analyzed further, neither other static types nor further "magic" interference works on them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

4 participants