diff --git a/source/fab/parse/fortran.py b/source/fab/parse/fortran.py index 433c516b..04b2bdb3 100644 --- a/source/fab/parse/fortran.py +++ b/source/fab/parse/fortran.py @@ -16,13 +16,10 @@ Function_Stmt, Language_Binding_Spec, Char_Literal_Constant, Interface_Block, Name, Comment, Module, Call_Stmt, Derived_Type_Def, Derived_Type_Stmt, Type_Attr_Spec_List, Type_Attr_Spec, Type_Name, - Subroutine_Subprogram, Function_Subprogram, Internal_Subprogram_Part) + Subroutine_Subprogram, Function_Subprogram, Internal_Subprogram_Part, + External_Stmt, Type_Declaration_Stmt) from fparser.two.utils import walk # type: ignore -# todo: what else should we be importing from 2008 instead of 2003? This seems fragile. -from fparser.two.Fortran2008 import ( # type: ignore - Type_Declaration_Stmt, Attr_Spec_List) - from fab.build_config import BuildConfig from fab.dep_tree import AnalysedDependent from fab.parse.fortran_common import _typed_child, FortranAnalyserBase @@ -234,7 +231,9 @@ def walk_nodes(self, fpath, file_hash, node_tree) -> AnalysedFortran: # Or the new match statement, Python 3.10 if obj_type == Use_Stmt: self._process_use_statement(analysed_fortran, obj) # raises - + elif obj_type == External_Stmt: + for external in obj.items[1].items: + analysed_fortran.add_symbol_dep(external.string) elif obj_type == Call_Stmt: called_name = _typed_child(obj, Name) # called_name will be None for calls like thing%method(), @@ -274,10 +273,10 @@ def walk_nodes(self, fpath, file_hash, node_tree) -> AnalysedFortran: analysed_fortran.add_symbol_dep(called_name.string) elif obj_type == Program_Stmt: - analysed_fortran.add_program_def(str(obj.get_name())) + analysed_fortran.add_program_def(obj.get_name().string) elif obj_type == Module_Stmt: - analysed_fortran.add_module_def(str(obj.get_name())) + analysed_fortran.add_module_def(obj.get_name().string) elif obj_type in (Subroutine_Stmt, Function_Stmt): self._process_subroutine_or_function(analysed_fortran, @@ -289,12 +288,8 @@ def walk_nodes(self, fpath, file_hash, node_tree) -> AnalysedFortran: # use in C. Variable bindings are bidirectional - does # this work the other way round, too? # Make sure we have a test for it. - elif obj_type == Type_Declaration_Stmt: - # bound? - specs = _typed_child(obj, Attr_Spec_List) - if specs and _typed_child(specs, Language_Binding_Spec): - self._process_variable_binding(analysed_fortran, obj) - + elif isinstance(obj, Type_Declaration_Stmt): + self._process_type_declaration(analysed_fortran, obj) elif obj_type == Comment: self._process_comment(analysed_fortran, obj) @@ -341,6 +336,28 @@ def _process_use_statement(self, analysed_file, obj): # found a dependency on fortran analysed_file.add_module_dep(use_name) + def _process_type_declaration(self, analysed_fortran, obj): + """ + Handles a type declaration statement. A type declaration symbol + implies an outside dependency if: + 1. there is a bind attribute (dependency to other language) + 2. external attribute. + + Syntax rule: + Type_Declaration_Stmt( ! obj + Intrinsic_Type_Spec ! obj.items[0] + Attr_Spec_List ! obj.items[1] + Entity_Decl_List ! obj.items[2] + """ + attr_spec_list = obj.items[1].items if obj.items[1] else [] + for attr in attr_spec_list: + if attr.string == "EXTERNAL": + for symbol in obj.items[2].items: + analysed_fortran.add_symbol_dep(symbol.string) + elif isinstance(attr, Language_Binding_Spec): + # Bind attribute + self._process_variable_binding(analysed_fortran, obj) + def _process_variable_binding(self, analysed_file, obj: Type_Declaration_Stmt): # The name keyword on the bind statement is optional. @@ -383,7 +400,23 @@ def _process_comment(self, analysed_file, obj): # Without .o means a Fortran symbol analysed_file.add_symbol_dep(dep) - def _process_subroutine_or_function(self, analysed_file, fpath, obj): + def _process_subroutine_or_function( + self, + analysed_file: AnalysedFortran, + fpath: Path, + obj: Union[Function_Stmt, Subroutine_Stmt]): + """ + Processes a subroutine statement. It handles: + - a potential 'bind' attribute (which can change the external symbol + used), + - declaration of subroutines/functions in modules (which will not be + visible as external symbols, any dependencies will be covered by + the module symbol) + - declarations contained in other subroutines (which will not + at all be visible) + - declarations in an interface block (which declare an external + dependency) + """ # binding? bind = _typed_child(obj, Language_Binding_Spec) if bind: @@ -407,18 +440,19 @@ def _process_subroutine_or_function(self, analysed_file, fpath, obj): analysed_file.add_symbol_def(bind_name) # Not bound, just record the presence of the Fortran symbol. - # We don't need to record stuff in modules. Do not record - # any functions/subroutine that are part of a module, contained, - # or an interface block (since these symbols will not be external - # visible, and might otherwise trigger duplicated symbols in Fab) + + elif self._find_ancestor(obj, Interface_Block): + # If the subroutine/function declaration is inside an interface + # block, we have an external dependency: + analysed_file.add_symbol_dep(str(obj.get_name())) + elif (not self._find_ancestor(obj, Module) and - not self._find_ancestor(obj, Internal_Subprogram_Part) and - not self._find_ancestor(obj, Interface_Block)): - if isinstance(obj, Subroutine_Stmt): - analysed_file.add_symbol_def(str(obj.get_name())) - elif isinstance(obj, Function_Stmt): - _, name, _, _ = obj.items - analysed_file.add_symbol_def(name.string) + not self._find_ancestor(obj, Internal_Subprogram_Part)): + # We don't need to record stuff in modules, and any functions / + # subroutines that contained in a subroutine either (since these + # will not be externally visible). But otherwise record the + # declaration of the subroutine/function. + analysed_file.add_symbol_def(str(obj.get_name())) class FortranParserWorkaround(): diff --git a/tests/unit_tests/parse/fortran/test_fortran_analyser.f90 b/tests/unit_tests/parse/fortran/test_fortran_analyser.f90 index 2c530269..f8c6e55f 100644 --- a/tests/unit_tests/parse/fortran/test_fortran_analyser.f90 +++ b/tests/unit_tests/parse/fortran/test_fortran_analyser.f90 @@ -14,6 +14,12 @@ MODULE foo_mod SUBROUTINE internal_sub ! DEPENDS ON: monty_func + external some_external_symbol + integer, external :: some_external_as_attribute + INTERFACE + SUBROUTINE sub_in_interface + END SUBROUTINE sub_in_interface + END INTERFACE RETURN END SUBROUTINE internal_sub diff --git a/tests/unit_tests/parse/fortran/test_fortran_analyser.py b/tests/unit_tests/parse/fortran/test_fortran_analyser.py index 49f2d6d1..49f8c502 100644 --- a/tests/unit_tests/parse/fortran/test_fortran_analyser.py +++ b/tests/unit_tests/parse/fortran/test_fortran_analyser.py @@ -12,7 +12,7 @@ from unittest import mock from fparser.common.readfortran import FortranStringReader # type: ignore -from fparser.two.Fortran2008 import Type_Declaration_Stmt # type: ignore +from fparser.two.Fortran2003 import Type_Declaration_Stmt # type: ignore from fparser.two.parser import ParserFactory # type: ignore from fparser.two.utils import walk # type: ignore import pytest @@ -37,11 +37,13 @@ def module_expected_fixture(module_fpath: Path) -> AnalysedFortran: test module.''' return AnalysedFortran( fpath=module_fpath, - file_hash=3737289404, + file_hash=3447500859, module_defs={'foo_mod'}, symbol_defs={'external_sub', 'external_func', 'foo_mod'}, module_deps={'bar_mod', 'compute_chunk_size_mod'}, - symbol_deps={'monty_func', 'bar_mod', 'compute_chunk_size_mod'}, + symbol_deps={'monty_func', 'bar_mod', 'compute_chunk_size_mod', + 'some_external_symbol', 'some_external_as_attribute', + 'sub_in_interface'}, file_deps=set(), mo_commented_file_deps={'some_file.o'}, ) @@ -156,7 +158,7 @@ def test_program_file(self, fpath=Path(tmp_file.name)) module_expected.fpath = Path(tmp_file.name) - module_expected._file_hash = 325155675 + module_expected._file_hash = 975186955 module_expected.program_defs = {'foo_mod'} module_expected.module_defs = set()