summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorYuri Sizov <yuris@humnom.net>2022-07-03 22:30:08 +0300
committerYuri Sizov <yuris@humnom.net>2022-07-05 22:01:40 +0300
commitf85bafaa11b60617bb049ce5834be70479377426 (patch)
tree7577e16c593096a6b34034424ae108134b39ed6a /modules
parentb4644e283556b499a22dada2db5cff12290440ca (diff)
Add grouping annotations for class properties in GDScript
Diffstat (limited to 'modules')
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp3
-rw-r--r--modules/gdscript/gdscript_compiler.cpp19
-rw-r--r--modules/gdscript/gdscript_editor.cpp4
-rw-r--r--modules/gdscript/gdscript_parser.cpp81
-rw-r--r--modules/gdscript/gdscript_parser.h21
-rw-r--r--modules/gdscript/language_server/gdscript_extend_parser.cpp4
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.gd4
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.out2
8 files changed, 123 insertions, 15 deletions
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index ea994654bf..2e8f962db7 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/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;
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index 6055d3df33..af8e4b3746 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/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.
}
diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp
index 474c8094f2..250413f705 100644
--- a/modules/gdscript/gdscript_editor.cpp
+++ b/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.
}
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index ca430b0f72..4e4f17f54b 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/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;
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index e3f8d4b8ba..59afa45227 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/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);
diff --git a/modules/gdscript/language_server/gdscript_extend_parser.cpp b/modules/gdscript/language_server/gdscript_extend_parser.cpp
index d3c5fed95a..03e93821c7 100644
--- a/modules/gdscript/language_server/gdscript_extend_parser.cpp
+++ b/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.
}
diff --git a/modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.gd b/modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.gd
index d13d713454..ada6030132 100644
--- a/modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.gd
+++ b/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
diff --git a/modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.out b/modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.out
index 0bcc8acc55..02b33c8692 100644
--- a/modules/gdscript/tests/scripts/parser/errors/class_name_after_annotation.out
+++ b/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).