Prechádzať zdrojové kódy

Merge pull request #62707 from YuriSizov/gdscript-group-those-props

Rémi Verschelde 3 rokov pred
rodič
commit
ea61cd3b32

+ 3 - 0
modules/gdscript/gdscript_analyzer.cpp

@@ -860,6 +860,9 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas
 			case GDScriptParser::ClassNode::Member::CLASS:
 				check_class_member_name_conflict(p_class, member.m_class->identifier->name, member.m_class);
 				break;
+			case GDScriptParser::ClassNode::Member::GROUP:
+				// No-op, but needed to silence warnings.
+				break;
 			case GDScriptParser::ClassNode::Member::UNDEFINED:
 				ERR_PRINT("Trying to resolve undefined member.");
 				break;

+ 19 - 0
modules/gdscript/gdscript_compiler.cpp

@@ -2452,6 +2452,25 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar
 				}
 #endif
 			} break;
+
+			case GDScriptParser::ClassNode::Member::GROUP: {
+				const GDScriptParser::AnnotationNode *annotation = member.annotation;
+				StringName name = annotation->export_info.name;
+
+				// This is not a normal member, but we need this to keep indices in order.
+				GDScript::MemberInfo minfo;
+				minfo.index = p_script->member_indices.size();
+
+				PropertyInfo prop_info;
+				prop_info.name = name;
+				prop_info.usage = annotation->export_info.usage;
+				prop_info.hint_string = annotation->export_info.hint_string;
+
+				p_script->member_info[name] = prop_info;
+				p_script->member_indices[name] = minfo;
+				p_script->members.insert(name);
+			} break;
+
 			default:
 				break; // Nothing to do here.
 		}

+ 4 - 0
modules/gdscript/gdscript_editor.cpp

@@ -950,6 +950,8 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class,
 						}
 						option = ScriptLanguage::CodeCompletionOption(member.signal->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location);
 						break;
+					case GDScriptParser::ClassNode::Member::GROUP:
+						break; // No-op, but silences warnings.
 					case GDScriptParser::ClassNode::Member::UNDEFINED:
 						break;
 				}
@@ -2082,6 +2084,8 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
 							r_type.type.kind = GDScriptParser::DataType::CLASS;
 							r_type.type.class_type = member.m_class;
 							return true;
+						case GDScriptParser::ClassNode::Member::GROUP:
+							return false; // No-op, but silences warnings.
 						case GDScriptParser::ClassNode::Member::UNDEFINED:
 							return false; // Unreachable.
 					}

+ 69 - 12
modules/gdscript/gdscript_parser.cpp

