diff options
Diffstat (limited to 'modules/gdscript/gdscript_parser.cpp')
-rw-r--r-- | modules/gdscript/gdscript_parser.cpp | 710 |
1 files changed, 518 insertions, 192 deletions
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index f3a4f2eaa6..d706f4e9a3 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -31,6 +31,7 @@ #include "gdscript_parser.h" #include "gdscript.h" +#include "gdscript_tokenizer_buffer.h" #include "core/config/project_settings.h" #include "core/io/file_access.h" @@ -100,6 +101,7 @@ GDScriptParser::GDScriptParser() { register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation); // Export annotations. register_annotation(MethodInfo("@export"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NONE, Variant::NIL>); + register_annotation(MethodInfo("@export_storage"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NONE, Variant::NIL>); register_annotation(MethodInfo("@export_enum", PropertyInfo(Variant::STRING, "names")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_ENUM, Variant::NIL>, varray(), true); register_annotation(MethodInfo("@export_file", PropertyInfo(Variant::STRING, "filter")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_FILE, Variant::STRING>, varray(""), true); register_annotation(MethodInfo("@export_dir"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_DIR, Variant::STRING>); @@ -119,12 +121,13 @@ GDScriptParser::GDScriptParser() { 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>); register_annotation(MethodInfo("@export_flags_avoidance"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_AVOIDANCE, Variant::INT>); + register_annotation(MethodInfo("@export_custom", PropertyInfo(Variant::INT, "hint", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_CLASS_IS_ENUM, "PropertyHint"), PropertyInfo(Variant::STRING, "hint_string"), PropertyInfo(Variant::INT, "usage", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_CLASS_IS_BITFIELD, "PropertyUsageFlags")), AnnotationInfo::VARIABLE, &GDScriptParser::export_custom_annotation, varray(PROPERTY_USAGE_DEFAULT)); // 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>, varray("")); register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_SUBGROUP>, varray("")); // 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, varray(), true); + register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS_LEVEL | AnnotationInfo::STATEMENT, &GDScriptParser::warning_annotations, varray(), 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::rpc_annotation, varray("authority", "call_remote", "unreliable", 0)); } @@ -178,47 +181,62 @@ void GDScriptParser::push_error(const String &p_message, const Node *p_origin) { #ifdef DEBUG_ENABLED void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_code, const Vector<String> &p_symbols) { ERR_FAIL_NULL(p_source); + ERR_FAIL_INDEX(p_code, GDScriptWarning::WARNING_MAX); + if (is_ignoring_warnings) { return; } if (GLOBAL_GET("debug/gdscript/warnings/exclude_addons").booleanize() && script_path.begins_with("res://addons/")) { return; } - - if (ignored_warnings.has(p_code)) { + GDScriptWarning::WarnLevel warn_level = (GDScriptWarning::WarnLevel)(int)GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(p_code)); + if (warn_level == GDScriptWarning::IGNORE) { return; } - int warn_level = (int)GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(p_code)); - if (!warn_level) { - return; - } + PendingWarning pw; + pw.source = p_source; + pw.code = p_code; + pw.treated_as_error = warn_level == GDScriptWarning::ERROR; + pw.symbols = p_symbols; - GDScriptWarning warning; - warning.code = p_code; - warning.symbols = p_symbols; - warning.start_line = p_source->start_line; - warning.end_line = p_source->end_line; - warning.leftmost_column = p_source->leftmost_column; - warning.rightmost_column = p_source->rightmost_column; + pending_warnings.push_back(pw); +} - if (warn_level == GDScriptWarning::WarnLevel::ERROR) { - push_error(warning.get_message() + String(" (Warning treated as error.)"), p_source); - return; - } +void GDScriptParser::apply_pending_warnings() { + for (const PendingWarning &pw : pending_warnings) { + if (warning_ignored_lines[pw.code].has(pw.source->start_line)) { + continue; + } - List<GDScriptWarning>::Element *before = nullptr; - for (List<GDScriptWarning>::Element *E = warnings.front(); E; E = E->next()) { - if (E->get().start_line > warning.start_line) { - break; + GDScriptWarning warning; + warning.code = pw.code; + warning.symbols = pw.symbols; + warning.start_line = pw.source->start_line; + warning.end_line = pw.source->end_line; + warning.leftmost_column = pw.source->leftmost_column; + warning.rightmost_column = pw.source->rightmost_column; + + if (pw.treated_as_error) { + push_error(warning.get_message() + String(" (Warning treated as error.)"), pw.source); + continue; + } + + List<GDScriptWarning>::Element *before = nullptr; + for (List<GDScriptWarning>::Element *E = warnings.front(); E; E = E->next()) { + if (E->get().start_line > warning.start_line) { + break; + } + before = E; + } + if (before) { + warnings.insert_after(before, warning); + } else { + warnings.push_front(warning); } - before = E; - } - if (before) { - warnings.insert_after(before, warning); - } else { - warnings.push_front(warning); } + + pending_warnings.clear(); } #endif @@ -226,7 +244,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) { return; } - if (previous.cursor_place != GDScriptTokenizer::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizer::CURSOR_END && current.cursor_place == GDScriptTokenizer::CURSOR_NONE) { + if (previous.cursor_place != GDScriptTokenizerText::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizerText::CURSOR_END && current.cursor_place == GDScriptTokenizerText::CURSOR_NONE) { return; } CompletionContext context; @@ -234,7 +252,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node context.current_class = current_class; context.current_function = current_function; context.current_suite = current_suite; - context.current_line = tokenizer.get_cursor_line(); + context.current_line = tokenizer->get_cursor_line(); context.current_argument = p_argument; context.node = p_node; completion_context = context; @@ -244,7 +262,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Typ if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) { return; } - if (previous.cursor_place != GDScriptTokenizer::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizer::CURSOR_END && current.cursor_place == GDScriptTokenizer::CURSOR_NONE) { + if (previous.cursor_place != GDScriptTokenizerText::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizerText::CURSOR_END && current.cursor_place == GDScriptTokenizerText::CURSOR_NONE) { return; } CompletionContext context; @@ -252,7 +270,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Typ context.current_class = current_class; context.current_function = current_function; context.current_suite = current_suite; - context.current_line = tokenizer.get_cursor_line(); + context.current_line = tokenizer->get_cursor_line(); context.builtin_type = p_builtin_type; completion_context = context; } @@ -265,7 +283,7 @@ void GDScriptParser::push_completion_call(Node *p_call) { call.call = p_call; call.argument = 0; completion_call_stack.push_back(call); - if (previous.cursor_place == GDScriptTokenizer::CURSOR_MIDDLE || previous.cursor_place == GDScriptTokenizer::CURSOR_END || current.cursor_place == GDScriptTokenizer::CURSOR_BEGINNING) { + if (previous.cursor_place == GDScriptTokenizerText::CURSOR_MIDDLE || previous.cursor_place == GDScriptTokenizerText::CURSOR_END || current.cursor_place == GDScriptTokenizerText::CURSOR_BEGINNING) { completion_call = call; } } @@ -328,17 +346,21 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ source = source.replace_first(String::chr(0xFFFF), String()); } - tokenizer.set_source_code(source); - tokenizer.set_cursor_position(cursor_line, cursor_column); - script_path = p_script_path; - current = tokenizer.scan(); + GDScriptTokenizerText *text_tokenizer = memnew(GDScriptTokenizerText); + text_tokenizer->set_source_code(source); + + tokenizer = text_tokenizer; + + tokenizer->set_cursor_position(cursor_line, cursor_column); + script_path = p_script_path.simplify_path(); + current = tokenizer->scan(); // Avoid error or newline as the first token. // The latter can mess with the parser when opening files filled exclusively with comments and newlines. while (current.type == GDScriptTokenizer::Token::ERROR || current.type == GDScriptTokenizer::Token::NEWLINE) { if (current.type == GDScriptTokenizer::Token::ERROR) { push_error(current.literal); } - current = tokenizer.scan(); + current = tokenizer->scan(); } #ifdef DEBUG_ENABLED @@ -359,6 +381,9 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ parse_program(); pop_multiline(); + memdelete(text_tokenizer); + tokenizer = nullptr; + #ifdef DEBUG_ENABLED if (multiline_stack.size() > 0) { ERR_PRINT("Parser bug: Imbalanced multiline stack."); @@ -372,6 +397,41 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ } } +Error GDScriptParser::parse_binary(const Vector<uint8_t> &p_binary, const String &p_script_path) { + GDScriptTokenizerBuffer *buffer_tokenizer = memnew(GDScriptTokenizerBuffer); + Error err = buffer_tokenizer->set_code_buffer(p_binary); + + if (err) { + memdelete(buffer_tokenizer); + return err; + } + + tokenizer = buffer_tokenizer; + script_path = p_script_path; + current = tokenizer->scan(); + // Avoid error or newline as the first token. + // The latter can mess with the parser when opening files filled exclusively with comments and newlines. + while (current.type == GDScriptTokenizer::Token::ERROR || current.type == GDScriptTokenizer::Token::NEWLINE) { + if (current.type == GDScriptTokenizer::Token::ERROR) { + push_error(current.literal); + } + current = tokenizer->scan(); + } + + push_multiline(false); // Keep one for the whole parsing. + parse_program(); + pop_multiline(); + + memdelete(buffer_tokenizer); + tokenizer = nullptr; + + if (errors.is_empty()) { + return OK; + } else { + return ERR_PARSE_ERROR; + } +} + GDScriptTokenizer::Token GDScriptParser::advance() { lambda_ended = false; // Empty marker since we're past the end in any case. @@ -379,16 +439,16 @@ GDScriptTokenizer::Token GDScriptParser::advance() { ERR_FAIL_COND_V_MSG(current.type == GDScriptTokenizer::Token::TK_EOF, current, "GDScript parser bug: Trying to advance past the end of stream."); } if (for_completion && !completion_call_stack.is_empty()) { - if (completion_call.call == nullptr && tokenizer.is_past_cursor()) { + if (completion_call.call == nullptr && tokenizer->is_past_cursor()) { completion_call = completion_call_stack.back()->get(); passed_cursor = true; } } previous = current; - current = tokenizer.scan(); + current = tokenizer->scan(); while (current.type == GDScriptTokenizer::Token::ERROR) { push_error(current.literal); - current = tokenizer.scan(); + current = tokenizer->scan(); } if (previous.type != GDScriptTokenizer::Token::DEDENT) { // `DEDENT` belongs to the next non-empty line. for (Node *n : nodes_in_progress) { @@ -457,19 +517,19 @@ void GDScriptParser::synchronize() { void GDScriptParser::push_multiline(bool p_state) { multiline_stack.push_back(p_state); - tokenizer.set_multiline_mode(p_state); + tokenizer->set_multiline_mode(p_state); if (p_state) { // Consume potential whitespace tokens already waiting in line. while (current.type == GDScriptTokenizer::Token::NEWLINE || current.type == GDScriptTokenizer::Token::INDENT || current.type == GDScriptTokenizer::Token::DEDENT) { - current = tokenizer.scan(); // Don't call advance() here, as we don't want to change the previous token. + current = tokenizer->scan(); // Don't call advance() here, as we don't want to change the previous token. } } } void GDScriptParser::pop_multiline() { - ERR_FAIL_COND_MSG(multiline_stack.size() == 0, "Parser bug: trying to pop from multiline stack without available value."); + ERR_FAIL_COND_MSG(multiline_stack.is_empty(), "Parser bug: trying to pop from multiline stack without available value."); multiline_stack.pop_back(); - tokenizer.set_multiline_mode(multiline_stack.size() > 0 ? multiline_stack.back()->get() : false); + tokenizer->set_multiline_mode(multiline_stack.size() > 0 ? multiline_stack.back()->get() : false); } bool GDScriptParser::is_statement_end_token() const { @@ -508,25 +568,53 @@ void GDScriptParser::end_statement(const String &p_context) { void GDScriptParser::parse_program() { head = alloc_node<ClassNode>(); - head->fqcn = script_path; + head->start_line = 1; + head->end_line = 1; + head->fqcn = GDScript::canonicalize_path(script_path); current_class = head; bool can_have_class_or_extends = true; +#define PUSH_PENDING_ANNOTATIONS_TO_HEAD \ + if (!annotation_stack.is_empty()) { \ + for (AnnotationNode * annot : annotation_stack) { \ + head->annotations.push_back(annot); \ + } \ + annotation_stack.clear(); \ + } + while (!check(GDScriptTokenizer::Token::TK_EOF)) { if (match(GDScriptTokenizer::Token::ANNOTATION)) { - AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL); + AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::CLASS_LEVEL | AnnotationInfo::STANDALONE); if (annotation != nullptr) { - if (annotation->applies_to(AnnotationInfo::SCRIPT)) { - // `@icon` needs to be applied in the parser. See GH-72444. - if (annotation->name == SNAME("@icon")) { - annotation->apply(this, head, nullptr); + if (annotation->applies_to(AnnotationInfo::CLASS)) { + // We do not know in advance what the annotation will be applied to: the `head` class or the subsequent inner class. + // If we encounter `class_name`, `extends` or pure `SCRIPT` annotation, then it's `head`, otherwise it's an inner class. + annotation_stack.push_back(annotation); + } else if (annotation->applies_to(AnnotationInfo::SCRIPT)) { + PUSH_PENDING_ANNOTATIONS_TO_HEAD; + if (annotation->name == SNAME("@tool") || annotation->name == SNAME("@icon")) { + // Some annotations need to be resolved in the parser. + annotation->apply(this, head, nullptr); // `head->outer == nullptr`. } else { head->annotations.push_back(annotation); } + } else if (annotation->applies_to(AnnotationInfo::STANDALONE)) { + if (previous.type != GDScriptTokenizer::Token::NEWLINE) { + push_error(R"(Expected newline after a standalone annotation.)"); + } + if (annotation->name == SNAME("@export_category") || annotation->name == SNAME("@export_group") || annotation->name == SNAME("@export_subgroup")) { + head->add_member_group(annotation); + // This annotation must appear after script-level annotations and `class_name`/`extends`, + // so we stop looking for script-level stuff. + can_have_class_or_extends = false; + break; + } else { + // For potential non-group standalone annotations. + push_error(R"(Unexpected standalone annotation.)"); + } } else { annotation_stack.push_back(annotation); - // This annotation must appear after script-level annotations - // and class_name/extends (ex: could be @onready or @export), + // This annotation must appear after script-level annotations and `class_name`/`extends`, // so we stop looking for script-level stuff. can_have_class_or_extends = false; break; @@ -547,6 +635,10 @@ 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: + PUSH_PENDING_ANNOTATIONS_TO_HEAD; + if (head->start_line == 1) { + reset_extents(head, current); + } advance(); if (head->identifier != nullptr) { push_error(R"("class_name" can only be used once.)"); @@ -555,6 +647,10 @@ void GDScriptParser::parse_program() { } break; case GDScriptTokenizer::Token::EXTENDS: + PUSH_PENDING_ANNOTATIONS_TO_HEAD; + if (head->start_line == 1) { + reset_extents(head, current); + } advance(); if (head->extends_used) { push_error(R"("extends" can only be used once.)"); @@ -563,6 +659,10 @@ void GDScriptParser::parse_program() { end_statement("superclass"); } break; + case GDScriptTokenizer::Token::TK_EOF: + PUSH_PENDING_ANNOTATIONS_TO_HEAD; + can_have_class_or_extends = false; + break; case GDScriptTokenizer::Token::LITERAL: if (current.literal.get_type() == Variant::STRING) { // Allow strings in class body as multiline comments. @@ -584,11 +684,13 @@ void GDScriptParser::parse_program() { } } +#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD + parse_class_body(true); complete_extents(head); #ifdef TOOLS_ENABLED - const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); + const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer->get_comments(); int line = MIN(max_script_doc_line, head->end_line); while (line > 0) { if (comments.has(line) && comments[line].new_line && comments[line].comment.begins_with("##")) { @@ -597,6 +699,7 @@ void GDScriptParser::parse_program() { } line--; } + #endif // TOOLS_ENABLED if (!check(GDScriptTokenizer::Token::TK_EOF)) { @@ -629,7 +732,7 @@ GDScriptParser::ClassNode *GDScriptParser::find_class(const String &p_qualified_ // Starts at index 1 because index 0 was handled above. for (int i = 1; result != nullptr && i < class_names.size(); i++) { - String current_name = class_names[i]; + const String ¤t_name = class_names[i]; GDScriptParser::ClassNode *next = nullptr; if (result->has_member(current_name)) { GDScriptParser::ClassNode::Member member = result->get_member(current_name); @@ -665,7 +768,7 @@ GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) { if (n_class->outer) { String fqcn = n_class->outer->fqcn; if (fqcn.is_empty()) { - fqcn = script_path; + fqcn = GDScript::canonicalize_path(script_path); } n_class->fqcn = fqcn + "::" + n_class->identifier->name; } else { @@ -753,7 +856,7 @@ void GDScriptParser::parse_extends() { } } -template <class T> +template <typename T> void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static) { advance(); @@ -793,7 +896,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b if (has_comment(member->start_line, true)) { // Inline doc comment. member->doc_data = parse_class_doc_comment(member->start_line, true); - } else if (has_comment(doc_comment_line, true) && tokenizer.get_comments()[doc_comment_line].new_line) { + } else if (has_comment(doc_comment_line, true) && tokenizer->get_comments()[doc_comment_line].new_line) { // Normal doc comment. Don't check `min_member_doc_line` because a class ends parsing after its members. // This may not work correctly for cases like `var a; class B`, but it doesn't matter in practice. member->doc_data = parse_class_doc_comment(doc_comment_line); @@ -802,7 +905,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b if (has_comment(member->start_line, true)) { // Inline doc comment. member->doc_data = parse_doc_comment(member->start_line, true); - } else if (doc_comment_line >= min_member_doc_line && has_comment(doc_comment_line, true) && tokenizer.get_comments()[doc_comment_line].new_line) { + } else if (doc_comment_line >= min_member_doc_line && has_comment(doc_comment_line, true) && tokenizer->get_comments()[doc_comment_line].new_line) { // Normal doc comment. member->doc_data = parse_doc_comment(doc_comment_line); } @@ -861,8 +964,8 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { case GDScriptTokenizer::Token::ANNOTATION: { advance(); - // Check for standalone and class-level annotations. - AnnotationNode *annotation = parse_annotation(AnnotationInfo::STANDALONE | AnnotationInfo::CLASS_LEVEL); + // Check for class-level and standalone annotations. + AnnotationNode *annotation = parse_annotation(AnnotationInfo::CLASS_LEVEL | AnnotationInfo::STANDALONE); if (annotation != nullptr) { if (annotation->applies_to(AnnotationInfo::STANDALONE)) { if (previous.type != GDScriptTokenizer::Token::NEWLINE) { @@ -872,9 +975,9 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) { current_class->add_member_group(annotation); } else { // For potential non-group standalone annotations. - push_error(R"(Unexpected standalone annotation in class body.)"); + push_error(R"(Unexpected standalone annotation.)"); } - } else { + } else { // `AnnotationInfo::CLASS_LEVEL`. annotation_stack.push_back(annotation); } } @@ -1120,7 +1223,12 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) { case VariableNode::PROP_INLINE: { FunctionNode *function = alloc_node<FunctionNode>(); - consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "get".)"); + if (match(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) { + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after "get(".)*"); + consume(GDScriptTokenizer::Token::COLON, R"*(Expected ":" after "get()".)*"); + } else { + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" or "(" after "get".)"); + } IdentifierNode *identifier = alloc_node<IdentifierNode>(); complete_extents(identifier); @@ -1268,8 +1376,7 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { EnumNode *enum_node = alloc_node<EnumNode>(); bool named = false; - if (check(GDScriptTokenizer::Token::IDENTIFIER)) { - advance(); + if (match(GDScriptTokenizer::Token::IDENTIFIER)) { enum_node->identifier = parse_identifier(); named = true; } @@ -1292,22 +1399,9 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { break; // Allow trailing comma. } if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for enum key.)")) { - EnumNode::Value item; GDScriptParser::IdentifierNode *identifier = parse_identifier(); -#ifdef DEBUG_ENABLED - if (!named) { // Named enum identifiers do not shadow anything since you can only access them with NamedEnum.ENUM_VALUE - for (MethodInfo &info : gdscript_funcs) { - if (info.name == identifier->name) { - push_warning(identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, "enum member", identifier->name, "built-in function"); - } - } - if (Variant::has_utility_function(identifier->name)) { - push_warning(identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, "enum member", identifier->name, "built-in function"); - } else if (ClassDB::class_exists(identifier->name)) { - push_warning(identifier, GDScriptWarning::SHADOWED_GLOBAL_IDENTIFIER, "enum member", identifier->name, "global class"); - } - } -#endif + + EnumNode::Value item; item.identifier = identifier; item.parent_enum = enum_node; item.line = previous.start_line; @@ -1353,7 +1447,7 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { if (i == enum_node->values.size() - 1 || enum_node->values[i + 1].line > enum_value_line) { doc_data = parse_doc_comment(enum_value_line, true); } - } else if (doc_comment_line >= min_enum_value_doc_line && has_comment(doc_comment_line, true) && tokenizer.get_comments()[doc_comment_line].new_line) { + } else if (doc_comment_line >= min_enum_value_doc_line && has_comment(doc_comment_line, true) && tokenizer->get_comments()[doc_comment_line].new_line) { // Normal doc comment. doc_data = parse_doc_comment(doc_comment_line); } @@ -1651,7 +1745,19 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { bool unreachable = current_suite->has_return && !current_suite->has_unreachable_code; #endif - bool is_annotation = false; + List<AnnotationNode *> annotations; + if (current.type != GDScriptTokenizer::Token::ANNOTATION) { + while (!annotation_stack.is_empty()) { + AnnotationNode *last_annotation = annotation_stack.back()->get(); + if (last_annotation->applies_to(AnnotationInfo::STATEMENT)) { + annotations.push_front(last_annotation); + annotation_stack.pop_back(); + } else { + push_error(vformat(R"(Annotation "%s" cannot be applied to a statement.)", last_annotation->name)); + clear_unused_annotations(); + } + } + } switch (current.type) { case GDScriptTokenizer::Token::PASS: @@ -1725,7 +1831,6 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { break; case GDScriptTokenizer::Token::ANNOTATION: { advance(); - is_annotation = true; AnnotationNode *annotation = parse_annotation(AnnotationInfo::STATEMENT); if (annotation != nullptr) { annotation_stack.push_back(annotation); @@ -1754,10 +1859,9 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { #ifdef DEBUG_ENABLED if (expression != nullptr) { switch (expression->type) { - case Node::CALL: case Node::ASSIGNMENT: case Node::AWAIT: - case Node::TERNARY_OPERATOR: + case Node::CALL: // Fine. break; case Node::LAMBDA: @@ -1765,11 +1869,14 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { push_error("Standalone lambdas cannot be accessed. Consider assigning it to a variable.", expression); break; case Node::LITERAL: - if (static_cast<GDScriptParser::LiteralNode *>(expression)->value.get_type() == Variant::STRING) { - // Allow strings as multiline comments. - break; + // Allow strings as multiline comments. + if (static_cast<GDScriptParser::LiteralNode *>(expression)->value.get_type() != Variant::STRING) { + push_warning(expression, GDScriptWarning::STANDALONE_EXPRESSION); } - [[fallthrough]]; + break; + case Node::TERNARY_OPERATOR: + push_warning(expression, GDScriptWarning::STANDALONE_TERNARY); + break; default: push_warning(expression, GDScriptWarning::STANDALONE_EXPRESSION); } @@ -1779,14 +1886,9 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { } } - while (!is_annotation && result != nullptr && !annotation_stack.is_empty()) { - AnnotationNode *last_annotation = annotation_stack.back()->get(); - if (last_annotation->applies_to(AnnotationInfo::STATEMENT)) { - result->annotations.push_front(last_annotation); - annotation_stack.pop_back(); - } else { - push_error(vformat(R"(Annotation "%s" cannot be applied to a statement.)", last_annotation->name)); - clear_unused_annotations(); + if (result != nullptr && !annotations.is_empty()) { + for (AnnotationNode *&annotation : annotations) { + result->annotations.push_back(annotation); } } @@ -1967,10 +2069,10 @@ GDScriptParser::IfNode *GDScriptParser::parse_if(const String &p_token) { } GDScriptParser::MatchNode *GDScriptParser::parse_match() { - MatchNode *match = alloc_node<MatchNode>(); + MatchNode *match_node = alloc_node<MatchNode>(); - match->test = parse_expression(false); - if (match->test == nullptr) { + match_node->test = parse_expression(false); + if (match_node->test == nullptr) { push_error(R"(Expected expression to test after "match".)"); } @@ -1978,20 +2080,45 @@ GDScriptParser::MatchNode *GDScriptParser::parse_match() { consume(GDScriptTokenizer::Token::NEWLINE, R"(Expected a newline after "match" statement.)"); if (!consume(GDScriptTokenizer::Token::INDENT, R"(Expected an indented block after "match" statement.)")) { - complete_extents(match); - return match; + complete_extents(match_node); + return match_node; } bool all_have_return = true; bool have_wildcard = false; + List<AnnotationNode *> match_branch_annotation_stack; + while (!check(GDScriptTokenizer::Token::DEDENT) && !is_at_end()) { + if (match(GDScriptTokenizer::Token::PASS)) { + consume(GDScriptTokenizer::Token::NEWLINE, R"(Expected newline after "pass".)"); + continue; + } + + if (match(GDScriptTokenizer::Token::ANNOTATION)) { + AnnotationNode *annotation = parse_annotation(AnnotationInfo::STATEMENT); + if (annotation == nullptr) { + continue; + } + if (annotation->name != SNAME("@warning_ignore")) { + push_error(vformat(R"(Annotation "%s" is not allowed in this level.)", annotation->name), annotation); + continue; + } + match_branch_annotation_stack.push_back(annotation); + continue; + } + MatchBranchNode *branch = parse_match_branch(); if (branch == nullptr) { advance(); continue; } + for (AnnotationNode *annotation : match_branch_annotation_stack) { + branch->annotations.push_back(annotation); + } + match_branch_annotation_stack.clear(); + #ifdef DEBUG_ENABLED if (have_wildcard && !branch->patterns.is_empty()) { push_warning(branch->patterns[0], GDScriptWarning::UNREACHABLE_PATTERN); @@ -2000,9 +2127,9 @@ GDScriptParser::MatchNode *GDScriptParser::parse_match() { have_wildcard = have_wildcard || branch->has_wildcard; all_have_return = all_have_return && branch->block->has_return; - match->branches.push_back(branch); + match_node->branches.push_back(branch); } - complete_extents(match); + complete_extents(match_node); consume(GDScriptTokenizer::Token::DEDENT, R"(Expected an indented block after "match" statement.)"); @@ -2010,7 +2137,12 @@ GDScriptParser::MatchNode *GDScriptParser::parse_match() { current_suite->has_return = true; } - return match; + for (const AnnotationNode *annotation : match_branch_annotation_stack) { + push_error(vformat(R"(Annotation "%s" does not precede a valid target, so it will have no effect.)", annotation->name), annotation); + } + match_branch_annotation_stack.clear(); + + return match_node; } GDScriptParser::MatchBranchNode *GDScriptParser::parse_match_branch() { @@ -2328,7 +2460,7 @@ GDScriptParser::IdentifierNode *GDScriptParser::parse_identifier() { IdentifierNode *identifier = static_cast<IdentifierNode *>(parse_identifier(nullptr, false)); #ifdef DEBUG_ENABLED // Check for spoofing here (if available in TextServer) since this isn't called inside expressions. This is only relevant for declarations. - if (identifier && TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY) && TS->spoof_check(identifier->name.operator String())) { + if (identifier && TS->has_feature(TextServer::FEATURE_UNICODE_SECURITY) && TS->spoof_check(identifier->name)) { push_warning(identifier, GDScriptWarning::CONFUSABLE_IDENTIFIER, identifier->name.operator String()); } #endif @@ -2342,6 +2474,9 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_identifier(ExpressionNode IdentifierNode *identifier = alloc_node<IdentifierNode>(); complete_extents(identifier); identifier->name = previous.get_identifier(); + if (identifier->name.operator String().is_empty()) { + print_line("Empty identifier found."); + } identifier->suite = current_suite; if (current_suite != nullptr && current_suite->has_local(identifier->name)) { @@ -3046,7 +3181,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_call(ExpressionNode *p_pre // Allow for trailing comma. break; } - bool use_identifier_completion = current.cursor_place == GDScriptTokenizer::CURSOR_END || current.cursor_place == GDScriptTokenizer::CURSOR_MIDDLE; + bool use_identifier_completion = current.cursor_place == GDScriptTokenizerText::CURSOR_END || current.cursor_place == GDScriptTokenizerText::CURSOR_MIDDLE; ExpressionNode *argument = parse_expression(false); if (argument == nullptr) { push_error(R"(Expected expression as the function argument.)"); @@ -3108,6 +3243,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p complete_extents(get_node); return nullptr; } + get_node->full_path += "%"; path_state = PATH_STATE_PERCENT; @@ -3151,6 +3287,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p path_state = PATH_STATE_NODE_NAME; } else if (current.is_node_name()) { advance(); + String identifier = previous.get_identifier(); #ifdef DEBUG_ENABLED // Check spoofing. @@ -3216,7 +3353,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p // Reset the multiline stack since we don't want the multiline mode one in the lambda body. push_multiline(false); if (multiline_context) { - tokenizer.push_expression_indented_block(); + tokenizer->push_expression_indented_block(); } push_multiline(true); // For the parameters. @@ -3263,9 +3400,9 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p if (multiline_context) { // If we're in multiline mode, we want to skip the spurious DEDENT and NEWLINE tokens. while (check(GDScriptTokenizer::Token::DEDENT) || check(GDScriptTokenizer::Token::INDENT) || check(GDScriptTokenizer::Token::NEWLINE)) { - current = tokenizer.scan(); // Not advance() since we don't want to change the previous token. + current = tokenizer->scan(); // Not advance() since we don't want to change the previous token. } - tokenizer.pop_expression_indented_block(); + tokenizer->pop_expression_indented_block(); } current_function = previous_function; @@ -3281,6 +3418,19 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p } GDScriptParser::ExpressionNode *GDScriptParser::parse_type_test(ExpressionNode *p_previous_operand, bool p_can_assign) { + // x is not int + // ^ ^^^ ExpressionNode, TypeNode + // ^^^^^^^^^^^^ TypeTestNode + // ^^^^^^^^^^^^ UnaryOpNode + UnaryOpNode *not_node = nullptr; + if (match(GDScriptTokenizer::Token::NOT)) { + not_node = alloc_node<UnaryOpNode>(); + not_node->operation = UnaryOpNode::OP_LOGIC_NOT; + not_node->variant_op = Variant::OP_NOT; + reset_extents(not_node, p_previous_operand); + update_extents(not_node); + } + TypeTestNode *type_test = alloc_node<TypeTestNode>(); reset_extents(type_test, p_previous_operand); update_extents(type_test); @@ -3289,8 +3439,21 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_type_test(ExpressionNode * type_test->test_type = parse_type(); complete_extents(type_test); + if (not_node != nullptr) { + not_node->operand = type_test; + complete_extents(not_node); + } + if (type_test->test_type == nullptr) { - push_error(R"(Expected type specifier after "is".)"); + if (not_node == nullptr) { + push_error(R"(Expected type specifier after "is".)"); + } else { + push_error(R"(Expected type specifier after "is not".)"); + } + } + + if (not_node != nullptr) { + return not_node; } return type_test; @@ -3381,6 +3544,7 @@ enum DocLineState { DOC_LINE_NORMAL, DOC_LINE_IN_CODE, DOC_LINE_IN_CODEBLOCK, + DOC_LINE_IN_KBD, }; static String _process_doc_line(const String &p_line, const String &p_text, const String &p_space_prefix, DocLineState &r_state) { @@ -3426,21 +3590,23 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons from = rb_pos + 1; String tag = line.substr(lb_pos + 1, rb_pos - lb_pos - 1); - if (tag == "code") { + if (tag == "code" || tag.begins_with("code ")) { r_state = DOC_LINE_IN_CODE; - } else if (tag == "codeblock") { + } else if (tag == "codeblock" || tag.begins_with("codeblock ")) { if (lb_pos == 0) { line_join = "\n"; } else { result += line.substr(buffer_start, lb_pos - buffer_start) + '\n'; } - result += "[codeblock]"; + result += "[" + tag + "]"; if (from < len) { result += '\n'; } r_state = DOC_LINE_IN_CODEBLOCK; buffer_start = from; + } else if (tag == "kbd") { + r_state = DOC_LINE_IN_KBD; } } break; case DOC_LINE_IN_CODE: { @@ -3450,7 +3616,7 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons break; } - from = pos + 7; + from = pos + 7; // `len("[/code]")`. r_state = DOC_LINE_NORMAL; } break; @@ -3461,7 +3627,7 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons break; } - from = pos + 12; + from = pos + 12; // `len("[/codeblock]")`. if (pos == 0) { line_join = "\n"; @@ -3476,6 +3642,17 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons r_state = DOC_LINE_NORMAL; buffer_start = from; } break; + case DOC_LINE_IN_KBD: { + int pos = line.find("[/kbd]", from); + if (pos < 0) { + process = false; + break; + } + + from = pos + 6; // `len("[/kbd]")`. + + r_state = DOC_LINE_NORMAL; + } break; } } @@ -3488,20 +3665,20 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons } bool GDScriptParser::has_comment(int p_line, bool p_must_be_doc) { - bool has_comment = tokenizer.get_comments().has(p_line); + bool has_comment = tokenizer->get_comments().has(p_line); // If there are no comments or if we don't care whether the comment // is a docstring, we have our result. if (!p_must_be_doc || !has_comment) { return has_comment; } - return tokenizer.get_comments()[p_line].comment.begins_with("##"); + return tokenizer->get_comments()[p_line].comment.begins_with("##"); } GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool p_single_line) { ERR_FAIL_COND_V(!has_comment(p_line, true), MemberDocData()); - const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); + const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer->get_comments(); int line = p_line; if (!p_single_line) { @@ -3532,11 +3709,17 @@ GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool if (state == DOC_LINE_NORMAL) { String stripped_line = doc_line.strip_edges(); - if (stripped_line.begins_with("@deprecated")) { + if (stripped_line == "@deprecated" || stripped_line.begins_with("@deprecated:")) { result.is_deprecated = true; + if (stripped_line.begins_with("@deprecated:")) { + result.deprecated_message = stripped_line.trim_prefix("@deprecated:").strip_edges(); + } continue; - } else if (stripped_line.begins_with("@experimental")) { + } else if (stripped_line == "@experimental" || stripped_line.begins_with("@experimental:")) { result.is_experimental = true; + if (stripped_line.begins_with("@experimental:")) { + result.experimental_message = stripped_line.trim_prefix("@experimental:").strip_edges(); + } continue; } } @@ -3550,7 +3733,7 @@ GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool GDScriptParser::ClassDocData GDScriptParser::parse_class_doc_comment(int p_line, bool p_single_line) { ERR_FAIL_COND_V(!has_comment(p_line, true), ClassDocData()); - const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); + const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer->get_comments(); int line = p_line; if (!p_single_line) { @@ -3635,11 +3818,17 @@ GDScriptParser::ClassDocData GDScriptParser::parse_class_doc_comment(int p_line, result.tutorials.append(Pair<String, String>(title, link)); continue; - } else if (stripped_line.begins_with("@deprecated")) { + } else if (stripped_line == "@deprecated" || stripped_line.begins_with("@deprecated:")) { result.is_deprecated = true; + if (stripped_line.begins_with("@deprecated:")) { + result.deprecated_message = stripped_line.trim_prefix("@deprecated:").strip_edges(); + } continue; - } else if (stripped_line.begins_with("@experimental")) { + } else if (stripped_line == "@experimental" || stripped_line.begins_with("@experimental:")) { result.is_experimental = true; + if (stripped_line.begins_with("@experimental:")) { + result.experimental_message = stripped_line.trim_prefix("@experimental:").strip_edges(); + } continue; } } @@ -3829,7 +4018,7 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation) return false; } - // `@icon`'s argument needs to be resolved in the parser. See GH-72444. + // Some annotations need to be resolved in the parser. if (p_annotation->name == SNAME("@icon")) { ExpressionNode *argument = p_annotation->arguments[0]; @@ -3855,12 +4044,12 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation) bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { #ifdef DEBUG_ENABLED - if (this->_is_tool) { + if (_is_tool) { push_error(R"("@tool" annotation can only be used once.)", p_annotation); return false; } #endif // DEBUG_ENABLED - this->_is_tool = true; + _is_tool = true; return true; } @@ -3916,6 +4105,55 @@ bool GDScriptParser::onready_annotation(const AnnotationNode *p_annotation, Node return true; } +static String _get_annotation_error_string(const StringName &p_annotation_name, const Vector<Variant::Type> &p_expected_types, const GDScriptParser::DataType &p_provided_type) { + Vector<String> types; + for (int i = 0; i < p_expected_types.size(); i++) { + const Variant::Type &type = p_expected_types[i]; + types.push_back(Variant::get_type_name(type)); + types.push_back("Array[" + Variant::get_type_name(type) + "]"); + switch (type) { + case Variant::INT: + types.push_back("PackedByteArray"); + types.push_back("PackedInt32Array"); + types.push_back("PackedInt64Array"); + break; + case Variant::FLOAT: + types.push_back("PackedFloat32Array"); + types.push_back("PackedFloat64Array"); + break; + case Variant::STRING: + types.push_back("PackedStringArray"); + break; + case Variant::VECTOR2: + types.push_back("PackedVector2Array"); + break; + case Variant::VECTOR3: + types.push_back("PackedVector3Array"); + break; + case Variant::COLOR: + types.push_back("PackedColorArray"); + break; + default: + break; + } + } + + String string; + if (types.size() == 1) { + string = types[0].quote(); + } else if (types.size() == 2) { + string = types[0].quote() + " or " + types[1].quote(); + } else if (types.size() >= 3) { + string = types[0].quote(); + for (int i = 1; i < types.size() - 1; i++) { + string += ", " + types[i].quote(); + } + string += ", or " + types[types.size() - 1].quote(); + } + + return vformat(R"("%s" annotation requires a variable of type %s, but type "%s" was given instead.)", p_annotation_name, string, p_provided_type.to_string()); +} + template <PropertyHint t_hint, Variant::Type t_type> bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); @@ -3996,59 +4234,56 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } hint_string += arg_string; } - variable->export_info.hint_string = hint_string; // This is called after the analyzer is done finding the type, so this should be set here. DataType export_type = variable->get_datatype(); + // Use initializer type if specified type is `Variant`. + if (export_type.is_variant() && variable->initializer != nullptr && variable->initializer->datatype.is_set()) { + export_type = variable->initializer->get_datatype(); + export_type.type_source = DataType::INFERRED; + } + + const Variant::Type original_export_type_builtin = export_type.builtin_type; + + // Process array and packed array annotations on the element type. + bool is_array = false; + if (export_type.builtin_type == Variant::ARRAY && export_type.has_container_element_type(0)) { + is_array = true; + export_type = export_type.get_container_element_type(0); + } else if (export_type.is_typed_container_type()) { + is_array = true; + export_type = export_type.get_typed_container_type(); + export_type.type_source = variable->datatype.type_source; + } + + bool use_default_variable_type_check = true; + if (p_annotation->name == SNAME("@export_range")) { if (export_type.builtin_type == Variant::INT) { variable->export_info.type = Variant::INT; } } else if (p_annotation->name == SNAME("@export_multiline")) { - if (export_type.builtin_type == Variant::ARRAY && export_type.has_container_element_type(0)) { - DataType inner_type = export_type.get_container_element_type(0); - if (inner_type.builtin_type != Variant::STRING) { - push_error(vformat(R"("%s" annotation on arrays requires a string type but type "%s" was given instead.)", p_annotation->name.operator String(), inner_type.to_string()), variable); - return false; - } + use_default_variable_type_check = false; - String hint_prefix = itos(inner_type.builtin_type) + "/" + itos(variable->export_info.hint); - variable->export_info.hint = PROPERTY_HINT_TYPE_STRING; - variable->export_info.hint_string = hint_prefix + ":" + variable->export_info.hint_string; - variable->export_info.type = Variant::ARRAY; + if (export_type.builtin_type != Variant::STRING && export_type.builtin_type != Variant::DICTIONARY) { + Vector<Variant::Type> expected_types = { Variant::STRING, Variant::DICTIONARY }; + push_error(_get_annotation_error_string(p_annotation->name, expected_types, variable->get_datatype()), p_annotation); + return false; + } - return true; - } else if (export_type.builtin_type == Variant::DICTIONARY) { + if (export_type.builtin_type == Variant::DICTIONARY) { variable->export_info.type = Variant::DICTIONARY; - - return true; - } else if (export_type.builtin_type == Variant::PACKED_STRING_ARRAY) { - String hint_prefix = itos(Variant::STRING) + "/" + itos(variable->export_info.hint); - variable->export_info.hint = PROPERTY_HINT_TYPE_STRING; - variable->export_info.hint_string = hint_prefix + ":" + variable->export_info.hint_string; - variable->export_info.type = Variant::PACKED_STRING_ARRAY; - - return true; } - } + } else if (p_annotation->name == SNAME("@export")) { + use_default_variable_type_check = false; - // WARNING: Do not merge with the previous `else if`! Otherwise `else` (default variable type check) - // will not work for the above annotations. `@export` and `@export_enum` validate the type separately. - if (p_annotation->name == SNAME("@export")) { if (variable->datatype_specifier == nullptr && variable->initializer == nullptr) { push_error(R"(Cannot use simple "@export" annotation with variable without type or initializer, since type can't be inferred.)", p_annotation); return false; } - bool is_array = false; - - if (export_type.builtin_type == Variant::ARRAY && export_type.has_container_element_type(0)) { - export_type = export_type.get_container_element_type(0); // Use inner type for. - is_array = true; - } - if (export_type.is_variant() || export_type.has_no_type()) { push_error(R"(Cannot use simple "@export" annotation because the type of the initialized value can't be inferred.)", p_annotation); return false; @@ -4070,7 +4305,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node variable->export_info.hint = PROPERTY_HINT_NODE_TYPE; variable->export_info.hint_string = export_type.native_type; } else { - push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", variable); + push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", p_annotation); return false; } break; @@ -4084,7 +4319,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node variable->export_info.hint = PROPERTY_HINT_NODE_TYPE; variable->export_info.hint_string = export_type.to_string(); } else { - push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", variable); + push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", p_annotation); return false; } @@ -4135,53 +4370,84 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } } break; default: - push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", variable); + push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", p_annotation); return false; } if (variable->export_info.hint == PROPERTY_HINT_NODE_TYPE && !ClassDB::is_parent_class(p_class->base_type.native_type, SNAME("Node"))) { - push_error(vformat(R"(Node export is only supported in Node-derived classes, but the current class inherits "%s".)", p_class->base_type.to_string()), variable); + push_error(vformat(R"(Node export is only supported in Node-derived classes, but the current class inherits "%s".)", p_class->base_type.to_string()), p_annotation); return false; } - - if (is_array) { - String hint_prefix = itos(variable->export_info.type); - if (variable->export_info.hint) { - hint_prefix += "/" + itos(variable->export_info.hint); - } - variable->export_info.hint = PROPERTY_HINT_TYPE_STRING; - variable->export_info.hint_string = hint_prefix + ":" + variable->export_info.hint_string; - variable->export_info.type = Variant::ARRAY; - } } else if (p_annotation->name == SNAME("@export_enum")) { + use_default_variable_type_check = false; + Variant::Type enum_type = Variant::INT; if (export_type.kind == DataType::BUILTIN && export_type.builtin_type == Variant::STRING) { enum_type = Variant::STRING; - } else if (export_type.is_variant() && variable->initializer != nullptr) { - DataType initializer_type = variable->initializer->get_datatype(); - if (initializer_type.kind == DataType::BUILTIN && initializer_type.builtin_type == Variant::STRING) { - enum_type = Variant::STRING; - } } variable->export_info.type = enum_type; if (!export_type.is_variant() && (export_type.kind != DataType::BUILTIN || export_type.builtin_type != enum_type)) { - push_error(vformat(R"("@export_enum" annotation requires a variable of type "int" or "String" but type "%s" was given instead.)", export_type.to_string()), variable); + Vector<Variant::Type> expected_types = { Variant::INT, Variant::STRING }; + push_error(_get_annotation_error_string(p_annotation->name, expected_types, variable->get_datatype()), p_annotation); return false; } - } else { + } else if (p_annotation->name == SNAME("@export_storage")) { + use_default_variable_type_check = false; // Can be applied to a variable of any type. + + // Save the info because the compiler uses export info for overwriting member info. + variable->export_info = export_type.to_property_info(variable->identifier->name); + variable->export_info.usage |= PROPERTY_USAGE_STORAGE; + } + + if (use_default_variable_type_check) { // Validate variable type with export. if (!export_type.is_variant() && (export_type.kind != DataType::BUILTIN || export_type.builtin_type != t_type)) { // Allow float/int conversion. if ((t_type != Variant::FLOAT || export_type.builtin_type != Variant::INT) && (t_type != Variant::INT || export_type.builtin_type != Variant::FLOAT)) { - push_error(vformat(R"("%s" annotation requires a variable of type "%s" but type "%s" was given instead.)", p_annotation->name.operator String(), Variant::get_type_name(t_type), export_type.to_string()), variable); + Vector<Variant::Type> expected_types = { t_type }; + push_error(_get_annotation_error_string(p_annotation->name, expected_types, variable->get_datatype()), p_annotation); return false; } } } + if (is_array) { + String hint_prefix = itos(variable->export_info.type); + if (variable->export_info.hint) { + hint_prefix += "/" + itos(variable->export_info.hint); + } + variable->export_info.hint = PROPERTY_HINT_TYPE_STRING; + variable->export_info.hint_string = hint_prefix + ":" + variable->export_info.hint_string; + variable->export_info.type = original_export_type_builtin; + } + + return true; +} + +bool GDScriptParser::export_custom_annotation(const AnnotationNode *p_annotation, Node *p_node, ClassNode *p_class) { + ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); + ERR_FAIL_COND_V_MSG(p_annotation->resolved_arguments.size() < 2, false, R"(Annotation "@export_custom" requires 2 arguments.)"); + + VariableNode *variable = static_cast<VariableNode *>(p_node); + if (variable->exported) { + push_error(vformat(R"(Annotation "%s" cannot be used with another "@export" annotation.)", p_annotation->name), p_annotation); + return false; + } + + variable->exported = true; + + DataType export_type = variable->get_datatype(); + + variable->export_info.type = export_type.builtin_type; + variable->export_info.hint = static_cast<PropertyHint>(p_annotation->resolved_arguments[0].operator int64_t()); + variable->export_info.hint_string = p_annotation->resolved_arguments[1]; + + if (p_annotation->resolved_arguments.size() >= 3) { + variable->export_info.usage = p_annotation->resolved_arguments[2].operator int64_t(); + } return true; } @@ -4219,23 +4485,77 @@ bool GDScriptParser::export_group_annotations(const AnnotationNode *p_annotation } bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { -#ifdef DEBUG_ENABLED +#ifndef DEBUG_ENABLED + // Only available in debug builds. + return true; +#else // DEBUG_ENABLED + if (is_ignoring_warnings) { + return true; // We already ignore all warnings, let's optimize it. + } + bool has_error = false; for (const Variant &warning_name : p_annotation->resolved_arguments) { - GDScriptWarning::Code warning = GDScriptWarning::get_code_from_name(String(warning_name).to_upper()); - if (warning == GDScriptWarning::WARNING_MAX) { + GDScriptWarning::Code warning_code = GDScriptWarning::get_code_from_name(String(warning_name).to_upper()); + if (warning_code == GDScriptWarning::WARNING_MAX) { push_error(vformat(R"(Invalid warning name: "%s".)", warning_name), p_annotation); has_error = true; } else { - p_target->ignored_warnings.push_back(warning); + int start_line = p_annotation->start_line; + int end_line = p_target->end_line; + + switch (p_target->type) { +#define SIMPLE_CASE(m_type, m_class, m_property) \ + case m_type: { \ + m_class *node = static_cast<m_class *>(p_target); \ + if (node->m_property == nullptr) { \ + end_line = node->start_line; \ + } else { \ + end_line = node->m_property->end_line; \ + } \ + } break; + + // Can contain properties (set/get). + SIMPLE_CASE(Node::VARIABLE, VariableNode, initializer) + + // Contain bodies. + SIMPLE_CASE(Node::FOR, ForNode, list) + SIMPLE_CASE(Node::IF, IfNode, condition) + SIMPLE_CASE(Node::MATCH, MatchNode, test) + SIMPLE_CASE(Node::WHILE, WhileNode, condition) +#undef SIMPLE_CASE + + case Node::CLASS: { + end_line = p_target->start_line; + for (const AnnotationNode *annotation : p_target->annotations) { + start_line = MIN(start_line, annotation->start_line); + end_line = MAX(end_line, annotation->end_line); + } + } break; + + case Node::FUNCTION: { + // `@warning_ignore` on function has a controversial feature that is used in tests. + // It's better not to remove it for now, while there is no way to mass-ignore warnings. + } break; + + case Node::MATCH_BRANCH: { + MatchBranchNode *branch = static_cast<MatchBranchNode *>(p_target); + end_line = branch->start_line; + for (int i = 0; i < branch->patterns.size(); i++) { + end_line = MAX(end_line, branch->patterns[i]->end_line); + } + } break; + + default: { + } break; + } + + end_line = MAX(start_line, end_line); // Prevent infinite loop. + for (int line = start_line; line <= end_line; line++) { + warning_ignored_lines[warning_code].insert(line); + } } } - return !has_error; - -#else // ! DEBUG_ENABLED - // Only available in debug builds. - return true; #endif // DEBUG_ENABLED } @@ -4997,6 +5317,9 @@ void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const for (const AnnotationNode *E : p_function->annotations) { print_annotation(E); } + if (p_function->is_static) { + push_text("Static "); + } push_text(p_context); push_text(" "); if (p_function->identifier) { @@ -5341,6 +5664,9 @@ void GDScriptParser::TreePrinter::print_variable(VariableNode *p_variable) { print_annotation(E); } + if (p_variable->is_static) { + push_text("Static "); + } push_text("Variable "); print_identifier(p_variable->identifier); |