# Functions for retrieving information in the current script. import re from collections import namedtuple import util import classes # Regex patterns for user declarations. _VAR_PATTERN = "\s*(?:export(?:\(.*\)\s+)?)?var\s+(\w+)" _CONST_PATTERN = "\s*const\s+(\w+)\s*=\s*(.+)" _FUNC_PATTERN = "\s*(static\s+)?func\s+(\w+)\(((\w|,|\s)*)\):" _ENUM_PATTERN = "\s*enum\s+(\w+)" _ENUM_VALUES_PATTERN = "\s*enum\s+\w+\s*\{(.*)\}" _CLASS_PATTERN = "\s*class\s+(\w+)(?:\s+extends\s+(\w+))?" # Flags for choosing which decl types to gather. VAR_DECLS = 1 CONST_DECLS = 2 FUNC_DECLS = 4 ENUM_DECLS = 8 CLASS_DECLS = 16 ANY_DECLS = VAR_DECLS | CONST_DECLS | FUNC_DECLS | ENUM_DECLS | CLASS_DECLS # These store info about user-declared items in the script. VarDecl = namedtuple("VarDecl", "line, name, type") ConstDecl = namedtuple("ConstDecl", "line, name, value") FuncDecl = namedtuple("FuncDecl", "line, static, name, args") EnumDecl = namedtuple("EnumDecl", "line, name") ClassDecl = namedtuple("ClassDecl", "line, name, extends") # These store parts of a "token chain". See 'get_token_chain()' for more info. VariableToken = namedtuple("VariableToken", "name, type") MethodToken = namedtuple("MethodToken", "name, returns, args, qualifiers") EnumToken = namedtuple("EnumToken", "name, line") ClassToken = namedtuple("ClassToken", "name, line") # This just acts as a marker with no extra data . Named tuples must have at # least one field, which is why this is an empty class instead. class SuperAccessorToken: pass # Parse a user declaration. # 'flags' indicates which decl types to look for. def _get_decl(lnum, flags): line = util.get_line(lnum) if flags & VAR_DECLS: m = re.match(_VAR_PATTERN, line) if m: return VarDecl(lnum, m.group(1), None) if flags & CONST_DECLS: m = re.match(_CONST_PATTERN, line) if m: return ConstDecl(lnum, m.group(1), m.group(2)) if flags & FUNC_DECLS: m = re.match(_FUNC_PATTERN, line) if m: static = m.group(1) != None name = m.group(2) args = m.group(3) if args: args = [a.strip() for a in args.split(",")] return FuncDecl(lnum, static, name, args) if flags & ENUM_DECLS: m = re.match(_ENUM_PATTERN, line) if m: return EnumDecl(lnum, m.group(1)) if flags & CLASS_DECLS: m = re.match(_CLASS_PATTERN, line) if m: return ClassDecl(lnum, m.group(1), m.group(2)) # Map function arguments to VarDecls. # Arguments are treated as VarDecls for simplicity's sake. # If the function overrides a built-in method, the arg types are mapped as well. def _args_to_vars(func_decl): vars = [] method = None extended_class = classes.get_class(get_extended_class(func_decl.line)) if extended_class: method = extended_class.get_method(func_decl.name) for i, arg in enumerate(func_decl.args): arg_type = None if method and len(method.args) > i: method_arg = method.args[i] if method_arg: arg_type = method_arg.type vars.append(VarDecl(func_decl.line, arg, arg_type)) return vars # Generator function that scans the current file and yields user declarations. # # 'direction' should be 1 for downwards, or -1 for upwards. # # When scanning downwards, 'start_line' should either be on an inner class decl, or # on an unindented line (usually the top of the script). If starting on a # class decl, only the decls within that class are yielded. Similarly, items # within inner classes are ignored when scanning for global decls. # # When scanning upwards, 'start_line' should be inside a function. This yields # the following items in this order: # 1. Function arguments. # 2. Function-local var declarations up until 'start_line'. # 3. The function itself. # 4. The inner class containing the function (if there is one) def iter_decls(start_line, direction, flags=None): if direction != 1 and direction != -1: raise ValueError("'direction' must be 1 or -1!") if not flags: flags = ANY_DECLS if direction == 1: return _iter_decls_down(start_line, flags) else: return _iter_decls_up(start_line, flags) def _iter_decls_down(start_line, flags): # Check whether the starting line is a class decl. # If so, the indent of the next line is used as a baseline to determine # which items are direct children of the inner class. in_class = False class_decl = _get_decl(start_line, CLASS_DECLS) if class_decl: in_class = True class_indent = util.get_indent(start_line) inner_indent = None if flags & CLASS_DECLS: yield class_decl for lnum in range(start_line+1, util.get_line_count()): if not util.get_line(lnum): continue indent = util.get_indent(lnum) if in_class: if indent <= class_indent: return if not inner_indent: inner_indent = indent elif indent > inner_indent: continue else: if indent > 0: continue decl = _get_decl(lnum, flags) if decl: yield decl def _iter_decls_up(start_line, flags): # Remove consts and enums from flags, since they can't exist inside functions. flags &= ~CONST_DECLS flags &= ~ENUM_DECLS # Gather decls, but don't yield them until we're sure that the start line # was inside a function. If it wasn't, only the class decl is yielded, or # nothing if the start line wasn't inside an inner class either. decls = [] start_indent = util.get_indent(start_line) if start_indent == 0: return # Upon reaching a func decl, the search continues until a class decl is found. # This only happens if the func decl is indented. found_func = False for lnum in range(start_line-1, 0, -1): indent = util.get_indent(lnum) if indent > start_indent: continue if found_func: # After finding a function, we only care finding the inner class. decl = _get_decl(lnum, CLASS_DECLS) else: # We need to know when a func or class is encountered, even if they # aren't part of the search flags. Funcs and classes are still only # yielded if part of the original search flags. decl = _get_decl(lnum, flags | FUNC_DECLS | CLASS_DECLS) if not decl: continue if indent < start_indent: decl_type = type(decl) if decl_type is FuncDecl: found_func = True start_indent = indent if flags & VAR_DECLS: # Yield function args if len(decl.args) > 0: mapped_args = _args_to_vars(decl) for arg in mapped_args: yield arg # Yield var decls gathered up until now. for stored_decl in reversed(decls): yield stored_decl if flags & FUNC_DECLS: yield decl if indent == 0: break elif decl_type is ClassDecl: if flags & CLASS_DECLS: yield decl break else: decls.append(decl) # Helper function for gathering statically accessible items in classes. def iter_static_decls(start_line, flags): # Vars can't be accessed statically. flags &= ~VAR_DECLS it = iter_decls(start_line, 1, flags) # The first decl will be the class itself, which we don't need. next(it) for decl in it: # Only yield static funcs if type(decl) is FuncDecl and not decl.static: continue yield decl # Search for a user decl with a particular name. def find_decl(start_line, name, flags=None): down_search_start = 1 for decl in iter_decls(start_line, -1, flags | CLASS_DECLS): if type(decl) == ClassDecl: if flags & CLASS_DECLS and decl.name == name: return decl else: down_search_start = decl.line break elif decl.name == name: return decl return find_decl_down(down_search_start, name, flags) def find_decl_down(start_line, name, flags=None): for decl in iter_decls(start_line, 1, flags): if decl.name == name: return decl # Search for the 'extends' keyword and return the name of the extended class. def get_extended_class(start_line=None): # Figure out if we're in an inner class and return its extended type if so. if not start_line: start_line = util.get_cursor_line_num() start_indent = util.get_indent(start_line) if start_indent > 0: for decl in iter_decls(start_line, -1, FUNC_DECLS | CLASS_DECLS): indent = util.get_indent(decl.line) if indent == start_indent: continue decl_type = type(decl) if decl_type is FuncDecl: start_indent = indent elif decl_type is ClassDecl: if decl.extends: return decl.extends else: return None if indent == 0: break # Search for 'extends' at the top of the file. for lnum in range(1, util.get_line_count()): line = util.get_line(lnum).rstrip() m = re.match("extends\s+(\w+)", line) if m: return m.group(1) # Only 'tool' can appear before 'extends', so stop searching if any other # text is encountered. elif line and not re.match("tool\s*$", line) and not re.match("\s*\#", line): return None def get_enum_values(line_num): lines = [util.strip_line(line_num, util.get_line(line_num))] line_count = util.get_line_count() while not lines[-1].endswith("}"): line_num += 1 if line_num > line_count: return lines.append(util.strip_line(line_num, util.get_line(line_num))) m = re.match(_ENUM_VALUES_PATTERN, "\n".join(lines), re.DOTALL) if m: values = [v.strip() for v in m.group(1).replace("\n", ",").split(",")] def map_value(v): m = re.match("(\w+)(?:\s*=\s*(.*))?", v) if m: return ConstDecl(-1, m.group(1), m.group(2)) return list(filter(lambda v: v, map(map_value, values))) # A token chain is a group of tokens chained via dot accessors. # "Token" is a loose term referring to anything that produces a value. # Example: # texture.get_data().get_pixel() # 'texture', 'get_data', and 'get_pixel' all produce values, and are therefore tokens. # # A token chain is only considered valid if every token has a discernible type. def get_token_chain(line, line_num, start_col): i = start_col paren_count = 0 is_method = False end_col = None # Find the name of the token, skipping over any text in parentheses. while True: i -= 1 char = line[i] if char == ")": is_method = True paren_count += 1 elif char == "(": paren_count -= 1 if paren_count == 0: start_col = i continue if paren_count <= 0 and not (char.isalnum() or char == "_"): end_col = i + 1 break elif i == 0: end_col = i break name = line[end_col:start_col] if not name: if util.get_syn_attr(col_num=i+1) == "gdString": return [VariableToken(None, "String")] else: # return [SuperAccessorToken()] chain = None if line[i] == ".": chain = get_token_chain(line, line_num, i) if not chain: return # If this is the beginning of the chain, search global scope. # TODO: search user funcs and vars with type annotations. if (not chain or type(chain[-1]) is SuperAccessorToken or chain[-1].name == "self") and is_method: extended_class = classes.get_class(get_extended_class(line_num)) if extended_class: method = extended_class.get_method(name, search_global=True) if method: return [MethodToken(name, method.returns, method.args, method.qualifiers)] decl = find_decl(line_num, name, FUNC_DECLS) if decl: return [MethodToken(name, None, decl.args, None)] elif not chain or chain[-1].name == "self": if not chain and name == "self": return [VariableToken(name, None)] decl = find_decl(line_num, name, ENUM_DECLS | CLASS_DECLS) if decl: decl_type = type(decl) if decl_type is EnumDecl: return [EnumToken(name, decl.line)] elif decl_type is ClassDecl: return [ClassToken(name, decl.line)] else: extended_class = classes.get_class(get_extended_class(line_num)) if extended_class: member = extended_class.get_member(name, search_global=True) if member: return [VariableToken(name, member.type)] c = classes.get_class(name) if c: return [ClassToken(name, -1)] # Not the beginning of a chain, so get the type of the previous token. else: prev_token = chain[-1] prev_token_type = type(prev_token) prev_class = None if prev_token_type is VariableToken: prev_class = classes.get_class(prev_token.type) elif prev_token_type is MethodToken: prev_class = classes.get_class(prev_token.returns) elif prev_token_type is ClassToken: if is_method and name == "new": if not (prev_token.line == -1 and classes.get_class(prev_token.name).is_built_in()): chain.append(MethodToken(name, prev_token.name, None, None)) return chain for decl in iter_static_decls(prev_token.line, ANY_DECLS): if decl.name == name: decl_type = type(decl) if decl_type is ClassDecl: chain.append(ClassToken(name, decl.line)) return chain elif decl_type is FuncDecl and decl.static: chain.append(MethodToken(name, None, decl.args, None)) return chain return if not prev_class: return if is_method: method = prev_class.get_method(name) if method: chain.append(MethodToken(name, method.returns, method.args, method.qualifiers)) return chain else: member = prev_class.get_member(name) if member: chain.append(VariableToken(name, member.type)) return chain