diff options
Diffstat (limited to 'modules/gdscript')
27 files changed, 601 insertions, 266 deletions
diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp index e488d6e266..1be690d894 100644 --- a/modules/gdscript/editor/gdscript_highlighter.cpp +++ b/modules/gdscript/editor/gdscript_highlighter.cpp @@ -52,6 +52,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l bool in_keyword = false; bool in_word = false; bool in_number = false; + bool in_raw_string = false; bool in_node_path = false; bool in_node_ref = false; bool in_annotation = false; @@ -234,15 +235,33 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l } if (str[from] == '\\') { - Dictionary escape_char_highlighter_info; - escape_char_highlighter_info["color"] = symbol_color; - color_map[from] = escape_char_highlighter_info; + if (!in_raw_string) { + Dictionary escape_char_highlighter_info; + escape_char_highlighter_info["color"] = symbol_color; + color_map[from] = escape_char_highlighter_info; + } from++; - Dictionary region_continue_highlighter_info; - region_continue_highlighter_info["color"] = region_color; - color_map[from + 1] = region_continue_highlighter_info; + if (!in_raw_string) { + int esc_len = 0; + if (str[from] == 'u') { + esc_len = 4; + } else if (str[from] == 'U') { + esc_len = 6; + } + for (int k = 0; k < esc_len && from < line_length - 1; k++) { + if (!is_hex_digit(str[from + 1])) { + break; + } + from++; + } + + Dictionary region_continue_highlighter_info; + region_continue_highlighter_info["color"] = region_color; + color_map[from + 1] = region_continue_highlighter_info; + } + continue; } @@ -489,6 +508,12 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l in_member_variable = false; } + if (!in_raw_string && in_region == -1 && str[j] == 'r' && j < line_length - 1 && (str[j + 1] == '"' || str[j + 1] == '\'')) { + in_raw_string = true; + } else if (in_raw_string && in_region == -1) { + in_raw_string = false; + } + // Keep symbol color for binary '&&'. In the case of '&&&' use StringName color for the last ampersand. if (!in_string_name && in_region == -1 && str[j] == '&' && !is_binary_op) { if (j >= 2 && str[j - 1] == '&' && str[j - 2] != '&' && prev_is_binary_op) { @@ -520,7 +545,9 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l in_annotation = false; } - if (in_node_ref) { + if (in_raw_string) { + color = string_color; + } else if (in_node_ref) { next_type = NODE_REF; color = node_ref_color; } else if (in_annotation) { @@ -692,7 +719,7 @@ void GDScriptSyntaxHighlighter::_update_cache() { } /* Strings */ - const Color string_color = EDITOR_GET("text_editor/theme/highlighting/string_color"); + string_color = EDITOR_GET("text_editor/theme/highlighting/string_color"); List<String> strings; gdscript->get_string_delimiters(&strings); for (const String &string : strings) { diff --git a/modules/gdscript/editor/gdscript_highlighter.h b/modules/gdscript/editor/gdscript_highlighter.h index fe3b63d713..090857f397 100644 --- a/modules/gdscript/editor/gdscript_highlighter.h +++ b/modules/gdscript/editor/gdscript_highlighter.h @@ -78,6 +78,7 @@ private: Color built_in_type_color; Color number_color; Color member_color; + Color string_color; Color node_path_color; Color node_ref_color; Color annotation_color; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 4b8d527881..04c86d60a8 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -2609,7 +2609,7 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig } // Check if assigned value is an array literal, so we can make it a typed array too if appropriate. - if (p_assignment->assigned_value->type == GDScriptParser::Node::ARRAY && assignee_type.has_container_element_type()) { + if (p_assignment->assigned_value->type == GDScriptParser::Node::ARRAY && assignee_type.is_hard_type() && assignee_type.has_container_element_type()) { update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_assignment->assigned_value), assignee_type.get_container_element_type()); } @@ -3204,10 +3204,16 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a bool is_constructor = (base_type.is_meta_type || (p_call->callee && p_call->callee->type == GDScriptParser::Node::IDENTIFIER)) && p_call->function_name == SNAME("new"); if (get_function_signature(p_call, is_constructor, base_type, p_call->function_name, return_type, par_types, default_arg_count, method_flags)) { - // If the function require typed arrays we must make literals be typed. + // If the method is implemented in the class hierarchy, the virtual flag will not be set for that MethodInfo and the search stops there. + // Virtual check only possible for super() calls because class hierarchy is known. Node/Objects may have scripts attached we don't know of at compile-time. + if (p_call->is_super && method_flags.has_flag(METHOD_FLAG_VIRTUAL)) { + push_error(vformat(R"*(Cannot call the parent class' virtual function "%s()" because it hasn't been defined.)*", p_call->function_name), p_call); + } + + // If the function requires typed arrays we must make literals be typed. for (const KeyValue<int, GDScriptParser::ArrayNode *> &E : arrays) { int index = E.key; - if (index < par_types.size() && par_types[index].has_container_element_type()) { + if (index < par_types.size() && par_types[index].is_hard_type() && par_types[index].has_container_element_type()) { update_array_literal_element_type(E.value, par_types[index].get_container_element_type()); } } @@ -4093,7 +4099,7 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri GDScriptParser::DataType base_type = p_subscript->base->get_datatype(); bool valid = false; // If the base is a metatype, use the analyzer instead. - if (p_subscript->base->is_constant && !base_type.is_meta_type) { + if (p_subscript->base->is_constant && !base_type.is_meta_type && base_type.kind != GDScriptParser::DataType::CLASS) { // Just try to get it. Variant value = p_subscript->base->reduced_value.get_named(p_subscript->attribute->name, valid); if (valid) { diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index aec8f56516..00d3df8fd0 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -59,6 +59,7 @@ void GDScriptLanguage::get_string_delimiters(List<String> *p_delimiters) const { p_delimiters->push_back("' '"); p_delimiters->push_back("\"\"\" \"\"\""); p_delimiters->push_back("''' '''"); + // NOTE: StringName, NodePath and r-strings are not listed here. } bool GDScriptLanguage::is_using_templates() { diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 52c1a5b141..1202e7e235 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -383,8 +383,10 @@ GDScriptTokenizer::Token GDScriptParser::advance() { push_error(current.literal); current = tokenizer.scan(); } - for (Node *n : nodes_in_progress) { - update_extents(n); + if (previous.type != GDScriptTokenizer::Token::DEDENT) { // `DEDENT` belongs to the next non-empty line. + for (Node *n : nodes_in_progress) { + update_extents(n); + } } return previous; } @@ -579,13 +581,14 @@ void GDScriptParser::parse_program() { complete_extents(head); #ifdef TOOLS_ENABLED - for (const KeyValue<int, GDScriptTokenizer::CommentData> &E : tokenizer.get_comments()) { - if (E.value.new_line && E.value.comment.begins_with("##")) { - class_doc_line = MIN(class_doc_line, E.key); + 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("##")) { + head->doc_data = parse_class_doc_comment(line); + break; } - } - if (has_comment(class_doc_line, true)) { - head->doc_data = parse_class_doc_comment(class_doc_line, false); + line--; } #endif // TOOLS_ENABLED @@ -747,10 +750,6 @@ template <class 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(); -#ifdef TOOLS_ENABLED - int doc_comment_line = previous.start_line - 1; -#endif // TOOLS_ENABLED - // Consume annotations. List<AnnotationNode *> annotations; while (!annotation_stack.is_empty()) { @@ -762,11 +761,6 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b push_error(vformat(R"(Annotation "%s" cannot be applied to a %s.)", last_annotation->name, p_member_kind)); clear_unused_annotations(); } -#ifdef TOOLS_ENABLED - if (last_annotation->start_line == doc_comment_line) { - doc_comment_line--; - } -#endif // TOOLS_ENABLED } T *member = (this->*p_parse_function)(p_is_static); @@ -774,28 +768,40 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b return; } +#ifdef TOOLS_ENABLED + int doc_comment_line = member->start_line - 1; +#endif // TOOLS_ENABLED + for (AnnotationNode *&annotation : annotations) { member->annotations.push_back(annotation); +#ifdef TOOLS_ENABLED + if (annotation->start_line <= doc_comment_line) { + doc_comment_line = annotation->start_line - 1; + } +#endif // TOOLS_ENABLED } #ifdef TOOLS_ENABLED - // Consume doc comments. - class_doc_line = MIN(class_doc_line, doc_comment_line - 1); - - // Check whether current line has a doc comment - if (has_comment(previous.start_line, true)) { - if constexpr (std::is_same_v<T, ClassNode>) { - member->doc_data = parse_class_doc_comment(previous.start_line, true, true); - } else { - member->doc_data = parse_doc_comment(previous.start_line, true); + if constexpr (std::is_same_v<T, ClassNode>) { + 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) { + // 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); } - } else if (has_comment(doc_comment_line, true)) { - if constexpr (std::is_same_v<T, ClassNode>) { - member->doc_data = parse_class_doc_comment(doc_comment_line, true); - } else { + } else { + 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) { + // Normal doc comment. member->doc_data = parse_doc_comment(doc_comment_line); } } + + min_member_doc_line = member->end_line + 1; // Prevent multiple members from using the same doc comment. #endif // TOOLS_ENABLED if (member->identifier != nullptr) { @@ -1263,6 +1269,9 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { push_multiline(true); consume(GDScriptTokenizer::Token::BRACE_OPEN, vformat(R"(Expected "{" after %s.)", named ? "enum name" : R"("enum")")); +#ifdef TOOLS_ENABLED + int min_enum_value_doc_line = previous.end_line + 1; +#endif HashMap<StringName, int> elements; @@ -1325,43 +1334,35 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { } } while (match(GDScriptTokenizer::Token::COMMA)); - pop_multiline(); - consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected closing "}" for enum.)"); - #ifdef TOOLS_ENABLED // Enum values documentation. for (int i = 0; i < enum_node->values.size(); i++) { - int doc_comment_line = enum_node->values[i].line; - bool single_line = false; - - if (has_comment(doc_comment_line, true)) { - single_line = true; - } else if (has_comment(doc_comment_line - 1, true)) { - doc_comment_line--; - } else { - continue; - } - - if (i == enum_node->values.size() - 1) { - // If close bracket is same line as last value. - if (doc_comment_line == previous.start_line) { - break; - } - } else { - // If two values are same line. - if (doc_comment_line == enum_node->values[i + 1].line) { - continue; + int enum_value_line = enum_node->values[i].line; + int doc_comment_line = enum_value_line - 1; + + MemberDocData doc_data; + if (has_comment(enum_value_line, true)) { + // Inline doc comment. + 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) { + // Normal doc comment. + doc_data = parse_doc_comment(doc_comment_line); } if (named) { - enum_node->values.write[i].doc_data = parse_doc_comment(doc_comment_line, single_line); + enum_node->values.write[i].doc_data = doc_data; } else { - current_class->set_enum_value_doc_data(enum_node->values[i].identifier->name, parse_doc_comment(doc_comment_line, single_line)); + current_class->set_enum_value_doc_data(enum_node->values[i].identifier->name, doc_data); } + + min_enum_value_doc_line = enum_value_line + 1; // Prevent multiple enum values from using the same doc comment. } #endif // TOOLS_ENABLED + pop_multiline(); + consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected closing "}" for enum.)"); complete_extents(enum_node); end_statement("enum"); @@ -3454,31 +3455,21 @@ bool GDScriptParser::has_comment(int p_line, bool p_must_be_doc) { } GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool p_single_line) { - MemberDocData result; + ERR_FAIL_COND_V(!has_comment(p_line, true), MemberDocData()); const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); - ERR_FAIL_COND_V(!comments.has(p_line), result); - - if (p_single_line) { - if (comments[p_line].comment.begins_with("##")) { - result.description = comments[p_line].comment.trim_prefix("##").strip_edges(); - return result; - } - return result; - } - int line = p_line; - DocLineState state = DOC_LINE_NORMAL; - while (comments.has(line - 1)) { - if (!comments[line - 1].new_line || !comments[line - 1].comment.begins_with("##")) { - break; + if (!p_single_line) { + while (comments.has(line - 1) && comments[line - 1].new_line && comments[line - 1].comment.begins_with("##")) { + line--; } - line--; } + max_script_doc_line = MIN(max_script_doc_line, line - 1); + String space_prefix; - if (comments.has(line) && comments[line].comment.begins_with("##")) { + { int i = 2; for (; i < comments[line].comment.length(); i++) { if (comments[line].comment[i] != ' ') { @@ -3488,11 +3479,10 @@ GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool space_prefix = String(" ").repeat(i - 2); } - while (comments.has(line)) { - if (!comments[line].new_line || !comments[line].comment.begins_with("##")) { - break; - } + DocLineState state = DOC_LINE_NORMAL; + MemberDocData result; + while (line <= p_line) { String doc_line = comments[line].comment.trim_prefix("##"); line++; @@ -3513,35 +3503,22 @@ GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool return result; } -GDScriptParser::ClassDocData GDScriptParser::parse_class_doc_comment(int p_line, bool p_inner_class, bool p_single_line) { - ClassDocData result; +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(); - ERR_FAIL_COND_V(!comments.has(p_line), result); - - if (p_single_line) { - if (comments[p_line].comment.begins_with("##")) { - result.brief = comments[p_line].comment.trim_prefix("##").strip_edges(); - return result; - } - return result; - } - int line = p_line; - DocLineState state = DOC_LINE_NORMAL; - bool is_in_brief = true; - if (p_inner_class) { - while (comments.has(line - 1)) { - if (!comments[line - 1].new_line || !comments[line - 1].comment.begins_with("##")) { - break; - } + if (!p_single_line) { + while (comments.has(line - 1) && comments[line - 1].new_line && comments[line - 1].comment.begins_with("##")) { line--; } } + max_script_doc_line = MIN(max_script_doc_line, line - 1); + String space_prefix; - if (comments.has(line) && comments[line].comment.begins_with("##")) { + { int i = 2; for (; i < comments[line].comment.length(); i++) { if (comments[line].comment[i] != ' ') { @@ -3551,11 +3528,11 @@ GDScriptParser::ClassDocData GDScriptParser::parse_class_doc_comment(int p_line, space_prefix = String(" ").repeat(i - 2); } - while (comments.has(line)) { - if (!comments[line].new_line || !comments[line].comment.begins_with("##")) { - break; - } + DocLineState state = DOC_LINE_NORMAL; + bool is_in_brief = true; + ClassDocData result; + while (line <= p_line) { String doc_line = comments[line].comment.trim_prefix("##"); line++; @@ -3630,14 +3607,6 @@ GDScriptParser::ClassDocData GDScriptParser::parse_class_doc_comment(int p_line, } } - if (current_class->members.size() > 0) { - const ClassNode::Member &m = current_class->members[0]; - int first_member_line = m.get_line(); - if (first_member_line == line) { - result = ClassDocData(); // Clear result. - } - } - return result; } #endif // TOOLS_ENABLED @@ -4095,25 +4064,29 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } } break; case GDScriptParser::DataType::ENUM: { - variable->export_info.type = Variant::INT; - variable->export_info.hint = PROPERTY_HINT_ENUM; - - String enum_hint_string; - bool first = true; - for (const KeyValue<StringName, int64_t> &E : export_type.enum_values) { - if (!first) { - enum_hint_string += ","; - } else { - first = false; + if (export_type.is_meta_type) { + variable->export_info.type = Variant::DICTIONARY; + } else { + variable->export_info.type = Variant::INT; + variable->export_info.hint = PROPERTY_HINT_ENUM; + + String enum_hint_string; + bool first = true; + for (const KeyValue<StringName, int64_t> &E : export_type.enum_values) { + if (!first) { + enum_hint_string += ","; + } else { + first = false; + } + enum_hint_string += E.key.operator String().capitalize().xml_escape(); + enum_hint_string += ":"; + enum_hint_string += String::num_int64(E.value).xml_escape(); } - enum_hint_string += E.key.operator String().capitalize().xml_escape(); - enum_hint_string += ":"; - enum_hint_string += String::num_int64(E.value).xml_escape(); - } - variable->export_info.hint_string = enum_hint_string; - variable->export_info.usage |= PROPERTY_USAGE_CLASS_IS_ENUM; - variable->export_info.class_name = String(export_type.native_type).replace("::", "."); + variable->export_info.hint_string = enum_hint_string; + variable->export_info.usage |= PROPERTY_USAGE_CLASS_IS_ENUM; + variable->export_info.class_name = String(export_type.native_type).replace("::", "."); + } } break; default: push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", variable); diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 27d4d0fb47..988524d058 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1517,10 +1517,11 @@ private: TypeNode *parse_type(bool p_allow_void = false); #ifdef TOOLS_ENABLED - int class_doc_line = 0x7FFFFFFF; + int max_script_doc_line = INT_MAX; + int min_member_doc_line = 1; bool has_comment(int p_line, bool p_must_be_doc = false); MemberDocData parse_doc_comment(int p_line, bool p_single_line = false); - ClassDocData parse_class_doc_comment(int p_line, bool p_inner_class, bool p_single_line = false); + ClassDocData parse_class_doc_comment(int p_line, bool p_single_line = false); #endif // TOOLS_ENABLED public: diff --git a/modules/gdscript/gdscript_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp index 42b983ef45..07f2b8b406 100644 --- a/modules/gdscript/gdscript_tokenizer.cpp +++ b/modules/gdscript/gdscript_tokenizer.cpp @@ -857,10 +857,14 @@ GDScriptTokenizer::Token GDScriptTokenizer::string() { STRING_NODEPATH, }; + bool is_raw = false; bool is_multiline = false; StringType type = STRING_REGULAR; - if (_peek(-1) == '&') { + if (_peek(-1) == 'r') { + is_raw = true; + _advance(); + } else if (_peek(-1) == '&') { type = STRING_NAME; _advance(); } else if (_peek(-1) == '^') { @@ -890,7 +894,12 @@ GDScriptTokenizer::Token GDScriptTokenizer::string() { char32_t ch = _peek(); if (ch == 0x200E || ch == 0x200F || (ch >= 0x202A && ch <= 0x202E) || (ch >= 0x2066 && ch <= 0x2069)) { - Token error = make_error("Invisible text direction control character present in the string, escape it (\"\\u" + String::num_int64(ch, 16) + "\") to avoid confusion."); + Token error; + if (is_raw) { + error = make_error("Invisible text direction control character present in the string, use regular string literal instead of r-string."); + } else { + error = make_error("Invisible text direction control character present in the string, escape it (\"\\u" + String::num_int64(ch, 16) + "\") to avoid confusion."); + } error.start_column = column; error.leftmost_column = error.start_column; error.end_column = column + 1; @@ -905,144 +914,164 @@ GDScriptTokenizer::Token GDScriptTokenizer::string() { return make_error("Unterminated string."); } - // Grab escape character. - char32_t code = _peek(); - _advance(); - if (_is_at_end()) { - return make_error("Unterminated string."); - } + if (is_raw) { + if (_peek() == quote_char) { + _advance(); + if (_is_at_end()) { + return make_error("Unterminated string."); + } + result += '\\'; + result += quote_char; + } else if (_peek() == '\\') { // For `\\\"`. + _advance(); + if (_is_at_end()) { + return make_error("Unterminated string."); + } + result += '\\'; + result += '\\'; + } else { + result += '\\'; + } + } else { + // Grab escape character. + char32_t code = _peek(); + _advance(); + if (_is_at_end()) { + return make_error("Unterminated string."); + } - char32_t escaped = 0; - bool valid_escape = true; + char32_t escaped = 0; + bool valid_escape = true; - switch (code) { - case 'a': - escaped = '\a'; - break; - case 'b': - escaped = '\b'; - break; - case 'f': - escaped = '\f'; - break; - case 'n': - escaped = '\n'; - break; - case 'r': - escaped = '\r'; - break; - case 't': - escaped = '\t'; - break; - case 'v': - escaped = '\v'; - break; - case '\'': - escaped = '\''; - break; - case '\"': - escaped = '\"'; - break; - case '\\': - escaped = '\\'; - break; - case 'U': - case 'u': { - // Hexadecimal sequence. - int hex_len = (code == 'U') ? 6 : 4; - for (int j = 0; j < hex_len; j++) { - if (_is_at_end()) { - return make_error("Unterminated string."); + switch (code) { + case 'a': + escaped = '\a'; + break; + case 'b': + escaped = '\b'; + break; + case 'f': + escaped = '\f'; + break; + case 'n': + escaped = '\n'; + break; + case 'r': + escaped = '\r'; + break; + case 't': + escaped = '\t'; + break; + case 'v': + escaped = '\v'; + break; + case '\'': + escaped = '\''; + break; + case '\"': + escaped = '\"'; + break; + case '\\': + escaped = '\\'; + break; + case 'U': + case 'u': { + // Hexadecimal sequence. + int hex_len = (code == 'U') ? 6 : 4; + for (int j = 0; j < hex_len; j++) { + if (_is_at_end()) { + return make_error("Unterminated string."); + } + + char32_t digit = _peek(); + char32_t value = 0; + if (is_digit(digit)) { + value = digit - '0'; + } else if (digit >= 'a' && digit <= 'f') { + value = digit - 'a'; + value += 10; + } else if (digit >= 'A' && digit <= 'F') { + value = digit - 'A'; + value += 10; + } else { + // Make error, but keep parsing the string. + Token error = make_error("Invalid hexadecimal digit in unicode escape sequence."); + error.start_column = column; + error.leftmost_column = error.start_column; + error.end_column = column + 1; + error.rightmost_column = error.end_column; + push_error(error); + valid_escape = false; + break; + } + + escaped <<= 4; + escaped |= value; + + _advance(); } - - char32_t digit = _peek(); - char32_t value = 0; - if (is_digit(digit)) { - value = digit - '0'; - } else if (digit >= 'a' && digit <= 'f') { - value = digit - 'a'; - value += 10; - } else if (digit >= 'A' && digit <= 'F') { - value = digit - 'A'; - value += 10; - } else { - // Make error, but keep parsing the string. - Token error = make_error("Invalid hexadecimal digit in unicode escape sequence."); - error.start_column = column; - error.leftmost_column = error.start_column; - error.end_column = column + 1; - error.rightmost_column = error.end_column; - push_error(error); - valid_escape = false; + } break; + case '\r': + if (_peek() != '\n') { + // Carriage return without newline in string. (???) + // Just add it to the string and keep going. + result += ch; + _advance(); break; } - - escaped <<= 4; - escaped |= value; - - _advance(); - } - } break; - case '\r': - if (_peek() != '\n') { - // Carriage return without newline in string. (???) - // Just add it to the string and keep going. - result += ch; - _advance(); + [[fallthrough]]; + case '\n': + // Escaping newline. + newline(false); + valid_escape = false; // Don't add to the string. break; - } - [[fallthrough]]; - case '\n': - // Escaping newline. - newline(false); - valid_escape = false; // Don't add to the string. - break; - default: - Token error = make_error("Invalid escape in string."); - error.start_column = column - 2; - error.leftmost_column = error.start_column; - push_error(error); - valid_escape = false; - break; - } - // Parse UTF-16 pair. - if (valid_escape) { - if ((escaped & 0xfffffc00) == 0xd800) { - if (prev == 0) { - prev = escaped; - prev_pos = column - 2; - continue; - } else { - Token error = make_error("Invalid UTF-16 sequence in string, unpaired lead surrogate"); + default: + Token error = make_error("Invalid escape in string."); error.start_column = column - 2; error.leftmost_column = error.start_column; push_error(error); valid_escape = false; - prev = 0; + break; + } + // Parse UTF-16 pair. + if (valid_escape) { + if ((escaped & 0xfffffc00) == 0xd800) { + if (prev == 0) { + prev = escaped; + prev_pos = column - 2; + continue; + } else { + Token error = make_error("Invalid UTF-16 sequence in string, unpaired lead surrogate."); + error.start_column = column - 2; + error.leftmost_column = error.start_column; + push_error(error); + valid_escape = false; + prev = 0; + } + } else if ((escaped & 0xfffffc00) == 0xdc00) { + if (prev == 0) { + Token error = make_error("Invalid UTF-16 sequence in string, unpaired trail surrogate."); + error.start_column = column - 2; + error.leftmost_column = error.start_column; + push_error(error); + valid_escape = false; + } else { + escaped = (prev << 10UL) + escaped - ((0xd800 << 10UL) + 0xdc00 - 0x10000); + prev = 0; + } } - } else if ((escaped & 0xfffffc00) == 0xdc00) { - if (prev == 0) { - Token error = make_error("Invalid UTF-16 sequence in string, unpaired trail surrogate"); - error.start_column = column - 2; + if (prev != 0) { + Token error = make_error("Invalid UTF-16 sequence in string, unpaired lead surrogate."); + error.start_column = prev_pos; error.leftmost_column = error.start_column; push_error(error); - valid_escape = false; - } else { - escaped = (prev << 10UL) + escaped - ((0xd800 << 10UL) + 0xdc00 - 0x10000); prev = 0; } } - if (prev != 0) { - Token error = make_error("Invalid UTF-16 sequence in string, unpaired lead surrogate"); - error.start_column = prev_pos; - error.leftmost_column = error.start_column; - push_error(error); - prev = 0; - } - } - if (valid_escape) { - result += escaped; + if (valid_escape) { + result += escaped; + } } } else if (ch == quote_char) { if (prev != 0) { @@ -1216,7 +1245,7 @@ void GDScriptTokenizer::check_indent() { if (line_continuation || multiline_mode) { // We cleared up all the whitespace at the beginning of the line. - // But if this is a continuation or multiline mode and we don't want any indentation change. + // If this is a line continuation or we're in multiline mode then we don't want any indentation changes. return; } @@ -1416,6 +1445,9 @@ GDScriptTokenizer::Token GDScriptTokenizer::scan() { if (is_digit(c)) { return number(); + } else if (c == 'r' && (_peek() == '"' || _peek() == '\'')) { + // Raw string literals. + return string(); } else if (is_unicode_identifier_start(c)) { return potential_identifier(); } diff --git a/modules/gdscript/gdscript_tokenizer.h b/modules/gdscript/gdscript_tokenizer.h index 068393cee9..f916407b18 100644 --- a/modules/gdscript/gdscript_tokenizer.h +++ b/modules/gdscript/gdscript_tokenizer.h @@ -187,6 +187,8 @@ public: #ifdef TOOLS_ENABLED struct CommentData { String comment; + // true: Comment starts at beginning of line or after indentation. + // false: Inline comment (starts after some code). bool new_line = false; CommentData() {} CommentData(const String &p_comment, bool p_new_line) { diff --git a/modules/gdscript/language_server/gdscript_language_server.cpp b/modules/gdscript/language_server/gdscript_language_server.cpp index 8c44483288..053be7eec2 100644 --- a/modules/gdscript/language_server/gdscript_language_server.cpp +++ b/modules/gdscript/language_server/gdscript_language_server.cpp @@ -36,6 +36,8 @@ #include "editor/editor_node.h" #include "editor/editor_settings.h" +int GDScriptLanguageServer::port_override = -1; + GDScriptLanguageServer::GDScriptLanguageServer() { _EDITOR_DEF("network/language_server/remote_host", host); _EDITOR_DEF("network/language_server/remote_port", port); @@ -62,7 +64,7 @@ void GDScriptLanguageServer::_notification(int p_what) { case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { String remote_host = String(_EDITOR_GET("network/language_server/remote_host")); - int remote_port = (int)_EDITOR_GET("network/language_server/remote_port"); + int remote_port = (GDScriptLanguageServer::port_override > -1) ? GDScriptLanguageServer::port_override : (int)_EDITOR_GET("network/language_server/remote_port"); bool remote_use_thread = (bool)_EDITOR_GET("network/language_server/use_thread"); if (remote_host != host || remote_port != port || remote_use_thread != use_thread) { stop(); @@ -84,10 +86,10 @@ void GDScriptLanguageServer::thread_main(void *p_userdata) { void GDScriptLanguageServer::start() { host = String(_EDITOR_GET("network/language_server/remote_host")); - port = (int)_EDITOR_GET("network/language_server/remote_port"); + port = (GDScriptLanguageServer::port_override > -1) ? GDScriptLanguageServer::port_override : (int)_EDITOR_GET("network/language_server/remote_port"); use_thread = (bool)_EDITOR_GET("network/language_server/use_thread"); if (protocol.start(port, IPAddress(host)) == OK) { - EditorNode::get_log()->add_message("--- GDScript language server started ---", EditorLog::MSG_TYPE_EDITOR); + EditorNode::get_log()->add_message("--- GDScript language server started on port " + itos(port) + " ---", EditorLog::MSG_TYPE_EDITOR); if (use_thread) { thread_running = true; thread.start(GDScriptLanguageServer::thread_main, this); diff --git a/modules/gdscript/language_server/gdscript_language_server.h b/modules/gdscript/language_server/gdscript_language_server.h index 75f9403a74..e845d139bf 100644 --- a/modules/gdscript/language_server/gdscript_language_server.h +++ b/modules/gdscript/language_server/gdscript_language_server.h @@ -53,6 +53,7 @@ private: void _notification(int p_what); public: + static int port_override; GDScriptLanguageServer(); void start(); void stop(); diff --git a/modules/gdscript/tests/scripts/analyzer/errors/virtual_super_not_implemented.gd b/modules/gdscript/tests/scripts/analyzer/errors/virtual_super_not_implemented.gd new file mode 100644 index 0000000000..57dfffdbee --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/virtual_super_not_implemented.gd @@ -0,0 +1,5 @@ +func _init(): + super() + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/virtual_super_not_implemented.out b/modules/gdscript/tests/scripts/analyzer/errors/virtual_super_not_implemented.out new file mode 100644 index 0000000000..e68759223c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/virtual_super_not_implemented.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot call the parent class' virtual function "_init()" because it hasn't been defined. diff --git a/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd new file mode 100644 index 0000000000..dafd2ec0c8 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd @@ -0,0 +1,17 @@ +class_name TestExportEnumAsDictionary + +enum MyEnum {A, B, C} + +const Utils = preload("../../utils.notest.gd") + +@export var x1 = MyEnum +@export var x2 = MyEnum.A +@export var x3 := MyEnum +@export var x4 := MyEnum.A +@export var x5: MyEnum + +func test(): + for property in get_property_list(): + if property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE: + print(Utils.get_property_signature(property)) + print(" ", Utils.get_property_additional_info(property)) diff --git a/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.out b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.out new file mode 100644 index 0000000000..f1a13f1045 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.out @@ -0,0 +1,11 @@ +GDTEST_OK +@export var x1: Dictionary + hint=NONE hint_string="" usage=DEFAULT|SCRIPT_VARIABLE +@export var x2: TestExportEnumAsDictionary.MyEnum + hint=ENUM hint_string="A:0,B:1,C:2" usage=DEFAULT|SCRIPT_VARIABLE|CLASS_IS_ENUM +@export var x3: Dictionary + hint=NONE hint_string="" usage=DEFAULT|SCRIPT_VARIABLE +@export var x4: TestExportEnumAsDictionary.MyEnum + hint=ENUM hint_string="A:0,B:1,C:2" usage=DEFAULT|SCRIPT_VARIABLE|CLASS_IS_ENUM +@export var x5: TestExportEnumAsDictionary.MyEnum + hint=ENUM hint_string="A:0,B:1,C:2" usage=DEFAULT|SCRIPT_VARIABLE|CLASS_IS_ENUM diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_array_dont_make_literal_typed_with_weak_type.gd b/modules/gdscript/tests/scripts/analyzer/features/typed_array_dont_make_literal_typed_with_weak_type.gd new file mode 100644 index 0000000000..e1a1f07e47 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/typed_array_dont_make_literal_typed_with_weak_type.gd @@ -0,0 +1,22 @@ +var _typed_array: Array[int] + +func weak_param_func(weak_param = _typed_array): + weak_param = [11] # Don't treat the literal as typed! + return weak_param + +func hard_param_func(hard_param := _typed_array): + hard_param = [12] + return hard_param + +func test(): + var weak_var = _typed_array + print(weak_var.is_typed()) + weak_var = [21] # Don't treat the literal as typed! + print(weak_var.is_typed()) + print(weak_param_func().is_typed()) + + var hard_var := _typed_array + print(hard_var.is_typed()) + hard_var = [22] + print(hard_var.is_typed()) + print(hard_param_func().is_typed()) diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_array_dont_make_literal_typed_with_weak_type.out b/modules/gdscript/tests/scripts/analyzer/features/typed_array_dont_make_literal_typed_with_weak_type.out new file mode 100644 index 0000000000..34b18dbe7c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/typed_array_dont_make_literal_typed_with_weak_type.out @@ -0,0 +1,7 @@ +GDTEST_OK +true +false +false +true +true +true diff --git a/modules/gdscript/tests/scripts/analyzer/features/virtual_method_implemented.gd b/modules/gdscript/tests/scripts/analyzer/features/virtual_method_implemented.gd new file mode 100644 index 0000000000..a8641e4f3b --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/virtual_method_implemented.gd @@ -0,0 +1,21 @@ +class BaseClass: + func _get_property_list(): + return {"property" : "definition"} + +class SuperClassMethodsRecognized extends BaseClass: + func _init(): + # Recognizes super class methods. + var _x = _get_property_list() + +class SuperMethodsRecognized extends BaseClass: + func _get_property_list(): + # Recognizes super method. + var result = super() + result["new"] = "new" + return result + +func test(): + var test1 = SuperClassMethodsRecognized.new() + print(test1._get_property_list()) # Calls base class's method. + var test2 = SuperMethodsRecognized.new() + print(test2._get_property_list()) diff --git a/modules/gdscript/tests/scripts/analyzer/features/virtual_method_implemented.out b/modules/gdscript/tests/scripts/analyzer/features/virtual_method_implemented.out new file mode 100644 index 0000000000..ccff660117 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/virtual_method_implemented.out @@ -0,0 +1,3 @@ +GDTEST_OK +{ "property": "definition" } +{ "property": "definition", "new": "new" } diff --git a/modules/gdscript/tests/scripts/parser/errors/bad_r_string_1.gd b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_1.gd new file mode 100644 index 0000000000..e5eecbb819 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_1.gd @@ -0,0 +1,2 @@ +func test(): + print(r"\") diff --git a/modules/gdscript/tests/scripts/parser/errors/bad_r_string_1.out b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_1.out new file mode 100644 index 0000000000..c8e843b0d7 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_1.out @@ -0,0 +1,2 @@ +GDTEST_PARSER_ERROR +Unterminated string. diff --git a/modules/gdscript/tests/scripts/parser/errors/bad_r_string_2.gd b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_2.gd new file mode 100644 index 0000000000..9168b69f86 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_2.gd @@ -0,0 +1,2 @@ +func test(): + print(r"\\"") diff --git a/modules/gdscript/tests/scripts/parser/errors/bad_r_string_2.out b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_2.out new file mode 100644 index 0000000000..c8e843b0d7 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_2.out @@ -0,0 +1,2 @@ +GDTEST_PARSER_ERROR +Unterminated string. diff --git a/modules/gdscript/tests/scripts/parser/errors/bad_r_string_3.gd b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_3.gd new file mode 100644 index 0000000000..37dc910e5f --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_3.gd @@ -0,0 +1,3 @@ +func test(): + # v + print(r"['"]*") diff --git a/modules/gdscript/tests/scripts/parser/errors/bad_r_string_3.out b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_3.out new file mode 100644 index 0000000000..dcb5c2f289 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/bad_r_string_3.out @@ -0,0 +1,2 @@ +GDTEST_PARSER_ERROR +Closing "]" doesn't have an opening counterpart. diff --git a/modules/gdscript/tests/scripts/parser/features/r_strings.gd b/modules/gdscript/tests/scripts/parser/features/r_strings.gd new file mode 100644 index 0000000000..6f546f28be --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/r_strings.gd @@ -0,0 +1,22 @@ +func test(): + print(r"test ' \' \" \\ \n \t \u2023 test") + print(r"\n\\[\t ]*(\w+)") + print(r"") + print(r"\"") + print(r"\\\"") + print(r"\\") + print(r"\" \\\" \\\\\"") + print(r"\ \\ \\\ \\\\ \\\\\ \\") + print(r'"') + print(r'"(?:\\.|[^"])*"') + print(r"""""") + print(r"""test \t "test"="" " \" \\\" \ \\ \\\ test""") + print(r'''r"""test \t "test"="" " \" \\\" \ \\ \\\ test"""''') + print(r"\t + \t") + print(r"\t \ + \t") + print(r"""\t + \t""") + print(r"""\t \ + \t""") diff --git a/modules/gdscript/tests/scripts/parser/features/r_strings.out b/modules/gdscript/tests/scripts/parser/features/r_strings.out new file mode 100644 index 0000000000..114ef0a6c3 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/r_strings.out @@ -0,0 +1,22 @@ +GDTEST_OK +test ' \' \" \\ \n \t \u2023 test +\n\\[\t ]*(\w+) + +\" +\\\" +\\ +\" \\\" \\\\\" +\ \\ \\\ \\\\ \\\\\ \\ +" +"(?:\\.|[^"])*" + +test \t "test"="" " \" \\\" \ \\ \\\ test +r"""test \t "test"="" " \" \\\" \ \\ \\\ test""" +\t + \t +\t \ + \t +\t + \t +\t \ + \t diff --git a/modules/gdscript/tests/scripts/utils.notest.gd b/modules/gdscript/tests/scripts/utils.notest.gd index 50444e62a1..fb20817117 100644 --- a/modules/gdscript/tests/scripts/utils.notest.gd +++ b/modules/gdscript/tests/scripts/utils.notest.gd @@ -19,6 +19,7 @@ static func get_type(property: Dictionary, is_return: bool = false) -> String: return property.class_name return variant_get_type_name(property.type) + static func get_property_signature(property: Dictionary, is_static: bool = false) -> String: var result: String = "" if not (property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE): @@ -30,6 +31,15 @@ static func get_property_signature(property: Dictionary, is_static: bool = false result += "var " + property.name + ": " + get_type(property) return result + +static func get_property_additional_info(property: Dictionary) -> String: + return 'hint=%s hint_string="%s" usage=%s' % [ + get_property_hint_name(property.hint).trim_prefix("PROPERTY_HINT_"), + str(property.hint_string).c_escape(), + get_property_usage_string(property.usage).replace("PROPERTY_USAGE_", ""), + ] + + static func get_method_signature(method: Dictionary, is_signal: bool = false) -> String: var result: String = "" if method.flags & METHOD_FLAG_STATIC: @@ -55,6 +65,7 @@ static func get_method_signature(method: Dictionary, is_signal: bool = false) -> result += " -> " + get_type(method.return, true) return result + static func variant_get_type_name(type: Variant.Type) -> String: match type: TYPE_NIL: @@ -135,3 +146,136 @@ static func variant_get_type_name(type: Variant.Type) -> String: return "PackedColorArray" push_error("Argument `type` is invalid. Use `TYPE_*` constants.") return "<invalid type>" + + +static func get_property_hint_name(hint: PropertyHint) -> String: + match hint: + PROPERTY_HINT_NONE: + return "PROPERTY_HINT_NONE" + PROPERTY_HINT_RANGE: + return "PROPERTY_HINT_RANGE" + PROPERTY_HINT_ENUM: + return "PROPERTY_HINT_ENUM" + PROPERTY_HINT_ENUM_SUGGESTION: + return "PROPERTY_HINT_ENUM_SUGGESTION" + PROPERTY_HINT_EXP_EASING: + return "PROPERTY_HINT_EXP_EASING" + PROPERTY_HINT_LINK: + return "PROPERTY_HINT_LINK" + PROPERTY_HINT_FLAGS: + return "PROPERTY_HINT_FLAGS" + PROPERTY_HINT_LAYERS_2D_RENDER: + return "PROPERTY_HINT_LAYERS_2D_RENDER" + PROPERTY_HINT_LAYERS_2D_PHYSICS: + return "PROPERTY_HINT_LAYERS_2D_PHYSICS" + PROPERTY_HINT_LAYERS_2D_NAVIGATION: + return "PROPERTY_HINT_LAYERS_2D_NAVIGATION" + PROPERTY_HINT_LAYERS_3D_RENDER: + return "PROPERTY_HINT_LAYERS_3D_RENDER" + PROPERTY_HINT_LAYERS_3D_PHYSICS: + return "PROPERTY_HINT_LAYERS_3D_PHYSICS" + PROPERTY_HINT_LAYERS_3D_NAVIGATION: + return "PROPERTY_HINT_LAYERS_3D_NAVIGATION" + PROPERTY_HINT_LAYERS_AVOIDANCE: + return "PROPERTY_HINT_LAYERS_AVOIDANCE" + PROPERTY_HINT_FILE: + return "PROPERTY_HINT_FILE" + PROPERTY_HINT_DIR: + return "PROPERTY_HINT_DIR" + PROPERTY_HINT_GLOBAL_FILE: + return "PROPERTY_HINT_GLOBAL_FILE" + PROPERTY_HINT_GLOBAL_DIR: + return "PROPERTY_HINT_GLOBAL_DIR" + PROPERTY_HINT_RESOURCE_TYPE: + return "PROPERTY_HINT_RESOURCE_TYPE" + PROPERTY_HINT_MULTILINE_TEXT: + return "PROPERTY_HINT_MULTILINE_TEXT" + PROPERTY_HINT_EXPRESSION: + return "PROPERTY_HINT_EXPRESSION" + PROPERTY_HINT_PLACEHOLDER_TEXT: + return "PROPERTY_HINT_PLACEHOLDER_TEXT" + PROPERTY_HINT_COLOR_NO_ALPHA: + return "PROPERTY_HINT_COLOR_NO_ALPHA" + PROPERTY_HINT_OBJECT_ID: + return "PROPERTY_HINT_OBJECT_ID" + PROPERTY_HINT_TYPE_STRING: + return "PROPERTY_HINT_TYPE_STRING" + PROPERTY_HINT_NODE_PATH_TO_EDITED_NODE: + return "PROPERTY_HINT_NODE_PATH_TO_EDITED_NODE" + PROPERTY_HINT_OBJECT_TOO_BIG: + return "PROPERTY_HINT_OBJECT_TOO_BIG" + PROPERTY_HINT_NODE_PATH_VALID_TYPES: + return "PROPERTY_HINT_NODE_PATH_VALID_TYPES" + PROPERTY_HINT_SAVE_FILE: + return "PROPERTY_HINT_SAVE_FILE" + PROPERTY_HINT_GLOBAL_SAVE_FILE: + return "PROPERTY_HINT_GLOBAL_SAVE_FILE" + PROPERTY_HINT_INT_IS_OBJECTID: + return "PROPERTY_HINT_INT_IS_OBJECTID" + PROPERTY_HINT_INT_IS_POINTER: + return "PROPERTY_HINT_INT_IS_POINTER" + PROPERTY_HINT_ARRAY_TYPE: + return "PROPERTY_HINT_ARRAY_TYPE" + PROPERTY_HINT_LOCALE_ID: + return "PROPERTY_HINT_LOCALE_ID" + PROPERTY_HINT_LOCALIZABLE_STRING: + return "PROPERTY_HINT_LOCALIZABLE_STRING" + PROPERTY_HINT_NODE_TYPE: + return "PROPERTY_HINT_NODE_TYPE" + PROPERTY_HINT_HIDE_QUATERNION_EDIT: + return "PROPERTY_HINT_HIDE_QUATERNION_EDIT" + PROPERTY_HINT_PASSWORD: + return "PROPERTY_HINT_PASSWORD" + push_error("Argument `hint` is invalid. Use `PROPERTY_HINT_*` constants.") + return "<invalid hint>" + + +static func get_property_usage_string(usage: int) -> String: + if usage == PROPERTY_USAGE_NONE: + return "PROPERTY_USAGE_NONE" + + const FLAGS: Array[Array] = [ + [PROPERTY_USAGE_DEFAULT, "PROPERTY_USAGE_DEFAULT"], + [PROPERTY_USAGE_STORAGE, "PROPERTY_USAGE_STORAGE"], + [PROPERTY_USAGE_EDITOR, "PROPERTY_USAGE_EDITOR"], + [PROPERTY_USAGE_INTERNAL, "PROPERTY_USAGE_INTERNAL"], + [PROPERTY_USAGE_CHECKABLE, "PROPERTY_USAGE_CHECKABLE"], + [PROPERTY_USAGE_CHECKED, "PROPERTY_USAGE_CHECKED"], + [PROPERTY_USAGE_GROUP, "PROPERTY_USAGE_GROUP"], + [PROPERTY_USAGE_CATEGORY, "PROPERTY_USAGE_CATEGORY"], + [PROPERTY_USAGE_SUBGROUP, "PROPERTY_USAGE_SUBGROUP"], + [PROPERTY_USAGE_CLASS_IS_BITFIELD, "PROPERTY_USAGE_CLASS_IS_BITFIELD"], + [PROPERTY_USAGE_NO_INSTANCE_STATE, "PROPERTY_USAGE_NO_INSTANCE_STATE"], + [PROPERTY_USAGE_RESTART_IF_CHANGED, "PROPERTY_USAGE_RESTART_IF_CHANGED"], + [PROPERTY_USAGE_SCRIPT_VARIABLE, "PROPERTY_USAGE_SCRIPT_VARIABLE"], + [PROPERTY_USAGE_STORE_IF_NULL, "PROPERTY_USAGE_STORE_IF_NULL"], + [PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED, "PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED"], + [PROPERTY_USAGE_SCRIPT_DEFAULT_VALUE, "PROPERTY_USAGE_SCRIPT_DEFAULT_VALUE"], + [PROPERTY_USAGE_CLASS_IS_ENUM, "PROPERTY_USAGE_CLASS_IS_ENUM"], + [PROPERTY_USAGE_NIL_IS_VARIANT, "PROPERTY_USAGE_NIL_IS_VARIANT"], + [PROPERTY_USAGE_ARRAY, "PROPERTY_USAGE_ARRAY"], + [PROPERTY_USAGE_ALWAYS_DUPLICATE, "PROPERTY_USAGE_ALWAYS_DUPLICATE"], + [PROPERTY_USAGE_NEVER_DUPLICATE, "PROPERTY_USAGE_NEVER_DUPLICATE"], + [PROPERTY_USAGE_HIGH_END_GFX, "PROPERTY_USAGE_HIGH_END_GFX"], + [PROPERTY_USAGE_NODE_PATH_FROM_SCENE_ROOT, "PROPERTY_USAGE_NODE_PATH_FROM_SCENE_ROOT"], + [PROPERTY_USAGE_RESOURCE_NOT_PERSISTENT, "PROPERTY_USAGE_RESOURCE_NOT_PERSISTENT"], + [PROPERTY_USAGE_KEYING_INCREMENTS, "PROPERTY_USAGE_KEYING_INCREMENTS"], + [PROPERTY_USAGE_DEFERRED_SET_RESOURCE, "PROPERTY_USAGE_DEFERRED_SET_RESOURCE"], + [PROPERTY_USAGE_EDITOR_INSTANTIATE_OBJECT, "PROPERTY_USAGE_EDITOR_INSTANTIATE_OBJECT"], + [PROPERTY_USAGE_EDITOR_BASIC_SETTING, "PROPERTY_USAGE_EDITOR_BASIC_SETTING"], + [PROPERTY_USAGE_READ_ONLY, "PROPERTY_USAGE_READ_ONLY"], + [PROPERTY_USAGE_SECRET, "PROPERTY_USAGE_SECRET"], + ] + + var result: String = "" + + for flag in FLAGS: + if usage & flag[0]: + result += flag[1] + "|" + usage &= ~flag[0] + + if usage != PROPERTY_USAGE_NONE: + push_error("Argument `usage` is invalid. Use `PROPERTY_USAGE_*` constants.") + return "<invalid usage flags>" + + return result.left(-1) |
