Browse Source

GDScript: Add return type covariance and parameter type contravariance

Danil Alexeev 1 year ago
parent
commit
cb8b89fd95
19 changed files with 169 additions and 4 deletions
  1. 31 4
      modules/gdscript/gdscript_analyzer.cpp
  2. 10 0
      modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_1.gd
  3. 2 0
      modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_1.out
  4. 10 0
      modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_2.gd
  5. 2 0
      modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_2.out
  6. 10 0
      modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_3.gd
  7. 2 0
      modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_3.out
  8. 10 0
      modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_1.gd
  9. 2 0
      modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_1.out
  10. 10 0
      modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_2.gd
  11. 2 0
      modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_2.out
  12. 10 0
      modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_3.gd
  13. 2 0
      modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_3.out
  14. 10 0
      modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_4.gd
  15. 2 0
      modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_4.out
  16. 20 0
      modules/gdscript/tests/scripts/analyzer/features/function_param_type_contravariance.gd
  17. 1 0
      modules/gdscript/tests/scripts/analyzer/features/function_param_type_contravariance.out
  18. 32 0
      modules/gdscript/tests/scripts/analyzer/features/function_return_type_covariance.gd
  19. 1 0
      modules/gdscript/tests/scripts/analyzer/features/function_return_type_covariance.out

+ 31 - 4
modules/gdscript/gdscript_analyzer.cpp

