diff options
Diffstat (limited to 'modules/gdscript/gdscript_parser.cpp')
-rw-r--r-- | modules/gdscript/gdscript_parser.cpp | 529 |
1 files changed, 365 insertions, 164 deletions
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index aae45274e0..f1a35c84b7 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -101,7 +101,6 @@ 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>); @@ -121,13 +120,14 @@ 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_storage"), AnnotationInfo::VARIABLE, &GDScriptParser::export_storage_annotation); 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)); } @@ -147,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) { @@ -172,7 +166,7 @@ void GDScriptParser::push_error(const String &p_message, const Node *p_origin) { panic_mode = true; // TODO: Improve positional information. if (p_origin == nullptr) { - errors.push_back({ p_message, current.start_line, current.start_column }); + errors.push_back({ p_message, previous.start_line, previous.start_column }); } else { errors.push_back({ p_message, p_origin->start_line, p_origin->leftmost_column }); } @@ -181,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 @@ -553,25 +562,53 @@ void GDScriptParser::end_statement(const String &p_context) { void GDScriptParser::parse_program() { head = alloc_node<ClassNode>(); + 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; @@ -592,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.)"); @@ -600,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.)"); @@ -608,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. @@ -629,6 +678,8 @@ void GDScriptParser::parse_program() { } } +#undef PUSH_PENDING_ANNOTATIONS_TO_HEAD + parse_class_body(true); complete_extents(head); @@ -652,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); @@ -799,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(); @@ -907,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) { @@ -918,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); } } @@ -1342,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; @@ -1701,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: @@ -1775,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); @@ -1804,22 +1872,28 @@ 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::PRELOAD: + // `preload` is a function-like keyword. + push_warning(expression, GDScriptWarning::RETURN_VALUE_DISCARDED, "preload"); + break; case Node::LAMBDA: // Standalone lambdas can't be used, so make this an error. 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); } @@ -1829,14 +1903,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); } } @@ -2017,10 +2086,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".)"); } @@ -2028,20 +2097,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); @@ -2050,9 +2144,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.)"); @@ -2060,7 +2154,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() { @@ -2188,28 +2287,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 { @@ -2252,6 +2354,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: { @@ -2378,7 +2481,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 @@ -2687,10 +2790,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 @@ -2699,8 +2798,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: @@ -2731,16 +2828,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; @@ -2796,16 +2887,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; } @@ -3122,6 +3203,9 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_call(ExpressionNode *p_pre } GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p_previous_operand, bool p_can_assign) { + // We want code completion after a DOLLAR even if the current code is invalid. + make_completion_context(COMPLETION_GET_NODE, nullptr, -1, true); + if (!current.is_node_name() && !check(GDScriptTokenizer::Token::LITERAL) && !check(GDScriptTokenizer::Token::SLASH) && !check(GDScriptTokenizer::Token::PERCENT)) { push_error(vformat(R"(Expected node path as string or identifier after "%s".)", previous.get_name())); return nullptr; @@ -3161,6 +3245,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p complete_extents(get_node); return nullptr; } + get_node->full_path += "%"; path_state = PATH_STATE_PERCENT; @@ -3176,7 +3261,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p path_state = PATH_STATE_SLASH; } - make_completion_context(COMPLETION_GET_NODE, get_node, context_argument++); + make_completion_context(COMPLETION_GET_NODE, get_node, context_argument++, true); if (match(GDScriptTokenizer::Token::LITERAL)) { if (previous.literal.get_type() != Variant::STRING) { @@ -3204,6 +3289,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. @@ -3460,6 +3546,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) { @@ -3505,21 +3592,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: { @@ -3529,7 +3618,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; @@ -3540,7 +3629,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"; @@ -3555,6 +3644,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; } } @@ -3920,7 +4020,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]; @@ -3991,6 +4091,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); @@ -4035,6 +4136,9 @@ static String _get_annotation_error_string(const StringName &p_annotation_name, case Variant::COLOR: types.push_back("PackedColorArray"); break; + case Variant::VECTOR4: + types.push_back("PackedVector4Array"); + break; default: break; } @@ -4195,7 +4299,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node case GDScriptParser::DataType::BUILTIN: variable->export_info.type = export_type.builtin_type; variable->export_info.hint = PROPERTY_HINT_NONE; - variable->export_info.hint_string = Variant::get_type_name(export_type.builtin_type); + variable->export_info.hint_string = String(); break; case GDScriptParser::DataType::NATIVE: if (ClassDB::is_parent_class(export_type.native_type, SNAME("Resource"))) { @@ -4211,39 +4315,55 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node return false; } break; - case GDScriptParser::DataType::CLASS: + case GDScriptParser::DataType::CLASS: { + StringName class_name; + if (export_type.class_type) { + class_name = export_type.class_type->get_global_name(); + } + if (class_name == StringName()) { + push_error(R"(Script export type must be a global class.)", p_annotation); + return false; + } if (ClassDB::is_parent_class(export_type.native_type, SNAME("Resource"))) { variable->export_info.type = Variant::OBJECT; variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE; - variable->export_info.hint_string = export_type.to_string(); + variable->export_info.hint_string = class_name; } else if (ClassDB::is_parent_class(export_type.native_type, SNAME("Node"))) { variable->export_info.type = Variant::OBJECT; variable->export_info.hint = PROPERTY_HINT_NODE_TYPE; - variable->export_info.hint_string = export_type.to_string(); + variable->export_info.hint_string = class_name; } else { push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", p_annotation); return false; } + } break; - break; case GDScriptParser::DataType::SCRIPT: { StringName class_name; - StringName native_base; if (export_type.script_type.is_valid()) { - class_name = export_type.script_type->get_language()->get_global_class_name(export_type.script_type->get_path()); - native_base = export_type.script_type->get_instance_base_type(); + class_name = export_type.script_type->get_global_name(); } if (class_name == StringName()) { Ref<Script> script = ResourceLoader::load(export_type.script_path, SNAME("Script")); if (script.is_valid()) { - class_name = script->get_language()->get_global_class_name(export_type.script_path); - native_base = script->get_instance_base_type(); + class_name = script->get_global_name(); } } - if (class_name != StringName() && native_base != StringName() && ClassDB::is_parent_class(native_base, SNAME("Resource"))) { + if (class_name == StringName()) { + push_error(R"(Script export type must be a global class.)", p_annotation); + return false; + } + if (ClassDB::is_parent_class(export_type.native_type, SNAME("Resource"))) { variable->export_info.type = Variant::OBJECT; variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE; variable->export_info.hint_string = class_name; + } else if (ClassDB::is_parent_class(export_type.native_type, SNAME("Node"))) { + variable->export_info.type = Variant::OBJECT; + variable->export_info.hint = PROPERTY_HINT_NODE_TYPE; + variable->export_info.hint_string = class_name; + } else { + push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", p_annotation); + return false; } } break; case GDScriptParser::DataType::ENUM: { @@ -4296,12 +4416,6 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node push_error(_get_annotation_error_string(p_annotation->name, expected_types, variable->get_datatype()), p_annotation); return false; } - } 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) { @@ -4321,19 +4435,50 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node if (variable->export_info.hint) { hint_prefix += "/" + itos(variable->export_info.hint); } + variable->export_info.type = original_export_type_builtin; 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; + variable->export_info.usage = PROPERTY_USAGE_DEFAULT; + variable->export_info.class_name = StringName(); } return true; } +// For `@export_storage` and `@export_custom`, there is no need to check the variable type, argument values, +// or handle array exports in a special way, so they are implemented as separate methods. + +bool GDScriptParser::export_storage_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)); + + VariableNode *variable = static_cast<VariableNode *>(p_node); + if (variable->is_static) { + push_error(vformat(R"(Annotation "%s" cannot be applied to a static variable.)", p_annotation->name), p_annotation); + return false; + } + 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; + + // Save the info because the compiler uses export info for overwriting member info. + variable->export_info = variable->get_datatype().to_property_info(variable->identifier->name); + variable->export_info.usage |= PROPERTY_USAGE_STORAGE; + + 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->is_static) { + push_error(vformat(R"(Annotation "%s" cannot be applied to a static variable.)", p_annotation->name), p_annotation); + return false; + } if (variable->exported) { push_error(vformat(R"(Annotation "%s" cannot be used with another "@export" annotation.)", p_annotation->name), p_annotation); return false; @@ -4387,23 +4532,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 } @@ -4679,6 +4878,8 @@ static Variant::Type _variant_type_to_typed_array_element_type(Variant::Type p_t return Variant::VECTOR3; case Variant::PACKED_COLOR_ARRAY: return Variant::COLOR; + case Variant::PACKED_VECTOR4_ARRAY: + return Variant::VECTOR4; default: return Variant::NIL; } |