From 05b9d52ae912dec1bd315c313ccdd0d302f91cbf Mon Sep 17 00:00:00 2001 From: gnikit Date: Wed, 16 Nov 2022 18:55:16 +0000 Subject: [PATCH 1/6] feat: add kind variable in Variable & Method class This should help distinguish between attributes, variables and functions using that name. --- fortls/ftypes.py | 8 +++++--- fortls/objects.py | 28 +++++++++++++++++++--------- fortls/parse_fortran.py | 21 ++++++++++++++------- test/test_server_completion.py | 2 +- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/fortls/ftypes.py b/fortls/ftypes.py index 474dbaef..dc8d8721 100644 --- a/fortls/ftypes.py +++ b/fortls/ftypes.py @@ -15,7 +15,8 @@ class VarInfo: #: keywords associated with this variable e.g. SAVE, DIMENSION, etc. keywords: list[str] #: Keywords associated with variable var_names: list[str] #: Variable names - var_kind: str = field(default=None) #: Kind of variable e.g. ``INTEGER*4`` etc. + #: Kind of variable e.g. ``INTEGER*4`` etc. + var_kind: str | None = field(default=None) @dataclass @@ -106,10 +107,11 @@ class SubInfo: class ResultSig: """Holds information about the RESULT section of a Fortran FUNCTION""" - name: str = field(default=None) #: Variable name of result - type: str = field(default=None) #: Variable type of result + name: str | None = field(default=None) #: Variable name of result + type: str | None = field(default=None) #: Variable type of result #: Keywords associated with the result variable, can append without init keywords: list[str] = field(default_factory=list) + kind: str | None = field(default=None) #: Variable kind of result @dataclass diff --git a/fortls/objects.py b/fortls/objects.py index b38edc83..61ccd2d1 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -1591,7 +1591,7 @@ def __init__( var_desc: str, keywords: list, keyword_info: dict = None, - # kind: int | str = None, + kind: str | None = None, link_obj=None, ): super().__init__() @@ -1604,7 +1604,7 @@ def __init__( self.desc: str = var_desc self.keywords: list = keywords self.keyword_info: dict = keyword_info - self.callable: bool = FRegex.CLASS_VAR.match(var_desc) is not None + self.kind: str | None = kind self.children: list = [] self.use: list[USE_line] = [] self.link_obj = None @@ -1613,7 +1613,7 @@ def __init__( self.is_external: bool = False self.param_val: str = None self.link_name: str = None - # self.kind: int | str = kind + self.callable: bool = FRegex.CLASS_VAR.match(self.get_desc(True)) is not None self.FQSN: str = self.name.lower() if link_obj is not None: self.link_name = link_obj.lower() @@ -1657,17 +1657,19 @@ def get_type(self, no_link=False): # Normal variable return VAR_TYPE_ID - def get_desc(self): - if self.link_obj is not None: + def get_desc(self, no_link=False): + if not no_link and self.link_obj is not None: return self.link_obj.get_desc() # Normal variable + if self.kind: + return self.desc + self.kind return self.desc def get_type_obj(self, obj_tree): if self.link_obj is not None: return self.link_obj.get_type_obj(obj_tree) if (self.type_obj is None) and (self.parent is not None): - type_name = get_paren_substring(self.desc) + type_name = get_paren_substring(self.get_desc(no_link=True)) if type_name is not None: search_scope = self.parent if search_scope.get_type() == CLASS_TYPE_ID: @@ -1739,7 +1741,7 @@ def set_external_attr(self): def check_definition(self, obj_tree, known_types={}, interface=False): # Check for type definition in scope - type_match = FRegex.DEF_KIND.match(self.desc) + type_match = FRegex.DEF_KIND.match(self.get_desc(no_link=True)) if type_match is not None: var_type = type_match.group(1).strip().lower() if var_type == "procedure": @@ -1803,14 +1805,22 @@ def __init__( keywords: list, keyword_info: dict, link_obj=None, + proc_ptr: str = "", # procedure pointer ): super().__init__( - file_ast, line_number, name, var_desc, keywords, keyword_info, link_obj + file_ast, + line_number, + name, + var_desc, + keywords, + keyword_info, + kind=proc_ptr, + link_obj=link_obj, ) self.drop_arg: int = -1 self.pass_name: str = keyword_info.get("pass") if link_obj is None: - self.link_name = get_paren_substring(var_desc.lower()) + self.link_name = get_paren_substring(self.get_desc(True).lower()) def set_parent(self, parent_obj): self.parent = parent_obj diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index dd9cfb41..aba363d6 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -202,11 +202,8 @@ def parse_kind(line: str): # defined kind try: kind_str, trailing_line = parse_kind(trailing_line) - var_type += kind_str # XXX: see below except ValueError: return None - except TypeError: # XXX: remove with explicit kind specification in VarInfo - pass # Class and Type statements need a kind spec if not kind_str and var_type in ("TYPE", "CLASS"): @@ -214,10 +211,13 @@ def parse_kind(line: str): # Make sure next character is space or comma or colon if not kind_str and not trailing_line[0] in (" ", ",", ":"): return None - # + keywords, trailing_line = parse_var_keywords(trailing_line) # Check if this is a function definition - fun_def = read_fun_def(trailing_line, ResultSig(type=var_type, keywords=keywords)) + fun_def = read_fun_def( + trailing_line, + ResultSig(type=var_type, keywords=keywords, kind=kind_str), + ) if fun_def or fun_only: return fun_def # Split the type and variable name @@ -234,7 +234,12 @@ def parse_kind(line: str): if var_words is None: var_words = [] - return "var", VarInfo(var_type, keywords, var_words, kind_str) + return "var", VarInfo( + var_type=var_type, + keywords=keywords, + var_names=var_words, + var_kind=kind_str, + ) def get_procedure_modifiers( @@ -1411,6 +1416,7 @@ def parse( desc, keywords, keyword_info=keyword_info, + proc_ptr=obj_info.var_kind, link_obj=link_name, ) else: @@ -1421,7 +1427,7 @@ def parse( desc, keywords, keyword_info=keyword_info, - # kind=obj_info.var_kind, + kind=obj_info.var_kind, link_obj=link_name, ) # If the object is fortran_var and a parameter include @@ -1493,6 +1499,7 @@ def parse( line_no, name=obj_info.result.name, var_desc=obj_info.result.type, + kind=obj_info.result.kind, keywords=keywords, keyword_info=keyword_info, ) diff --git a/test/test_server_completion.py b/test/test_server_completion.py index d904aac1..4a780ad8 100644 --- a/test/test_server_completion.py +++ b/test/test_server_completion.py @@ -34,7 +34,7 @@ def test_comp1(): string += comp_request(file_path, 21, 20) string += comp_request(file_path, 21, 42) string += comp_request(file_path, 23, 26) - errcode, results = run_request(string, ["--use_signature_help"]) + errcode, results = run_request(string, ["--use_signature_help", "-n1"]) assert errcode == 0 exp_results = ( From a07ed70bd395e5c4f97db1a0c66140a636c25fdb Mon Sep 17 00:00:00 2001 From: gnikit Date: Wed, 16 Nov 2022 19:02:35 +0000 Subject: [PATCH 2/6] refactor: minor changes --- fortls/ftypes.py | 2 +- fortls/objects.py | 12 +++++++++--- fortls/parse_fortran.py | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/fortls/ftypes.py b/fortls/ftypes.py index dc8d8721..15ece4a0 100644 --- a/fortls/ftypes.py +++ b/fortls/ftypes.py @@ -109,9 +109,9 @@ class ResultSig: name: str | None = field(default=None) #: Variable name of result type: str | None = field(default=None) #: Variable type of result + kind: str | None = field(default=None) #: Variable kind of result #: Keywords associated with the result variable, can append without init keywords: list[str] = field(default_factory=list) - kind: str | None = field(default=None) #: Variable kind of result @dataclass diff --git a/fortls/objects.py b/fortls/objects.py index 61ccd2d1..28bf02f8 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -1749,11 +1749,17 @@ def check_definition(self, obj_tree, known_types={}, interface=False): desc_obj_name = type_match.group(2).strip().lower() if desc_obj_name not in known_types: type_def = find_in_scope( - self.parent, desc_obj_name, obj_tree, interface=interface + self.parent, + desc_obj_name, + obj_tree, + interface=interface, ) if type_def is None: type_defs = find_in_workspace( - obj_tree, desc_obj_name, filter_public=True, exact_match=True + obj_tree, + desc_obj_name, + filter_public=True, + exact_match=True, ) known_types[desc_obj_name] = None var_type = type_match.group(1).strip().lower() @@ -1804,8 +1810,8 @@ def __init__( var_desc: str, keywords: list, keyword_info: dict, + proc_ptr: str = "", # procedure pointer e.g. `foo` in `procedure(foo)` link_obj=None, - proc_ptr: str = "", # procedure pointer ): super().__init__( file_ast, diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index aba363d6..fb4154cd 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -167,7 +167,7 @@ def parse_var_keywords(test_str: str) -> tuple[list[str], str]: return keywords, test_str -def read_var_def(line: str, var_type: str = None, fun_only: bool = False): +def read_var_def(line: str, var_type: str | None = None, fun_only: bool = False): """Attempt to read variable definition line""" def parse_kind(line: str): @@ -1499,9 +1499,9 @@ def parse( line_no, name=obj_info.result.name, var_desc=obj_info.result.type, - kind=obj_info.result.kind, keywords=keywords, keyword_info=keyword_info, + kind=obj_info.result.kind, ) file_ast.add_variable(new_obj) log.debug("%s !!! FUNCTION - Ln:%d", line, line_no) From 112f1d43c6de18650a3ec3c908ac530bcc0dea04 Mon Sep 17 00:00:00 2001 From: gnikit Date: Wed, 16 Nov 2022 22:07:37 +0000 Subject: [PATCH 3/6] fix: superficial fix of #173 --- fortls/regex_patterns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fortls/regex_patterns.py b/fortls/regex_patterns.py index 9ee313f4..2847edbb 100644 --- a/fortls/regex_patterns.py +++ b/fortls/regex_patterns.py @@ -135,7 +135,7 @@ class FortranRegularExpressions: ) # Object regex patterns CLASS_VAR: Pattern = compile(r"(TYPE|CLASS)[ ]*\(", I) - DEF_KIND: Pattern = compile(r"([a-z]*)[ ]*\((?:KIND|LEN)?[ =]*([a-z_]\w*)", I) + DEF_KIND: Pattern = compile(r"(\w*)[ ]*\((?:KIND|LEN)?[ =]*(\w*)", I) OBJBREAK: Pattern = compile(r"[\/\-(.,+*<>=$: ]", I) From 144b292b34c9b2bb584da7dd8bfc2c4870c8c62b Mon Sep 17 00:00:00 2001 From: gnikit Date: Wed, 16 Nov 2022 23:16:36 +0000 Subject: [PATCH 4/6] test: add unittest for #173 --- test/test_server_diagnostics.py | 12 ++++++++++++ .../diag/test_var_shadowing_keyword_arg.f90 | 11 +++++++++++ 2 files changed, 23 insertions(+) create mode 100644 test/test_source/diag/test_var_shadowing_keyword_arg.f90 diff --git a/test/test_server_diagnostics.py b/test/test_server_diagnostics.py index f8dc7ada..bd4b4631 100644 --- a/test/test_server_diagnostics.py +++ b/test/test_server_diagnostics.py @@ -416,3 +416,15 @@ def test_keyword_arg_list_var_names(): errcode, results = run_request(string, ["-n", "1"]) assert errcode == 0 assert results[1]["diagnostics"] == [] + + +def test_attribute_and_variable_name_collision(): + """Test variables named with attribute names do not cause a collision.""" + string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "diag")}) + file_path = str(test_dir / "diag" / "var_shadowing_keyword_arg.f90") + string += write_rpc_notification( + "textDocument/didOpen", {"textDocument": {"uri": file_path}} + ) + errcode, results = run_request(string, ["-n", "1"]) + assert errcode == 0 + assert results[1]["diagnostics"] == [] diff --git a/test/test_source/diag/test_var_shadowing_keyword_arg.f90 b/test/test_source/diag/test_var_shadowing_keyword_arg.f90 new file mode 100644 index 00000000..e6ed957c --- /dev/null +++ b/test/test_source/diag/test_var_shadowing_keyword_arg.f90 @@ -0,0 +1,11 @@ +module var_shadowing_keyword_arg + character(len=6), parameter :: TEST = "4.10.4" + character(len=6, kind=4), parameter :: TEST2 = "4.10.4" + real(kind=8) :: a +end module var_shadowing_keyword_arg + +program program_var_shadowing_keyword_arg + use var_shadowing_keyword_arg + integer :: len + integer :: kind +end program program_var_shadowing_keyword_arg From 1d195c21e635ca4e28cf82c3799879f82119888a Mon Sep 17 00:00:00 2001 From: gnikit Date: Thu, 17 Nov 2022 01:56:01 +0000 Subject: [PATCH 5/6] build(cov): updates pytest and pytest-cov There were some issues with multiprocessing and incorrect coverage reports being reported locally. Restricting the minimum versions should solve this issue. --- .coveragerc | 3 +++ pyproject.toml | 2 +- setup.cfg | 5 +++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 0a9ffd3c..01ef38ab 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,6 +3,9 @@ omit = fortls/__init__.py fortls/version.py fortls/schema.py +concurrency = multiprocessing +parallel = true +sigterm = true [report] exclude_lines = diff --git a/pyproject.toml b/pyproject.toml index 69446d19..22d96bea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,6 @@ write_to = "fortls/_version.py" profile = "black" [tool.pytest.ini_options] -minversion = "7.0" +minversion = "7.2.0" addopts = "-v --cov=fortls --cov-report=html --cov-report=xml --cov-context=test" testpaths = ["fortls", "test"] diff --git a/setup.cfg b/setup.cfg index 7a4a5e66..62d5b743 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,8 +56,9 @@ console_scripts = [options.extras_require] dev = - pytest >= 5.4.3 - pytest-cov >= 2.12.1 + pytest >= 7.2.0 + pytest-cov >= 4.0.0 + pytest-xdist >= 3.0.2 black isort pre-commit From 37db70f2e9692fc612d93a95cefe9747248f393a Mon Sep 17 00:00:00 2001 From: gnikit Date: Thu, 17 Nov 2022 02:26:00 +0000 Subject: [PATCH 6/6] docs: update CHANGELOG [skip ci] --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd4ce31a..e932e62d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ ### Fixed +- Fixed bug where diagnostic messages were raised for non-existent variables + ([#173](https://github.com/fortran-lang/fortls/issues/173)) + ([#175](https://github.com/fortran-lang/fortls/issues/175)) - Fixed submodule crashing bug and document/Symbol request failure ([#233](https://github.com/fortran-lang/fortls/issues/233)) - Fixed debug interface parser not loading all configuration files