@@ -1671,15 +1671,42 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
 		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, method_flags, &native_base)) {
 			bool valid = p_function->is_static == method_flags.has_flag(METHOD_FLAG_STATIC);
-			valid = valid && parent_return_type == p_function->get_datatype();
+
+			if (p_function->return_type != nullptr) {
+				// Check return type covariance.
+				GDScriptParser::DataType return_type = p_function->get_datatype();
+				if (return_type.is_variant()) {
+					// `is_type_compatible()` returns `true` if one of the types is `Variant`.
+					// Don't allow an explicitly specified `Variant` if the parent return type is narrower.
+					valid = valid && parent_return_type.is_variant();
+				} else if (return_type.kind == GDScriptParser::DataType::BUILTIN && return_type.builtin_type == Variant::NIL) {
+					// `is_type_compatible()` returns `true` if target is an `Object` and source is `null`.
+					// Don't allow `void` if the parent return type is a hard non-`void` type.
+					if (parent_return_type.is_hard_type() && !(parent_return_type.kind == GDScriptParser::DataType::BUILTIN && parent_return_type.builtin_type == Variant::NIL)) {
+						valid = false;
+					}
+				} else {
+					valid = valid && is_type_compatible(parent_return_type, return_type);
+				}
+			}
 
 			int par_count_diff = p_function->parameters.size() - parameters_types.size();
 			valid = valid && par_count_diff >= 0;
 			valid = valid && default_value_count >= default_par_count + par_count_diff;
 
-			int i = 0;
-			for (const GDScriptParser::DataType &par_type : parameters_types) {
-				valid = valid && par_type == p_function->parameters[i++]->get_datatype();
+			if (valid) {
+				int i = 0;
+				for (const GDScriptParser::DataType &parent_par_type : parameters_types) {
+					// Check parameter type contravariance.
+					GDScriptParser::DataType current_par_type = p_function->parameters[i++]->get_datatype();
+					if (parent_par_type.is_variant() && parent_par_type.is_hard_type()) {
+						// `is_type_compatible()` returns `true` if one of the types is `Variant`.
+						// Don't allow narrowing a hard `Variant`.
+						valid = valid && current_par_type.is_variant();
+					} else {
+						valid = valid && is_type_compatible(current_par_type, parent_par_type);
+					}
+				}
 			}
 
 			if (!valid) {

+ 10 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_1.gd

@@ -0,0 +1,10 @@
+class A:
+	func f(_p: Object):
+		pass
+
+class B extends A:
+	func f(_p: Node):
+		pass
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_1.out

@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+The function signature doesn't match the parent. Parent signature is "f(Object) -> Variant".

+ 10 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_2.gd

@@ -0,0 +1,10 @@
+class A:
+	func f(_p: Variant):
+		pass
+
+class B extends A:
+	func f(_p: Node): # No `is_type_compatible()` misuse.
+		pass
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_2.out

@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+The function signature doesn't match the parent. Parent signature is "f(Variant) -> Variant".

+ 10 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_3.gd

@@ -0,0 +1,10 @@
+class A:
+	func f(_p: int):
+		pass
+
+class B extends A:
+	func f(_p: float): # No implicit conversion.
+		pass
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_param_type_invalid_contravariance_3.out

@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+The function signature doesn't match the parent. Parent signature is "f(int) -> Variant".

+ 10 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_1.gd

@@ -0,0 +1,10 @@
+class A:
+	func f() -> Node:
+		return null
+
+class B extends A:
+	func f() -> Object:
+		return null
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_1.out

@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+The function signature doesn't match the parent. Parent signature is "f() -> Node".

+ 10 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_2.gd

@@ -0,0 +1,10 @@
+class A:
+	func f() -> Node:
+		return null
+
+class B extends A:
+	func f() -> Variant: # No `is_type_compatible()` misuse.
+		return null
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_2.out

@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+The function signature doesn't match the parent. Parent signature is "f() -> Node".

+ 10 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_3.gd

@@ -0,0 +1,10 @@
+class A:
+	func f() -> Node:
+		return null
+
+class B extends A:
+	func f() -> void: # No `is_type_compatible()` misuse.
+		return
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_3.out

@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+The function signature doesn't match the parent. Parent signature is "f() -> Node".

+ 10 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_4.gd

@@ -0,0 +1,10 @@
+class A:
+	func f() -> float:
+		return 0.0
+
+class B extends A:
+	func f() -> int: # No implicit conversion.
+		return 0
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/analyzer/errors/function_return_type_invalid_covariance_4.out

@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+The function signature doesn't match the parent. Parent signature is "f() -> float".

+ 20 - 0
modules/gdscript/tests/scripts/analyzer/features/function_param_type_contravariance.gd

@@ -0,0 +1,20 @@
+class A:
+	func int_to_variant(_p: int): pass
+	func node_to_variant(_p: Node): pass
+	func node_2d_to_node(_p: Node2D): pass
+
+	func variant_to_untyped(_p: Variant): pass
+	func int_to_untyped(_p: int): pass
+	func node_to_untyped(_p: Node): pass
+
+class B extends A:
+	func int_to_variant(_p: Variant): pass
+	func node_to_variant(_p: Variant): pass
+	func node_2d_to_node(_p: Node): pass
+
+	func variant_to_untyped(_p): pass
+	func int_to_untyped(_p): pass
+	func node_to_untyped(_p): pass
+
+func test():
+	pass

+ 1 - 0
modules/gdscript/tests/scripts/analyzer/features/function_param_type_contravariance.out

@@ -0,0 +1 @@
+GDTEST_OK

+ 32 - 0
modules/gdscript/tests/scripts/analyzer/features/function_return_type_covariance.gd

@@ -0,0 +1,32 @@
+class A:
+	func variant_to_int() -> Variant: return 0
+	func variant_to_node() -> Variant: return null
+	func node_to_node_2d() -> Node: return null
+
+	func untyped_to_void(): pass
+	func untyped_to_variant(): pass
+	func untyped_to_int(): pass
+	func untyped_to_node(): pass
+
+	func void_to_untyped() -> void: pass
+	func variant_to_untyped() -> Variant: return null
+	func int_to_untyped() -> int: return 0
+	func node_to_untyped() -> Node: return null
+
+class B extends A:
+	func variant_to_int() -> int: return 0
+	func variant_to_node() -> Node: return null
+	func node_to_node_2d() -> Node2D: return null
+
+	func untyped_to_void() -> void: pass
+	func untyped_to_variant() -> Variant: return null
+	func untyped_to_int() -> int: return 0
+	func untyped_to_node() -> Node: return null
+
+	func void_to_untyped(): pass
+	func variant_to_untyped(): pass
+	func int_to_untyped(): pass
+	func node_to_untyped(): pass
+
+func test():
+	pass

+ 1 - 0
modules/gdscript/tests/scripts/analyzer/features/function_return_type_covariance.out

@@ -0,0 +1 @@
+GDTEST_OK