/*************************************************************************/ /* gdscript_parser.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ #include "cscript_parser.h" #include "core/core_string_names.h" #include "core/config/engine.h" #include "core/io/resource_loader.h" #include "core/os/file_access.h" #include "core/string/print_string.h" #include "core/config/project_settings.h" #include "core/object/reference.h" #include "core/object/script_language.h" #include "cscript.h" template T *CScriptParser::alloc_node() { T *t = memnew(T); t->next = list; list = t; if (!head) { head = t; } t->line = tokenizer->get_token_line(); t->column = tokenizer->get_token_column(); return t; } #ifdef DEBUG_ENABLED static String _find_function_name(const CScriptParser::OperatorNode *p_call); #endif // DEBUG_ENABLED bool CScriptParser::_end_statement() { if (tokenizer->get_token() == CScriptTokenizer::TK_SEMICOLON) { tokenizer->advance(); return true; //handle next } else if (tokenizer->get_token() == CScriptTokenizer::TK_NEWLINE || tokenizer->get_token() == CScriptTokenizer::TK_EOF) { return true; //will be handled properly } return false; } void CScriptParser::_set_end_statement_error(String p_name) { String error_msg; if (tokenizer->get_token() == CScriptTokenizer::TK_IDENTIFIER) { error_msg = vformat("Expected end of statement (\"%s\"), got %s (\"%s\") instead.", p_name, tokenizer->get_token_name(tokenizer->get_token()), tokenizer->get_token_identifier()); } else { error_msg = vformat("Expected end of statement (\"%s\"), got %s instead.", p_name, tokenizer->get_token_name(tokenizer->get_token())); } _set_error(error_msg); } bool CScriptParser::_enter_indent_block(BlockNode *p_block) { if (tokenizer->get_token() != CScriptTokenizer::TK_COLON) { // report location at the previous token (on the previous line) int error_line = tokenizer->get_token_line(-1); int error_column = tokenizer->get_token_column(-1); _set_error("':' expected at end of line.", error_line, error_column); return false; } tokenizer->advance(); if (tokenizer->get_token() == CScriptTokenizer::TK_EOF) { return false; } if (tokenizer->get_token() != CScriptTokenizer::TK_NEWLINE) { // be more python-like IndentLevel current_level = indent_level.back()->get(); indent_level.push_back(current_level); return true; //_set_error("newline expected after ':'."); //return false; } while (true) { if (tokenizer->get_token() != CScriptTokenizer::TK_NEWLINE) { return false; //wtf } else if (tokenizer->get_token(1) == CScriptTokenizer::TK_EOF) { return false; } else if (tokenizer->get_token(1) != CScriptTokenizer::TK_NEWLINE) { int indent = tokenizer->get_token_line_indent(); int tabs = tokenizer->get_token_line_tab_indent(); IndentLevel current_level = indent_level.back()->get(); IndentLevel new_indent(indent, tabs); if (new_indent.is_mixed(current_level)) { _set_error("Mixed tabs and spaces in indentation."); return false; } if (indent <= current_level.indent) { return false; } indent_level.push_back(new_indent); tokenizer->advance(); return true; } else if (p_block) { NewLineNode *nl = alloc_node(); nl->line = tokenizer->get_token_line(); p_block->statements.push_back(nl); } tokenizer->advance(); // go to next newline } } bool CScriptParser::_parse_arguments(Node *p_parent, Vector &p_args, bool p_static, bool p_can_codecomplete, bool p_parsing_constant) { if (tokenizer->get_token() == CScriptTokenizer::TK_PARENTHESIS_CLOSE) { tokenizer->advance(); } else { parenthesis++; int argidx = 0; while (true) { if (tokenizer->get_token() == CScriptTokenizer::TK_CURSOR) { _make_completable_call(argidx); completion_node = p_parent; } else if (tokenizer->get_token() == CScriptTokenizer::TK_CONSTANT && tokenizer->get_token_constant().get_type() == Variant::STRING && tokenizer->get_token(1) == CScriptTokenizer::TK_CURSOR) { //completing a string argument.. completion_cursor = tokenizer->get_token_constant(); _make_completable_call(argidx); completion_node = p_parent; tokenizer->advance(1); return false; } Node *arg = _parse_expression(p_parent, p_static, false, p_parsing_constant); if (!arg) { return false; } p_args.push_back(arg); if (tokenizer->get_token() == CScriptTokenizer::TK_PARENTHESIS_CLOSE) { tokenizer->advance(); break; } else if (tokenizer->get_token() == CScriptTokenizer::TK_COMMA) { if (tokenizer->get_token(1) == CScriptTokenizer::TK_PARENTHESIS_CLOSE) { _set_error("Expression expected"); return false; } tokenizer->advance(); argidx++; } else { // something is broken _set_error("Expected ',' or ')'"); return false; } } parenthesis--; } return true; } void CScriptParser::_make_completable_call(int p_arg) { completion_cursor = StringName(); completion_type = COMPLETION_CALL_ARGUMENTS; completion_class = current_class; completion_function = current_function; completion_line = tokenizer->get_token_line(); completion_argument = p_arg; completion_block = current_block; completion_found = true; tokenizer->advance(); } bool CScriptParser::_get_completable_identifier(CompletionType p_type, StringName &identifier) { identifier = StringName(); if (tokenizer->is_token_literal()) { identifier = tokenizer->get_token_literal(); tokenizer->advance(); } if (tokenizer->get_token() == CScriptTokenizer::TK_CURSOR) { completion_cursor = identifier; completion_type = p_type; completion_class = current_class; completion_function = current_function; completion_line = tokenizer->get_token_line(); completion_block = current_block; completion_found = true; completion_ident_is_call = false; tokenizer->advance(); if (tokenizer->is_token_literal()) { identifier = identifier.operator String() + tokenizer->get_token_literal().operator String(); tokenizer->advance(); } if (tokenizer->get_token() == CScriptTokenizer::TK_PARENTHESIS_OPEN) { completion_ident_is_call = true; } return true; } return false; } CScriptParser::Node *CScriptParser::_parse_expression(Node *p_parent, bool p_static, bool p_allow_assign, bool p_parsing_constant) { //Vector expressions; //Vector operators; Vector expression; Node *expr = nullptr; int op_line = tokenizer->get_token_line(); // when operators are created at the bottom, the line might have been changed (\n found) while (true) { /*****************/ /* Parse Operand */ /*****************/ if (parenthesis > 0) { //remove empty space (only allowed if inside parenthesis while (tokenizer->get_token() == CScriptTokenizer::TK_NEWLINE) { tokenizer->advance(); } } // Check that the next token is not TK_CURSOR and if it is, the offset should be incremented. int next_valid_offset = 1; if (tokenizer->get_token(next_valid_offset) == CScriptTokenizer::TK_CURSOR) { next_valid_offset++; // There is a chunk of the identifier that also needs to be ignored (not always there!) if (tokenizer->get_token(next_valid_offset) == CScriptTokenizer::TK_IDENTIFIER) { next_valid_offset++; } } if (tokenizer->get_token() == CScriptTokenizer::TK_PARENTHESIS_OPEN) { //subexpression () tokenizer->advance(); parenthesis++; Node *subexpr = _parse_expression(p_parent, p_static, p_allow_assign, p_parsing_constant); parenthesis--; if (!subexpr) { return nullptr; } if (tokenizer->get_token() != CScriptTokenizer::TK_PARENTHESIS_CLOSE) { _set_error("Expected ')' in expression"); return nullptr; } tokenizer->advance(); expr = subexpr; } else if (tokenizer->get_token() == CScriptTokenizer::TK_DOLLAR) { tokenizer->advance(); String path; bool need_identifier = true; bool done = false; int line = tokenizer->get_token_line(); while (!done) { switch (tokenizer->get_token()) { case CScriptTokenizer::TK_CURSOR: { completion_type = COMPLETION_GET_NODE; completion_class = current_class; completion_function = current_function; completion_line = tokenizer->get_token_line(); completion_cursor = path; completion_argument = 0; completion_block = current_block; completion_found = true; tokenizer->advance(); } break; case CScriptTokenizer::TK_CONSTANT: { if (!need_identifier) { done = true; break; } if (tokenizer->get_token_constant().get_type() != Variant::STRING) { _set_error("Expected string constant or identifier after '$' or '/'."); return nullptr; } path += String(tokenizer->get_token_constant()); tokenizer->advance(); need_identifier = false; } break; case CScriptTokenizer::TK_OP_DIV: { if (need_identifier) { done = true; break; } path += "/"; tokenizer->advance(); need_identifier = true; } break; default: { // Instead of checking for TK_IDENTIFIER, we check with is_token_literal, as this allows us to use match/sync/etc. as a name if (need_identifier && tokenizer->is_token_literal()) { path += String(tokenizer->get_token_literal()); tokenizer->advance(); need_identifier = false; } else { done = true; } break; } } } if (path == "") { _set_error("Path expected after $."); return nullptr; } OperatorNode *op = alloc_node(); op->op = OperatorNode::OP_CALL; op->line = line; op->arguments.push_back(alloc_node()); op->arguments[0]->line = line; IdentifierNode *funcname = alloc_node(); funcname->name = "get_node"; funcname->line = line; op->arguments.push_back(funcname); ConstantNode *nodepath = alloc_node(); nodepath->value = NodePath(StringName(path)); nodepath->datatype = _type_from_variant(nodepath->value); nodepath->line = line; op->arguments.push_back(nodepath); expr = op; } else if (tokenizer->get_token() == CScriptTokenizer::TK_CURSOR) { tokenizer->advance(); continue; //no point in cursor in the middle of expression } else if (tokenizer->get_token() == CScriptTokenizer::TK_CONSTANT) { //constant defined by tokenizer ConstantNode *constant = alloc_node(); constant->value = tokenizer->get_token_constant(); constant->datatype = _type_from_variant(constant->value); tokenizer->advance(); expr = constant; } else if (tokenizer->get_token() == CScriptTokenizer::TK_CONST_PI) { //constant defined by tokenizer ConstantNode *constant = alloc_node(); constant->value = Math_PI; constant->datatype = _type_from_variant(constant->value); tokenizer->advance(); expr = constant; } else if (tokenizer->get_token() == CScriptTokenizer::TK_CONST_TAU) { //constant defined by tokenizer ConstantNode *constant = alloc_node(); constant->value = Math_TAU; constant->datatype = _type_from_variant(constant->value); tokenizer->advance(); expr = constant; } else if (tokenizer->get_token() == CScriptTokenizer::TK_CONST_INF) { //constant defined by tokenizer ConstantNode *constant = alloc_node(); constant->value = Math_INF; constant->datatype = _type_from_variant(constant->value); tokenizer->advance(); expr = constant; } else if (tokenizer->get_token() == CScriptTokenizer::TK_CONST_NAN) { //constant defined by tokenizer ConstantNode *constant = alloc_node(); constant->value = Math_NAN; constant->datatype = _type_from_variant(constant->value); tokenizer->advance(); expr = constant; } else if (tokenizer->get_token() == CScriptTokenizer::TK_PR_PRELOAD) { //constant defined by tokenizer tokenizer->advance(); if (tokenizer->get_token() != CScriptTokenizer::TK_PARENTHESIS_OPEN) { _set_error("Expected '(' after 'preload'"); return nullptr; } tokenizer->advance(); if (tokenizer->get_token() == CScriptTokenizer::TK_CURSOR) { completion_cursor = StringName(); completion_node = p_parent; completion_type = COMPLETION_RESOURCE_PATH; completion_class = current_class; completion_function = current_function; completion_line = tokenizer->get_token_line(); completion_block = current_block; completion_argument = 0; completion_found = true; tokenizer->advance(); } String path; bool found_constant = false; bool valid = false; ConstantNode *cn; parenthesis++; Node *subexpr = _parse_and_reduce_expression(p_parent, p_static); parenthesis--; if (subexpr) { if (subexpr->type == Node::TYPE_CONSTANT) { cn = static_cast(subexpr); found_constant = true; } if (subexpr->type == Node::TYPE_IDENTIFIER) { IdentifierNode *in = static_cast(subexpr); // Try to find the constant expression by the identifier if (current_class->constant_expressions.has(in->name)) { Node *cn_exp = current_class->constant_expressions[in->name].expression; if (cn_exp->type == Node::TYPE_CONSTANT) { cn = static_cast(cn_exp); found_constant = true; } } } if (found_constant && cn->value.get_type() == Variant::STRING) { valid = true; path = (String)cn->value; } } if (!valid) { _set_error("expected string constant as 'preload' argument."); return nullptr; } if (!path.is_abs_path() && base_path != "") { path = base_path.plus_file(path); } path = path.replace("///", "//").simplify_path(); if (path == self_path) { _set_error("Can't preload itself (use 'get_script()')."); return nullptr; } Ref res; dependencies.push_back(path); if (!dependencies_only) { if (!validating) { //this can be too slow for just validating code if (for_completion && ScriptCodeCompletionCache::get_singleton() && FileAccess::exists(path)) { res = ScriptCodeCompletionCache::get_singleton()->get_cached_resource(path); } else if (!for_completion || FileAccess::exists(path)) { res = ResourceLoader::load(path); } } else { if (!FileAccess::exists(path)) { _set_error("Can't preload resource at path: " + path); return nullptr; } else if (ScriptCodeCompletionCache::get_singleton()) { res = ScriptCodeCompletionCache::get_singleton()->get_cached_resource(path); } } if (!res.is_valid()) { _set_error("Can't preload resource at path: " + path); return nullptr; } } if (tokenizer->get_token() != CScriptTokenizer::TK_PARENTHESIS_CLOSE) { _set_error("Expected ')' after 'preload' path"); return nullptr; } Ref gds = res; if (gds.is_valid() && !gds->is_valid()) { _set_error("Couldn't fully preload the script, possible cyclic reference or compilation error. Use \"load()\" instead if a cyclic reference is intended."); return nullptr; } tokenizer->advance(); ConstantNode *constant = alloc_node(); constant->value = res; constant->datatype = _type_from_variant(constant->value); expr = constant; } else if (tokenizer->get_token() == CScriptTokenizer::TK_SELF) { if (p_static) { _set_error("\"self\" isn't allowed in a static function or constant expression."); return nullptr; } //constant defined by tokenizer SelfNode *self = alloc_node(); tokenizer->advance(); expr = self; } else if (tokenizer->get_token() == CScriptTokenizer::TK_BUILT_IN_TYPE && tokenizer->get_token(1) == CScriptTokenizer::TK_PERIOD) { Variant::Type bi_type = tokenizer->get_token_type(); tokenizer->advance(2); StringName identifier; if (_get_completable_identifier(COMPLETION_BUILT_IN_TYPE_CONSTANT, identifier)) { completion_built_in_constant = bi_type; } if (identifier == StringName()) { _set_error("Built-in type constant or static function expected after \".\"."); return nullptr; } if (!Variant::has_constant(bi_type, identifier)) { if (tokenizer->get_token() == CScriptTokenizer::TK_PARENTHESIS_OPEN && Variant::is_method_const(bi_type, identifier) && Variant::get_method_return_type(bi_type, identifier) == bi_type) { tokenizer->advance(); OperatorNode *construct = alloc_node(); construct->op = OperatorNode::OP_CALL; TypeNode *tn = alloc_node(); tn->vtype = bi_type; construct->arguments.push_back(tn); OperatorNode *op = alloc_node(); op->op = OperatorNode::OP_CALL; op->arguments.push_back(construct); IdentifierNode *id = alloc_node(); id->name = identifier; op->arguments.push_back(id); if (!_parse_arguments(op, op->arguments, p_static, true, p_parsing_constant)) { return nullptr; } expr = op; } else { // Object is a special case bool valid = false; if (bi_type == Variant::OBJECT) { int object_constant = ClassDB::get_integer_constant("Object", identifier, &valid); if (valid) { ConstantNode *cn = alloc_node(); cn->value = object_constant; cn->datatype = _type_from_variant(cn->value); expr = cn; } } if (!valid) { _set_error("Static constant '" + identifier.operator String() + "' not present in built-in type " + Variant::get_type_name(bi_type) + "."); return nullptr; } } } else { ConstantNode *cn = alloc_node(); cn->value = Variant::get_constant_value(bi_type, identifier); cn->datatype = _type_from_variant(cn->value); expr = cn; } } else if (tokenizer->get_token(next_valid_offset) == CScriptTokenizer::TK_PARENTHESIS_OPEN && tokenizer->is_token_literal()) { // We check with is_token_literal, as this allows us to use match/sync/etc. as a name //function or constructor OperatorNode *op = alloc_node(); op->op = OperatorNode::OP_CALL; //Do a quick Array and Dictionary Check. Replace if either require no arguments. bool replaced = false; if (tokenizer->get_token() == CScriptTokenizer::TK_BUILT_IN_TYPE) { Variant::Type ct = tokenizer->get_token_type(); if (!p_parsing_constant) { if (ct == Variant::ARRAY) { if (tokenizer->get_token(2) == CScriptTokenizer::TK_PARENTHESIS_CLOSE) { ArrayNode *arr = alloc_node(); expr = arr; replaced = true; tokenizer->advance(3); } } if (ct == Variant::DICTIONARY) { if (tokenizer->get_token(2) == CScriptTokenizer::TK_PARENTHESIS_CLOSE) { DictionaryNode *dict = alloc_node(); expr = dict; replaced = true; tokenizer->advance(3); } } } if (!replaced) { TypeNode *tn = alloc_node(); tn->vtype = tokenizer->get_token_type(); op->arguments.push_back(tn); tokenizer->advance(2); } } else if (tokenizer->get_token() == CScriptTokenizer::TK_BUILT_IN_FUNC) { BuiltInFunctionNode *bn = alloc_node(); bn->function = tokenizer->get_token_built_in_func(); op->arguments.push_back(bn); tokenizer->advance(2); } else { SelfNode *self = alloc_node(); op->arguments.push_back(self); StringName identifier; if (_get_completable_identifier(COMPLETION_FUNCTION, identifier)) { } IdentifierNode *id = alloc_node(); id->name = identifier; op->arguments.push_back(id); tokenizer->advance(1); } if (tokenizer->get_token() == CScriptTokenizer::TK_CURSOR) { _make_completable_call(0); completion_node = op; } if (!replaced) { if (!_parse_arguments(op, op->arguments, p_static, true, p_parsing_constant)) { return nullptr; } expr = op; } } else if (tokenizer->is_token_literal(0, true)) { // We check with is_token_literal, as this allows us to use match/sync/etc. as a name //identifier (reference) const ClassNode *cln = current_class; bool bfn = false; StringName identifier; int id_line = tokenizer->get_token_line(); if (_get_completable_identifier(COMPLETION_IDENTIFIER, identifier)) { } BlockNode *b = current_block; while (!bfn && b) { if (b->variables.has(identifier)) { IdentifierNode *id = alloc_node(); id->name = identifier; id->declared_block = b; id->line = id_line; expr = id; bfn = true; #ifdef DEBUG_ENABLED LocalVarNode *lv = b->variables[identifier]; switch (tokenizer->get_token()) { case CScriptTokenizer::TK_OP_ASSIGN_ADD: case CScriptTokenizer::TK_OP_ASSIGN_BIT_AND: case CScriptTokenizer::TK_OP_ASSIGN_BIT_OR: case CScriptTokenizer::TK_OP_ASSIGN_BIT_XOR: case CScriptTokenizer::TK_OP_ASSIGN_DIV: case CScriptTokenizer::TK_OP_ASSIGN_MOD: case CScriptTokenizer::TK_OP_ASSIGN_MUL: case CScriptTokenizer::TK_OP_ASSIGN_SHIFT_LEFT: case CScriptTokenizer::TK_OP_ASSIGN_SHIFT_RIGHT: case CScriptTokenizer::TK_OP_ASSIGN_SUB: { if (lv->assignments == 0) { if (!lv->datatype.has_type) { _set_error("Using assignment with operation on a variable that was never assigned."); return nullptr; } _add_warning(CScriptWarning::UNASSIGNED_VARIABLE_OP_ASSIGN, -1, identifier.operator String()); } FALLTHROUGH; } case CScriptTokenizer::TK_OP_ASSIGN: { lv->assignments += 1; lv->usages--; // Assignment is not really usage } break; default: { lv->usages++; } } #endif // DEBUG_ENABLED break; } b = b->parent_block; } if (!bfn && p_parsing_constant) { if (cln->constant_expressions.has(identifier)) { expr = cln->constant_expressions[identifier].expression; bfn = true; } else if (CScriptLanguage::get_singleton()->get_global_map().has(identifier)) { //check from constants ConstantNode *constant = alloc_node(); constant->value = CScriptLanguage::get_singleton()->get_global_array()[CScriptLanguage::get_singleton()->get_global_map()[identifier]]; constant->datatype = _type_from_variant(constant->value); constant->line = id_line; expr = constant; bfn = true; } if (!bfn && CScriptLanguage::get_singleton()->get_named_globals_map().has(identifier)) { //check from singletons ConstantNode *constant = alloc_node(); constant->value = CScriptLanguage::get_singleton()->get_named_globals_map()[identifier]; expr = constant; bfn = true; } if (!dependencies_only) { if (!bfn && ScriptServer::is_global_class(identifier)) { Ref