@@ -131,6 +131,11 @@ GDScriptParser::GDScriptParser() {
 	register_annotation(MethodInfo("@export_flags_3d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_RENDER, Variant::INT>);
 	register_annotation(MethodInfo("@export_flags_3d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_PHYSICS, Variant::INT>);
 	register_annotation(MethodInfo("@export_flags_3d_navigation"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_NAVIGATION, Variant::INT>);
+	// Export grouping annotations.
+	register_annotation(MethodInfo("@export_category", PropertyInfo(Variant::STRING, "name")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_CATEGORY>);
+	register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_GROUP>, 1);
+	register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_SUBGROUP>, 1);
+	// Warning annotations.
 	register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS | AnnotationInfo::VARIABLE | AnnotationInfo::SIGNAL | AnnotationInfo::CONSTANT | AnnotationInfo::FUNCTION | AnnotationInfo::STATEMENT, &GDScriptParser::warning_annotations, 0, true);
 	// Networking.
 	register_annotation(MethodInfo("@rpc", PropertyInfo(Variant::STRING, "mode"), PropertyInfo(Variant::STRING, "sync"), PropertyInfo(Variant::STRING, "transfer_mode"), PropertyInfo(Variant::INT, "transfer_channel")), AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations<Multiplayer::RPC_MODE_AUTHORITY>, 4, true);
@@ -519,9 +524,13 @@ void GDScriptParser::parse_program() {
 	head = alloc_node<ClassNode>();
 	current_class = head;
 
+	// If we happen to parse an annotation before extends or class_name keywords, track it.
+	// @tool is allowed, but others should fail.
+	AnnotationNode *premature_annotation = nullptr;
+
 	if (match(GDScriptTokenizer::Token::ANNOTATION)) {
-		// Check for @tool annotation.
-		AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::CLASS_LEVEL);
+		// Check for @tool, script-level, or standalone annotation.
+		AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL);
 		if (annotation != nullptr) {
 			if (annotation->name == SNAME("@tool")) {
 				// TODO: don't allow @tool anywhere else. (Should all script annotations be the first thing?).
@@ -531,7 +540,14 @@ void GDScriptParser::parse_program() {
 				}
 				// @tool annotation has no specific target.
 				annotation->apply(this, nullptr);
+			} else if (annotation->applies_to(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE)) {
+				premature_annotation = annotation;
+				if (previous.type != GDScriptTokenizer::Token::NEWLINE) {
+					push_error(R"(Expected newline after a standalone annotation.)");
+				}
+				annotation->apply(this, head);
 			} else {
+				premature_annotation = annotation;
 				annotation_stack.push_back(annotation);
 			}
 		}
@@ -541,8 +557,8 @@ void GDScriptParser::parse_program() {
 		// Order here doesn't matter, but there should be only one of each at most.
 		switch (current.type) {
 			case GDScriptTokenizer::Token::CLASS_NAME:
-				if (!annotation_stack.is_empty()) {
-					push_error(R"("class_name" should be used before annotations.)");
+				if (premature_annotation != nullptr) {
+					push_error(R"("class_name" should be used before annotations (except @tool).)");
 				}
 				advance();
 				if (head->identifier != nullptr) {
@@ -552,8 +568,8 @@ void GDScriptParser::parse_program() {
 				}
 				break;
 			case GDScriptTokenizer::Token::EXTENDS:
-				if (!annotation_stack.is_empty()) {
-					push_error(R"("extends" should be used before annotations.)");
+				if (premature_annotation != nullptr) {
+					push_error(R"("extends" should be used before annotations (except @tool).)");
 				}
 				advance();
 				if (head->extends_used) {
@@ -574,12 +590,12 @@ void GDScriptParser::parse_program() {
 	}
 
 	if (match(GDScriptTokenizer::Token::ANNOTATION)) {
-		// Check for @icon annotation.
-		AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::CLASS_LEVEL);
+		// Check for a script-level, or standalone annotation.
+		AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL);
 		if (annotation != nullptr) {
-			if (annotation->name == SNAME("@icon")) {
+			if (annotation->applies_to(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE)) {
 				if (previous.type != GDScriptTokenizer::Token::NEWLINE) {
-					push_error(R"(Expected newline after "@icon" annotation.)");
+					push_error(R"(Expected newline after a standalone annotation.)");
 				}
 				annotation->apply(this, head);
 			} else {
@@ -807,9 +823,18 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
 				break;
 			case GDScriptTokenizer::Token::ANNOTATION: {
 				advance();
-				AnnotationNode *annotation = parse_annotation(AnnotationInfo::CLASS_LEVEL);
+
+				// Check for class-level annotations.
+				AnnotationNode *annotation = parse_annotation(AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL);
 				if (annotation != nullptr) {
-					annotation_stack.push_back(annotation);
+					if (annotation->applies_to(AnnotationInfo::STANDALONE)) {
+						if (previous.type != GDScriptTokenizer::Token::NEWLINE) {
+							push_error(R"(Expected newline after a standalone annotation.)");
+						}
+						annotation->apply(this, head);
+					} else {
+						annotation_stack.push_back(annotation);
+					}
 				}
 				break;
 			}
@@ -3662,6 +3687,36 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node
 	return true;
 }
 
+template <PropertyUsageFlags t_usage>
+bool GDScriptParser::export_group_annotations(const AnnotationNode *p_annotation, Node *p_node) {
+	AnnotationNode *annotation = const_cast<AnnotationNode *>(p_annotation);
+
+	annotation->export_info.name = annotation->resolved_arguments[0];
+
+	switch (t_usage) {
+		case PROPERTY_USAGE_CATEGORY: {
+			annotation->export_info.usage = t_usage;
+		} break;
+
+		case PROPERTY_USAGE_GROUP: {
+			annotation->export_info.usage = t_usage;
+			if (annotation->resolved_arguments.size() == 2) {
+				annotation->export_info.hint_string = annotation->resolved_arguments[1];
+			}
+		} break;
+
+		case PROPERTY_USAGE_SUBGROUP: {
+			annotation->export_info.usage = t_usage;
+			if (annotation->resolved_arguments.size() == 2) {
+				annotation->export_info.hint_string = annotation->resolved_arguments[1];
+			}
+		} break;
+	}
+
+	current_class->add_member_group(annotation);
+	return true;
+}
+
 bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Node *p_node) {
 #ifdef DEBUG_ENABLED
 	bool has_error = false;
@@ -4145,6 +4200,8 @@ void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) {
 				break;
 			case ClassNode::Member::ENUM_VALUE:
 				break; // Nothing. Will be printed by enum.
+			case ClassNode::Member::GROUP:
+				break; // Nothing. Groups are only used by inspector.
 			case ClassNode::Member::UNDEFINED:
 				push_line("<unknown member>");
 				break;

+ 21 - 0
modules/gdscript/gdscript_parser.h

@@ -325,6 +325,7 @@ public:
 		Vector<Variant> resolved_arguments;
 
 		AnnotationInfo *info = nullptr;
+		PropertyInfo export_info;
 
 		bool apply(GDScriptParser *p_this, Node *p_target) const;
 		bool applies_to(uint32_t p_target_kinds) const;
@@ -500,6 +501,7 @@ public:
 				VARIABLE,
 				ENUM,
 				ENUM_VALUE, // For unnamed enums.
+				GROUP, // For member grouping.
 			};
 
 			Type type = UNDEFINED;
@@ -511,6 +513,7 @@ public:
 				SignalNode *signal;
 				VariableNode *variable;
 				EnumNode *m_enum;
+				AnnotationNode *annotation;
 			};
 			EnumNode::Value enum_value;
 
@@ -532,6 +535,8 @@ public:
 						return "enum";
 					case ENUM_VALUE:
 						return "enum value";
+					case GROUP:
+						return "group";
 				}
 				return "";
 			}
@@ -552,6 +557,8 @@ public:
 						return m_enum->start_line;
 					case SIGNAL:
 						return signal->start_line;
+					case GROUP:
+						return annotation->start_line;
 					case UNDEFINED:
 						ERR_FAIL_V_MSG(-1, "Reaching undefined member type.");
 				}
@@ -586,6 +593,9 @@ public:
 						// TODO: Add parameter info.
 						return type;
 					}
+					case GROUP: {
+						return DataType();
+					}
 					case UNDEFINED:
 						return DataType();
 				}
@@ -622,6 +632,10 @@ public:
 				type = ENUM_VALUE;
 				enum_value = p_enum_value;
 			}
+			Member(AnnotationNode *p_annotation) {
+				type = GROUP;
+				annotation = p_annotation;
+			}
 		};
 
 		IdentifierNode *identifier = nullptr;
@@ -668,6 +682,10 @@ public:
 			members_indices[p_enum_value.identifier->name] = members.size();
 			members.push_back(Member(p_enum_value));
 		}
+		void add_member_group(AnnotationNode *p_annotation_node) {
+			members_indices[p_annotation_node->export_info.name] = members.size();
+			members.push_back(Member(p_annotation_node));
+		}
 
 		ClassNode() {
 			type = CLASS;
@@ -1238,6 +1256,7 @@ private:
 			SIGNAL = 1 << 4,
 			FUNCTION = 1 << 5,
 			STATEMENT = 1 << 6,
+			STANDALONE = 1 << 7,
 			CLASS_LEVEL = CLASS | VARIABLE | FUNCTION,
 		};
 		uint32_t target_kind = 0; // Flags.
@@ -1348,6 +1367,8 @@ private:
 	bool onready_annotation(const AnnotationNode *p_annotation, Node *p_target);
 	template <PropertyHint t_hint, Variant::Type t_type>
 	bool export_annotations(const AnnotationNode *p_annotation, Node *p_target);
+	template <PropertyUsageFlags t_usage>
+	bool export_group_annotations(const AnnotationNode *p_annotation, Node *p_target);
 	bool warning_annotations(const AnnotationNode *p_annotation, Node *p_target);
 	template <Multiplayer::RPCMode t_mode>
 	bool network_annotations(const AnnotationNode *p_annotation, Node *p_target);

+ 4 - 0
modules/gdscript/language_server/gdscript_extend_parser.cpp

@@ -307,6 +307,8 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p
 				parse_class_symbol(m.m_class, symbol);
 				r_symbol.children.push_back(symbol);
 			} break;
+			case ClassNode::Member::GROUP:
+				break; // No-op, but silences warnings.
 			case ClassNode::Member::UNDEFINED:
 				break; // Unreachable.
 		}
@@ -815,6 +817,8 @@ Dictionary ExtendGDScriptParser::dump_class_api(const GDScriptParser::ClassNode
 					methods.append(dump_function_api(m.function));
 				}
 			} break;
+			case ClassNode::Member::GROUP:
+				break; // No-op, but silences warnings.
 			case ClassNode::Member::UNDEFINED:
 				break; // Unreachable.
 		}

+ 2 - 2
modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.gd

@@ -1,6 +1,6 @@
-# Error here. `class_name` should be used *before* annotations, not after.
+# Error here. `class_name` should be used *before* annotations, not after (except @tool).
 @icon("res://path/to/optional/icon.svg")
 class_name HelloWorld
 
 func test():
-	pass
+    pass

+ 1 - 1
modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.out

@@ -1,2 +1,2 @@
 GDTEST_PARSER_ERROR
-"class_name" should be used before annotations.
+"class_name" should be used before annotations (except @tool).