From 11649f0d0a9d4a414913b57143185602be4a0d95 Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 10 May 2022 09:54:47 +0100 Subject: [PATCH 1/3] Hover on associated blocks does not display types Fixes #62 Adds rudimentary hover support for hovering over associate blocks TODO: - Support slices of existing AST nodes, currently the associate node inherits the keywords from the link_obj so the slices display the wrong dimensions. - Support literals, given that Fortran literals are not currently registered as AST nodes it is difficult to provide information on the fly without creating more spaghetti code (see current literals). The only elegant solution I can think of is parsing the literals as AST nodes. --- fortls/objects.py | 76 ++++++++++++++++++---- fortls/parse_fortran.py | 1 + test/test_server_hover.py | 12 ++++ test/test_source/hover/associate_block.f90 | 13 ++++ test/test_source/tmp.py | 18 ----- 5 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 test/test_source/hover/associate_block.f90 delete mode 100644 test/test_source/tmp.py 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_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..baae630e --- /dev/null +++ b/test/test_source/hover/associate_block.f90 @@ -0,0 +1,13 @@ +PROGRAM associate_block_test + 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 associate_block_test \ 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) From b957d6c2186bebaf051fb3208f3dbe247607d01e Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 10 May 2022 09:56:16 +0100 Subject: [PATCH 2/3] Updated CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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 From e624a9f5e0e4e2d247ebd95a1078c106608b1349 Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 10 May 2022 10:05:55 +0100 Subject: [PATCH 3/3] Update workspace fymbols test --- test/test_server.py | 1 + test/test_source/hover/associate_block.f90 | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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_source/hover/associate_block.f90 b/test/test_source/hover/associate_block.f90 index baae630e..ef4a6507 100644 --- a/test/test_source/hover/associate_block.f90 +++ b/test/test_source/hover/associate_block.f90 @@ -1,4 +1,4 @@ -PROGRAM associate_block_test +PROGRAM test_associate_block IMPLICIT NONE REAL :: A(5), B(5,5), C, III = 1 ASSOCIATE (X => A, Y => C) @@ -10,4 +10,4 @@ PROGRAM associate_block_test ASSOCIATE (ARRAY => B(:,1)) ARRAY (3) = ARRAY (1) + ARRAY (2) END ASSOCIATE -END PROGRAM associate_block_test \ No newline at end of file +END PROGRAM test_associate_block \ No newline at end of file