diff options
Diffstat (limited to 'modules/gdscript/gdscript_parser.cpp')
-rw-r--r-- | modules/gdscript/gdscript_parser.cpp | 627 |
1 files changed, 427 insertions, 200 deletions
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 4625855329..9799c6e610 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -101,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>); @@ -120,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)); } @@ -145,23 +147,17 @@ GDScriptParser::GDScriptParser() { } GDScriptParser::~GDScriptParser() { - clear(); -} - -void GDScriptParser::clear() { while (list != nullptr) { Node *element = list; list = list->next; memdelete(element); } +} - head = nullptr; - list = nullptr; - _is_tool = false; - for_completion = false; - errors.clear(); - multiline_stack.clear(); - nodes_in_progress.clear(); +void GDScriptParser::clear() { + GDScriptParser tmp; + tmp = *this; + *this = GDScriptParser(); } void GDScriptParser::push_error(const String &p_message, const Node *p_origin) { @@ -179,47 +175,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 @@ -551,25 +562,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; @@ -590,6 +629,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.)"); @@ -598,6 +641,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.)"); @@ -606,6 +653,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. @@ -627,6 +678,8 @@ void GDScriptParser::parse_program() { } } +#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD + parse_class_body(true); complete_extents(head); @@ -650,6 +703,25 @@ void GDScriptParser::parse_program() { clear_unused_annotations(); } +Ref<GDScriptParserRef> GDScriptParser::get_depended_parser_for(const String &p_path) { + Ref<GDScriptParserRef> ref; + if (depended_parsers.has(p_path)) { + ref = depended_parsers[p_path]; + } else { + Error err = OK; + ref = GDScriptCache::get_parser(p_path, GDScriptParserRef::EMPTY, err, script_path); + if (ref.is_valid()) { + depended_parsers[p_path] = ref; + } + } + + return ref; +} + +const HashMap<String, Ref<GDScriptParserRef>> &GDScriptParser::get_depended_parsers() { + return depended_parsers; +} + GDScriptParser::ClassNode *GDScriptParser::find_class(const String &p_qualified_name) const { String first = p_qualified_name.get_slice("::", 0); @@ -709,7 +781,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 { @@ -797,7 +869,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(); @@ -905,8 +977,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) { @@ -916,9 +988,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); } } @@ -1340,22 +1412,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; @@ -1699,7 +1758,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: @@ -1773,7 +1844,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); @@ -1802,10 +1872,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: @@ -1813,11 +1882,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); } @@ -1827,14 +1899,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); } } @@ -2015,10 +2082,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".)"); } @@ -2026,20 +2093,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); @@ -2048,9 +2140,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.)"); @@ -2058,7 +2150,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() { @@ -2186,28 +2283,31 @@ GDScriptParser::PatternNode *GDScriptParser::parse_match_pattern(PatternNode *p_ break; case GDScriptTokenizer::Token::BRACKET_OPEN: { // Array. + push_multiline(true); advance(); pattern->pattern_type = PatternNode::PT_ARRAY; - - if (!check(GDScriptTokenizer::Token::BRACKET_CLOSE)) { - do { - PatternNode *sub_pattern = parse_match_pattern(p_root_pattern != nullptr ? p_root_pattern : pattern); - if (sub_pattern == nullptr) { - continue; - } - if (pattern->rest_used) { - push_error(R"(The ".." pattern must be the last element in the pattern array.)"); - } else if (sub_pattern->pattern_type == PatternNode::PT_REST) { - pattern->rest_used = true; - } - pattern->array.push_back(sub_pattern); - } while (match(GDScriptTokenizer::Token::COMMA)); - } + do { + if (is_at_end() || check(GDScriptTokenizer::Token::BRACKET_CLOSE)) { + break; + } + PatternNode *sub_pattern = parse_match_pattern(p_root_pattern != nullptr ? p_root_pattern : pattern); + if (sub_pattern == nullptr) { + continue; + } + if (pattern->rest_used) { + push_error(R"(The ".." pattern must be the last element in the pattern array.)"); + } else if (sub_pattern->pattern_type == PatternNode::PT_REST) { + pattern->rest_used = true; + } + pattern->array.push_back(sub_pattern); + } while (match(GDScriptTokenizer::Token::COMMA)); consume(GDScriptTokenizer::Token::BRACKET_CLOSE, R"(Expected "]" to close the array pattern.)"); + pop_multiline(); break; } case GDScriptTokenizer::Token::BRACE_OPEN: { // Dictionary. + push_multiline(true); advance(); pattern->pattern_type = PatternNode::PT_DICTIONARY; do { @@ -2250,6 +2350,7 @@ GDScriptParser::PatternNode *GDScriptParser::parse_match_pattern(PatternNode *p_ } } while (match(GDScriptTokenizer::Token::COMMA)); consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected "}" to close the dictionary pattern.)"); + pop_multiline(); break; } default: { @@ -2376,7 +2477,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 @@ -2685,10 +2786,6 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode return parse_expression(false); // Return the following expression. } -#ifdef DEBUG_ENABLED - VariableNode *source_variable = nullptr; -#endif - switch (p_previous_operand->type) { case Node::IDENTIFIER: { #ifdef DEBUG_ENABLED @@ -2697,8 +2794,6 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode IdentifierNode *id = static_cast<IdentifierNode *>(p_previous_operand); switch (id->source) { case IdentifierNode::LOCAL_VARIABLE: - - source_variable = id->variable_source; id->variable_source->usages--; break; case IdentifierNode::LOCAL_CONSTANT: @@ -2729,16 +2824,10 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode update_extents(assignment); make_completion_context(COMPLETION_ASSIGN, assignment); -#ifdef DEBUG_ENABLED - bool has_operator = true; -#endif switch (previous.type) { case GDScriptTokenizer::Token::EQUAL: assignment->operation = AssignmentNode::OP_NONE; assignment->variant_op = Variant::OP_MAX; -#ifdef DEBUG_ENABLED - has_operator = false; -#endif break; case GDScriptTokenizer::Token::PLUS_EQUAL: assignment->operation = AssignmentNode::OP_ADDITION; @@ -2794,16 +2883,6 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode } complete_extents(assignment); -#ifdef DEBUG_ENABLED - if (source_variable != nullptr) { - if (has_operator && source_variable->assignments == 0) { - push_warning(assignment, GDScriptWarning::UNASSIGNED_VARIABLE_OP_ASSIGN, source_variable->identifier->name); - } - - source_variable->assignments += 1; - } -#endif - return assignment; } @@ -3159,6 +3238,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p complete_extents(get_node); return nullptr; } + get_node->full_path += "%"; path_state = PATH_STATE_PERCENT; @@ -3202,6 +3282,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. @@ -3458,6 +3539,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) { @@ -3503,21 +3585,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: { @@ -3527,7 +3611,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; @@ -3538,7 +3622,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"; @@ -3553,6 +3637,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; } } @@ -3918,7 +4013,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]; @@ -3989,6 +4084,7 @@ bool GDScriptParser::onready_annotation(const AnnotationNode *p_annotation, Node if (current_class && !ClassDB::is_parent_class(current_class->get_datatype().native_type, SNAME("Node"))) { push_error(R"("@onready" can only be used in classes that inherit "Node".)", p_annotation); + return false; } VariableNode *variable = static_cast<VariableNode *>(p_target); @@ -4005,6 +4101,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)); @@ -4085,59 +4230,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; @@ -4159,7 +4301,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; @@ -4173,7 +4315,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; } @@ -4224,53 +4366,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; } @@ -4308,23 +4481,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 } |