From b85385a92294cdf9b79188f92c16489e37d77162 Mon Sep 17 00:00:00 2001 From: gnikit Date: Mon, 23 Oct 2023 23:33:05 +0100 Subject: [PATCH] refactor(parser): move all remaining AST nodes This had to happen in a single commit to ensure that circular dependencies were correclty resolved. ## TODO: - Increase converage - Fix poor quality code Issues - Simpligy certain parts of the AST nodes --- fortls/__init__.py | 4 +- fortls/langserver.py | 20 +- fortls/objects.py | 2138 ----------------- fortls/parsers/internal/associate.py | 87 + fortls/parsers/internal/ast.py | 325 +++ fortls/parsers/internal/base.py | 2 +- fortls/parsers/internal/block.py | 28 + fortls/parsers/internal/do.py | 21 + fortls/parsers/internal/enum.py | 21 + fortls/parsers/internal/function.py | 146 ++ fortls/parsers/internal/if_block.py | 21 + fortls/parsers/internal/imports.py | 45 + fortls/parsers/internal/include.py | 8 + fortls/parsers/internal/interface.py | 52 + .../internal}/intrinsic.modules.json | 0 .../internal}/intrinsic.procedures.json | 0 .../intrinsic.procedures.markdown.json | 0 fortls/{ => parsers/internal}/intrinsics.py | 19 +- fortls/{ => parsers/internal}/keywords.json | 0 fortls/parsers/internal/method.py | 136 ++ fortls/parsers/internal/module.py | 21 + .../internal/parser.py} | 44 +- fortls/parsers/internal/program.py | 8 + fortls/parsers/internal/scope.py | 268 +++ fortls/parsers/internal/select.py | 74 + fortls/{ => parsers/internal}/statements.json | 0 fortls/parsers/internal/submodule.py | 115 + fortls/parsers/internal/subroutine.py | 258 ++ fortls/parsers/internal/type.py | 185 ++ fortls/parsers/internal/utilities.py | 296 +++ fortls/parsers/internal/variable.py | 233 ++ fortls/parsers/internal/where.py | 21 + setup.cfg | 2 +- test/setup_tests.py | 8 +- test/test_parser.py | 2 +- test/test_server_hover.py | 11 +- 36 files changed, 2427 insertions(+), 2192 deletions(-) delete mode 100644 fortls/objects.py create mode 100644 fortls/parsers/internal/associate.py create mode 100644 fortls/parsers/internal/ast.py create mode 100644 fortls/parsers/internal/block.py create mode 100644 fortls/parsers/internal/do.py create mode 100644 fortls/parsers/internal/enum.py create mode 100644 fortls/parsers/internal/function.py create mode 100644 fortls/parsers/internal/if_block.py create mode 100644 fortls/parsers/internal/imports.py create mode 100644 fortls/parsers/internal/include.py create mode 100644 fortls/parsers/internal/interface.py rename fortls/{ => parsers/internal}/intrinsic.modules.json (100%) rename fortls/{ => parsers/internal}/intrinsic.procedures.json (100%) rename fortls/{ => parsers/internal}/intrinsic.procedures.markdown.json (100%) rename fortls/{ => parsers/internal}/intrinsics.py (97%) rename fortls/{ => parsers/internal}/keywords.json (100%) create mode 100644 fortls/parsers/internal/method.py create mode 100644 fortls/parsers/internal/module.py rename fortls/{parse_fortran.py => parsers/internal/parser.py} (99%) create mode 100644 fortls/parsers/internal/program.py create mode 100644 fortls/parsers/internal/scope.py create mode 100644 fortls/parsers/internal/select.py rename fortls/{ => parsers/internal}/statements.json (100%) create mode 100644 fortls/parsers/internal/submodule.py create mode 100644 fortls/parsers/internal/subroutine.py create mode 100644 fortls/parsers/internal/type.py create mode 100644 fortls/parsers/internal/utilities.py create mode 100644 fortls/parsers/internal/variable.py create mode 100644 fortls/parsers/internal/where.py diff --git a/fortls/__init__.py b/fortls/__init__.py index edcec288..faebde88 100644 --- a/fortls/__init__.py +++ b/fortls/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os import pprint @@ -8,7 +10,7 @@ from .interface import cli from .jsonrpc import JSONRPC2Connection, ReadWriter, path_from_uri from .langserver import LangServer -from .parse_fortran import FortranFile +from .parsers.internal.parser import FortranFile from .version import __version__ __all__ = ["__version__"] diff --git a/fortls/langserver.py b/fortls/langserver.py index 5555d2dd..422061d9 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -41,26 +41,26 @@ resolve_globs, set_keyword_ordering, ) -from fortls.intrinsics import ( +from fortls.json_templates import change_json, symbol_json, uri_json +from fortls.jsonrpc import JSONRPC2Connection, path_from_uri, path_to_uri +from fortls.parsers.internal.ast import FortranAST +from fortls.parsers.internal.imports import Import +from fortls.parsers.internal.intrinsics import ( Intrinsic, get_intrinsic_keywords, load_intrinsics, set_lowercase_intrinsics, ) -from fortls.json_templates import change_json, symbol_json, uri_json -from fortls.jsonrpc import JSONRPC2Connection, path_from_uri, path_to_uri -from fortls.objects import ( - FortranAST, - Import, - Scope, - Use, - Variable, +from fortls.parsers.internal.parser import FortranFile, get_line_context +from fortls.parsers.internal.scope import Scope +from fortls.parsers.internal.use import Use +from fortls.parsers.internal.utilities import ( climb_type_tree, find_in_scope, find_in_workspace, get_use_tree, ) -from fortls.parse_fortran import FortranFile, get_line_context +from fortls.parsers.internal.variable import Variable from fortls.regex_patterns import create_src_file_exts_str from fortls.version import __version__ diff --git a/fortls/objects.py b/fortls/objects.py deleted file mode 100644 index 27a13196..00000000 --- a/fortls/objects.py +++ /dev/null @@ -1,2138 +0,0 @@ -from __future__ import annotations - -import contextlib -import copy -import os -import re -from dataclasses import dataclass -from typing import Pattern -from typing import Type as T - -from fortls.constants import ( - ASSOC_TYPE_ID, - BASE_TYPE_ID, - BLOCK_TYPE_ID, - CLASS_TYPE_ID, - DO_TYPE_ID, - ENUM_TYPE_ID, - FUNCTION_TYPE_ID, - IF_TYPE_ID, - INTERFACE_TYPE_ID, - KEYWORD_ID_DICT, - METH_TYPE_ID, - MODULE_TYPE_ID, - SELECT_TYPE_ID, - SUBMODULE_TYPE_ID, - SUBROUTINE_TYPE_ID, - VAR_TYPE_ID, - WHERE_TYPE_ID, - FRegex, -) -from fortls.ftypes import IncludeInfo -from fortls.helper_functions import ( - fortran_md, - get_keywords, - get_paren_substring, - get_placeholders, - get_var_stack, -) -from fortls.json_templates import diagnostic_json, range_json -from fortls.jsonrpc import path_to_uri -from fortls.parsers.internal.base import FortranObj -from fortls.parsers.internal.diagnostics import Diagnostic -from fortls.parsers.internal.use import Use - - -def get_use_tree( - scope: Scope, - use_dict: dict[str, Use | Import], - obj_tree: dict, - only_list: list[str] = None, - rename_map: dict[str, str] = None, - curr_path: list[str] = None, -): - if only_list is None: - only_list = set() - if rename_map is None: - rename_map = {} - if curr_path is None: - curr_path = [] - - def intersect_only(use_stmnt: Use | Import): - tmp_list = [] - tmp_map = rename_map.copy() - for val1 in only_list: - mapped1 = tmp_map.get(val1, val1) - if mapped1 in use_stmnt.only_list: - tmp_list.append(val1) - new_rename = use_stmnt.rename_map.get(mapped1, None) - if new_rename is not None: - tmp_map[val1] = new_rename - else: - tmp_map.pop(val1, None) - return tmp_list, tmp_map - - # Detect and break circular references - if scope.FQSN in curr_path: - return use_dict - new_path = curr_path + [scope.FQSN] - # Add recursively - for use_stmnt in scope.use: - # if use_stmnt.mod_name not in obj_tree: - if type(use_stmnt) is Use and use_stmnt.mod_name not in obj_tree: - continue - # Escape any IMPORT, NONE statements - if type(use_stmnt) is Import and use_stmnt.import_type is ImportTypes.NONE: - continue - # Intersect parent and current ONLY list and renaming - if not only_list: - merged_use_list = use_stmnt.only_list.copy() - merged_rename = use_stmnt.rename_map.copy() - elif len(use_stmnt.only_list) == 0: - merged_use_list = only_list.copy() - merged_rename = rename_map.copy() - else: - merged_use_list, merged_rename = intersect_only(use_stmnt) - if len(merged_use_list) == 0: - continue - # Update ONLY list and renaming for current module - # If you have - # USE MOD, ONLY: A - # USE MOD, ONLY: B - # or - # IMPORT VAR - # IMPORT VAR2 - use_dict_mod = use_dict.get(use_stmnt.mod_name) - if use_dict_mod is not None: - old_len = len(use_dict_mod.only_list) - if old_len > 0 and merged_use_list: - only_len = old_len - for only_name in merged_use_list: - use_dict_mod.only_list.add(only_name) - if len(use_dict_mod.only_list) == only_len: - continue - only_len = len(use_dict_mod.only_list) - new_rename = merged_rename.get(only_name) - if new_rename is None: - continue - use_dict_mod.rename_map = merged_rename - use_dict[use_stmnt.mod_name] = use_dict_mod - else: - use_dict[use_stmnt.mod_name] = Use(use_stmnt.mod_name) - # Skip if we have already visited module with the same only list - if old_len == len(use_dict_mod.only_list): - continue - else: - if type(use_stmnt) is Use: - use_dict[use_stmnt.mod_name] = Use( - mod_name=use_stmnt.mod_name, - only_list=set(merged_use_list), - rename_map=merged_rename, - ) - elif type(use_stmnt) is Import: - use_dict[use_stmnt.mod_name] = Import( - name=use_stmnt.mod_name, - import_type=use_stmnt.import_type, - only_list=set(merged_use_list), - rename_map=merged_rename, - ) - with contextlib.suppress(AttributeError): - use_dict[use_stmnt.mod_name].scope = scope.parent.parent - # Do not descent the IMPORT tree, because it does not exist - if type(use_stmnt) is Import: - continue - # Descend USE tree - use_dict = get_use_tree( - obj_tree[use_stmnt.mod_name][0], - use_dict, - obj_tree, - merged_use_list, - merged_rename, - new_path, - ) - return use_dict - - -def find_in_scope( - scope: Scope, - var_name: str, - obj_tree: dict, - interface: bool = False, - local_only: bool = False, - var_line_number: int = None, -): - def check_scope( - local_scope: Scope, - var_name_lower: str, - filter_public: bool = False, - var_line_number: int = None, - ): - for child in local_scope.get_children(): - if child.name.startswith("#GEN_INT"): - tmp_var = check_scope(child, var_name_lower, filter_public) - if tmp_var is not None: - return tmp_var - is_private = child.vis < 0 or (local_scope.def_vis < 0 and child.vis <= 0) - if filter_public and is_private: - continue - if child.name.lower() == var_name_lower: - # For functions with an implicit result() variable the name - # of the function is used. If we are hovering over the function - # definition, we do not want the implicit result() to be returned. - # If scope is from a function and child's name is same as functions name - # and start of scope i.e. function definition is equal to the request ln - # then we are need to skip this child - if ( - isinstance(local_scope, Function) - and local_scope.name.lower() == child.name.lower() - and var_line_number in (local_scope.sline, local_scope.eline) - ): - return None - - return child - return None - - def check_import_scope(scope: Scope, var_name_lower: str): - for use_stmnt in scope.use: - if type(use_stmnt) is not Import: - continue - if use_stmnt.import_type == ImportTypes.ONLY: - # Check if name is in only list - if var_name_lower in use_stmnt.only_list: - return ImportTypes.ONLY - # Get the parent scope - elif use_stmnt.import_type == ImportTypes.ALL: - return ImportTypes.ALL - # Skip looking for parent scope - elif use_stmnt.import_type == ImportTypes.NONE: - return ImportTypes.NONE - return None - - # - var_name_lower = var_name.lower() - # Check local scope - if scope is None: - return None - tmp_var = check_scope(scope, var_name_lower, var_line_number=var_line_number) - if local_only or (tmp_var is not None): - return tmp_var - # Check INCLUDE statements - if scope.file_ast.include_statements: - strip_str = var_name.replace('"', "") - strip_str = strip_str.replace("'", "") - for inc in scope.file_ast.include_statements: - if strip_str == inc.path: - if inc.file is None: - return None - return Include(inc.file.ast, inc.line_number, inc.path) - - # Setup USE search - use_dict = get_use_tree(scope, {}, obj_tree) - # Look in found use modules - for use_mod, use_info in use_dict.items(): - # If use_mod is Import then it will not exist in the obj_tree - if type(use_info) is Import: - continue - use_scope = obj_tree[use_mod][0] - # Module name is request - if use_mod.lower() == var_name_lower: - return use_scope - # Filter children by only_list - if len(use_info.only_list) > 0 and var_name_lower not in use_info.only_list: - continue - mod_name = use_info.rename_map.get(var_name_lower, var_name_lower) - tmp_var = check_scope(use_scope, mod_name, filter_public=True) - if tmp_var is not None: - return tmp_var - # Only search local and imported names for interfaces - import_type = ImportTypes.DEFAULT - if interface: - import_type = check_import_scope(scope, var_name_lower) - if import_type is None: - return None - # Check parent scopes - if scope.parent is not None and import_type != ImportTypes.NONE: - tmp_var = find_in_scope(scope.parent, var_name, obj_tree) - if tmp_var is not None: - return tmp_var - # Check ancestor scopes - for ancestor in scope.get_ancestors(): - tmp_var = find_in_scope(ancestor, var_name, obj_tree) - if tmp_var is not None: - return tmp_var - return None - - -def find_in_workspace( - obj_tree: dict, query: str, filter_public: bool = False, exact_match: bool = False -): - def add_children(mod_obj, query: str): - tmp_list = [] - for child_obj in mod_obj.get_children(filter_public): - if child_obj.name.lower().find(query) >= 0: - tmp_list.append(child_obj) - return tmp_list - - matching_symbols = [] - query = query.lower() - for _, obj_packed in obj_tree.items(): - top_obj = obj_packed[0] - top_uri = obj_packed[1] - if top_uri is not None: - if top_obj.name.lower().find(query) > -1: - matching_symbols.append(top_obj) - if top_obj.get_type() == MODULE_TYPE_ID: - matching_symbols += add_children(top_obj, query) - if exact_match: - filtered_symbols = [] - n = len(query) - for symbol in matching_symbols: - if len(symbol.name) == n: - filtered_symbols.append(symbol) - matching_symbols = filtered_symbols - return matching_symbols - - -def climb_type_tree(var_stack, curr_scope: Scope, obj_tree: dict): - """Walk up user-defined type sequence to determine final field type""" - # Find base variable in current scope - iVar = 0 - var_name = var_stack[iVar].strip().lower() - var_obj = find_in_scope(curr_scope, var_name, obj_tree) - if var_obj is None: - return None - # Search for type, then next variable in stack and so on - for _ in range(30): - # Find variable type object - type_obj = var_obj.get_type_obj(obj_tree) - # Return if not found - if type_obj is None: - return None - # Go to next variable in stack and exit if done - iVar += 1 - if iVar == len(var_stack) - 1: - break - # Find next variable by name in type - var_name = var_stack[iVar].strip().lower() - var_obj = find_in_scope(type_obj, var_name, obj_tree, local_only=True) - # Return if not found - if var_obj is None: - return None - else: - raise KeyError - return type_obj - - -class ImportTypes: - DEFAULT = -1 - NONE = 0 - ALL = 1 - ONLY = 2 - - -class Import(Use): - """AST node for IMPORT statement""" - - def __init__( - self, - name: str, - import_type: ImportTypes = ImportTypes.DEFAULT, - only_list: set[str] = None, - rename_map: dict[str, str] = None, - line_number: int = 0, - ): - if only_list is None: - only_list = set() - if rename_map is None: - rename_map = {} - super().__init__(name, only_list, rename_map, line_number) - self.import_type = import_type - self._scope: Scope | Module | None = None - - @property - def scope(self): - """Parent scope of IMPORT statement i.e. parent of the interface""" - return self._scope - - @scope.setter - def scope(self, scope: Scope): - self._scope = scope - - -@dataclass -class AssociateMap: - var: Variable - bind_name: str - link_name: str - - -class Scope(FortranObj): - def __init__(self, file_ast, line_number: int, name: str, keywords: list = None): - super().__init__() - if keywords is None: - keywords = [] - self.file_ast: FortranAST = file_ast - self.sline: int = line_number - self.eline: int = line_number - self.name: str = name - self.children: list[T[Scope]] = [] - self.members: list = [] - self.use: list[Use | Import] = [] - self.keywords: list = keywords - self.inherit = None - self.parent = None - self.contains_start = None - self.implicit_line = None - self.FQSN: str = self.name.lower() - if file_ast.enc_scope_name is not None: - self.FQSN = f"{file_ast.enc_scope_name.lower()}::{self.name.lower()}" - - def copy_from(self, copy_source: Scope): - # Pass the reference, we don't want shallow copy since that would still - # result into 2 versions of attributes between copy_source and self - for k, v in copy_source.__dict__.items(): - setattr(self, k, v) - - def add_use(self, use_mod: Use | Import): - self.use.append(use_mod) - - def set_inherit(self, inherit_type): - self.inherit = inherit_type - - def set_parent(self, parent_obj): - self.parent = parent_obj - - def set_implicit(self, implicit_flag, line_number): - self.implicit_vars = implicit_flag - self.implicit_line = line_number - - def mark_contains(self, line_number): - if self.contains_start is not None: - raise ValueError - self.contains_start = line_number - - def add_child(self, child): - self.children.append(child) - child.set_parent(self) - - def update_fqsn(self, enc_scope=None): - if enc_scope is not None: - self.FQSN = f"{enc_scope.lower()}::{self.name.lower()}" - else: - self.FQSN = self.name.lower() - for child in self.children: - child.update_fqsn(self.FQSN) - - def add_member(self, member): - self.members.append(member) - - def get_children(self, public_only=False) -> list[T[FortranObj]]: - if not public_only: - return copy.copy(self.children) - pub_children = [] - for child in self.children: - if (child.vis < 0) or ((self.def_vis < 0) and (child.vis <= 0)): - continue - if child.name.startswith("#GEN_INT"): - pub_children.append(child) - continue - pub_children.append(child) - return pub_children - - def check_definitions(self, obj_tree) -> list[Diagnostic]: - """Check for definition errors in scope""" - fqsn_dict: dict[str, int] = {} - errors: list[Diagnostic] = [] - known_types: dict[str, FortranObj] = {} - - for child in self.children: - # Skip masking/double checks for interfaces - if child.get_type() == INTERFACE_TYPE_ID: - continue - # Check other variables in current scope - if child.FQSN in fqsn_dict: - if child.sline < fqsn_dict[child.FQSN]: - fqsn_dict[child.FQSN] = child.sline - 1 - else: - fqsn_dict[child.FQSN] = child.sline - 1 - - contains_line = -1 - if self.get_type() in ( - MODULE_TYPE_ID, - SUBMODULE_TYPE_ID, - SUBROUTINE_TYPE_ID, - FUNCTION_TYPE_ID, - ): - contains_line = ( - self.contains_start if self.contains_start is not None else self.eline - ) - # Detect interface definitions - is_interface = ( - self.parent is not None - and self.parent.get_type() == INTERFACE_TYPE_ID - and not self.is_mod_scope() - ) - - for child in self.children: - if child.name.startswith("#"): - continue - line_number = child.sline - 1 - # Check for type definition in scope - def_error, known_types = child.check_definition( - obj_tree, known_types=known_types, interface=is_interface - ) - if def_error is not None: - errors.append(def_error) - # Detect contains errors - if contains_line >= child.sline and child.get_type(no_link=True) in ( - SUBROUTINE_TYPE_ID, - FUNCTION_TYPE_ID, - ): - new_diag = Diagnostic( - line_number, - message="Subroutine/Function definition before CONTAINS statement", - severity=1, - ) - errors.append(new_diag) - # Skip masking/double checks for interfaces and members - if ( - self.get_type() == INTERFACE_TYPE_ID - or child.get_type() == INTERFACE_TYPE_ID - ): - continue - # Check other variables in current scope - if child.FQSN in fqsn_dict and line_number > fqsn_dict[child.FQSN]: - new_diag = Diagnostic( - line_number, - message=f'Variable "{child.name}" declared twice in scope', - severity=1, - find_word=child.name, - ) - new_diag.add_related( - path=self.file_ast.path, - line=fqsn_dict[child.FQSN], - message="First declaration", - ) - errors.append(new_diag) - continue - # Check for masking from parent scope in subroutines, functions, and blocks - if self.parent is not None and self.get_type() in ( - SUBROUTINE_TYPE_ID, - FUNCTION_TYPE_ID, - BLOCK_TYPE_ID, - ): - parent_var = find_in_scope(self.parent, child.name, obj_tree) - if parent_var is not None: - # Ignore if function return variable - if ( - self.get_type() == FUNCTION_TYPE_ID - and parent_var.FQSN == self.FQSN - ): - continue - - new_diag = Diagnostic( - line_number, - message=( - f'Variable "{child.name}" masks variable in parent scope' - ), - severity=2, - find_word=child.name, - ) - new_diag.add_related( - path=parent_var.file_ast.path, - line=parent_var.sline - 1, - message="First declaration", - ) - errors.append(new_diag) - - return errors - - def check_use(self, obj_tree): - errors = [] - last_use_line = -1 - for use_stmnt in self.use: - last_use_line = max(last_use_line, use_stmnt.line_number) - if type(use_stmnt) == Import: - if (self.parent is None) or ( - self.parent.get_type() != INTERFACE_TYPE_ID - ): - new_diag = Diagnostic( - use_stmnt.line_number - 1, - message="IMPORT statement outside of interface", - severity=1, - ) - errors.append(new_diag) - continue - if use_stmnt.mod_name not in obj_tree: - new_diag = Diagnostic( - use_stmnt.line_number - 1, - message=f'Module "{use_stmnt.mod_name}" not found in project', - severity=3, - find_word=use_stmnt.mod_name, - ) - errors.append(new_diag) - if (self.implicit_line is not None) and (last_use_line >= self.implicit_line): - new_diag = Diagnostic( - self.implicit_line - 1, - message="USE statements after IMPLICIT statement", - severity=1, - find_word="IMPLICIT", - ) - errors.append(new_diag) - return errors - - def add_subroutine(self, interface_string, no_contains=False): - edits = [] - line_number = self.eline - 1 - if (self.contains_start is None) and (not no_contains): - first_sub_line = line_number - for child in self.children: - if child.get_type() in (SUBROUTINE_TYPE_ID, FUNCTION_TYPE_ID): - first_sub_line = min(first_sub_line, child.sline - 1) - edits.append( - { - **range_json(first_sub_line, 0, first_sub_line, 0), - "newText": "CONTAINS\n", - } - ) - edits.append( - { - **range_json(line_number, 0, line_number, 0), - "newText": interface_string + "\n", - } - ) - return self.file_ast.path, edits - - -class Module(Scope): - def get_type(self, no_link=False): - return MODULE_TYPE_ID - - def get_desc(self): - return "MODULE" - - def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: - hover = f"{self.get_desc()} {self.name}" - doc_str = self.get_documentation() - return hover, doc_str - - def check_valid_parent(self) -> bool: - return self.parent is None - - -class Include(Scope): - def get_desc(self): - return "INCLUDE" - - -class Program(Module): - def get_desc(self): - return "PROGRAM" - - -class Submodule(Module): - def __init__( - self, - file_ast: FortranAST, - line_number: int, - name: str, - ancestor_name: str = "", - ): - super().__init__(file_ast, line_number, name) - self.ancestor_name = ancestor_name - self.ancestor_obj = None - - def get_type(self, no_link=False): - return SUBMODULE_TYPE_ID - - def get_desc(self): - return "SUBMODULE" - - def get_ancestors(self): - if self.ancestor_obj is not None: - great_ancestors = self.ancestor_obj.get_ancestors() - if great_ancestors is not None: - return [self.ancestor_obj] + great_ancestors - return [self.ancestor_obj] - return [] - - def resolve_inherit(self, obj_tree, inherit_version): - if not self.ancestor_name: - return - if self.ancestor_name in obj_tree: - self.ancestor_obj = obj_tree[self.ancestor_name][0] - - def require_inherit(self): - return True - - def resolve_link(self, obj_tree): - def get_ancestor_interfaces( - ancestor_children: list[Scope], - ) -> list[T[Interface]]: - interfaces = [] - for child in ancestor_children: - if child.get_type() != INTERFACE_TYPE_ID: - continue - for interface in child.children: - interface_type = interface.get_type() - if ( - interface_type - in (SUBROUTINE_TYPE_ID, FUNCTION_TYPE_ID, BASE_TYPE_ID) - ) and interface.is_mod_scope(): - interfaces.append(interface) - return interfaces - - def create_child_from_prototype(child: Scope, interface: Interface): - if interface.get_type() == SUBROUTINE_TYPE_ID: - return Subroutine(child.file_ast, child.sline, child.name) - elif interface.get_type() == FUNCTION_TYPE_ID: - return Function(child.file_ast, child.sline, child.name) - else: - raise ValueError(f"Unsupported interface type: {interface.get_type()}") - - def replace_child_in_scope_list(child: Scope, child_old: Scope): - for i, file_scope in enumerate(child.file_ast.scope_list): - if file_scope is child_old: - child.file_ast.scope_list[i] = child - return child - - # Link subroutine/function implementations to prototypes - if self.ancestor_obj is None: - return - - ancestor_interfaces = get_ancestor_interfaces(self.ancestor_obj.children) - # Match interface definitions to implementations - for interface in ancestor_interfaces: - for i, child in enumerate(self.children): - if child.name.lower() != interface.name.lower(): - continue - - if child.get_type() == BASE_TYPE_ID: - child_old = child - child = create_child_from_prototype(child_old, interface) - child.copy_from(child_old) - self.children[i] = child - child = replace_child_in_scope_list(child, child_old) - - if child.get_type() == interface.get_type(): - interface.link_obj = child - interface.resolve_link(obj_tree) - child.copy_interface(interface) - break - - def require_link(self): - return True - - -class Subroutine(Scope): - def __init__( - self, - file_ast: FortranAST, - line_number: int, - name: str, - args: str = "", - mod_flag: bool = False, - keywords: list = None, - ): - super().__init__(file_ast, line_number, name, keywords) - self.args: str = args.replace(" ", "") - self.args_snip: str = self.args - self.arg_objs: list = [] - self.in_children: list = [] - self.missing_args: list = [] - self.mod_scope: bool = mod_flag - self.link_obj: Subroutine | Function | None = None - - def is_mod_scope(self): - return self.mod_scope - - def is_callable(self): - return True - - def copy_interface(self, copy_source: Subroutine) -> list[str]: - # Copy arguments - self.args = copy_source.args - self.args_snip = copy_source.args_snip - self.arg_objs = copy_source.arg_objs - # Get current fields - child_names = [child.name.lower() for child in self.children] - # Import arg_objs from copy object - self.in_children = [] - for child in copy_source.arg_objs: - if child is None: - continue - if child.name.lower() not in child_names: - self.in_children.append(child) - return child_names - - def get_children(self, public_only=False): - tmp_list = copy.copy(self.children) - tmp_list.extend(self.in_children) - return tmp_list - - def resolve_arg_link(self, obj_tree): - if (self.args == "") or (len(self.in_children) > 0): - return - arg_list = self.args.replace(" ", "").split(",") - arg_list_lower = self.args.lower().replace(" ", "").split(",") - self.arg_objs = [None] * len(arg_list) - # check_objs = copy.copy(self.children) - # for child in self.children: - # if child.is_external_int(): - # check_objs += child.get_children() - self.missing_args = [] - for child in self.children: - ind = -1 - for i, arg in enumerate(arg_list_lower): - if arg == child.name.lower(): - ind = i - break - # If an argument is part of an interface block go through the - # block's children i.e. functions and subroutines to see if one matches - elif child.name.lower().startswith("#gen_int"): - for sub_child in child.children: - if arg == sub_child.name: - self.arg_objs[i] = sub_child - break - - if ind < 0: - if child.keywords.count(KEYWORD_ID_DICT["intent"]) > 0: - self.missing_args.append(child) - else: - self.arg_objs[ind] = child - if child.is_optional(): - arg_list[ind] = f"{arg_list[ind]}={arg_list[ind]}" - self.args_snip = ",".join(arg_list) - - def resolve_link(self, obj_tree): - self.resolve_arg_link(obj_tree) - - def require_link(self): - return True - - def get_type(self, no_link=False): - return SUBROUTINE_TYPE_ID - - def get_snippet(self, name_replace=None, drop_arg=-1): - arg_list = self.args_snip.split(",") - if (drop_arg >= 0) and (drop_arg < len(arg_list)): - del arg_list[drop_arg] - arg_snip = None - if len(arg_list) > 0: - arg_str, arg_snip = get_placeholders(arg_list) - else: - arg_str = "()" - name = name_replace if name_replace is not None else self.name - snippet = name + arg_snip if arg_snip is not None else None - return name + arg_str, snippet - - def get_desc(self): - return "SUBROUTINE" - - def get_hover(self, long=False, drop_arg=-1): - sub_sig, _ = self.get_snippet(drop_arg=drop_arg) - keyword_list = get_keywords(self.keywords) - keyword_list.append(f"{self.get_desc()} ") - hover_array = [" ".join(keyword_list) + sub_sig] - hover_array, docs = self.get_docs_full(hover_array, long, drop_arg) - return "\n ".join(hover_array), " \n".join(docs) - - def get_hover_md(self, long=False, drop_arg=-1): - return fortran_md(*self.get_hover(long, drop_arg)) - - def get_docs_full( - self, hover_array: list[str], long=False, drop_arg=-1 - ) -> tuple[list[str], list[str]]: - """Construct the full documentation with the code signature and the - documentation string + the documentation of any arguments. - - Parameters - ---------- - hover_array : list[str] - The list of strings to append the documentation to. - long : bool, optional - Whether or not to fetch the docs of the arguments, by default False - drop_arg : int, optional - Whether or not to drop certain arguments from the results, by default -1 - - Returns - ------- - tuple[list[str], list[str]] - Tuple containing the Fortran signature that should be in code blocks - and the documentation string that should be in normal Markdown. - """ - doc_strs: list[str] = [] - doc_str = self.get_documentation() - if doc_str is not None: - doc_strs.append(doc_str) - if long: - has_args = True - for i, arg_obj in enumerate(self.arg_objs): - if arg_obj is None or i == drop_arg: - continue - arg, doc_str = arg_obj.get_hover() - hover_array.append(arg) - if doc_str: # If doc_str is not None or "" - if has_args: - doc_strs.append("\n**Parameters:** ") - has_args = False - # stripping prevents multiple \n characters from the parser - doc_strs.append(f"`{arg_obj.name}` {doc_str}".strip()) - return hover_array, doc_strs - - def get_signature(self, drop_arg=-1): - arg_sigs = [] - arg_list = self.args.split(",") - for i, arg_obj in enumerate(self.arg_objs): - if i == drop_arg: - continue - if arg_obj is None: - arg_sigs.append({"label": arg_list[i]}) - else: - if arg_obj.is_optional(): - label = f"{arg_obj.name.lower()}={arg_obj.name.lower()}" - else: - label = arg_obj.name.lower() - msg = arg_obj.get_hover_md() - # Create MarkupContent object - msg = {"kind": "markdown", "value": msg} - arg_sigs.append({"label": label, "documentation": msg}) - call_sig, _ = self.get_snippet() - return call_sig, self.get_documentation(), arg_sigs - - # TODO: fix this - def get_interface_array( - self, keywords: list[str], signature: str, drop_arg=-1, change_strings=None - ): - interface_array = [" ".join(keywords) + signature] - for i, arg_obj in enumerate(self.arg_objs): - if arg_obj is None: - return None - arg_doc, docs = arg_obj.get_hover() - if i == drop_arg: - i0 = arg_doc.lower().find(change_strings[0].lower()) - if i0 >= 0: - i1 = i0 + len(change_strings[0]) - arg_doc = arg_doc[:i0] + change_strings[1] + arg_doc[i1:] - interface_array.append(f"{arg_doc} :: {arg_obj.name}") - return interface_array - - def get_interface(self, name_replace=None, drop_arg=-1, change_strings=None): - sub_sig, _ = self.get_snippet(name_replace=name_replace) - keyword_list = get_keywords(self.keywords) - keyword_list.append("SUBROUTINE ") - interface_array = self.get_interface_array( - keyword_list, sub_sig, drop_arg, change_strings - ) - name = name_replace if name_replace is not None else self.name - interface_array.append(f"END SUBROUTINE {name}") - return "\n".join(interface_array) - - def check_valid_parent(self): - if self.parent is not None: - parent_type = self.parent.get_type() - if (parent_type == CLASS_TYPE_ID) or (parent_type >= BLOCK_TYPE_ID): - return False - return True - - def get_diagnostics(self): - errors = [] - for missing_obj in self.missing_args: - new_diag = Diagnostic( - missing_obj.sline - 1, - f'Variable "{missing_obj.name}" with INTENT keyword not found in' - " argument list", - severity=1, - find_word=missing_obj.name, - ) - errors.append(new_diag) - implicit_flag = self.get_implicit() - if (implicit_flag is None) or implicit_flag: - return errors - arg_list = self.args.replace(" ", "").split(",") - for i, arg_obj in enumerate(self.arg_objs): - if arg_obj is None: - arg_name = arg_list[i].strip() - new_diag = Diagnostic( - self.sline - 1, - f'No matching declaration found for argument "{arg_name}"', - severity=1, - find_word=arg_name, - ) - errors.append(new_diag) - return errors - - -class Function(Subroutine): - def __init__( - self, - file_ast: FortranAST, - line_number: int, - name: str, - args: str = "", - mod_flag: bool = False, - keywords: list = None, - keyword_info: dict = None, - result_type: str = None, - result_name: str = None, - ): - super().__init__(file_ast, line_number, name, args, mod_flag, keywords) - self.args: str = args.replace(" ", "").lower() - self.args_snip: str = self.args - self.arg_objs: list = [] - self.in_children: list = [] - self.missing_args: list = [] - self.mod_scope: bool = mod_flag - self.result_name: str = result_name - self.result_type: str = result_type - self.result_obj: Variable = None - self.keyword_info: dict = keyword_info - # Set the implicit result() name to be the function name - if self.result_name is None: - self.result_name = self.name - # Used in Associated blocks - if self.keyword_info is None: - self.keyword_info = {} - - def copy_interface(self, copy_source: Function): - # Call the parent class method - child_names = super().copy_interface(copy_source) - # Return specific options - self.result_name = copy_source.result_name - self.result_type = copy_source.result_type - self.result_obj = copy_source.result_obj - if ( - copy_source.result_obj is not None - and copy_source.result_obj.name.lower() not in child_names - ): - self.in_children.append(copy_source.result_obj) - - def resolve_link(self, obj_tree): - self.resolve_arg_link(obj_tree) - result_var_lower = self.result_name.lower() - for child in self.children: - if child.name.lower() == result_var_lower: - self.result_obj = child - # Update result value and type - self.result_name = child.name - self.result_type = child.get_desc() - - def get_type(self, no_link=False): - return FUNCTION_TYPE_ID - - def get_desc(self): - token = "FUNCTION" - return f"{self.result_type} {token}" if self.result_type else token - - def is_callable(self): - return False - - def get_hover(self, long: bool = False, drop_arg: int = -1) -> tuple[str, str]: - """Construct the hover message for a FUNCTION. - Two forms are produced here the `long` i.e. the normal for hover requests - - [MODIFIERS] FUNCTION NAME([ARGS]) RESULT(RESULT_VAR) - TYPE, [ARG_MODIFIERS] :: [ARGS] - TYPE, [RESULT_MODIFIERS] :: RESULT_VAR - - note: intrinsic functions will display slightly different, - `RESULT_VAR` and its `TYPE` might not always be present - - short form, used when functions are arguments in functions and subroutines: - - FUNCTION NAME([ARGS]) :: ARG_LIST_NAME - - Parameters - ---------- - long : bool, optional - toggle between long and short hover results, by default False - drop_arg : int, optional - Ignore argument at position `drop_arg` in the argument list, by default -1 - - Returns - ------- - tuple[str, bool] - String representative of the hover message and the `long` flag used - """ - fun_sig, _ = self.get_snippet(drop_arg=drop_arg) - # short hover messages do not include the result() - fun_sig += f" RESULT({self.result_name})" if long else "" - keyword_list = get_keywords(self.keywords) - keyword_list.append("FUNCTION") - - hover_array = [f"{' '.join(keyword_list)} {fun_sig}"] - hover_array, docs = self.get_docs_full(hover_array, long, drop_arg) - # Only append the return value if using long form - if self.result_obj and long: - # Parse the documentation from the result variable - arg_doc, doc_str = self.result_obj.get_hover() - if doc_str is not None: - docs.append(f"\n**Return:** \n`{self.result_obj.name}`{doc_str}") - hover_array.append(arg_doc) - # intrinsic functions, where the return type is missing but can be inferred - elif self.result_type and long: - # prepend type to function signature - hover_array[0] = f"{self.result_type} {hover_array[0]}" - return "\n ".join(hover_array), " \n".join(docs) - - # TODO: fix this - def get_interface(self, name_replace=None, drop_arg=-1, change_strings=None): - fun_sig, _ = self.get_snippet(name_replace=name_replace) - fun_sig += f" RESULT({self.result_name})" - # XXX: - keyword_list = [] - if self.result_type: - keyword_list.append(self.result_type) - keyword_list += get_keywords(self.keywords) - keyword_list.append("FUNCTION ") - - interface_array = self.get_interface_array( - keyword_list, fun_sig, drop_arg, change_strings - ) - if self.result_obj is not None: - arg_doc, docs = self.result_obj.get_hover() - interface_array.append(f"{arg_doc} :: {self.result_obj.name}") - name = name_replace if name_replace is not None else self.name - interface_array.append(f"END FUNCTION {name}") - return "\n".join(interface_array) - - -class Type(Scope): - def __init__( - self, file_ast: FortranAST, line_number: int, name: str, keywords: list - ): - super().__init__(file_ast, line_number, name, keywords) - self.in_children: list = [] - self.inherit = None - self.inherit_var = None - self.inherit_tmp = None - self.inherit_version = -1 - self.abstract = self.keywords.count(KEYWORD_ID_DICT["abstract"]) > 0 - if self.keywords.count(KEYWORD_ID_DICT["public"]) > 0: - self.vis = 1 - if self.keywords.count(KEYWORD_ID_DICT["private"]) > 0: - self.vis = -1 - - def get_type(self, no_link=False): - return CLASS_TYPE_ID - - def get_desc(self): - return "TYPE" - - def get_children(self, public_only=False): - tmp_list = copy.copy(self.children) - tmp_list.extend(self.in_children) - return tmp_list - - def resolve_inherit(self, obj_tree, inherit_version): - if (self.inherit is None) or (self.inherit_version == inherit_version): - return - self.inherit_version = inherit_version - self.inherit_var = find_in_scope(self.parent, self.inherit, obj_tree) - if self.inherit_var is not None: - self._resolve_inherit_parent(obj_tree, inherit_version) - - def _resolve_inherit_parent(self, obj_tree, inherit_version): - # Resolve parent inheritance while avoiding circular recursion - self.inherit_tmp = self.inherit - self.inherit = None - self.inherit_var.resolve_inherit(obj_tree, inherit_version) - self.inherit = self.inherit_tmp - self.inherit_tmp = None - # Get current fields - child_names = [child.name.lower() for child in self.children] - # Import for parent objects - self.in_children = [] - for child in self.inherit_var.get_children(): - if child.name.lower() not in child_names: - self.in_children.append(child) - - def require_inherit(self): - return True - - def get_overridden(self, field_name): - ret_list = [] - field_name = field_name.lower() - for child in self.children: - if field_name == child.name.lower(): - ret_list.append(child) - break - if self.inherit_var is not None: - ret_list += self.inherit_var.get_overridden(field_name) - return ret_list - - def check_valid_parent(self): - if self.parent is None: - return False - parent_type = self.parent.get_type() - return parent_type != CLASS_TYPE_ID and parent_type < BLOCK_TYPE_ID - - def get_diagnostics(self): - errors = [] - for in_child in self.in_children: - if (not self.abstract) and ( - in_child.keywords.count(KEYWORD_ID_DICT["deferred"]) > 0 - ): - new_diag = Diagnostic( - self.eline - 1, - f'Deferred procedure "{in_child.name}" not implemented', - severity=1, - ) - new_diag.add_related( - path=in_child.file_ast.path, - line=in_child.sline - 1, - message="Inherited procedure declaration", - ) - errors.append(new_diag) - return errors - - def get_actions(self, sline, eline): - actions = [] - edits = [] - line_number = self.eline - 1 - if (line_number < sline) or (line_number > eline): - return actions - if self.contains_start is None: - edits.append( - { - **range_json(line_number, 0, line_number, 0), - "newText": "CONTAINS\n", - } - ) - # - diagnostics = [] - has_edits = False - file_uri = path_to_uri(self.file_ast.path) - for in_child in self.in_children: - if in_child.keywords.count(KEYWORD_ID_DICT["deferred"]) > 0: - # Get interface - interface_string = in_child.get_interface( - name_replace=in_child.name, - change_strings=( - f"class({in_child.parent.name})", - f"CLASS({self.name})", - ), - ) - if interface_string is None: - continue - interface_path, interface_edits = self.parent.add_subroutine( - interface_string, no_contains=has_edits - ) - if interface_path != self.file_ast.path: - continue - edits.append( - { - **range_json(line_number, 0, line_number, 0), - "newText": " PROCEDURE :: {0} => {0}\n".format(in_child.name), - } - ) - edits += interface_edits - new_diag = Diagnostic( - line_number, - f'Deferred procedure "{in_child.name}" not implemented', - severity=1, - ) - new_diag.add_related( - path=in_child.file_ast.path, - line=in_child.sline - 1, - message="Inherited procedure declaration", - ) - diagnostics.append(new_diag) - has_edits = True - # - if has_edits: - actions = [ - { - "title": "Implement deferred procedures", - "kind": "quickfix", - "edit": {"changes": {file_uri: edits}}, - "diagnostics": diagnostics, - } - ] - return actions - - def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: - keywords = [self.get_desc()] - if self.abstract: - keywords.append("ABSTRACT") - if self.inherit: - keywords.append(f"EXTENDS({self.inherit})") - decl = ", ".join(keywords) - hover = f"{decl} :: {self.name}" - doc_str = self.get_documentation() - return hover, doc_str - - -class Block(Scope): - def __init__(self, file_ast: FortranAST, line_number: int, name: str): - super().__init__(file_ast, line_number, name) - - def get_type(self, no_link=False): - return BLOCK_TYPE_ID - - def get_desc(self): - return "BLOCK" - - def get_children(self, public_only=False): - return copy.copy(self.children) - - def req_named_end(self): - return True - - -class Do(Block): - def __init__(self, file_ast: FortranAST, line_number: int, name: str): - super().__init__(file_ast, line_number, name) - - def get_type(self, no_link=False): - return DO_TYPE_ID - - def get_desc(self): - return "DO" - - -class Where(Block): - def __init__(self, file_ast: FortranAST, line_number: int, name: str): - super().__init__(file_ast, line_number, name) - - def get_type(self, no_link=False): - return WHERE_TYPE_ID - - def get_desc(self): - return "WHERE" - - -class If(Block): - def __init__(self, file_ast: FortranAST, line_number: int, name: str): - super().__init__(file_ast, line_number, name) - - def get_type(self, no_link=False): - return IF_TYPE_ID - - def get_desc(self): - return "IF" - - -class Associate(Block): - def __init__(self, file_ast: FortranAST, line_number: int, name: str): - super().__init__(file_ast, line_number, name) - self.links: list[AssociateMap] = [] # holds the info to associate variables - - def get_type(self, no_link=False): - return ASSOC_TYPE_ID - - def get_desc(self): - return "ASSOCIATE" - - def create_binding_variable( - self, file_ast: FortranAST, line_number: int, bind_name: str, link_name: str - ) -> Variable: - """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 = Variable(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): - # 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) - else: - var_obj = find_in_scope(self, assoc.link_name, obj_tree) - if var_obj is not None: - assoc.var.link_obj = var_obj - - def require_link(self): - return True - - -class Enum(Block): - def __init__(self, file_ast: FortranAST, line_number: int, name: str): - super().__init__(file_ast, line_number, name) - - def get_type(self, no_link=False): - return ENUM_TYPE_ID - - def get_desc(self): - return "ENUM" - - -class Select(Block): - def __init__(self, file_ast: FortranAST, line_number: int, name: str, select_info): - super().__init__(file_ast, line_number, name) - self.select_type = select_info.type - self.binding_name = None - self.bound_var = None - self.binding_type = None - if self.select_type == 2: - binding_split = select_info.binding.split("=>") - if len(binding_split) == 1: - self.bound_var = binding_split[0].strip() - elif len(binding_split) == 2: - self.binding_name = binding_split[0].strip() - self.bound_var = binding_split[1].strip() - elif self.select_type == 3: - self.binding_type = select_info.binding - # Close previous "TYPE IS" region if open - if ( - (file_ast.current_scope is not None) - and (file_ast.current_scope.get_type() == SELECT_TYPE_ID) - and file_ast.current_scope.is_type_region() - ): - file_ast.end_scope(line_number) - - def get_type(self, no_link=False): - return SELECT_TYPE_ID - - def get_desc(self): - return "SELECT" - - def is_type_binding(self): - return self.select_type == 2 - - def is_type_region(self): - return self.select_type in [3, 4] - - def create_binding_variable(self, file_ast, line_number, var_desc, case_type): - if self.parent.get_type() != SELECT_TYPE_ID: - return None - binding_name = None - bound_var = None - if (self.parent is not None) and self.parent.is_type_binding(): - binding_name = self.parent.binding_name - bound_var = self.parent.bound_var - # Check for default case - if (binding_name is not None) and (case_type != 4): - bound_var = None - # Create variable - if binding_name is not None: - return Variable( - file_ast, line_number, binding_name, var_desc, [], link_obj=bound_var - ) - elif bound_var is not None: - return Variable(file_ast, line_number, bound_var, var_desc, []) - return None - - -class Interface(Scope): - def __init__( - self, - file_ast: FortranAST, - line_number: int, - name: str, - abstract: bool = False, - ): - super().__init__(file_ast, line_number, name) - self.mems = [] - self.abstract = abstract - self.external = name.startswith("#GEN_INT") and (not abstract) - - def get_type(self, no_link=False): - return INTERFACE_TYPE_ID - - def get_desc(self): - return "INTERFACE" - - def is_callable(self): - return True - - def is_external_int(self): - return self.external - - def is_abstract(self): - return self.abstract - - def resolve_link(self, obj_tree): - if self.parent is None: - return - self.mems = [] - for member in self.members: - mem_obj = find_in_scope(self.parent, member, obj_tree) - if mem_obj is not None: - self.mems.append(mem_obj) - - def require_link(self): - return True - - -class Variable(FortranObj): - def __init__( - self, - file_ast: FortranAST, - line_number: int, - name: str, - var_desc: str, - keywords: list, - keyword_info: dict = None, - kind: str | None = None, - link_obj=None, - ): - super().__init__() - if keyword_info is None: - keyword_info = {} - self.file_ast: FortranAST = file_ast - self.sline: int = line_number - self.eline: int = line_number - self.name: str = name - self.desc: str = var_desc - self.keywords: list = keywords - self.keyword_info: dict = keyword_info - self.kind: str | None = kind - self.children: list = [] - self.use: list[Use | Import] = [] - self.link_obj = None - self.type_obj = None - self.is_const: bool = False - self.is_external: bool = False - self.param_val: str = None - self.link_name: str = None - 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() - if file_ast.enc_scope_name is not None: - self.FQSN = f"{file_ast.enc_scope_name.lower()}::{self.name.lower()}" - if self.keywords.count(KEYWORD_ID_DICT["public"]) > 0: - self.vis = 1 - if self.keywords.count(KEYWORD_ID_DICT["private"]) > 0: - self.vis = -1 - if self.keywords.count(KEYWORD_ID_DICT["parameter"]) > 0: - self.is_const = True - if ( - self.keywords.count(KEYWORD_ID_DICT["external"]) > 0 - or self.desc.lower() == "external" - ): - self.is_external = True - - def update_fqsn(self, enc_scope=None): - if enc_scope is not None: - self.FQSN = f"{enc_scope.lower()}::{self.name.lower()}" - else: - self.FQSN = self.name.lower() - for child in self.children: - child.update_fqsn(self.FQSN) - - def resolve_link(self, obj_tree): - self.link_obj = None - if self.link_name is None: - return - if self.parent is not None: - link_obj = find_in_scope(self.parent, self.link_name, obj_tree) - if link_obj is not None: - self.link_obj = link_obj - - def require_link(self): - return self.link_name is not None - - def get_type(self, no_link=False): - if (not no_link) and (self.link_obj is not None): - return self.link_obj.get_type() - # Normal variable - return VAR_TYPE_ID - - 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 - return self.desc + self.kind if self.kind else 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.get_desc(no_link=True)) - if type_name is not None: - search_scope = self.parent - if search_scope.get_type() == CLASS_TYPE_ID: - search_scope = search_scope.parent - if search_scope is not None: - type_name = type_name.strip().lower() - type_obj = find_in_scope(search_scope, type_name, obj_tree) - if type_obj is not None: - 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"]) - self.keyword_info["dimension"] = dim_str - self.keywords.sort() - - def get_snippet(self, name_replace=None, drop_arg=-1): - name = name_replace if name_replace is not None else self.name - if self.link_obj is not None: - return self.link_obj.get_snippet(name, drop_arg) - # Normal variable - return None, None - - def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: - doc_str = self.get_documentation() - # In associated blocks we need to fetch the desc and keywords of the - # linked object - hover_str = ", ".join([self.get_desc()] + self.get_keywords()) - # If this is not a preprocessor variable, we can append the variable name - if not hover_str.startswith("#"): - hover_str += f" :: {self.name}" - if self.is_parameter() and self.param_val: - hover_str += f" = {self.param_val}" - return hover_str, doc_str - - def get_hover_md(self, long=False, drop_arg=-1): - return fortran_md(*self.get_hover(long, drop_arg)) - - 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): - return self.keywords.count(KEYWORD_ID_DICT["optional"]) > 0 - - def is_callable(self): - return self.callable - - def is_parameter(self): - return self.is_const - - def set_parameter_val(self, val: str): - self.param_val = val - - def set_external_attr(self): - self.keywords.append(KEYWORD_ID_DICT["external"]) - self.is_external = True - - def check_definition(self, obj_tree, known_types=None, interface=False): - if known_types is None: - known_types = {} - # Check for type definition in scope - 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": - return None, known_types - 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, - ) - if type_def is None: - self._check_definition_type_def( - obj_tree, desc_obj_name, known_types, type_match - ) - else: - known_types[desc_obj_name] = (0, type_def) - type_info = known_types[desc_obj_name] - if type_info is not None and type_info[0] == 1: - if interface: - out_diag = Diagnostic( - self.sline - 1, - message=f'Object "{desc_obj_name}" not imported in interface', - severity=1, - find_word=desc_obj_name, - ) - else: - out_diag = Diagnostic( - self.sline - 1, - message=f'Object "{desc_obj_name}" not found in scope', - severity=1, - find_word=desc_obj_name, - ) - type_def = type_info[1] - out_diag.add_related( - path=type_def.file_ast.path, - line=type_def.sline - 1, - message="Possible object", - ) - return out_diag, known_types - return None, known_types - - def _check_definition_type_def( - self, obj_tree, desc_obj_name, known_types, type_match - ): - type_defs = find_in_workspace( - 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() - filter_id = VAR_TYPE_ID - if var_type in ["class", "type"]: - filter_id = CLASS_TYPE_ID - for type_def in type_defs: - if type_def.get_type() == filter_id: - known_types[desc_obj_name] = (1, type_def) - break - - -class Method(Variable): # i.e. TypeBound procedure - def __init__( - self, - file_ast: FortranAST, - line_number: int, - name: str, - var_desc: str, - keywords: list, - keyword_info: dict, - proc_ptr: str = "", # procedure pointer e.g. `foo` in `procedure(foo)` - link_obj=None, - ): - super().__init__( - 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(self.get_desc(True).lower()) - - def set_parent(self, parent_obj): - self.parent = parent_obj - if self.parent.get_type() == CLASS_TYPE_ID: - if self.keywords.count(KEYWORD_ID_DICT["nopass"]) == 0: - self.drop_arg = 0 - if ( - (self.parent.contains_start is not None) - and (self.sline > self.parent.contains_start) - and (self.link_name is None) - ): - self.link_name = self.name.lower() - - def get_snippet(self, name_replace=None, drop_arg=-1): - if self.link_obj is not None: - name = self.name if name_replace is None else name_replace - return self.link_obj.get_snippet(name, self.drop_arg) - return None, None - - def get_type(self, no_link=False): - if (not no_link) and (self.link_obj is not None): - return self.link_obj.get_type() - # Generic - return METH_TYPE_ID - - def get_documentation(self): - if (self.link_obj is not None) and (self.doc_str is None): - return self.link_obj.get_documentation() - return self.doc_str - - def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: - docs = self.get_documentation() - # Long hover message - if self.link_obj is None: - sub_sig, _ = self.get_snippet() - hover_str = f"{self.get_desc()} {sub_sig}" - else: - link_msg, link_docs = self.link_obj.get_hover( - long=True, drop_arg=self.drop_arg - ) - # Replace the name of the linked object with the name of this object - hover_str = link_msg.replace(self.link_obj.name, self.name, 1) - if isinstance(link_docs, str): - # Get just the docstring of the link, if any, no args - link_doc_top = self.link_obj.get_documentation() - # Replace the linked objects topmost documentation with the - # documentation of the procedure pointer if one is present - if link_doc_top is not None: - docs = link_docs.replace(link_doc_top, docs, 1) - # If no top docstring is present at the linked object but there - # are docstrings for the arguments, add them to the end of the - # documentation for this object - elif link_docs: - if docs is None: - docs = "" - docs += " \n" + link_docs - return hover_str, docs - - def get_signature(self, drop_arg=-1): - if self.link_obj is not None: - call_sig, _ = self.get_snippet() - _, _, arg_sigs = self.link_obj.get_signature(self.drop_arg) - return call_sig, self.get_documentation(), arg_sigs - return None, None, None - - def get_interface(self, name_replace=None, drop_arg=-1, change_strings=None): - if self.link_obj is not None: - return self.link_obj.get_interface( - name_replace, self.drop_arg, change_strings - ) - return None - - def resolve_link(self, obj_tree): - if self.link_name is None: - return - if self.parent is not None: - if self.parent.get_type() == CLASS_TYPE_ID: - link_obj = find_in_scope(self.parent.parent, self.link_name, obj_tree) - else: - link_obj = find_in_scope(self.parent, self.link_name, obj_tree) - if link_obj is not None: - self.link_obj = link_obj - if self.pass_name is not None: - self.pass_name = self.pass_name.lower() - for i, arg in enumerate(link_obj.args_snip.split(",")): - if arg.lower() == self.pass_name: - self.drop_arg = i - break - - def is_callable(self): - return True - - def check_definition(self, obj_tree, known_types=None, interface=False): - if known_types is None: - known_types = {} - return None, known_types - - -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.global_dict: dict = {} - self.scope_list: list = [] - self.variable_list: list = [] - self.public_list: list = [] - self.private_list: list = [] - self.scope_stack: list = [] - self.end_stack: list = [] - self.pp_if: list = [] - self.include_statements: list = [] - self.end_errors: list = [] - self.parse_errors: list = [] - self.inherit_objs: list = [] - self.linkable_objs: list = [] - self.external_objs: list = [] - 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.last_obj = None - self.pending_doc: str = None - - def create_none_scope(self): - """Create empty scope to hold non-module contained items""" - if self.none_scope is not None: - raise ValueError - self.none_scope = Program(self, 1, "main") - self.add_scope( - self.none_scope, re.compile(r"[ ]*END[ ]*PROGRAM", re.I), exportable=False - ) - - def get_enc_scope_name(self): - """Get current enclosing scope name""" - return None if self.current_scope is None else self.current_scope.FQSN - - def add_scope( - self, - new_scope: Scope, - END_SCOPE_REGEX: Pattern[str], - exportable: bool = True, - req_container: bool = False, - ): - self.scope_list.append(new_scope) - if new_scope.require_inherit(): - self.inherit_objs.append(new_scope) - if new_scope.require_link(): - self.linkable_objs.append(new_scope) - if self.current_scope is None: - if req_container: - self.create_none_scope() - new_scope.FQSN = f"{self.none_scope.FQSN}::{new_scope.name.lower()}" - self.current_scope.add_child(new_scope) - self.scope_stack.append(self.current_scope) - elif exportable: - self.global_dict[new_scope.FQSN] = new_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) - self.current_scope = new_scope - 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: - self.last_obj.add_doc(self.pending_doc) - self.pending_doc = None - - def end_scope(self, line_number: int, check: bool = True): - if ( - (self.current_scope is None) or (self.current_scope is self.none_scope) - ) and check: - self.end_errors.append([-1, line_number]) - return - self.current_scope.end(line_number) - if len(self.scope_stack) > 0: - self.current_scope = self.scope_stack.pop() - else: - self.current_scope = None - if len(self.end_stack) > 0: - self.END_SCOPE_REGEX = self.end_stack.pop() - else: - self.END_SCOPE_REGEX = None - self.enc_scope_name = self.get_enc_scope_name() - - def add_variable(self, new_var: Variable): - if self.current_scope is None: - self.create_none_scope() - new_var.FQSN = f"{self.none_scope.FQSN}::{new_var.name.lower()}" - self.current_scope.add_child(new_var) - self.variable_list.append(new_var) - if new_var.is_external: - self.external_objs.append(new_var) - if new_var.require_link(): - self.linkable_objs.append(new_var) - self.last_obj = new_var - if self.pending_doc is not None: - self.last_obj.add_doc(self.pending_doc) - self.pending_doc = None - - def add_int_member(self, key): - self.current_scope.add_member(key) - - def add_private(self, name: str): - self.private_list.append(f"{self.enc_scope_name}::{name}") - - def add_public(self, name: str): - self.public_list.append(f"{self.enc_scope_name}::{name}") - - def add_use(self, use_mod: Use | Import): - if self.current_scope is None: - self.create_none_scope() - self.current_scope.add_use(use_mod) - - def add_include(self, path: str, line_number: int): - self.include_statements.append(IncludeInfo(line_number, path, None, [])) - - def add_doc(self, doc_string: str, forward: bool = False): - if not doc_string: - return - if forward: - self.pending_doc = doc_string - elif self.last_obj is not None: - self.last_obj.add_doc(doc_string) - - def add_error(self, msg: str, sev: int, ln: int, sch: int, ech: int = None): - """Add a Diagnostic error, encountered during parsing, for a range - in the document. - - Parameters - ---------- - msg : str - Error message - sev : int - Severity, Error, Warning, Notification - ln : int - Line number - sch : int - Start character - ech : int - End character - """ - # Convert from Editor line numbers 1-base index to LSP index which is 0-based - self.parse_errors.append(diagnostic_json(ln - 1, sch, ln - 1, ech, msg, sev)) - - def start_ppif(self, line_number: int): - self.pp_if.append([line_number - 1, -1]) - - def end_ppif(self, line_number): - if len(self.pp_if) > 0: - self.pp_if[-1][1] = line_number - 1 - - def get_scopes(self, line_number: int = None): - """Get a list of all the scopes present in the line number provided. - - Parameters - ---------- - line_number : int, optional - Document line number, if None return all document scopes, by default None - - Returns - ------- - Variable,Type,Function,Subroutine,Module,Program,Interface,BlockData - A list of scopes - """ - if line_number is 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) == Interface: - for use_stmnt in scope.use: - if type(use_stmnt) != 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] - - 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) - ): - curr_scope = scope - scope_sline = scope.sline - if (curr_scope is None) and (self.none_scope is not None): - return self.none_scope - 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: - 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 - - def resolve_includes(self, workspace, path: str = 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)) - if path and path != file_path: - continue - parent_scope = self.get_inner_scope(inc.line_number) - added_entities = inc.scope_objs - if file_path in workspace: - include_file = workspace[file_path] - include_ast = include_file.ast - inc.file = include_file - if include_ast.none_scope: - if include_ast.inc_scope is None: - include_ast.inc_scope = include_ast.none_scope - # Remove old objects - for obj in added_entities: - parent_scope.children.remove(obj) - added_entities = [] - for child in include_ast.inc_scope.children: - added_entities.append(child) - parent_scope.add_child(child) - child.update_fqsn(parent_scope.FQSN) - include_ast.none_scope = parent_scope - inc.scope_objs = added_entities - - def resolve_links(self, obj_tree, link_version): - for inherit_obj in self.inherit_objs: - inherit_obj.resolve_inherit(obj_tree, inherit_version=link_version) - for linkable_obj in self.linkable_objs: - linkable_obj.resolve_link(obj_tree) - - def close_file(self, line_number: int): - # Close open scopes - while self.current_scope is not None: - self.end_scope(line_number, check=False) - # Close and delist none_scope - if self.none_scope is not None: - self.none_scope.end(line_number) - self.scope_list.remove(self.none_scope) - # Tasks to be done when file parsing is finished - for private_name in self.private_list: - obj = self.get_object(private_name) - if obj is not None: - obj.set_visibility(-1) - for public_name in self.public_list: - obj = self.get_object(public_name) - if obj is not None: - obj.set_visibility(1) - - def check_file(self, obj_tree): - errors = [] - tmp_list = self.scope_list[:] # shallow copy - if self.none_scope is not None: - tmp_list += [self.none_scope] - for error in self.end_errors: - if error[0] >= 0: - message = f"Unexpected end of scope at line {error[0]}" - else: - message = "Unexpected end statement: No open scopes" - errors.append(Diagnostic(error[1] - 1, message=message, severity=1)) - for scope in tmp_list: - if not scope.check_valid_parent(): - errors.append( - Diagnostic( - scope.sline - 1, - message=f'Invalid parent for "{scope.get_desc()}" declaration', - severity=1, - ) - ) - errors += scope.check_use(obj_tree) - errors += scope.check_definitions(obj_tree) - errors += scope.get_diagnostics() - return errors, self.parse_errors diff --git a/fortls/parsers/internal/associate.py b/fortls/parsers/internal/associate.py new file mode 100644 index 00000000..758e38ca --- /dev/null +++ b/fortls/parsers/internal/associate.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from fortls.constants import ASSOC_TYPE_ID +from fortls.helper_functions import get_var_stack + +from .block import Block +from .utilities import climb_type_tree, find_in_scope +from .variable import Variable + +if TYPE_CHECKING: + from .ast import FortranAST + + +@dataclass +class AssociateMap: + var: Variable + bind_name: str + link_name: str + + +class Associate(Block): + def __init__(self, file_ast: FortranAST, line_number: int, name: str): + super().__init__(file_ast, line_number, name) + self.links: list[AssociateMap] = [] # holds the info to associate variables + + def get_type(self, no_link=False): + return ASSOC_TYPE_ID + + def get_desc(self): + return "ASSOCIATE" + + def create_binding_variable( + self, file_ast: FortranAST, line_number: int, bind_name: str, link_name: str + ) -> Variable: + """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 = Variable(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): + # 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) + else: + var_obj = find_in_scope(self, assoc.link_name, obj_tree) + if var_obj is not None: + assoc.var.link_obj = var_obj + + def require_link(self): + return True diff --git a/fortls/parsers/internal/ast.py b/fortls/parsers/internal/ast.py new file mode 100644 index 00000000..d34d7a49 --- /dev/null +++ b/fortls/parsers/internal/ast.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +import os +import re +from re import Pattern + +from fortls.ftypes import IncludeInfo +from fortls.json_templates import diagnostic_json + +from .diagnostics import Diagnostic +from .imports import Import, ImportTypes +from .interface import Interface +from .program import Program +from .scope import Scope +from .use import Use +from .variable import Variable + + +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.global_dict: dict = {} + self.scope_list: list = [] + self.variable_list: list = [] + self.public_list: list = [] + self.private_list: list = [] + self.scope_stack: list = [] + self.end_stack: list = [] + self.pp_if: list = [] + self.include_statements: list = [] + self.end_errors: list = [] + self.parse_errors: list = [] + self.inherit_objs: list = [] + self.linkable_objs: list = [] + self.external_objs: list = [] + 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.last_obj = None + self.pending_doc: str = None + + def create_none_scope(self): + """Create empty scope to hold non-module contained items""" + if self.none_scope is not None: + raise ValueError + self.none_scope = Program(self, 1, "main") + self.add_scope( + self.none_scope, re.compile(r"[ ]*END[ ]*PROGRAM", re.I), exportable=False + ) + + def get_enc_scope_name(self): + """Get current enclosing scope name""" + return None if self.current_scope is None else self.current_scope.FQSN + + def add_scope( + self, + new_scope: Scope, + END_SCOPE_REGEX: Pattern[str], + exportable: bool = True, + req_container: bool = False, + ): + self.scope_list.append(new_scope) + if new_scope.require_inherit(): + self.inherit_objs.append(new_scope) + if new_scope.require_link(): + self.linkable_objs.append(new_scope) + if self.current_scope is None: + if req_container: + self.create_none_scope() + new_scope.FQSN = f"{self.none_scope.FQSN}::{new_scope.name.lower()}" + self.current_scope.add_child(new_scope) + self.scope_stack.append(self.current_scope) + elif exportable: + self.global_dict[new_scope.FQSN] = new_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) + self.current_scope = new_scope + 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: + self.last_obj.add_doc(self.pending_doc) + self.pending_doc = None + + def end_scope(self, line_number: int, check: bool = True): + if ( + (self.current_scope is None) or (self.current_scope is self.none_scope) + ) and check: + self.end_errors.append([-1, line_number]) + return + self.current_scope.end(line_number) + if len(self.scope_stack) > 0: + self.current_scope = self.scope_stack.pop() + else: + self.current_scope = None + if len(self.end_stack) > 0: + self.END_SCOPE_REGEX = self.end_stack.pop() + else: + self.END_SCOPE_REGEX = None + self.enc_scope_name = self.get_enc_scope_name() + + def add_variable(self, new_var: Variable): + if self.current_scope is None: + self.create_none_scope() + new_var.FQSN = f"{self.none_scope.FQSN}::{new_var.name.lower()}" + self.current_scope.add_child(new_var) + self.variable_list.append(new_var) + if new_var.is_external: + self.external_objs.append(new_var) + if new_var.require_link(): + self.linkable_objs.append(new_var) + self.last_obj = new_var + if self.pending_doc is not None: + self.last_obj.add_doc(self.pending_doc) + self.pending_doc = None + + def add_int_member(self, key): + self.current_scope.add_member(key) + + def add_private(self, name: str): + self.private_list.append(f"{self.enc_scope_name}::{name}") + + def add_public(self, name: str): + self.public_list.append(f"{self.enc_scope_name}::{name}") + + def add_use(self, use_mod: Use | Import): + if self.current_scope is None: + self.create_none_scope() + self.current_scope.add_use(use_mod) + + def add_include(self, path: str, line_number: int): + self.include_statements.append(IncludeInfo(line_number, path, None, [])) + + def add_doc(self, doc_string: str, forward: bool = False): + if not doc_string: + return + if forward: + self.pending_doc = doc_string + elif self.last_obj is not None: + self.last_obj.add_doc(doc_string) + + def add_error(self, msg: str, sev: int, ln: int, sch: int, ech: int = None): + """Add a Diagnostic error, encountered during parsing, for a range + in the document. + + Parameters + ---------- + msg : str + Error message + sev : int + Severity, Error, Warning, Notification + ln : int + Line number + sch : int + Start character + ech : int + End character + """ + # Convert from Editor line numbers 1-base index to LSP index which is 0-based + self.parse_errors.append(diagnostic_json(ln - 1, sch, ln - 1, ech, msg, sev)) + + def start_ppif(self, line_number: int): + self.pp_if.append([line_number - 1, -1]) + + def end_ppif(self, line_number): + if len(self.pp_if) > 0: + self.pp_if[-1][1] = line_number - 1 + + def get_scopes(self, line_number: int = None): + """Get a list of all the scopes present in the line number provided. + + Parameters + ---------- + line_number : int, optional + Document line number, if None return all document scopes, by default None + + Returns + ------- + Variable,Type,Function,Subroutine,Module,Program,Interface,BlockData + A list of scopes + """ + if line_number is 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) == Interface: + for use_stmnt in scope.use: + if type(use_stmnt) != 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] + + 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) + ): + curr_scope = scope + scope_sline = scope.sline + if (curr_scope is None) and (self.none_scope is not None): + return self.none_scope + 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: + 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 + + def resolve_includes(self, workspace, path: str = 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)) + if path and path != file_path: + continue + parent_scope = self.get_inner_scope(inc.line_number) + added_entities = inc.scope_objs + if file_path in workspace: + include_file = workspace[file_path] + include_ast = include_file.ast + inc.file = include_file + if include_ast.none_scope: + if include_ast.inc_scope is None: + include_ast.inc_scope = include_ast.none_scope + # Remove old objects + for obj in added_entities: + parent_scope.children.remove(obj) + added_entities = [] + for child in include_ast.inc_scope.children: + added_entities.append(child) + parent_scope.add_child(child) + child.update_fqsn(parent_scope.FQSN) + include_ast.none_scope = parent_scope + inc.scope_objs = added_entities + + def resolve_links(self, obj_tree, link_version): + for inherit_obj in self.inherit_objs: + inherit_obj.resolve_inherit(obj_tree, inherit_version=link_version) + for linkable_obj in self.linkable_objs: + linkable_obj.resolve_link(obj_tree) + + def close_file(self, line_number: int): + # Close open scopes + while self.current_scope is not None: + self.end_scope(line_number, check=False) + # Close and delist none_scope + if self.none_scope is not None: + self.none_scope.end(line_number) + self.scope_list.remove(self.none_scope) + # Tasks to be done when file parsing is finished + for private_name in self.private_list: + obj = self.get_object(private_name) + if obj is not None: + obj.set_visibility(-1) + for public_name in self.public_list: + obj = self.get_object(public_name) + if obj is not None: + obj.set_visibility(1) + + def check_file(self, obj_tree): + errors = [] + tmp_list = self.scope_list[:] # shallow copy + if self.none_scope is not None: + tmp_list += [self.none_scope] + for error in self.end_errors: + if error[0] >= 0: + message = f"Unexpected end of scope at line {error[0]}" + else: + message = "Unexpected end statement: No open scopes" + errors.append(Diagnostic(error[1] - 1, message=message, severity=1)) + for scope in tmp_list: + if not scope.check_valid_parent(): + errors.append( + Diagnostic( + scope.sline - 1, + message=f'Invalid parent for "{scope.get_desc()}" declaration', + severity=1, + ) + ) + errors += scope.check_use(obj_tree) + errors += scope.check_definitions(obj_tree) + errors += scope.get_diagnostics() + return errors, self.parse_errors diff --git a/fortls/parsers/internal/base.py b/fortls/parsers/internal/base.py index 1646a16e..9c160f18 100644 --- a/fortls/parsers/internal/base.py +++ b/fortls/parsers/internal/base.py @@ -1,7 +1,7 @@ from __future__ import annotations +from fortls.constants import BASE_TYPE_ID from fortls.helper_functions import fortran_md -from fortls.objects import BASE_TYPE_ID # Fortran object classes diff --git a/fortls/parsers/internal/block.py b/fortls/parsers/internal/block.py new file mode 100644 index 00000000..a85ce210 --- /dev/null +++ b/fortls/parsers/internal/block.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +from fortls.constants import BLOCK_TYPE_ID + +from .scope import Scope + +if TYPE_CHECKING: + from .ast import FortranAST + + +class Block(Scope): + def __init__(self, file_ast: FortranAST, line_number: int, name: str): + super().__init__(file_ast, line_number, name) + + def get_type(self, no_link=False): + return BLOCK_TYPE_ID + + def get_desc(self): + return "BLOCK" + + def get_children(self, public_only=False): + return copy.copy(self.children) + + def req_named_end(self): + return True diff --git a/fortls/parsers/internal/do.py b/fortls/parsers/internal/do.py new file mode 100644 index 00000000..52d036ea --- /dev/null +++ b/fortls/parsers/internal/do.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fortls.constants import DO_TYPE_ID + +from .block import Block + +if TYPE_CHECKING: + from .ast import FortranAST + + +class Do(Block): + def __init__(self, file_ast: FortranAST, line_number: int, name: str): + super().__init__(file_ast, line_number, name) + + def get_type(self, no_link=False): + return DO_TYPE_ID + + def get_desc(self): + return "DO" diff --git a/fortls/parsers/internal/enum.py b/fortls/parsers/internal/enum.py new file mode 100644 index 00000000..9bc1b174 --- /dev/null +++ b/fortls/parsers/internal/enum.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fortls.constants import ENUM_TYPE_ID + +from .block import Block + +if TYPE_CHECKING: + from .ast import FortranAST + + +class Enum(Block): + def __init__(self, file_ast: FortranAST, line_number: int, name: str): + super().__init__(file_ast, line_number, name) + + def get_type(self, no_link=False): + return ENUM_TYPE_ID + + def get_desc(self): + return "ENUM" diff --git a/fortls/parsers/internal/function.py b/fortls/parsers/internal/function.py new file mode 100644 index 00000000..d7feec68 --- /dev/null +++ b/fortls/parsers/internal/function.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fortls.constants import FUNCTION_TYPE_ID +from fortls.helper_functions import get_keywords + +from .subroutine import Subroutine + +if TYPE_CHECKING: + from .ast import FortranAST + from .variable import Variable + + +class Function(Subroutine): + def __init__( + self, + file_ast: FortranAST, + line_number: int, + name: str, + args: str = "", + mod_flag: bool = False, + keywords: list = None, + keyword_info: dict = None, + result_type: str = None, + result_name: str = None, + ): + super().__init__(file_ast, line_number, name, args, mod_flag, keywords) + self.args: str = args.replace(" ", "").lower() + self.args_snip: str = self.args + self.arg_objs: list = [] + self.in_children: list = [] + self.missing_args: list = [] + self.mod_scope: bool = mod_flag + self.result_name: str = result_name + self.result_type: str = result_type + self.result_obj: Variable = None + self.keyword_info: dict = keyword_info + # Set the implicit result() name to be the function name + if self.result_name is None: + self.result_name = self.name + # Used in Associated blocks + if self.keyword_info is None: + self.keyword_info = {} + + def copy_interface(self, copy_source: Function): + # Call the parent class method + child_names = super().copy_interface(copy_source) + # Return specific options + self.result_name = copy_source.result_name + self.result_type = copy_source.result_type + self.result_obj = copy_source.result_obj + if ( + copy_source.result_obj is not None + and copy_source.result_obj.name.lower() not in child_names + ): + self.in_children.append(copy_source.result_obj) + + def resolve_link(self, obj_tree): + self.resolve_arg_link(obj_tree) + result_var_lower = self.result_name.lower() + for child in self.children: + if child.name.lower() == result_var_lower: + self.result_obj = child + # Update result value and type + self.result_name = child.name + self.result_type = child.get_desc() + + def get_type(self, no_link=False): + return FUNCTION_TYPE_ID + + def get_desc(self): + token = "FUNCTION" + return f"{self.result_type} {token}" if self.result_type else token + + def is_callable(self): + return False + + def get_hover(self, long: bool = False, drop_arg: int = -1) -> tuple[str, str]: + """Construct the hover message for a FUNCTION. + Two forms are produced here the `long` i.e. the normal for hover requests + + [MODIFIERS] FUNCTION NAME([ARGS]) RESULT(RESULT_VAR) + TYPE, [ARG_MODIFIERS] :: [ARGS] + TYPE, [RESULT_MODIFIERS] :: RESULT_VAR + + note: intrinsic functions will display slightly different, + `RESULT_VAR` and its `TYPE` might not always be present + + short form, used when functions are arguments in functions and subroutines: + + FUNCTION NAME([ARGS]) :: ARG_LIST_NAME + + Parameters + ---------- + long : bool, optional + toggle between long and short hover results, by default False + drop_arg : int, optional + Ignore argument at position `drop_arg` in the argument list, by default -1 + + Returns + ------- + tuple[str, bool] + String representative of the hover message and the `long` flag used + """ + fun_sig, _ = self.get_snippet(drop_arg=drop_arg) + # short hover messages do not include the result() + fun_sig += f" RESULT({self.result_name})" if long else "" + keyword_list = get_keywords(self.keywords) + keyword_list.append("FUNCTION") + + hover_array = [f"{' '.join(keyword_list)} {fun_sig}"] + hover_array, docs = self.get_docs_full(hover_array, long, drop_arg) + # Only append the return value if using long form + if self.result_obj and long: + # Parse the documentation from the result variable + arg_doc, doc_str = self.result_obj.get_hover() + if doc_str is not None: + docs.append(f"\n**Return:** \n`{self.result_obj.name}`{doc_str}") + hover_array.append(arg_doc) + # intrinsic functions, where the return type is missing but can be inferred + elif self.result_type and long: + # prepend type to function signature + hover_array[0] = f"{self.result_type} {hover_array[0]}" + return "\n ".join(hover_array), " \n".join(docs) + + # TODO: fix this + def get_interface(self, name_replace=None, drop_arg=-1, change_strings=None): + fun_sig, _ = self.get_snippet(name_replace=name_replace) + fun_sig += f" RESULT({self.result_name})" + # XXX: + keyword_list = [] + if self.result_type: + keyword_list.append(self.result_type) + keyword_list += get_keywords(self.keywords) + keyword_list.append("FUNCTION ") + + interface_array = self.get_interface_array( + keyword_list, fun_sig, drop_arg, change_strings + ) + if self.result_obj is not None: + arg_doc, docs = self.result_obj.get_hover() + interface_array.append(f"{arg_doc} :: {self.result_obj.name}") + name = name_replace if name_replace is not None else self.name + interface_array.append(f"END FUNCTION {name}") + return "\n".join(interface_array) diff --git a/fortls/parsers/internal/if_block.py b/fortls/parsers/internal/if_block.py new file mode 100644 index 00000000..788e7eb6 --- /dev/null +++ b/fortls/parsers/internal/if_block.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fortls.constants import IF_TYPE_ID + +from .block import Block + +if TYPE_CHECKING: + from .ast import FortranAST + + +class If(Block): + def __init__(self, file_ast: FortranAST, line_number: int, name: str): + super().__init__(file_ast, line_number, name) + + def get_type(self, no_link=False): + return IF_TYPE_ID + + def get_desc(self): + return "IF" diff --git a/fortls/parsers/internal/imports.py b/fortls/parsers/internal/imports.py new file mode 100644 index 00000000..5ade996c --- /dev/null +++ b/fortls/parsers/internal/imports.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .use import Use + +if TYPE_CHECKING: + from .module import Module + from .scope import Scope + + +class ImportTypes: + DEFAULT = -1 + NONE = 0 + ALL = 1 + ONLY = 2 + + +class Import(Use): + """AST node for IMPORT statement""" + + def __init__( + self, + name: str, + import_type: ImportTypes = ImportTypes.DEFAULT, + only_list: set[str] = None, + rename_map: dict[str, str] = None, + line_number: int = 0, + ): + if only_list is None: + only_list = set() + if rename_map is None: + rename_map = {} + super().__init__(name, only_list, rename_map, line_number) + self.import_type = import_type + self._scope: Scope | Module | None = None + + @property + def scope(self): + """Parent scope of IMPORT statement i.e. parent of the interface""" + return self._scope + + @scope.setter + def scope(self, scope: Scope): + self._scope = scope diff --git a/fortls/parsers/internal/include.py b/fortls/parsers/internal/include.py new file mode 100644 index 00000000..a8002314 --- /dev/null +++ b/fortls/parsers/internal/include.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .scope import Scope + + +class Include(Scope): + def get_desc(self): + return "INCLUDE" diff --git a/fortls/parsers/internal/interface.py b/fortls/parsers/internal/interface.py new file mode 100644 index 00000000..36325a2a --- /dev/null +++ b/fortls/parsers/internal/interface.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fortls.constants import INTERFACE_TYPE_ID + +from .scope import Scope +from .utilities import find_in_scope + +if TYPE_CHECKING: + from .ast import FortranAST + + +class Interface(Scope): + def __init__( + self, + file_ast: FortranAST, + line_number: int, + name: str, + abstract: bool = False, + ): + super().__init__(file_ast, line_number, name) + self.mems = [] + self.abstract = abstract + self.external = name.startswith("#GEN_INT") and (not abstract) + + def get_type(self, no_link=False): + return INTERFACE_TYPE_ID + + def get_desc(self): + return "INTERFACE" + + def is_callable(self): + return True + + def is_external_int(self): + return self.external + + def is_abstract(self): + return self.abstract + + def resolve_link(self, obj_tree): + if self.parent is None: + return + self.mems = [] + for member in self.members: + mem_obj = find_in_scope(self.parent, member, obj_tree) + if mem_obj is not None: + self.mems.append(mem_obj) + + def require_link(self): + return True diff --git a/fortls/intrinsic.modules.json b/fortls/parsers/internal/intrinsic.modules.json similarity index 100% rename from fortls/intrinsic.modules.json rename to fortls/parsers/internal/intrinsic.modules.json diff --git a/fortls/intrinsic.procedures.json b/fortls/parsers/internal/intrinsic.procedures.json similarity index 100% rename from fortls/intrinsic.procedures.json rename to fortls/parsers/internal/intrinsic.procedures.json diff --git a/fortls/intrinsic.procedures.markdown.json b/fortls/parsers/internal/intrinsic.procedures.markdown.json similarity index 100% rename from fortls/intrinsic.procedures.markdown.json rename to fortls/parsers/internal/intrinsic.procedures.markdown.json diff --git a/fortls/intrinsics.py b/fortls/parsers/internal/intrinsics.py similarity index 97% rename from fortls/intrinsics.py rename to fortls/parsers/internal/intrinsics.py index 701e3724..dfe02430 100644 --- a/fortls/intrinsics.py +++ b/fortls/parsers/internal/intrinsics.py @@ -6,16 +6,15 @@ import pathlib from fortls.helper_functions import fortran_md, get_placeholders, map_keywords -from fortls.objects import ( - FortranAST, - FortranObj, - Function, - Module, - Subroutine, - Type, - Use, - Variable, -) + +from .ast import FortranAST +from .base import FortranObj +from .function import Function +from .module import Module +from .subroutine import Subroutine +from .type import Type +from .use import Use +from .variable import Variable intrinsic_ast = FortranAST() lowercase_intrinsics = False diff --git a/fortls/keywords.json b/fortls/parsers/internal/keywords.json similarity index 100% rename from fortls/keywords.json rename to fortls/parsers/internal/keywords.json diff --git a/fortls/parsers/internal/method.py b/fortls/parsers/internal/method.py new file mode 100644 index 00000000..f8383c35 --- /dev/null +++ b/fortls/parsers/internal/method.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fortls.constants import CLASS_TYPE_ID, KEYWORD_ID_DICT, METH_TYPE_ID +from fortls.helper_functions import get_paren_substring + +from .utilities import find_in_scope +from .variable import Variable + +if TYPE_CHECKING: + from .ast import FortranAST + + +class Method(Variable): # i.e. TypeBound procedure + def __init__( + self, + file_ast: FortranAST, + line_number: int, + name: str, + var_desc: str, + keywords: list, + keyword_info: dict, + proc_ptr: str = "", # procedure pointer e.g. `foo` in `procedure(foo)` + link_obj=None, + ): + super().__init__( + 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(self.get_desc(True).lower()) + + def set_parent(self, parent_obj): + self.parent = parent_obj + if self.parent.get_type() == CLASS_TYPE_ID: + if self.keywords.count(KEYWORD_ID_DICT["nopass"]) == 0: + self.drop_arg = 0 + if ( + (self.parent.contains_start is not None) + and (self.sline > self.parent.contains_start) + and (self.link_name is None) + ): + self.link_name = self.name.lower() + + def get_snippet(self, name_replace=None, drop_arg=-1): + if self.link_obj is not None: + name = self.name if name_replace is None else name_replace + return self.link_obj.get_snippet(name, self.drop_arg) + return None, None + + def get_type(self, no_link=False): + if (not no_link) and (self.link_obj is not None): + return self.link_obj.get_type() + # Generic + return METH_TYPE_ID + + def get_documentation(self): + if (self.link_obj is not None) and (self.doc_str is None): + return self.link_obj.get_documentation() + return self.doc_str + + def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: + docs = self.get_documentation() + # Long hover message + if self.link_obj is None: + sub_sig, _ = self.get_snippet() + hover_str = f"{self.get_desc()} {sub_sig}" + else: + link_msg, link_docs = self.link_obj.get_hover( + long=True, drop_arg=self.drop_arg + ) + # Replace the name of the linked object with the name of this object + hover_str = link_msg.replace(self.link_obj.name, self.name, 1) + if isinstance(link_docs, str): + # Get just the docstring of the link, if any, no args + link_doc_top = self.link_obj.get_documentation() + # Replace the linked objects topmost documentation with the + # documentation of the procedure pointer if one is present + if link_doc_top is not None: + docs = link_docs.replace(link_doc_top, docs, 1) + # If no top docstring is present at the linked object but there + # are docstrings for the arguments, add them to the end of the + # documentation for this object + elif link_docs: + if docs is None: + docs = "" + docs += " \n" + link_docs + return hover_str, docs + + def get_signature(self, drop_arg=-1): + if self.link_obj is not None: + call_sig, _ = self.get_snippet() + _, _, arg_sigs = self.link_obj.get_signature(self.drop_arg) + return call_sig, self.get_documentation(), arg_sigs + return None, None, None + + def get_interface(self, name_replace=None, drop_arg=-1, change_strings=None): + if self.link_obj is not None: + return self.link_obj.get_interface( + name_replace, self.drop_arg, change_strings + ) + return None + + def resolve_link(self, obj_tree): + if self.link_name is None: + return + if self.parent is not None: + if self.parent.get_type() == CLASS_TYPE_ID: + link_obj = find_in_scope(self.parent.parent, self.link_name, obj_tree) + else: + link_obj = find_in_scope(self.parent, self.link_name, obj_tree) + if link_obj is not None: + self.link_obj = link_obj + if self.pass_name is not None: + self.pass_name = self.pass_name.lower() + for i, arg in enumerate(link_obj.args_snip.split(",")): + if arg.lower() == self.pass_name: + self.drop_arg = i + break + + def is_callable(self): + return True + + def check_definition(self, obj_tree, known_types=None, interface=False): + if known_types is None: + known_types = {} + return None, known_types diff --git a/fortls/parsers/internal/module.py b/fortls/parsers/internal/module.py new file mode 100644 index 00000000..06994205 --- /dev/null +++ b/fortls/parsers/internal/module.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from fortls.constants import MODULE_TYPE_ID + +from .scope import Scope + + +class Module(Scope): + def get_type(self, no_link=False): + return MODULE_TYPE_ID + + def get_desc(self): + return "MODULE" + + def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: + hover = f"{self.get_desc()} {self.name}" + doc_str = self.get_documentation() + return hover, doc_str + + def check_valid_parent(self) -> bool: + return self.parent is None diff --git a/fortls/parse_fortran.py b/fortls/parsers/internal/parser.py similarity index 99% rename from fortls/parse_fortran.py rename to fortls/parsers/internal/parser.py index b707aea9..ae5cfa1e 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parsers/internal/parser.py @@ -49,29 +49,27 @@ strip_line_label, strip_strings, ) -from fortls.objects import ( - Associate, - Block, - Do, - Enum, - FortranAST, - Function, - If, - Import, - ImportTypes, - Interface, - Method, - Module, - Program, - Scope, - Select, - Submodule, - Subroutine, - Type, - Use, - Variable, - Where, -) + +from .associate import Associate +from .ast import FortranAST +from .block import Block +from .do import Do +from .enum import Enum +from .function import Function +from .if_block import If +from .imports import Import, ImportTypes +from .interface import Interface +from .method import Method +from .module import Module +from .program import Program +from .scope import Scope +from .select import Select +from .submodule import Submodule +from .subroutine import Subroutine +from .type import Type +from .use import Use +from .variable import Variable +from .where import Where def get_line_context(line: str) -> tuple[str, None] | tuple[str, str]: diff --git a/fortls/parsers/internal/program.py b/fortls/parsers/internal/program.py new file mode 100644 index 00000000..665c4399 --- /dev/null +++ b/fortls/parsers/internal/program.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .module import Module + + +class Program(Module): + def get_desc(self): + return "PROGRAM" diff --git a/fortls/parsers/internal/scope.py b/fortls/parsers/internal/scope.py new file mode 100644 index 00000000..a5d83f3a --- /dev/null +++ b/fortls/parsers/internal/scope.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING +from typing import Type as T + +from fortls.constants import ( + BLOCK_TYPE_ID, + FUNCTION_TYPE_ID, + INTERFACE_TYPE_ID, + MODULE_TYPE_ID, + SUBMODULE_TYPE_ID, + SUBROUTINE_TYPE_ID, +) +from fortls.json_templates import range_json + +from .base import FortranObj +from .diagnostics import Diagnostic +from .imports import Import +from .utilities import find_in_scope + +if TYPE_CHECKING: + from .ast import FortranAST + from .use import Use + + +class Scope(FortranObj): + def __init__( + self, + file_ast: FortranAST, + line_number: int, + name: str, + keywords: list = None, + ): + super().__init__() + if keywords is None: + keywords = [] + self.file_ast: FortranAST = file_ast + self.sline: int = line_number + self.eline: int = line_number + self.name: str = name + self.children: list[T[Scope]] = [] + self.members: list = [] + self.use: list[Use | Import] = [] + self.keywords: list = keywords + self.inherit = None + self.parent = None + self.contains_start = None + self.implicit_line = None + self.FQSN: str = self.name.lower() + if file_ast.enc_scope_name is not None: + self.FQSN = f"{file_ast.enc_scope_name.lower()}::{self.name.lower()}" + + def copy_from(self, copy_source: Scope): + # Pass the reference, we don't want shallow copy since that would still + # result into 2 versions of attributes between copy_source and self + for k, v in copy_source.__dict__.items(): + setattr(self, k, v) + + def add_use(self, use_mod: Use | Import): + self.use.append(use_mod) + + def set_inherit(self, inherit_type): + self.inherit = inherit_type + + def set_parent(self, parent_obj): + self.parent = parent_obj + + def set_implicit(self, implicit_flag, line_number): + self.implicit_vars = implicit_flag + self.implicit_line = line_number + + def mark_contains(self, line_number): + if self.contains_start is not None: + raise ValueError + self.contains_start = line_number + + def add_child(self, child): + self.children.append(child) + child.set_parent(self) + + def update_fqsn(self, enc_scope=None): + if enc_scope is not None: + self.FQSN = f"{enc_scope.lower()}::{self.name.lower()}" + else: + self.FQSN = self.name.lower() + for child in self.children: + child.update_fqsn(self.FQSN) + + def add_member(self, member): + self.members.append(member) + + def get_children(self, public_only=False) -> list[T[FortranObj]]: + if not public_only: + return copy.copy(self.children) + pub_children = [] + for child in self.children: + if (child.vis < 0) or ((self.def_vis < 0) and (child.vis <= 0)): + continue + if child.name.startswith("#GEN_INT"): + pub_children.append(child) + continue + pub_children.append(child) + return pub_children + + def check_definitions(self, obj_tree) -> list[Diagnostic]: + """Check for definition errors in scope""" + fqsn_dict: dict[str, int] = {} + errors: list[Diagnostic] = [] + known_types: dict[str, FortranObj] = {} + + for child in self.children: + # Skip masking/double checks for interfaces + if child.get_type() == INTERFACE_TYPE_ID: + continue + # Check other variables in current scope + if child.FQSN in fqsn_dict: + if child.sline < fqsn_dict[child.FQSN]: + fqsn_dict[child.FQSN] = child.sline - 1 + else: + fqsn_dict[child.FQSN] = child.sline - 1 + + contains_line = -1 + if self.get_type() in ( + MODULE_TYPE_ID, + SUBMODULE_TYPE_ID, + SUBROUTINE_TYPE_ID, + FUNCTION_TYPE_ID, + ): + contains_line = ( + self.contains_start if self.contains_start is not None else self.eline + ) + # Detect interface definitions + is_interface = ( + self.parent is not None + and self.parent.get_type() == INTERFACE_TYPE_ID + and not self.is_mod_scope() + ) + + for child in self.children: + if child.name.startswith("#"): + continue + line_number = child.sline - 1 + # Check for type definition in scope + def_error, known_types = child.check_definition( + obj_tree, known_types=known_types, interface=is_interface + ) + if def_error is not None: + errors.append(def_error) + # Detect contains errors + if contains_line >= child.sline and child.get_type(no_link=True) in ( + SUBROUTINE_TYPE_ID, + FUNCTION_TYPE_ID, + ): + new_diag = Diagnostic( + line_number, + message="Subroutine/Function definition before CONTAINS statement", + severity=1, + ) + errors.append(new_diag) + # Skip masking/double checks for interfaces and members + if ( + self.get_type() == INTERFACE_TYPE_ID + or child.get_type() == INTERFACE_TYPE_ID + ): + continue + # Check other variables in current scope + if child.FQSN in fqsn_dict and line_number > fqsn_dict[child.FQSN]: + new_diag = Diagnostic( + line_number, + message=f'Variable "{child.name}" declared twice in scope', + severity=1, + find_word=child.name, + ) + new_diag.add_related( + path=self.file_ast.path, + line=fqsn_dict[child.FQSN], + message="First declaration", + ) + errors.append(new_diag) + continue + # Check for masking from parent scope in subroutines, functions, and blocks + if self.parent is not None and self.get_type() in ( + SUBROUTINE_TYPE_ID, + FUNCTION_TYPE_ID, + BLOCK_TYPE_ID, + ): + parent_var = find_in_scope(self.parent, child.name, obj_tree) + if parent_var is not None: + # Ignore if function return variable + if ( + self.get_type() == FUNCTION_TYPE_ID + and parent_var.FQSN == self.FQSN + ): + continue + + new_diag = Diagnostic( + line_number, + message=( + f'Variable "{child.name}" masks variable in parent scope' + ), + severity=2, + find_word=child.name, + ) + new_diag.add_related( + path=parent_var.file_ast.path, + line=parent_var.sline - 1, + message="First declaration", + ) + errors.append(new_diag) + + return errors + + def check_use(self, obj_tree): + errors = [] + last_use_line = -1 + for use_stmnt in self.use: + last_use_line = max(last_use_line, use_stmnt.line_number) + if type(use_stmnt) == Import: + if (self.parent is None) or ( + self.parent.get_type() != INTERFACE_TYPE_ID + ): + new_diag = Diagnostic( + use_stmnt.line_number - 1, + message="IMPORT statement outside of interface", + severity=1, + ) + errors.append(new_diag) + continue + if use_stmnt.mod_name not in obj_tree: + new_diag = Diagnostic( + use_stmnt.line_number - 1, + message=f'Module "{use_stmnt.mod_name}" not found in project', + severity=3, + find_word=use_stmnt.mod_name, + ) + errors.append(new_diag) + if (self.implicit_line is not None) and (last_use_line >= self.implicit_line): + new_diag = Diagnostic( + self.implicit_line - 1, + message="USE statements after IMPLICIT statement", + severity=1, + find_word="IMPLICIT", + ) + errors.append(new_diag) + return errors + + def add_subroutine(self, interface_string, no_contains=False): + edits = [] + line_number = self.eline - 1 + if (self.contains_start is None) and (not no_contains): + first_sub_line = line_number + for child in self.children: + if child.get_type() in (SUBROUTINE_TYPE_ID, FUNCTION_TYPE_ID): + first_sub_line = min(first_sub_line, child.sline - 1) + edits.append( + { + **range_json(first_sub_line, 0, first_sub_line, 0), + "newText": "CONTAINS\n", + } + ) + edits.append( + { + **range_json(line_number, 0, line_number, 0), + "newText": interface_string + "\n", + } + ) + return self.file_ast.path, edits diff --git a/fortls/parsers/internal/select.py b/fortls/parsers/internal/select.py new file mode 100644 index 00000000..2ea2b2b5 --- /dev/null +++ b/fortls/parsers/internal/select.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fortls.constants import SELECT_TYPE_ID + +from .block import Block +from .variable import Variable + +if TYPE_CHECKING: + from .ast import FortranAST + + +class Select(Block): + def __init__( + self, + file_ast: FortranAST, + line_number: int, + name: str, + select_info, + ): + super().__init__(file_ast, line_number, name) + self.select_type = select_info.type + self.binding_name = None + self.bound_var = None + self.binding_type = None + if self.select_type == 2: + binding_split = select_info.binding.split("=>") + if len(binding_split) == 1: + self.bound_var = binding_split[0].strip() + elif len(binding_split) == 2: + self.binding_name = binding_split[0].strip() + self.bound_var = binding_split[1].strip() + elif self.select_type == 3: + self.binding_type = select_info.binding + # Close previous "TYPE IS" region if open + if ( + (file_ast.current_scope is not None) + and (file_ast.current_scope.get_type() == SELECT_TYPE_ID) + and file_ast.current_scope.is_type_region() + ): + file_ast.end_scope(line_number) + + def get_type(self, no_link=False): + return SELECT_TYPE_ID + + def get_desc(self): + return "SELECT" + + def is_type_binding(self): + return self.select_type == 2 + + def is_type_region(self): + return self.select_type in [3, 4] + + def create_binding_variable(self, file_ast, line_number, var_desc, case_type): + if self.parent.get_type() != SELECT_TYPE_ID: + return None + binding_name = None + bound_var = None + if (self.parent is not None) and self.parent.is_type_binding(): + binding_name = self.parent.binding_name + bound_var = self.parent.bound_var + # Check for default case + if (binding_name is not None) and (case_type != 4): + bound_var = None + # Create variable + if binding_name is not None: + return Variable( + file_ast, line_number, binding_name, var_desc, [], link_obj=bound_var + ) + elif bound_var is not None: + return Variable(file_ast, line_number, bound_var, var_desc, []) + return None diff --git a/fortls/statements.json b/fortls/parsers/internal/statements.json similarity index 100% rename from fortls/statements.json rename to fortls/parsers/internal/statements.json diff --git a/fortls/parsers/internal/submodule.py b/fortls/parsers/internal/submodule.py new file mode 100644 index 00000000..8e44e0c9 --- /dev/null +++ b/fortls/parsers/internal/submodule.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Type as T + +from fortls.constants import ( + BASE_TYPE_ID, + FUNCTION_TYPE_ID, + INTERFACE_TYPE_ID, + SUBMODULE_TYPE_ID, + SUBROUTINE_TYPE_ID, +) + +from .function import Function +from .module import Module +from .subroutine import Subroutine + +if TYPE_CHECKING: + from .ast import FortranAST + from .interface import Interface + from .scope import Scope + + +class Submodule(Module): + def __init__( + self, + file_ast: FortranAST, + line_number: int, + name: str, + ancestor_name: str = "", + ): + super().__init__(file_ast, line_number, name) + self.ancestor_name = ancestor_name + self.ancestor_obj = None + + def get_type(self, no_link=False): + return SUBMODULE_TYPE_ID + + def get_desc(self): + return "SUBMODULE" + + def get_ancestors(self): + if self.ancestor_obj is not None: + great_ancestors = self.ancestor_obj.get_ancestors() + if great_ancestors is not None: + return [self.ancestor_obj] + great_ancestors + return [self.ancestor_obj] + return [] + + def resolve_inherit(self, obj_tree, inherit_version): + if not self.ancestor_name: + return + if self.ancestor_name in obj_tree: + self.ancestor_obj = obj_tree[self.ancestor_name][0] + + def require_inherit(self): + return True + + def resolve_link(self, obj_tree): + def get_ancestor_interfaces( + ancestor_children: list[Scope], + ) -> list[T[Interface]]: + interfaces = [] + for child in ancestor_children: + if child.get_type() != INTERFACE_TYPE_ID: + continue + for interface in child.children: + interface_type = interface.get_type() + if ( + interface_type + in (SUBROUTINE_TYPE_ID, FUNCTION_TYPE_ID, BASE_TYPE_ID) + ) and interface.is_mod_scope(): + interfaces.append(interface) + return interfaces + + def create_child_from_prototype(child: Scope, interface: Interface): + if interface.get_type() == SUBROUTINE_TYPE_ID: + return Subroutine(child.file_ast, child.sline, child.name) + elif interface.get_type() == FUNCTION_TYPE_ID: + return Function(child.file_ast, child.sline, child.name) + else: + raise ValueError(f"Unsupported interface type: {interface.get_type()}") + + def replace_child_in_scope_list(child: Scope, child_old: Scope): + for i, file_scope in enumerate(child.file_ast.scope_list): + if file_scope is child_old: + child.file_ast.scope_list[i] = child + return child + + # Link subroutine/function implementations to prototypes + if self.ancestor_obj is None: + return + + ancestor_interfaces = get_ancestor_interfaces(self.ancestor_obj.children) + # Match interface definitions to implementations + for interface in ancestor_interfaces: + for i, child in enumerate(self.children): + if child.name.lower() != interface.name.lower(): + continue + + if child.get_type() == BASE_TYPE_ID: + child_old = child + child = create_child_from_prototype(child_old, interface) + child.copy_from(child_old) + self.children[i] = child + child = replace_child_in_scope_list(child, child_old) + + if child.get_type() == interface.get_type(): + interface.link_obj = child + interface.resolve_link(obj_tree) + child.copy_interface(interface) + break + + def require_link(self): + return True diff --git a/fortls/parsers/internal/subroutine.py b/fortls/parsers/internal/subroutine.py new file mode 100644 index 00000000..20801cf2 --- /dev/null +++ b/fortls/parsers/internal/subroutine.py @@ -0,0 +1,258 @@ +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +from fortls.constants import ( + BLOCK_TYPE_ID, + CLASS_TYPE_ID, + KEYWORD_ID_DICT, + SUBROUTINE_TYPE_ID, +) +from fortls.helper_functions import fortran_md, get_keywords, get_placeholders + +from .diagnostics import Diagnostic +from .scope import Scope + +if TYPE_CHECKING: + from .ast import FortranAST + from .function import Function + + +class Subroutine(Scope): + def __init__( + self, + file_ast: FortranAST, + line_number: int, + name: str, + args: str = "", + mod_flag: bool = False, + keywords: list = None, + ): + super().__init__(file_ast, line_number, name, keywords) + self.args: str = args.replace(" ", "") + self.args_snip: str = self.args + self.arg_objs: list = [] + self.in_children: list = [] + self.missing_args: list = [] + self.mod_scope: bool = mod_flag + self.link_obj: Subroutine | Function | None = None + + def is_mod_scope(self): + return self.mod_scope + + def is_callable(self): + return True + + def copy_interface(self, copy_source: Subroutine) -> list[str]: + # Copy arguments + self.args = copy_source.args + self.args_snip = copy_source.args_snip + self.arg_objs = copy_source.arg_objs + # Get current fields + child_names = [child.name.lower() for child in self.children] + # Import arg_objs from copy object + self.in_children = [] + for child in copy_source.arg_objs: + if child is None: + continue + if child.name.lower() not in child_names: + self.in_children.append(child) + return child_names + + def get_children(self, public_only=False): + tmp_list = copy.copy(self.children) + tmp_list.extend(self.in_children) + return tmp_list + + def resolve_arg_link(self, obj_tree): + if (self.args == "") or (len(self.in_children) > 0): + return + arg_list = self.args.replace(" ", "").split(",") + arg_list_lower = self.args.lower().replace(" ", "").split(",") + self.arg_objs = [None] * len(arg_list) + # check_objs = copy.copy(self.children) + # for child in self.children: + # if child.is_external_int(): + # check_objs += child.get_children() + self.missing_args = [] + for child in self.children: + ind = -1 + for i, arg in enumerate(arg_list_lower): + if arg == child.name.lower(): + ind = i + break + # If an argument is part of an interface block go through the + # block's children i.e. functions and subroutines to see if one matches + elif child.name.lower().startswith("#gen_int"): + for sub_child in child.children: + if arg == sub_child.name: + self.arg_objs[i] = sub_child + break + + if ind < 0: + if child.keywords.count(KEYWORD_ID_DICT["intent"]) > 0: + self.missing_args.append(child) + else: + self.arg_objs[ind] = child + if child.is_optional(): + arg_list[ind] = f"{arg_list[ind]}={arg_list[ind]}" + self.args_snip = ",".join(arg_list) + + def resolve_link(self, obj_tree): + self.resolve_arg_link(obj_tree) + + def require_link(self): + return True + + def get_type(self, no_link=False): + return SUBROUTINE_TYPE_ID + + def get_snippet(self, name_replace=None, drop_arg=-1): + arg_list = self.args_snip.split(",") + if (drop_arg >= 0) and (drop_arg < len(arg_list)): + del arg_list[drop_arg] + arg_snip = None + if len(arg_list) > 0: + arg_str, arg_snip = get_placeholders(arg_list) + else: + arg_str = "()" + name = name_replace if name_replace is not None else self.name + snippet = name + arg_snip if arg_snip is not None else None + return name + arg_str, snippet + + def get_desc(self): + return "SUBROUTINE" + + def get_hover(self, long=False, drop_arg=-1): + sub_sig, _ = self.get_snippet(drop_arg=drop_arg) + keyword_list = get_keywords(self.keywords) + keyword_list.append(f"{self.get_desc()} ") + hover_array = [" ".join(keyword_list) + sub_sig] + hover_array, docs = self.get_docs_full(hover_array, long, drop_arg) + return "\n ".join(hover_array), " \n".join(docs) + + def get_hover_md(self, long=False, drop_arg=-1): + return fortran_md(*self.get_hover(long, drop_arg)) + + def get_docs_full( + self, hover_array: list[str], long=False, drop_arg=-1 + ) -> tuple[list[str], list[str]]: + """Construct the full documentation with the code signature and the + documentation string + the documentation of any arguments. + + Parameters + ---------- + hover_array : list[str] + The list of strings to append the documentation to. + long : bool, optional + Whether or not to fetch the docs of the arguments, by default False + drop_arg : int, optional + Whether or not to drop certain arguments from the results, by default -1 + + Returns + ------- + tuple[list[str], list[str]] + Tuple containing the Fortran signature that should be in code blocks + and the documentation string that should be in normal Markdown. + """ + doc_strs: list[str] = [] + doc_str = self.get_documentation() + if doc_str is not None: + doc_strs.append(doc_str) + if long: + has_args = True + for i, arg_obj in enumerate(self.arg_objs): + if arg_obj is None or i == drop_arg: + continue + arg, doc_str = arg_obj.get_hover() + hover_array.append(arg) + if doc_str: # If doc_str is not None or "" + if has_args: + doc_strs.append("\n**Parameters:** ") + has_args = False + # stripping prevents multiple \n characters from the parser + doc_strs.append(f"`{arg_obj.name}` {doc_str}".strip()) + return hover_array, doc_strs + + def get_signature(self, drop_arg=-1): + arg_sigs = [] + arg_list = self.args.split(",") + for i, arg_obj in enumerate(self.arg_objs): + if i == drop_arg: + continue + if arg_obj is None: + arg_sigs.append({"label": arg_list[i]}) + else: + if arg_obj.is_optional(): + label = f"{arg_obj.name.lower()}={arg_obj.name.lower()}" + else: + label = arg_obj.name.lower() + msg = arg_obj.get_hover_md() + # Create MarkupContent object + msg = {"kind": "markdown", "value": msg} + arg_sigs.append({"label": label, "documentation": msg}) + call_sig, _ = self.get_snippet() + return call_sig, self.get_documentation(), arg_sigs + + # TODO: fix this + def get_interface_array( + self, keywords: list[str], signature: str, drop_arg=-1, change_strings=None + ): + interface_array = [" ".join(keywords) + signature] + for i, arg_obj in enumerate(self.arg_objs): + if arg_obj is None: + return None + arg_doc, docs = arg_obj.get_hover() + if i == drop_arg: + i0 = arg_doc.lower().find(change_strings[0].lower()) + if i0 >= 0: + i1 = i0 + len(change_strings[0]) + arg_doc = arg_doc[:i0] + change_strings[1] + arg_doc[i1:] + interface_array.append(f"{arg_doc} :: {arg_obj.name}") + return interface_array + + def get_interface(self, name_replace=None, drop_arg=-1, change_strings=None): + sub_sig, _ = self.get_snippet(name_replace=name_replace) + keyword_list = get_keywords(self.keywords) + keyword_list.append("SUBROUTINE ") + interface_array = self.get_interface_array( + keyword_list, sub_sig, drop_arg, change_strings + ) + name = name_replace if name_replace is not None else self.name + interface_array.append(f"END SUBROUTINE {name}") + return "\n".join(interface_array) + + def check_valid_parent(self): + if self.parent is not None: + parent_type = self.parent.get_type() + if (parent_type == CLASS_TYPE_ID) or (parent_type >= BLOCK_TYPE_ID): + return False + return True + + def get_diagnostics(self): + errors = [] + for missing_obj in self.missing_args: + new_diag = Diagnostic( + missing_obj.sline - 1, + f'Variable "{missing_obj.name}" with INTENT keyword not found in' + " argument list", + severity=1, + find_word=missing_obj.name, + ) + errors.append(new_diag) + implicit_flag = self.get_implicit() + if (implicit_flag is None) or implicit_flag: + return errors + arg_list = self.args.replace(" ", "").split(",") + for i, arg_obj in enumerate(self.arg_objs): + if arg_obj is None: + arg_name = arg_list[i].strip() + new_diag = Diagnostic( + self.sline - 1, + f'No matching declaration found for argument "{arg_name}"', + severity=1, + find_word=arg_name, + ) + errors.append(new_diag) + return errors diff --git a/fortls/parsers/internal/type.py b/fortls/parsers/internal/type.py new file mode 100644 index 00000000..fcda2f33 --- /dev/null +++ b/fortls/parsers/internal/type.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +import copy +from typing import TYPE_CHECKING + +from fortls.constants import BLOCK_TYPE_ID, CLASS_TYPE_ID, KEYWORD_ID_DICT +from fortls.json_templates import range_json +from fortls.jsonrpc import path_to_uri + +from .diagnostics import Diagnostic +from .scope import Scope +from .utilities import find_in_scope + +if TYPE_CHECKING: + from .ast import FortranAST + + +class Type(Scope): + def __init__( + self, + file_ast: FortranAST, + line_number: int, + name: str, + keywords: list, + ): + super().__init__(file_ast, line_number, name, keywords) + self.in_children: list = [] + self.inherit = None + self.inherit_var = None + self.inherit_tmp = None + self.inherit_version = -1 + self.abstract = self.keywords.count(KEYWORD_ID_DICT["abstract"]) > 0 + if self.keywords.count(KEYWORD_ID_DICT["public"]) > 0: + self.vis = 1 + if self.keywords.count(KEYWORD_ID_DICT["private"]) > 0: + self.vis = -1 + + def get_type(self, no_link=False): + return CLASS_TYPE_ID + + def get_desc(self): + return "TYPE" + + def get_children(self, public_only=False): + tmp_list = copy.copy(self.children) + tmp_list.extend(self.in_children) + return tmp_list + + def resolve_inherit(self, obj_tree, inherit_version): + if (self.inherit is None) or (self.inherit_version == inherit_version): + return + self.inherit_version = inherit_version + self.inherit_var = find_in_scope(self.parent, self.inherit, obj_tree) + if self.inherit_var is not None: + self._resolve_inherit_parent(obj_tree, inherit_version) + + def _resolve_inherit_parent(self, obj_tree, inherit_version): + # Resolve parent inheritance while avoiding circular recursion + self.inherit_tmp = self.inherit + self.inherit = None + self.inherit_var.resolve_inherit(obj_tree, inherit_version) + self.inherit = self.inherit_tmp + self.inherit_tmp = None + # Get current fields + child_names = [child.name.lower() for child in self.children] + # Import for parent objects + self.in_children = [] + for child in self.inherit_var.get_children(): + if child.name.lower() not in child_names: + self.in_children.append(child) + + def require_inherit(self): + return True + + def get_overridden(self, field_name): + ret_list = [] + field_name = field_name.lower() + for child in self.children: + if field_name == child.name.lower(): + ret_list.append(child) + break + if self.inherit_var is not None: + ret_list += self.inherit_var.get_overridden(field_name) + return ret_list + + def check_valid_parent(self): + if self.parent is None: + return False + parent_type = self.parent.get_type() + return parent_type != CLASS_TYPE_ID and parent_type < BLOCK_TYPE_ID + + def get_diagnostics(self): + errors = [] + for in_child in self.in_children: + if (not self.abstract) and ( + in_child.keywords.count(KEYWORD_ID_DICT["deferred"]) > 0 + ): + new_diag = Diagnostic( + self.eline - 1, + f'Deferred procedure "{in_child.name}" not implemented', + severity=1, + ) + new_diag.add_related( + path=in_child.file_ast.path, + line=in_child.sline - 1, + message="Inherited procedure declaration", + ) + errors.append(new_diag) + return errors + + def get_actions(self, sline, eline): + actions = [] + edits = [] + line_number = self.eline - 1 + if (line_number < sline) or (line_number > eline): + return actions + if self.contains_start is None: + edits.append( + { + **range_json(line_number, 0, line_number, 0), + "newText": "CONTAINS\n", + } + ) + # + diagnostics = [] + has_edits = False + file_uri = path_to_uri(self.file_ast.path) + for in_child in self.in_children: + if in_child.keywords.count(KEYWORD_ID_DICT["deferred"]) > 0: + # Get interface + interface_string = in_child.get_interface( + name_replace=in_child.name, + change_strings=( + f"class({in_child.parent.name})", + f"CLASS({self.name})", + ), + ) + if interface_string is None: + continue + interface_path, interface_edits = self.parent.add_subroutine( + interface_string, no_contains=has_edits + ) + if interface_path != self.file_ast.path: + continue + edits.append( + { + **range_json(line_number, 0, line_number, 0), + "newText": " PROCEDURE :: {0} => {0}\n".format(in_child.name), + } + ) + edits += interface_edits + new_diag = Diagnostic( + line_number, + f'Deferred procedure "{in_child.name}" not implemented', + severity=1, + ) + new_diag.add_related( + path=in_child.file_ast.path, + line=in_child.sline - 1, + message="Inherited procedure declaration", + ) + diagnostics.append(new_diag) + has_edits = True + # + if has_edits: + actions = [ + { + "title": "Implement deferred procedures", + "kind": "quickfix", + "edit": {"changes": {file_uri: edits}}, + "diagnostics": diagnostics, + } + ] + return actions + + def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: + keywords = [self.get_desc()] + if self.abstract: + keywords.append("ABSTRACT") + if self.inherit: + keywords.append(f"EXTENDS({self.inherit})") + decl = ", ".join(keywords) + hover = f"{decl} :: {self.name}" + doc_str = self.get_documentation() + return hover, doc_str diff --git a/fortls/parsers/internal/utilities.py b/fortls/parsers/internal/utilities.py new file mode 100644 index 00000000..ab41c6b5 --- /dev/null +++ b/fortls/parsers/internal/utilities.py @@ -0,0 +1,296 @@ +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING + +from fortls.constants import MODULE_TYPE_ID + +from .imports import Import, ImportTypes +from .use import Use + +if TYPE_CHECKING: + from .scope import Scope + + +def get_use_tree( + scope: Scope, + use_dict: dict[str, Use | Import], + obj_tree: dict, + only_list: list[str] = None, + rename_map: dict[str, str] = None, + curr_path: list[str] = None, +): + if only_list is None: + only_list = set() + if rename_map is None: + rename_map = {} + if curr_path is None: + curr_path = [] + + def intersect_only(use_stmnt: Use | Import): + tmp_list = [] + tmp_map = rename_map.copy() + for val1 in only_list: + mapped1 = tmp_map.get(val1, val1) + if mapped1 in use_stmnt.only_list: + tmp_list.append(val1) + new_rename = use_stmnt.rename_map.get(mapped1, None) + if new_rename is not None: + tmp_map[val1] = new_rename + else: + tmp_map.pop(val1, None) + return tmp_list, tmp_map + + # Detect and break circular references + if scope.FQSN in curr_path: + return use_dict + new_path = curr_path + [scope.FQSN] + # Add recursively + for use_stmnt in scope.use: + # if use_stmnt.mod_name not in obj_tree: + if type(use_stmnt) is Use and use_stmnt.mod_name not in obj_tree: + continue + # Escape any IMPORT, NONE statements + if type(use_stmnt) is Import and use_stmnt.import_type is ImportTypes.NONE: + continue + # Intersect parent and current ONLY list and renaming + if not only_list: + merged_use_list = use_stmnt.only_list.copy() + merged_rename = use_stmnt.rename_map.copy() + elif len(use_stmnt.only_list) == 0: + merged_use_list = only_list.copy() + merged_rename = rename_map.copy() + else: + merged_use_list, merged_rename = intersect_only(use_stmnt) + if len(merged_use_list) == 0: + continue + # Update ONLY list and renaming for current module + # If you have + # USE MOD, ONLY: A + # USE MOD, ONLY: B + # or + # IMPORT VAR + # IMPORT VAR2 + use_dict_mod = use_dict.get(use_stmnt.mod_name) + if use_dict_mod is not None: + old_len = len(use_dict_mod.only_list) + if old_len > 0 and merged_use_list: + only_len = old_len + for only_name in merged_use_list: + use_dict_mod.only_list.add(only_name) + if len(use_dict_mod.only_list) == only_len: + continue + only_len = len(use_dict_mod.only_list) + new_rename = merged_rename.get(only_name) + if new_rename is None: + continue + use_dict_mod.rename_map = merged_rename + use_dict[use_stmnt.mod_name] = use_dict_mod + else: + use_dict[use_stmnt.mod_name] = Use(use_stmnt.mod_name) + # Skip if we have already visited module with the same only list + if old_len == len(use_dict_mod.only_list): + continue + else: + if type(use_stmnt) is Use: + use_dict[use_stmnt.mod_name] = Use( + mod_name=use_stmnt.mod_name, + only_list=set(merged_use_list), + rename_map=merged_rename, + ) + elif type(use_stmnt) is Import: + use_dict[use_stmnt.mod_name] = Import( + name=use_stmnt.mod_name, + import_type=use_stmnt.import_type, + only_list=set(merged_use_list), + rename_map=merged_rename, + ) + with contextlib.suppress(AttributeError): + use_dict[use_stmnt.mod_name].scope = scope.parent.parent + # Do not descent the IMPORT tree, because it does not exist + if type(use_stmnt) is Import: + continue + # Descend USE tree + use_dict = get_use_tree( + obj_tree[use_stmnt.mod_name][0], + use_dict, + obj_tree, + merged_use_list, + merged_rename, + new_path, + ) + return use_dict + + +def find_in_scope( + scope: Scope, + var_name: str, + obj_tree: dict, + interface: bool = False, + local_only: bool = False, + var_line_number: int = None, +): + from .include import Include + + def check_scope( + local_scope: Scope, + var_name_lower: str, + filter_public: bool = False, + var_line_number: int = None, + ): + from .function import Function + + for child in local_scope.get_children(): + if child.name.startswith("#GEN_INT"): + tmp_var = check_scope(child, var_name_lower, filter_public) + if tmp_var is not None: + return tmp_var + is_private = child.vis < 0 or (local_scope.def_vis < 0 and child.vis <= 0) + if filter_public and is_private: + continue + if child.name.lower() == var_name_lower: + # For functions with an implicit result() variable the name + # of the function is used. If we are hovering over the function + # definition, we do not want the implicit result() to be returned. + # If scope is from a function and child's name is same as functions name + # and start of scope i.e. function definition is equal to the request ln + # then we are need to skip this child + if ( + isinstance(local_scope, Function) + and local_scope.name.lower() == child.name.lower() + and var_line_number in (local_scope.sline, local_scope.eline) + ): + return None + + return child + return None + + def check_import_scope(scope: Scope, var_name_lower: str): + for use_stmnt in scope.use: + if type(use_stmnt) is not Import: + continue + if use_stmnt.import_type == ImportTypes.ONLY: + # Check if name is in only list + if var_name_lower in use_stmnt.only_list: + return ImportTypes.ONLY + # Get the parent scope + elif use_stmnt.import_type == ImportTypes.ALL: + return ImportTypes.ALL + # Skip looking for parent scope + elif use_stmnt.import_type == ImportTypes.NONE: + return ImportTypes.NONE + return None + + # + var_name_lower = var_name.lower() + # Check local scope + if scope is None: + return None + tmp_var = check_scope(scope, var_name_lower, var_line_number=var_line_number) + if local_only or (tmp_var is not None): + return tmp_var + # Check INCLUDE statements + if scope.file_ast.include_statements: + strip_str = var_name.replace('"', "") + strip_str = strip_str.replace("'", "") + for inc in scope.file_ast.include_statements: + if strip_str == inc.path: + if inc.file is None: + return None + return Include(inc.file.ast, inc.line_number, inc.path) + + # Setup USE search + use_dict = get_use_tree(scope, {}, obj_tree) + # Look in found use modules + for use_mod, use_info in use_dict.items(): + # If use_mod is Import then it will not exist in the obj_tree + if type(use_info) is Import: + continue + use_scope = obj_tree[use_mod][0] + # Module name is request + if use_mod.lower() == var_name_lower: + return use_scope + # Filter children by only_list + if len(use_info.only_list) > 0 and var_name_lower not in use_info.only_list: + continue + mod_name = use_info.rename_map.get(var_name_lower, var_name_lower) + tmp_var = check_scope(use_scope, mod_name, filter_public=True) + if tmp_var is not None: + return tmp_var + # Only search local and imported names for interfaces + import_type = ImportTypes.DEFAULT + if interface: + import_type = check_import_scope(scope, var_name_lower) + if import_type is None: + return None + # Check parent scopes + if scope.parent is not None and import_type != ImportTypes.NONE: + tmp_var = find_in_scope(scope.parent, var_name, obj_tree) + if tmp_var is not None: + return tmp_var + # Check ancestor scopes + for ancestor in scope.get_ancestors(): + tmp_var = find_in_scope(ancestor, var_name, obj_tree) + if tmp_var is not None: + return tmp_var + return None + + +def find_in_workspace( + obj_tree: dict, query: str, filter_public: bool = False, exact_match: bool = False +): + def add_children(mod_obj, query: str): + tmp_list = [] + for child_obj in mod_obj.get_children(filter_public): + if child_obj.name.lower().find(query) >= 0: + tmp_list.append(child_obj) + return tmp_list + + matching_symbols = [] + query = query.lower() + for _, obj_packed in obj_tree.items(): + top_obj = obj_packed[0] + top_uri = obj_packed[1] + if top_uri is not None: + if top_obj.name.lower().find(query) > -1: + matching_symbols.append(top_obj) + if top_obj.get_type() == MODULE_TYPE_ID: + matching_symbols += add_children(top_obj, query) + if exact_match: + filtered_symbols = [] + n = len(query) + for symbol in matching_symbols: + if len(symbol.name) == n: + filtered_symbols.append(symbol) + matching_symbols = filtered_symbols + return matching_symbols + + +def climb_type_tree(var_stack, curr_scope: Scope, obj_tree: dict): + """Walk up user-defined type sequence to determine final field type""" + # Find base variable in current scope + iVar = 0 + var_name = var_stack[iVar].strip().lower() + var_obj = find_in_scope(curr_scope, var_name, obj_tree) + if var_obj is None: + return None + # Search for type, then next variable in stack and so on + for _ in range(30): + # Find variable type object + type_obj = var_obj.get_type_obj(obj_tree) + # Return if not found + if type_obj is None: + return None + # Go to next variable in stack and exit if done + iVar += 1 + if iVar == len(var_stack) - 1: + break + # Find next variable by name in type + var_name = var_stack[iVar].strip().lower() + var_obj = find_in_scope(type_obj, var_name, obj_tree, local_only=True) + # Return if not found + if var_obj is None: + return None + else: + raise KeyError + return type_obj diff --git a/fortls/parsers/internal/variable.py b/fortls/parsers/internal/variable.py new file mode 100644 index 00000000..eef06e57 --- /dev/null +++ b/fortls/parsers/internal/variable.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fortls.constants import CLASS_TYPE_ID, KEYWORD_ID_DICT, VAR_TYPE_ID, FRegex +from fortls.helper_functions import fortran_md, get_keywords, get_paren_substring + +from .base import FortranObj +from .diagnostics import Diagnostic +from .utilities import find_in_scope, find_in_workspace + +if TYPE_CHECKING: + from .ast import FortranAST + from .imports import Import + from .use import Use + + +class Variable(FortranObj): + def __init__( + self, + file_ast: FortranAST, + line_number: int, + name: str, + var_desc: str, + keywords: list, + keyword_info: dict = None, + kind: str | None = None, + link_obj=None, + ): + super().__init__() + if keyword_info is None: + keyword_info = {} + self.file_ast: FortranAST = file_ast + self.sline: int = line_number + self.eline: int = line_number + self.name: str = name + self.desc: str = var_desc + self.keywords: list = keywords + self.keyword_info: dict = keyword_info + self.kind: str | None = kind + self.children: list = [] + self.use: list[Use | Import] = [] + self.link_obj = None + self.type_obj = None + self.is_const: bool = False + self.is_external: bool = False + self.param_val: str = None + self.link_name: str = None + 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() + if file_ast.enc_scope_name is not None: + self.FQSN = f"{file_ast.enc_scope_name.lower()}::{self.name.lower()}" + if self.keywords.count(KEYWORD_ID_DICT["public"]) > 0: + self.vis = 1 + if self.keywords.count(KEYWORD_ID_DICT["private"]) > 0: + self.vis = -1 + if self.keywords.count(KEYWORD_ID_DICT["parameter"]) > 0: + self.is_const = True + if ( + self.keywords.count(KEYWORD_ID_DICT["external"]) > 0 + or self.desc.lower() == "external" + ): + self.is_external = True + + def update_fqsn(self, enc_scope=None): + if enc_scope is not None: + self.FQSN = f"{enc_scope.lower()}::{self.name.lower()}" + else: + self.FQSN = self.name.lower() + for child in self.children: + child.update_fqsn(self.FQSN) + + def resolve_link(self, obj_tree): + self.link_obj = None + if self.link_name is None: + return + if self.parent is not None: + link_obj = find_in_scope(self.parent, self.link_name, obj_tree) + if link_obj is not None: + self.link_obj = link_obj + + def require_link(self): + return self.link_name is not None + + def get_type(self, no_link=False): + if (not no_link) and (self.link_obj is not None): + return self.link_obj.get_type() + # Normal variable + return VAR_TYPE_ID + + 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 + return self.desc + self.kind if self.kind else 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.get_desc(no_link=True)) + if type_name is not None: + search_scope = self.parent + if search_scope.get_type() == CLASS_TYPE_ID: + search_scope = search_scope.parent + if search_scope is not None: + type_name = type_name.strip().lower() + type_obj = find_in_scope(search_scope, type_name, obj_tree) + if type_obj is not None: + 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"]) + self.keyword_info["dimension"] = dim_str + self.keywords.sort() + + def get_snippet(self, name_replace=None, drop_arg=-1): + name = name_replace if name_replace is not None else self.name + if self.link_obj is not None: + return self.link_obj.get_snippet(name, drop_arg) + # Normal variable + return None, None + + def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: + doc_str = self.get_documentation() + # In associated blocks we need to fetch the desc and keywords of the + # linked object + hover_str = ", ".join([self.get_desc()] + self.get_keywords()) + # If this is not a preprocessor variable, we can append the variable name + if not hover_str.startswith("#"): + hover_str += f" :: {self.name}" + if self.is_parameter() and self.param_val: + hover_str += f" = {self.param_val}" + return hover_str, doc_str + + def get_hover_md(self, long=False, drop_arg=-1): + return fortran_md(*self.get_hover(long, drop_arg)) + + 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): + return self.keywords.count(KEYWORD_ID_DICT["optional"]) > 0 + + def is_callable(self): + return self.callable + + def is_parameter(self): + return self.is_const + + def set_parameter_val(self, val: str): + self.param_val = val + + def set_external_attr(self): + self.keywords.append(KEYWORD_ID_DICT["external"]) + self.is_external = True + + def check_definition(self, obj_tree, known_types=None, interface=False): + if known_types is None: + known_types = {} + # Check for type definition in scope + 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": + return None, known_types + 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, + ) + if type_def is None: + self._check_definition_type_def( + obj_tree, desc_obj_name, known_types, type_match + ) + else: + known_types[desc_obj_name] = (0, type_def) + type_info = known_types[desc_obj_name] + if type_info is not None and type_info[0] == 1: + if interface: + out_diag = Diagnostic( + self.sline - 1, + message=f'Object "{desc_obj_name}" not imported in interface', + severity=1, + find_word=desc_obj_name, + ) + else: + out_diag = Diagnostic( + self.sline - 1, + message=f'Object "{desc_obj_name}" not found in scope', + severity=1, + find_word=desc_obj_name, + ) + type_def = type_info[1] + out_diag.add_related( + path=type_def.file_ast.path, + line=type_def.sline - 1, + message="Possible object", + ) + return out_diag, known_types + return None, known_types + + def _check_definition_type_def( + self, obj_tree, desc_obj_name, known_types, type_match + ): + type_defs = find_in_workspace( + 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() + filter_id = VAR_TYPE_ID + if var_type in ["class", "type"]: + filter_id = CLASS_TYPE_ID + for type_def in type_defs: + if type_def.get_type() == filter_id: + known_types[desc_obj_name] = (1, type_def) + break diff --git a/fortls/parsers/internal/where.py b/fortls/parsers/internal/where.py new file mode 100644 index 00000000..5b7d149c --- /dev/null +++ b/fortls/parsers/internal/where.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from fortls.constants import WHERE_TYPE_ID + +from .block import Block + +if TYPE_CHECKING: + from .ast import FortranAST + + +class Where(Block): + def __init__(self, file_ast: FortranAST, line_number: int, name: str): + super().__init__(file_ast, line_number, name) + + def get_type(self, no_link=False): + return WHERE_TYPE_ID + + def get_desc(self): + return "WHERE" diff --git a/setup.cfg b/setup.cfg index dc9ea37d..93a97a3a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,7 @@ install_requires = typing-extensions; python_version < "3.8" [options.package_data] -fortls = *.json +fortls = parsers/internal/*.json [options.entry_points] console_scripts = diff --git a/test/setup_tests.py b/test/setup_tests.py index f34a53eb..8443c579 100644 --- a/test/setup_tests.py +++ b/test/setup_tests.py @@ -52,14 +52,14 @@ def run_request(request, fortls_args: list[str] = None): try: # Present in `method`s parsed_results.append(result["params"]) - except: + except Exception as exc: raise RuntimeError( "Only 'result' and 'params' keys have been implemented for testing." " Please add the new key." - ) - except: + ) from exc + except Exception as exc: raise RuntimeError( "Unexpected error encountered trying to extract server results" - ) + ) from exc errcode = pid.poll() return errcode, parsed_results diff --git a/test/test_parser.py b/test/test_parser.py index e7a3d03b..d5e0a48d 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -1,6 +1,6 @@ from setup_tests import test_dir -from fortls.parse_fortran import FortranFile +from fortls.parsers.internal.parser import FortranFile def test_line_continuations(): diff --git a/test/test_server_hover.py b/test/test_server_hover.py index 285d2a97..c995e127 100644 --- a/test/test_server_hover.py +++ b/test/test_server_hover.py @@ -627,9 +627,14 @@ def test_intrinsics(): string += hover_req(file_path, 39, 23) errcode, results = run_request(string, fortls_args=["-n", "1"]) assert errcode == 0 - with open( - test_dir.parent.parent / "fortls" / "intrinsic.procedures.markdown.json" - ) as f: + path = ( + test_dir.parent.parent + / "fortls" + / "parsers" + / "internal" + / "intrinsic.procedures.markdown.json" + ) + with open(path, encoding="utf-8") as f: intrinsics = json.load(f) ref_results = ["\n-----\n" + intrinsics["SIZE"]] validate_hover(results, ref_results)