Browse Source

GDScript: Allow void functions to return calls to other void functions

George Marques 2 years ago
parent
commit
a47d4d57ca

+ 3 - 0
doc/classes/ProjectSettings.xml

@@ -474,6 +474,9 @@
 		<member name="debug/gdscript/warnings/unsafe_property_access" type="int" setter="" getter="" default="0">
 		<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.
 			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>
+		<member name="debug/gdscript/warnings/unsafe_void_return" type="int" setter="" getter="" default="0">
+			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">
 		<member name="debug/gdscript/warnings/unused_local_constant" 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 local constant is never used.
 			When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a local constant is never used.
 		</member>
 		</member>

+ 33 - 10
modules/gdscript/gdscript_analyzer.cpp

@@ -1972,17 +1972,40 @@ void GDScriptAnalyzer::resolve_return(GDScriptParser::ReturnNode *p_return) {
 	}
 	}
 
 
 	if (p_return->return_value != nullptr) {
 	if (p_return->return_value != nullptr) {
-		reduce_expression(p_return->return_value);
-		if (p_return->return_value->type == GDScriptParser::Node::ARRAY && has_expected_type && expected_type.has_container_element_type()) {
-			update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_return->return_value), expected_type.get_container_element_type());
-		}
-		if (has_expected_type && expected_type.is_hard_type() && expected_type.kind == GDScriptParser::DataType::BUILTIN && expected_type.builtin_type == Variant::NIL) {
-			push_error("A void function cannot return a value.", p_return);
-		}
-		if (has_expected_type && expected_type.is_hard_type() && p_return->return_value->is_constant) {
-			update_const_expression_builtin_type(p_return->return_value, expected_type, "return");
+		bool is_void_function = has_expected_type && expected_type.is_hard_type() && expected_type.kind == GDScriptParser::DataType::BUILTIN && expected_type.builtin_type == Variant::NIL;
+		bool is_call = p_return->return_value->type == GDScriptParser::Node::CALL;
+		if (is_void_function && is_call) {
+			// Pretend the call is a root expression to allow those that are "void".
+			reduce_call(static_cast<GDScriptParser::CallNode *>(p_return->return_value), false, true);
+		} else {
+			reduce_expression(p_return->return_value);
+		}
+		if (is_void_function) {
+			p_return->void_return = true;
+			const GDScriptParser::DataType &return_type = p_return->return_value->datatype;
+			if (is_call && !return_type.is_hard_type()) {
+				String function_name = parser->current_function->identifier ? parser->current_function->identifier->name.operator String() : String("<anonymous function>");
+				String called_function_name = static_cast<GDScriptParser::CallNode *>(p_return->return_value)->function_name.operator String();
+#ifdef DEBUG_ENABLED
+				parser->push_warning(p_return, GDScriptWarning::UNSAFE_VOID_RETURN, function_name, called_function_name);
+#endif
+				mark_node_unsafe(p_return);
+			} else if (!is_call) {
+				push_error("A void function cannot return a value.", p_return);
+			}
+			result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
+			result.kind = GDScriptParser::DataType::BUILTIN;
+			result.builtin_type = Variant::NIL;
+			result.is_constant = true;
+		} else {
+			if (p_return->return_value->type == GDScriptParser::Node::ARRAY && has_expected_type && expected_type.has_container_element_type()) {
+				update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_return->return_value), expected_type.get_container_element_type());
+			}
+			if (has_expected_type && expected_type.is_hard_type() && p_return->return_value->is_constant) {
+				update_const_expression_builtin_type(p_return->return_value, expected_type, "return");
+			}
+			result = p_return->return_value->get_datatype();
 		}
 		}
-		result = p_return->return_value->get_datatype();
 	} else {
 	} else {
 		// Return type is null by default.
 		// Return type is null by default.
 		result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
 		result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;

+ 6 - 1
modules/gdscript/gdscript_compiler.cpp

@@ -1859,7 +1859,12 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Sui
 					}
 					}
 				}
 				}
 
 
-				gen->write_return(return_value);
+				if (return_n->void_return) {
+					// Always return "null", even if the expression is a call to a void function.
+					gen->write_return(codegen.add_constant(Variant()));
+				} else {
+					gen->write_return(return_value);
+				}
 				if (return_value.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
 				if (return_value.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
 					codegen.generator->pop_temporary();
 					codegen.generator->pop_temporary();
 				}
 				}

+ 1 - 0
modules/gdscript/gdscript_parser.h

