From 518076515f3805d662545cdd393770e49f203d53 Mon Sep 17 00:00:00 2001 From: gnikit Date: Sun, 5 May 2024 14:30:21 +0100 Subject: [PATCH 1/7] refactor(ast): optimize scope filtering logic --- fortls/parsers/internal/ast.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/fortls/parsers/internal/ast.py b/fortls/parsers/internal/ast.py index 0780ad6f..f7187706 100644 --- a/fortls/parsers/internal/ast.py +++ b/fortls/parsers/internal/ast.py @@ -191,28 +191,26 @@ def get_scopes(self, line_number: int = None): return self.scope_list scope_list = [] for scope in self.scope_list: - if (line_number >= scope.sline) and (line_number <= scope.eline): - if type(scope.parent) is Interface: - for use_stmnt in scope.use: - if type(use_stmnt) is not Import: - continue - # Exclude the parent and all other scopes - if use_stmnt.import_type == ImportTypes.NONE: - return [scope] - scope_list.append(scope) - scope_list.extend(iter(scope.get_ancestors())) + if not scope.sline <= line_number <= scope.eline: + continue + if type(scope.parent) is Interface: + for use_stmnt in scope.use: + if type(use_stmnt) is not Import: + continue + # Exclude the parent and all other scopes + if use_stmnt.import_type == ImportTypes.NONE: + return [scope] + scope_list.append(scope) + scope_list.extend(iter(scope.get_ancestors())) if scope_list or self.none_scope is None: return scope_list - else: - return [self.none_scope] + return [self.none_scope] def get_inner_scope(self, line_number: int): scope_sline = -1 curr_scope = None for scope in self.scope_list: - if scope.sline > scope_sline and ( - (line_number >= scope.sline) and (line_number <= scope.eline) - ): + if scope.sline > scope_sline and scope.sline <= line_number <= scope.eline: curr_scope = scope scope_sline = scope.sline if (curr_scope is None) and (self.none_scope is not None): From de8b655ab51f7c85cdfab208e99864d6625aa69c Mon Sep 17 00:00:00 2001 From: gnikit Date: Sun, 5 May 2024 15:34:04 +0100 Subject: [PATCH 2/7] refactor(ast): update variable names for clarity --- fortls/parsers/internal/ast.py | 14 +++++++------- fortls/parsers/internal/parser.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/fortls/parsers/internal/ast.py b/fortls/parsers/internal/ast.py index f7187706..5894d13d 100644 --- a/fortls/parsers/internal/ast.py +++ b/fortls/parsers/internal/ast.py @@ -39,7 +39,7 @@ def __init__(self, file_obj=None): self.none_scope = None self.inc_scope = None self.current_scope = None - self.END_SCOPE_REGEX: Pattern = None + self.end_scope_regex: Pattern = None self.enc_scope_name: str = None self.last_obj = None self.pending_doc: str = None @@ -60,7 +60,7 @@ def get_enc_scope_name(self): def add_scope( self, new_scope: Scope, - END_SCOPE_REGEX: Pattern[str], + end_scope_regex: Pattern[str], exportable: bool = True, req_container: bool = False, ): @@ -80,10 +80,10 @@ def add_scope( else: self.current_scope.add_child(new_scope) self.scope_stack.append(self.current_scope) - if self.END_SCOPE_REGEX is not None: - self.end_stack.append(self.END_SCOPE_REGEX) + if self.end_scope_regex is not None: + self.end_stack.append(self.end_scope_regex) self.current_scope = new_scope - self.END_SCOPE_REGEX = END_SCOPE_REGEX + self.end_scope_regex = end_scope_regex self.enc_scope_name = self.get_enc_scope_name() self.last_obj = new_scope if self.pending_doc is not None: @@ -102,9 +102,9 @@ def end_scope(self, line_number: int, check: bool = True): else: self.current_scope = None if len(self.end_stack) > 0: - self.END_SCOPE_REGEX = self.end_stack.pop() + self.end_scope_regex = self.end_stack.pop() else: - self.END_SCOPE_REGEX = None + self.end_scope_regex = None self.enc_scope_name = self.get_enc_scope_name() def add_variable(self, new_var: Variable): diff --git a/fortls/parsers/internal/parser.py b/fortls/parsers/internal/parser.py index 688371eb..1d5e4e18 100644 --- a/fortls/parsers/internal/parser.py +++ b/fortls/parsers/internal/parser.py @@ -1352,7 +1352,7 @@ def parse( line = multi_lines.pop() line_stripped = line # Test for scope end - if file_ast.END_SCOPE_REGEX is not None: + if file_ast.end_scope_regex is not None: match = FRegex.END_WORD.match(line_no_comment) # Handle end statement if self.parse_end_scope_word(line_no_comment, line_no, file_ast, match): @@ -1766,7 +1766,7 @@ def parse_end_scope_word( ): file_ast.end_errors.append([ln, file_ast.current_scope.sline]) else: - scope_match = file_ast.END_SCOPE_REGEX.match(line[match.start(1) :]) + scope_match = file_ast.end_scope_regex.match(line[match.start(1) :]) if scope_match is not None: end_scope_word = scope_match.group(0) if end_scope_word is not None: From 7fb13c2e6d9c488b49be994e2b4c2b84fe6e7ad8 Mon Sep 17 00:00:00 2001 From: gnikit Date: Sun, 5 May 2024 15:39:45 +0100 Subject: [PATCH 3/7] refactor(ast): update variable typings for clarity --- fortls/parsers/internal/ast.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/fortls/parsers/internal/ast.py b/fortls/parsers/internal/ast.py index 5894d13d..2d560031 100644 --- a/fortls/parsers/internal/ast.py +++ b/fortls/parsers/internal/ast.py @@ -19,9 +19,7 @@ class FortranAST: def __init__(self, file_obj=None): self.file = file_obj - self.path: str = None - if file_obj is not None: - self.path = file_obj.path + self.path: str | None = file_obj.path if file_obj is not None else None self.global_dict: dict = {} self.scope_list: list = [] self.variable_list: list = [] @@ -39,10 +37,10 @@ def __init__(self, file_obj=None): self.none_scope = None self.inc_scope = None self.current_scope = None - self.end_scope_regex: Pattern = None - self.enc_scope_name: str = None + self.end_scope_regex: Pattern | None = None + self.enc_scope_name: str | None = None self.last_obj = None - self.pending_doc: str = None + self.pending_doc: str | None = None def create_none_scope(self): """Create empty scope to hold non-module contained items""" From ad91f4b3a9e9de1063eae4e5376cfbe613aaf7e4 Mon Sep 17 00:00:00 2001 From: gnikit Date: Sun, 5 May 2024 16:11:43 +0100 Subject: [PATCH 4/7] refactor(ast): improve object lookup efficiency --- fortls/parsers/internal/ast.py | 60 +++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/fortls/parsers/internal/ast.py b/fortls/parsers/internal/ast.py index 2d560031..7787b322 100644 --- a/fortls/parsers/internal/ast.py +++ b/fortls/parsers/internal/ast.py @@ -216,34 +216,40 @@ def get_inner_scope(self, line_number: int): return curr_scope def get_object(self, FQSN: str): - FQSN_split = FQSN.split("::") - curr_obj = self.global_dict.get(FQSN_split[0]) - if curr_obj is None: - # Look for non-exportable scopes - for scope in self.scope_list: - if FQSN_split[0] == scope.FQSN: - curr_obj = scope - break - if curr_obj is None: + def find_child_by_name(parent, name): + for child in parent.children: + if child.name == name: + return child + if child.name.startswith("#GEN_INT"): + found = next( + ( + int_child + for int_child in child.get_children() + if int_child.name == name + ), + None, + ) + if found: + return found return None - if len(FQSN_split) > 1: - for name in FQSN_split[1:]: - next_obj = None - for child in curr_obj.children: - if child.name.startswith("#GEN_INT"): - for int_child in child.get_children(): - if int_child.name == name: - next_obj = int_child - break - if next_obj is not None: - break - if child.name == name: - next_obj = child - break - if next_obj is None: - return None - curr_obj = next_obj - return curr_obj + + parts = FQSN.split("::") + current = self.global_dict.get(parts[0]) + + # Look for non-exportable scopes + if current is None: + current = next( + (scope for scope in self.scope_list if scope.FQSN == parts[0]), None + ) + if current is None: + return None + + for part in parts[1:]: + current = find_child_by_name(current, part) + if current is None: + return None + + return current def resolve_includes(self, workspace, path: str = None): file_dir = os.path.dirname(self.path) From f42fc348b53d8384312fe39ca83f1833c9455794 Mon Sep 17 00:00:00 2001 From: gnikit Date: Mon, 6 May 2024 20:37:23 +0100 Subject: [PATCH 5/7] refactor(ast): update resolve_includes method signature's typing to include None --- fortls/parsers/internal/ast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fortls/parsers/internal/ast.py b/fortls/parsers/internal/ast.py index 7787b322..48b09e9c 100644 --- a/fortls/parsers/internal/ast.py +++ b/fortls/parsers/internal/ast.py @@ -251,7 +251,7 @@ def find_child_by_name(parent, name): return current - def resolve_includes(self, workspace, path: str = None): + def resolve_includes(self, workspace, path: str | None = None): file_dir = os.path.dirname(self.path) for inc in self.include_statements: file_path = os.path.normpath(os.path.join(file_dir, inc.path)) From 6aaeedc1bcefacd3ce06d9e1d52cc9f7e0e63d6b Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 7 May 2024 14:53:12 +0100 Subject: [PATCH 6/7] test(parser): add test for private interfaces --- fortls/parsers/internal/parser.py | 5 ++--- test/test_parser.py | 8 ++++++++ test/test_source/.fortls | 3 ++- test/test_source/vis/private.f90 | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 test/test_source/vis/private.f90 diff --git a/fortls/parsers/internal/parser.py b/fortls/parsers/internal/parser.py index 1d5e4e18..da08ca1e 100644 --- a/fortls/parsers/internal/parser.py +++ b/fortls/parsers/internal/parser.py @@ -18,7 +18,6 @@ from fortls.constants import ( DO_TYPE_ID, INTERFACE_TYPE_ID, - MODULE_TYPE_ID, SELECT_TYPE_ID, SUBMODULE_TYPE_ID, FRegex, @@ -1657,10 +1656,10 @@ def parse( msg = "Visibility statement without enclosing scope" file_ast.add_error(msg, Severity.error, line_no, 0) else: - if (len(obj_info.obj_names) == 0) and (obj_info.type == 1): + if len(obj_info.obj_names) == 0 and obj_info.type == 1: # private file_ast.current_scope.set_default_vis(-1) else: - if obj_info.type == MODULE_TYPE_ID: + if obj_info.type == 1: # private for word in obj_info.obj_names: file_ast.add_private(word) else: diff --git a/test/test_parser.py b/test/test_parser.py index d5e0a48d..9b48dcaf 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -31,3 +31,11 @@ def test_submodule(): except Exception as e: print(e) assert False + + +def test_private_visibility_interfaces(): + file_path = test_dir / "vis" / "private.f90" + file = FortranFile(str(file_path)) + err_str, _ = file.load_from_disk() + file.parse() + assert err_str is None diff --git a/test/test_source/.fortls b/test/test_source/.fortls index 868923f9..7f3131a0 100644 --- a/test/test_source/.fortls +++ b/test/test_source/.fortls @@ -9,7 +9,8 @@ "./diag/", "docs", "rename", - "parse" + "parse", + "vis" ] } diff --git a/test/test_source/vis/private.f90 b/test/test_source/vis/private.f90 new file mode 100644 index 00000000..e0d188f7 --- /dev/null +++ b/test/test_source/vis/private.f90 @@ -0,0 +1,17 @@ +module visibility + private :: name + private :: generic_interf + interface name + module procedure :: name_sp + end interface name + interface + subroutine generic_interf(noop) + integer, intent(in) :: noop + end subroutine generic_interf + end interface +contains + subroutine name_sp(val) + real(4), intent(in) :: val + print *, 'name_sp', val + end subroutine name_sp +end module visibility From 09e87a65853aa6947172ad7bf0c312779e517ca0 Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 7 May 2024 14:53:23 +0100 Subject: [PATCH 7/7] refactor(ast): update get_scopes method signature's typing to include None --- fortls/parsers/internal/ast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fortls/parsers/internal/ast.py b/fortls/parsers/internal/ast.py index 48b09e9c..61b36838 100644 --- a/fortls/parsers/internal/ast.py +++ b/fortls/parsers/internal/ast.py @@ -168,11 +168,11 @@ def add_error(self, msg: str, sev: int, ln: int, sch: int, ech: int = None): def start_ppif(self, line_number: int): self.pp_if.append([line_number - 1, -1]) - def end_ppif(self, line_number): + def end_ppif(self, line_number: int): if len(self.pp_if) > 0: self.pp_if[-1][1] = line_number - 1 - def get_scopes(self, line_number: int = None): + def get_scopes(self, line_number: int | None = None): """Get a list of all the scopes present in the line number provided. Parameters