diff --git a/CHANGELOG.md b/CHANGELOG.md index c11edcb1..23f4ede3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Added - Added Code of Conduct +- Added basic support for hovering over `ASSOCIATE` blocks + ([#62](https://github.com/gnikit/fortls/issues/62)) ## 2.3.1 diff --git a/fortls/objects.py b/fortls/objects.py index 7ace630b..05b01884 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -3,7 +3,7 @@ import copy import os import re -from dataclasses import replace +from dataclasses import dataclass, replace from typing import Pattern from fortls.constants import ( @@ -291,6 +291,13 @@ def __init__( } +@dataclass +class AssociateMap: + var: fortran_var + bind_name: str + link_name: str + + class fortran_diagnostic: def __init__( self, sline: int, message: str, severity: int = 1, find_word: str = None @@ -1365,7 +1372,7 @@ def get_desc(self): class fortran_associate(fortran_block): def __init__(self, file_ast: fortran_ast, line_number: int, name: str): super().__init__(file_ast, line_number, name) - self.assoc_links = [] + self.links: list[AssociateMap] = [] # holds the info to associate variables def get_type(self, no_link=False): return ASSOC_TYPE_ID @@ -1373,25 +1380,57 @@ def get_type(self, no_link=False): def get_desc(self): return "ASSOCIATE" - def create_binding_variable(self, file_ast, line_number, bound_name, link_var): - new_var = fortran_var(file_ast, line_number, bound_name, "UNKNOWN", []) - self.assoc_links.append([new_var, bound_name, link_var]) + def create_binding_variable( + self, file_ast: fortran_ast, line_number: int, bind_name: str, link_name: str + ) -> fortran_var: + """Create a new variable to be linked upon resolution to the real variable + that contains the information of the mapping from the parent scope to the + ASSOCIATE block scope. + + Parameters + ---------- + file_ast : fortran_ast + AST file + line_number : int + Line number + bind_name : str + Name of the ASSOCIATE block variable + link_name : str + Name of the parent scope variable + + Returns + ------- + fortran_var + Variable object holding the ASSOCIATE block variable, pending resolution + """ + new_var = fortran_var(file_ast, line_number, bind_name, "UNKNOWN", []) + self.links.append(AssociateMap(new_var, bind_name, link_name)) return new_var def resolve_link(self, obj_tree): - for assoc_link in self.assoc_links: - var_stack = get_var_stack(assoc_link[2]) - if len(var_stack) > 1: + # Loop through the list of the associated variables map and resolve the links + # find the AST node that that corresponds to the variable with link_name + for assoc in self.links: + # TODO: extract the dimensions component from the link_name + # re.sub(r'\(.*\)', '', link_name) removes the dimensions component + # keywords = re.match(r'(.*)\((.*)\)', link_name).groups() + # now pass the keywords through the dimension_parser and set the keywords + # in the associate object. Hover should now pick the local keywords + # over the linked_object keywords + assoc.link_name = re.sub(r"\(.*\)", "", assoc.link_name) + var_stack = get_var_stack(assoc.link_name) + is_member = len(var_stack) > 1 + if is_member: type_scope = climb_type_tree(var_stack, self, obj_tree) if type_scope is None: continue var_obj = find_in_scope(type_scope, var_stack[-1], obj_tree) if var_obj is not None: - assoc_link[0].link_obj = var_obj + assoc.var.link_obj = var_obj else: - var_obj = find_in_scope(self, assoc_link[2], obj_tree) + var_obj = find_in_scope(self, assoc.link_name, obj_tree) if var_obj is not None: - assoc_link[0].link_obj = var_obj + assoc.var.link_obj = var_obj def require_link(self): return True @@ -1601,6 +1640,7 @@ def get_type_obj(self, obj_tree): self.type_obj = type_obj return self.type_obj + # XXX: unused delete or use for associate blocks def set_dim(self, dim_str): if KEYWORD_ID_DICT["dimension"] not in self.keywords: self.keywords.append(KEYWORD_ID_DICT["dimension"]) @@ -1618,9 +1658,9 @@ def get_snippet(self, name_replace=None, drop_arg=-1): def get_hover(self, long=False, include_doc=True, drop_arg=-1): doc_str = self.get_documentation() - hover_str = ", ".join( - [self.desc] + get_keywords(self.keywords, self.keyword_info) - ) + # In associated blocks we need to fetch the desc and keywords of the + # linked object + hover_str = ", ".join([self.get_desc()] + self.get_keywords()) # TODO: at this stage we can mae this lowercase # Add parameter value in the output if self.is_parameter() and self.param_val: @@ -1629,6 +1669,14 @@ def get_hover(self, long=False, include_doc=True, drop_arg=-1): hover_str += "\n {0}".format("\n ".join(doc_str.splitlines())) return hover_str, True + def get_keywords(self): + # TODO: if local keywords are set they should take precedence over link_obj + # Alternatively, I could do a dictionary merge with local variables + # having precedence by default and use a flag to override? + if self.link_obj is not None: + return get_keywords(self.link_obj.keywords, self.link_obj.keyword_info) + return get_keywords(self.keywords, self.keyword_info) + def is_optional(self): if self.keywords.count(KEYWORD_ID_DICT["optional"]) > 0: return True diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index 03fc8d32..3f0d21b1 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -1357,6 +1357,7 @@ def parse( else: name_raw = var_name.split("=")[0] # Add dimension if specified + # TODO: turn into function and add support for co-arrays i.e. [*] key_tmp = obj_info.keywords[:] iparen = name_raw.find("(") if iparen == 0: diff --git a/test/test_server.py b/test/test_server.py index 2a482e08..639ef427 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -177,6 +177,7 @@ def check_return(result_array): objs = ( ["test", 6, 7], ["test_abstract", 2, 0], + ["test_associate_block", 2, 0], ["test_free", 2, 0], ["test_gen_type", 5, 1], ["test_generic", 2, 0], diff --git a/test/test_server_hover.py b/test/test_server_hover.py index 7b9b956f..7c3ffc09 100644 --- a/test/test_server_hover.py +++ b/test/test_server_hover.py @@ -315,3 +315,15 @@ def test_hover_interface_as_argument(): REAL :: arg3""", ) validate_hover(results, ref_results) + + +def test_hover_block(): + string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "hover")}) + file_path = test_dir / "hover" / "associate_block.f90" + string += hover_req(file_path, 4, 17) + string += hover_req(file_path, 4, 20) + # string += hover_req(file_path, 10, 11) # slice of array + errcode, results = run_request(string, fortls_args=["--sort_keywords", "-n", "1"]) + assert errcode == 0 + ref_results = ["REAL, DIMENSION(5)", "REAL"] + validate_hover(results, ref_results) diff --git a/test/test_source/hover/associate_block.f90 b/test/test_source/hover/associate_block.f90 new file mode 100644 index 00000000..ef4a6507 --- /dev/null +++ b/test/test_source/hover/associate_block.f90 @@ -0,0 +1,13 @@ +PROGRAM test_associate_block + IMPLICIT NONE + REAL :: A(5), B(5,5), C, III = 1 + ASSOCIATE (X => A, Y => C) + PRINT*, X, Y, III + END ASSOCIATE + ASSOCIATE (X => 1) + PRINT*, X + END ASSOCIATE + ASSOCIATE (ARRAY => B(:,1)) + ARRAY (3) = ARRAY (1) + ARRAY (2) + END ASSOCIATE +END PROGRAM test_associate_block \ No newline at end of file diff --git a/test/test_source/tmp.py b/test/test_source/tmp.py deleted file mode 100644 index 5e3bc411..00000000 --- a/test/test_source/tmp.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -from fparser.common.readfortran import FortranFileReader -from fparser.two.parser import ParserFactory -from fparser.two.utils import Base, walk - -os.chdir("/home/gn/Code/Python/fortls/test/test_source") - -reader = FortranFileReader("test.f90", include_dirs=["subdir"], ignore_comments=False) -parser = ParserFactory().create(std="f2008") -parse_tree = parser(reader) - -for i, node in enumerate(walk(parse_tree)): - if node is None: - continue - if not isinstance(node, Base): - continue - print(i, type(node), ": ", node)