@@ -970,6 +970,7 @@ public:
 
 
 	struct ReturnNode : public Node {
 	struct ReturnNode : public Node {
 		ExpressionNode *return_value = nullptr;
 		ExpressionNode *return_value = nullptr;
+		bool void_return = false;
 
 
 		ReturnNode() {
 		ReturnNode() {
 			type = RETURN;
 			type = RETURN;

+ 5 - 0
modules/gdscript/gdscript_warning.cpp

@@ -125,6 +125,10 @@ String GDScriptWarning::get_message() const {
 			CHECK_SYMBOLS(4);
 			CHECK_SYMBOLS(4);
 			return "The argument '" + symbols[0] + "' of the function '" + symbols[1] + "' requires a the subtype '" + symbols[2] + "' but the supertype '" + symbols[3] + "' was provided";
 			return "The argument '" + symbols[0] + "' of the function '" + symbols[1] + "' requires a the subtype '" + symbols[2] + "' but the supertype '" + symbols[3] + "' was provided";
 		} break;
 		} break;
+		case UNSAFE_VOID_RETURN: {
+			CHECK_SYMBOLS(2);
+			return "The method '" + symbols[0] + "()' returns 'void' but it's trying to return a call to '" + symbols[1] + "()' that can't be ensured to also be 'void'.";
+		} break;
 		case DEPRECATED_KEYWORD: {
 		case DEPRECATED_KEYWORD: {
 			CHECK_SYMBOLS(2);
 			CHECK_SYMBOLS(2);
 			return "The '" + symbols[0] + "' keyword is deprecated and will be removed in a future release, please replace its uses by '" + symbols[1] + "'.";
 			return "The '" + symbols[0] + "' keyword is deprecated and will be removed in a future release, please replace its uses by '" + symbols[1] + "'.";
@@ -218,6 +222,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) {
 		"UNSAFE_METHOD_ACCESS",
 		"UNSAFE_METHOD_ACCESS",
 		"UNSAFE_CAST",
 		"UNSAFE_CAST",
 		"UNSAFE_CALL_ARGUMENT",
 		"UNSAFE_CALL_ARGUMENT",
+		"UNSAFE_VOID_RETURN",
 		"DEPRECATED_KEYWORD",
 		"DEPRECATED_KEYWORD",
 		"STANDALONE_TERNARY",
 		"STANDALONE_TERNARY",
 		"ASSERT_ALWAYS_TRUE",
 		"ASSERT_ALWAYS_TRUE",

+ 1 - 0
modules/gdscript/gdscript_warning.h

@@ -69,6 +69,7 @@ public:
 		UNSAFE_METHOD_ACCESS, // Function not found in the detected type (but can be in subtypes).
 		UNSAFE_METHOD_ACCESS, // Function not found in the detected type (but can be in subtypes).
 		UNSAFE_CAST, // Cast used in an unknown type.
 		UNSAFE_CAST, // Cast used in an unknown type.
 		UNSAFE_CALL_ARGUMENT, // Function call argument is of a supertype of the require argument.
 		UNSAFE_CALL_ARGUMENT, // Function call argument is of a supertype of the require argument.
+		UNSAFE_VOID_RETURN, // Function returns void but returned a call to a function that can't be type checked.
 		DEPRECATED_KEYWORD, // The keyword is deprecated and should be replaced.
 		DEPRECATED_KEYWORD, // The keyword is deprecated and should be replaced.
 		STANDALONE_TERNARY, // Return value of ternary expression is discarded.
 		STANDALONE_TERNARY, // Return value of ternary expression is discarded.
 		ASSERT_ALWAYS_TRUE, // Expression for assert argument is always true.
 		ASSERT_ALWAYS_TRUE, // Expression for assert argument is always true.

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

@@ -0,0 +1,20 @@
+func test():
+	return_call()
+	return_nothing()
+	return_side_effect()
+	var r = return_side_effect.call() # Untyped call to check return value.
+	prints(r, typeof(r) == TYPE_NIL)
+	print("end")
+
+func side_effect(v):
+	print("effect")
+	return v
+
+func return_call() -> void:
+	return print("hello")
+
+func return_nothing() -> void:
+	return
+
+func return_side_effect() -> void:
+	return side_effect("x")

+ 10 - 0
modules/gdscript/tests/scripts/analyzer/features/allow_void_function_to_return_void.out

@@ -0,0 +1,10 @@
+GDTEST_OK
+>> WARNING
+>> Line: 20
+>> UNSAFE_VOID_RETURN
+>> The method 'return_side_effect()' returns 'void' but it's trying to return a call to 'side_effect()' that can't be ensured to also be 'void'.
+hello
+effect
+effect
+<null> true
+end