From 4407b5d7c9da655700454ebb0abca27bef1d07af Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 7 Sep 2021 09:25:34 +0100 Subject: [PATCH 01/10] Move list of intrinsics to separate module --- ford/intrinsics.py | 458 +++++++++++++++++++++++++++++++++++++++++++++ ford/sourceform.py | 456 +------------------------------------------- 2 files changed, 459 insertions(+), 455 deletions(-) create mode 100644 ford/intrinsics.py diff --git a/ford/intrinsics.py b/ford/intrinsics.py new file mode 100644 index 00000000..eeb30fbb --- /dev/null +++ b/ford/intrinsics.py @@ -0,0 +1,458 @@ +"""This module just has a big list of the intrinsic functions to save space in sourceform.py +""" + +INTRINSICS = [ + "abort", + "abs", + "abstract", + "access", + "achar", + "acos", + "acosh", + "adjustl", + "adjustr", + "aimag", + "aint", + "alarm", + "all", + "allocatable", + "allocate", + "allocated", + "and", + "anint", + "any", + "asin", + "asinh", + "assign", + "associate", + "associated", + "asynchronous", + "atan", + "atan2", + "atanh", + "atomic_add", + "atomic_and", + "atomic_cas", + "atomic_define", + "atomic_fetch_add", + "atomic_fetch_and", + "atomic_fetch_or", + "atomic_fetch_xor", + "atomic_or", + "atomic_ref", + "atomic_xor", + "backtrace", + "backspace", + "bessel_j0", + "bessel_j1", + "bessel_jn", + "bessel_y0", + "bessel_y1", + "bessel_yn", + "bge", + "bgt", + "bind", + "bit_size", + "ble", + "block", + "block data", + "blt", + "btest", + "c_associated", + "c_f_pointer", + "c_f_procpointer", + "c_funloc", + "c_loc", + "c_sizeof", + "cabs", + "call", + "case", + "case default", + "cdabs", + "ceiling", + "char", + "character", + "chdir", + "chmod", + "class", + "close", + "cmplx", + "codimension", + "co_broadcast", + "co_max", + "co_min", + "co_reduce", + "co_sum", + "command_argument_count", + "common", + "compiler_options", + "compiler_version", + "complex", + "concurrent", + "conjg", + "contains", + "contiguous", + "continue", + "cos", + "cosh", + "count", + "cpu_time", + "critical", + "cshift", + "cycle", + "data", + "ctime", + "dabs", + "date_and_time", + "dble", + "dcmplx", + "deallocate", + "deferred", + "digits", + "dim", + "dimension", + "do", + "dlog", + "dlog10", + "dmax1", + "dmin1", + "dot_product", + "double complex", + "double precision", + "dprod", + "dreal", + "dshiftl", + "dshiftr", + "dsqrt", + "dtime", + "elemental", + "else", + "else if", + "elseif", + "elsewhere", + "end", + "end associate", + "end block", + "end block data", + "end critical", + "end do", + "end enum", + "end forall", + "end function", + "end if", + "end interface", + "end module", + "end program", + "end select", + "end submodule", + "end subroutine", + "end type", + "end where", + "endfile", + "endif", + "entry", + "enum", + "enumerator", + "eoshift", + "epsilon", + "equivalence", + "erf", + "erfc", + "erfc_scaled", + "etime", + "error stop", + "execute_command_line", + "exit", + "exp", + "exponent", + "extends", + "extends_type_of", + "external", + "fget", + "fgetc", + "final", + "findloc", + "fdate", + "floor", + "flush", + "fnum", + "forall", + "format", + "fput", + "fputc", + "fraction", + "function", + "free", + "fseek", + "fstat", + "ftell", + "gamma", + "generic", + "gerror", + "getarg", + "get_command", + "get_command_argument", + "getcwd", + "getenv", + "get_environment_variable", + "go to", + "goto", + "getgid", + "getlog", + "getpid", + "getuid", + "gmtime", + "hostnm", + "huge", + "hypot", + "iabs", + "iachar", + "iall", + "iand", + "iany", + "iargc", + "ibclr", + "ibits", + "ibset", + "ichar", + "idate", + "ieee_class", + "ieee_copy_sign", + "ieee_get_flag", + "ieee_get_halting_mode", + "ieee_get_rounding_mode", + "ieee_get_status", + "ieee_get_underflow_mode", + "ieee_is_finite", + "ieee_is_nan", + "ieee_is_negative", + "ieee_is_normal", + "ieee_logb", + "ieee_next_after", + "ieee_rem", + "ieee_rint", + "ieee_scalb", + "ieee_selected_real_kind", + "ieee_set_flag", + "ieee_set_halting_mode", + "ieee_set_rounding_mode", + "ieee_set_status", + "ieee_support_datatype", + "ieee_support_denormal", + "ieee_support_divide", + "ieee_support_flag", + "ieee_support_halting", + "ieee_support_inf", + "ieee_support_io", + "ieee_support_nan", + "ieee_support_rounding", + "ieee_support_sqrt", + "ieee_support_standard", + "ieee_support_underflow_control", + "ieee_unordered", + "ieee_value", + "ieor", + "ierrno", + "if", + "imag", + "image_index", + "implicit", + "implicit none", + "import", + "include", + "index", + "inquire", + "int", + "integer", + "intent", + "interface", + "intrinsic", + "int2", + "int8", + "ior", + "iparity", + "irand", + "is", + "is_contiguous", + "is_iostat_end", + "is_iostat_eor", + "isatty", + "ishft", + "ishftc", + "isnan", + "itime", + "kill", + "kind", + "lbound", + "lcobound", + "leadz", + "len", + "len_trim", + "lge", + "lgt", + "link", + "lle", + "llt", + "lock", + "lnblnk", + "loc", + "log", + "log_gamma", + "log10", + "logical", + "long", + "lshift", + "lstat", + "ltime", + "malloc", + "maskl", + "maskr", + "matmul", + "max", + "max0", + "maxexponent", + "maxloc", + "maxval", + "mclock", + "mclock8", + "merge", + "merge_bits", + "min", + "min0", + "minexponent", + "minloc", + "minval", + "mod", + "module", + "module procedure", + "modulo", + "move_alloc", + "mvbits", + "namelist", + "nearest", + "new_line", + "nint", + "non_overridable", + "none", + "nopass", + "norm2", + "not", + "null", + "nullify", + "num_images", + "only", + "open", + "or", + "operator", + "optional", + "pack", + "parameter", + "parity", + "pass", + "pause", + "pointer", + "perror", + "popcnt", + "poppar", + "precision", + "present", + "print", + "private", + "procedure", + "product", + "program", + "protected", + "public", + "pure", + "radix", + "ran", + "rand", + "random_number", + "random_seed", + "range", + "rank", + "read", + "real", + "recursive", + "rename", + "repeat", + "reshape", + "result", + "return", + "rewind", + "rewrite", + "rrspacing", + "rshift", + "same_type_as", + "save", + "scale", + "scan", + "secnds", + "second", + "select", + "select case", + "select type", + "selected_char_kind", + "selected_int_kind", + "selected_real_kind", + "sequence", + "set_exponent", + "shape", + "shifta", + "shiftl", + "shiftr", + "sign", + "signal", + "sin", + "sinh", + "size", + "sizeof", + "sleep", + "spacing", + "spread", + "sqrt", + "srand", + "stat", + "stop", + "storage_size", + "submodule", + "subroutine", + "sum", + "sync all", + "sync images", + "sync memory", + "symlnk", + "system", + "system_clock", + "tan", + "tanh", + "target", + "then", + "this_image", + "time", + "time8", + "tiny", + "trailz", + "transfer", + "transpose", + "trim", + "ttynam", + "type", + "type_as", + "ubound", + "ucobound", + "umask", + "unlock", + "unlink", + "unpack", + "use", + "value", + "verify", + "volatile", + "wait", + "where", + "while", + "write", + "xor", + "zabs", +] diff --git a/ford/sourceform.py b/ford/sourceform.py index 3100b47c..fecc917c 100644 --- a/ford/sourceform.py +++ b/ford/sourceform.py @@ -40,6 +40,7 @@ import ford.reader import ford.utils +from ford.intrinsics import INTRINSICS VAR_TYPE_STRING = r"^integer|real|double\s*precision|character|complex|double\s*complex|logical|type|class|procedure|enumerator" VARKIND_RE = re.compile(r"\((.*)\)|\*\s*(\d+|\(.*\))") @@ -58,461 +59,6 @@ NBSP_RE = re.compile(r" (?= )|(?<= ) ") DIM_RE = re.compile(r"^\w+\s*(\(.*\))\s*$") -INTRINSICS = [ - "abort", - "abs", - "abstract", - "access", - "achar", - "acos", - "acosh", - "adjustl", - "adjustr", - "aimag", - "aint", - "alarm", - "all", - "allocatable", - "allocate", - "allocated", - "and", - "anint", - "any", - "asin", - "asinh", - "assign", - "associate", - "associated", - "asynchronous", - "atan", - "atan2", - "atanh", - "atomic_add", - "atomic_and", - "atomic_cas", - "atomic_define", - "atomic_fetch_add", - "atomic_fetch_and", - "atomic_fetch_or", - "atomic_fetch_xor", - "atomic_or", - "atomic_ref", - "atomic_xor", - "backtrace", - "backspace", - "bessel_j0", - "bessel_j1", - "bessel_jn", - "bessel_y0", - "bessel_y1", - "bessel_yn", - "bge", - "bgt", - "bind", - "bit_size", - "ble", - "block", - "block data", - "blt", - "btest", - "c_associated", - "c_f_pointer", - "c_f_procpointer", - "c_funloc", - "c_loc", - "c_sizeof", - "cabs", - "call", - "case", - "case default", - "cdabs", - "ceiling", - "char", - "character", - "chdir", - "chmod", - "class", - "close", - "cmplx", - "codimension", - "co_broadcast", - "co_max", - "co_min", - "co_reduce", - "co_sum", - "command_argument_count", - "common", - "compiler_options", - "compiler_version", - "complex", - "concurrent", - "conjg", - "contains", - "contiguous", - "continue", - "cos", - "cosh", - "count", - "cpu_time", - "critical", - "cshift", - "cycle", - "data", - "ctime", - "dabs", - "date_and_time", - "dble", - "dcmplx", - "deallocate", - "deferred", - "digits", - "dim", - "dimension", - "do", - "dlog", - "dlog10", - "dmax1", - "dmin1", - "dot_product", - "double complex", - "double precision", - "dprod", - "dreal", - "dshiftl", - "dshiftr", - "dsqrt", - "dtime", - "elemental", - "else", - "else if", - "elseif", - "elsewhere", - "end", - "end associate", - "end block", - "end block data", - "end critical", - "end do", - "end enum", - "end forall", - "end function", - "end if", - "end interface", - "end module", - "end program", - "end select", - "end submodule", - "end subroutine", - "end type", - "end where", - "endfile", - "endif", - "entry", - "enum", - "enumerator", - "eoshift", - "epsilon", - "equivalence", - "erf", - "erfc", - "erfc_scaled", - "etime", - "error stop", - "execute_command_line", - "exit", - "exp", - "exponent", - "extends", - "extends_type_of", - "external", - "fget", - "fgetc", - "final", - "findloc", - "fdate", - "floor", - "flush", - "fnum", - "forall", - "format", - "fput", - "fputc", - "fraction", - "function", - "free", - "fseek", - "fstat", - "ftell", - "gamma", - "generic", - "gerror", - "getarg", - "get_command", - "get_command_argument", - "getcwd", - "getenv", - "get_environment_variable", - "go to", - "goto", - "getgid", - "getlog", - "getpid", - "getuid", - "gmtime", - "hostnm", - "huge", - "hypot", - "iabs", - "iachar", - "iall", - "iand", - "iany", - "iargc", - "ibclr", - "ibits", - "ibset", - "ichar", - "idate", - "ieee_class", - "ieee_copy_sign", - "ieee_get_flag", - "ieee_get_halting_mode", - "ieee_get_rounding_mode", - "ieee_get_status", - "ieee_get_underflow_mode", - "ieee_is_finite", - "ieee_is_nan", - "ieee_is_negative", - "ieee_is_normal", - "ieee_logb", - "ieee_next_after", - "ieee_rem", - "ieee_rint", - "ieee_scalb", - "ieee_selected_real_kind", - "ieee_set_flag", - "ieee_set_halting_mode", - "ieee_set_rounding_mode", - "ieee_set_status", - "ieee_support_datatype", - "ieee_support_denormal", - "ieee_support_divide", - "ieee_support_flag", - "ieee_support_halting", - "ieee_support_inf", - "ieee_support_io", - "ieee_support_nan", - "ieee_support_rounding", - "ieee_support_sqrt", - "ieee_support_standard", - "ieee_support_underflow_control", - "ieee_unordered", - "ieee_value", - "ieor", - "ierrno", - "if", - "imag", - "image_index", - "implicit", - "implicit none", - "import", - "include", - "index", - "inquire", - "int", - "integer", - "intent", - "interface", - "intrinsic", - "int2", - "int8", - "ior", - "iparity", - "irand", - "is", - "is_contiguous", - "is_iostat_end", - "is_iostat_eor", - "isatty", - "ishft", - "ishftc", - "isnan", - "itime", - "kill", - "kind", - "lbound", - "lcobound", - "leadz", - "len", - "len_trim", - "lge", - "lgt", - "link", - "lle", - "llt", - "lock", - "lnblnk", - "loc", - "log", - "log_gamma", - "log10", - "logical", - "long", - "lshift", - "lstat", - "ltime", - "malloc", - "maskl", - "maskr", - "matmul", - "max", - "max0", - "maxexponent", - "maxloc", - "maxval", - "mclock", - "mclock8", - "merge", - "merge_bits", - "min", - "min0", - "minexponent", - "minloc", - "minval", - "mod", - "module", - "module procedure", - "modulo", - "move_alloc", - "mvbits", - "namelist", - "nearest", - "new_line", - "nint", - "non_overridable", - "none", - "nopass", - "norm2", - "not", - "null", - "nullify", - "num_images", - "only", - "open", - "or", - "operator", - "optional", - "pack", - "parameter", - "parity", - "pass", - "pause", - "pointer", - "perror", - "popcnt", - "poppar", - "precision", - "present", - "print", - "private", - "procedure", - "product", - "program", - "protected", - "public", - "pure", - "radix", - "ran", - "rand", - "random_number", - "random_seed", - "range", - "rank", - "read", - "real", - "recursive", - "rename", - "repeat", - "reshape", - "result", - "return", - "rewind", - "rewrite", - "rrspacing", - "rshift", - "same_type_as", - "save", - "scale", - "scan", - "secnds", - "second", - "select", - "select case", - "select type", - "selected_char_kind", - "selected_int_kind", - "selected_real_kind", - "sequence", - "set_exponent", - "shape", - "shifta", - "shiftl", - "shiftr", - "sign", - "signal", - "sin", - "sinh", - "size", - "sizeof", - "sleep", - "spacing", - "spread", - "sqrt", - "srand", - "stat", - "stop", - "storage_size", - "submodule", - "subroutine", - "sum", - "sync all", - "sync images", - "sync memory", - "symlnk", - "system", - "system_clock", - "tan", - "tanh", - "target", - "then", - "this_image", - "time", - "time8", - "tiny", - "trailz", - "transfer", - "transpose", - "trim", - "ttynam", - "type", - "type_as", - "ubound", - "ucobound", - "umask", - "unlock", - "unlink", - "unpack", - "use", - "value", - "verify", - "volatile", - "wait", - "where", - "while", - "write", - "xor", - "zabs", -] base_url = "" From 8a1653b85d3241b11f4b39e2c476041972c33a93 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 7 Sep 2021 18:44:56 +0100 Subject: [PATCH 02/10] Ensure that names are re-exported correctly Fixes #319 --- ford/sourceform.py | 149 ++++++++++----------- test/test_project.py | 282 ++++++++++++++++++++++++++++++++++++++++ test/test_sourceform.py | 204 +++++++++++++++++++++++++++++ 3 files changed, 562 insertions(+), 73 deletions(-) diff --git a/ford/sourceform.py b/ford/sourceform.py index fecc917c..c3953b33 100644 --- a/ford/sourceform.py +++ b/ford/sourceform.py @@ -90,13 +90,10 @@ class FortranBase(object): } def __init__( - self, source, first_line, parent=None, inherited_permission=None, strings=[] + self, source, first_line, parent=None, inherited_permission="public", strings=[] ): self.visible = False - if inherited_permission is not None: - self.permission = inherited_permission.lower() - else: - self.permission = None + self.permission = inherited_permission.lower() self.strings = strings self.parent = parent if self.parent: @@ -617,7 +614,7 @@ class FortranContainer(FortranBase): ) def __init__( - self, source, first_line, parent=None, inherited_permission=None, strings=[] + self, source, first_line, parent=None, inherited_permission="public", strings=[] ): self.num_lines = 0 if not isinstance(self, FortranSourceFile): @@ -628,9 +625,9 @@ def __init__( ) incontains = False if type(self) is FortranSubmodule: - permission = "private" + self.permission = "private" else: - permission = "public" + self.permission = inherited_permission typestr = "" for vtype in self.settings["extra_vartypes"]: @@ -665,7 +662,7 @@ def __init__( if not incontains and type(self) in _can_have_contains: incontains = True if isinstance(self, FortranType): - permission = "public" + self.permission = "public" elif incontains: self.print_error( line, "Multiple CONTAINS statements present in scope" @@ -678,11 +675,11 @@ def __init__( ), ) elif line.lower() == "public": - permission = "public" + self.permission = "public" elif line.lower() == "private": - permission = "private" + self.permission = "private" elif line.lower() == "protected": - permission = "protected" + self.permission = "protected" elif line.lower() == "sequence": if type(self) == FortranType: self.sequence = True @@ -771,7 +768,7 @@ def __init__( # Module procedure implementing an interface in a SUBMODULE self.modprocedures.append( FortranSubmoduleProcedure( - source, self.MODPROC_RE.match(line), self, permission + source, self.MODPROC_RE.match(line), self, self.permission ) ) self.num_lines += self.modprocedures[-1].num_lines - 1 @@ -840,7 +837,10 @@ def __init__( if hasattr(self, "subroutines"): self.subroutines.append( FortranSubroutine( - source, self.SUBROUTINE_RE.match(line), self, permission + source, + self.SUBROUTINE_RE.match(line), + self, + self.permission, ) ) self.num_lines += self.subroutines[-1].num_lines - 1 @@ -857,7 +857,7 @@ def __init__( if hasattr(self, "functions"): self.functions.append( FortranFunction( - source, self.FUNCTION_RE.match(line), self, permission + source, self.FUNCTION_RE.match(line), self, self.permission ) ) self.num_lines += self.functions[-1].num_lines - 1 @@ -869,7 +869,9 @@ def __init__( elif self.TYPE_RE.match(line) and blocklevel == 0: if hasattr(self, "types"): self.types.append( - FortranType(source, self.TYPE_RE.match(line), self, permission) + FortranType( + source, self.TYPE_RE.match(line), self, self.permission + ) ) self.num_lines += self.types[-1].num_lines - 1 else: @@ -882,7 +884,7 @@ def __init__( elif self.INTERFACE_RE.match(line) and blocklevel == 0: if hasattr(self, "interfaces"): intr = FortranInterface( - source, self.INTERFACE_RE.match(line), self, permission + source, self.INTERFACE_RE.match(line), self, self.permission ) self.num_lines += intr.num_lines - 1 if intr.abstract: @@ -899,7 +901,9 @@ def __init__( elif self.ENUM_RE.match(line) and blocklevel == 0: if hasattr(self, "enums"): self.enums.append( - FortranEnum(source, self.ENUM_RE.match(line), self, permission) + FortranEnum( + source, self.ENUM_RE.match(line), self, self.permission + ) ) self.num_lines += self.enums[-1].num_lines - 1 else: @@ -914,7 +918,10 @@ def __init__( if match.group(1).lower() == "generic" or len(split) == 1: self.boundprocs.append( FortranBoundProcedure( - source, self.BOUNDPROC_RE.match(line), self, permission + source, + self.BOUNDPROC_RE.match(line), + self, + self.permission, ) ) else: @@ -925,7 +932,7 @@ def __init__( source, self.BOUNDPROC_RE.match(pseudo_line), self, - permission, + self.permission, ) ) else: @@ -994,7 +1001,7 @@ def __init__( elif self.VARIABLE_RE.match(line) and blocklevel == 0: if hasattr(self, "variables"): self.variables.extend( - line_to_variables(source, line, permission, self) + line_to_variables(source, line, self.permission, self) ) else: self.print_error( @@ -1130,40 +1137,26 @@ def correlate(self, project): proc.module = intr intr.procedure.module = proc + def should_be_public(name: str) -> bool: + """Is name public?""" + return self.permission == "public" or name in self.public_list + + def filter_public(collection: dict) -> dict: + """Return a new dict of only the public objects from collection""" + return { + name: obj for name, obj in collection.items() if should_be_public(name) + } + # Add procedures and types from USED modules to our lists for mod, extra in self.uses: if type(mod) is str: continue procs, absints, types, variables = mod.get_used_entities(extra) if self.obj == "module": - self.pub_procs.update( - [ - (name, proc) - for name, proc in procs.items() - if name in self.public_list - ] - ) - self.pub_absints.update( - [ - (name, absint) - for name, absint in absints.items() - if name in self.public_list - ] - ) - self.pub_types.update( - [ - (name, dtype) - for name, dtype in types.items() - if name in self.public_list - ] - ) - self.pub_vars.update( - [ - (name, var) - for name, var in variables.items() - if name in self.public_list - ] - ) + self.pub_procs.update(filter_public(procs)) + self.pub_absints.update(filter_public(absints)) + self.pub_types.update(filter_public(types)) + self.pub_vars.update(filter_public(variables)) self.all_procs.update(procs) self.all_absinterfaces.update(absints) self.all_types.update(types) @@ -1257,9 +1250,11 @@ def correlate(self, project): self.modsubroutines = [sub for sub in self.subroutines if sub.module] self.subroutines = [sub for sub in self.subroutines if not sub.module] - del self.public_list - def process_attribs(self): + """Attach standalone attributes to the correct object, and compute the + list of public objects + """ + # IMPORTANT: Make sure types processed before interfaces--import when # determining permissions of derived types and overridden constructors for item in self.iterator( @@ -1281,6 +1276,7 @@ def process_attribs(self): del self.attr_dict[item.name.lower()] except KeyError: pass + for var in self.variables: for attr in self.attr_dict.get(var.name.lower(), []): if attr == "public" or attr == "private" or attr == "protected": @@ -1302,10 +1298,22 @@ def process_attribs(self): del self.attr_dict[var.name.lower()] except KeyError: pass - self.public_list = [] - for item, attrs in self.attr_dict.items(): - if "public" in attrs: - self.public_list.append(item) + + # Now we want a list of all the objects we've declared, plus + # any we've imported that have a "public" attribute + self.public_list = [ + item.name.lower() + for item in self.iterator( + "functions", + "subroutines", + "types", + "interfaces", + "absinterfaces", + "variables", + ) + if item.permission == "public" + ] + [item for item, attr in self.attr_dict.items() if "public" in attr] + del self.attr_dict def prune(self): @@ -1457,8 +1465,8 @@ def _initialize(self, line): self.param_dict = dict() def _cleanup(self): - # Create list of all local procedures. Ones coming from other modules - # will be added later, during correlation. + """Create list of all local procedures. Ones coming from other modules + will be added later, during correlation.""" self.all_procs = {} for p in self.routines: self.all_procs[p.name.lower()] = p @@ -1470,22 +1478,17 @@ def _cleanup(self): self.all_procs[proc.name.lower()] = proc self.process_attribs() self.variables = [v for v in self.variables if "external" not in v.attribs] - self.pub_procs = {} - for p, proc in self.all_procs.items(): - if proc.permission == "public": - self.pub_procs[p] = proc - self.pub_vars = {} - for var in self.variables: - if var.permission == "public" or var.permission == "protected": - self.pub_vars[var.name] = var - self.pub_types = {} - for dt in self.types: - if dt.permission == "public": - self.pub_types[dt.name] = dt - self.pub_absints = {} - for ai in self.absinterfaces: - if ai.permission == "public": - self.pub_absints[ai.name] = ai + + def should_be_public(item: str) -> bool: + return item.permission == "public" or item.permission == "protected" + + def filter_public(collection: list) -> dict: + return {obj.name: obj for obj in collection if should_be_public(obj)} + + self.pub_procs = filter_public(self.all_procs.values()) + self.pub_vars = filter_public(self.variables) + self.pub_types = filter_public(self.types) + self.pub_absints = filter_public(self.absinterfaces) def get_used_entities(self, use_specs): """ diff --git a/test/test_project.py b/test/test_project.py index 98fcf04d..10df0490 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -82,3 +82,285 @@ def test_use_and_rename(copy_fortran_file): project = Project(settings) project.correlate() assert set(project.modules[0].all_procs.keys()) == {"routine_1", "routine"} + + +def test_module_use_only_everything(copy_fortran_file): + data = """\ + module default_access + ! No access keyword + integer :: int_public, int_private + private :: int_private + real :: real_public + real, private :: real_private + + type :: type_public + complex :: component_public + complex, private :: component_private + end type type_public + + type :: type_private + character(len=1) :: string_public + character(len=1), private :: string_private + end type type_private + + private :: sub_private, func_private, type_private + + contains + subroutine sub_public + end subroutine sub_public + + subroutine sub_private + end subroutine sub_private + + integer function func_public() + end function func_public + + integer function func_private() + end function func_private + end module default_access + + module use_only_everything + use default_access, only : int_public, real_public, type_public, sub_public, func_public + end module use_only_everything + """ + + settings = copy_fortran_file(data) + project = Project(settings) + project.correlate() + + # Double-check we're looking at the right module + assert project.modules[1].name == "use_only_everything" + assert set(project.modules[1].all_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(project.modules[1].pub_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(project.modules[1].all_types.keys()) == { + "type_public", + } + assert set(project.modules[1].pub_types.keys()) == { + "type_public", + } + assert set(project.modules[1].all_vars.keys()) == { + "int_public", + "real_public", + } + assert set(project.modules[1].pub_vars.keys()) == { + "int_public", + "real_public", + } + + +def test_module_use_only_everything_change_access(copy_fortran_file): + data = """\ + module default_access + ! No access keyword + integer :: int_public, int_private + private :: int_private + real :: real_public + real, private :: real_private + + type :: type_public + complex :: component_public + complex, private :: component_private + end type type_public + + type :: type_private + character(len=1) :: string_public + character(len=1), private :: string_private + end type type_private + + private :: sub_private, func_private, type_private + + contains + subroutine sub_public + end subroutine sub_public + + subroutine sub_private + end subroutine sub_private + + integer function func_public() + end function func_public + + integer function func_private() + end function func_private + end module default_access + + module use_only_change_access + use default_access, only : int_public, real_public, type_public, sub_public, func_public + private + public :: int_public, sub_public + end module use_only_change_access + """ + + settings = copy_fortran_file(data) + project = Project(settings) + project.correlate() + + # Double-check we're looking at the right module + assert project.modules[1].name == "use_only_change_access" + assert set(project.modules[1].all_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(project.modules[1].pub_procs.keys()) == { + "sub_public", + } + assert set(project.modules[1].all_types.keys()) == { + "type_public", + } + assert set(project.modules[1].pub_types.keys()) == set() + assert set(project.modules[1].all_vars.keys()) == { + "int_public", + "real_public", + } + assert set(project.modules[1].pub_vars.keys()) == { + "int_public", + } + + +def test_module_use_everything(copy_fortran_file): + data = """\ + module default_access + ! No access keyword + integer :: int_public, int_private + private :: int_private + real :: real_public + real, private :: real_private + + type :: type_public + complex :: component_public + complex, private :: component_private + end type type_public + + type :: type_private + character(len=1) :: string_public + character(len=1), private :: string_private + end type type_private + + private :: sub_private, func_private, type_private + + contains + subroutine sub_public + end subroutine sub_public + + subroutine sub_private + end subroutine sub_private + + integer function func_public() + end function func_public + + integer function func_private() + end function func_private + end module default_access + + module use_everything + use default_access + end module use_everything + """ + + settings = copy_fortran_file(data) + project = Project(settings) + project.correlate() + + # Double-check we're looking at the right module + assert project.modules[1].name == "use_everything" + assert set(project.modules[1].all_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(project.modules[1].pub_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(project.modules[1].all_types.keys()) == { + "type_public", + } + assert set(project.modules[1].pub_types.keys()) == { + "type_public", + } + assert set(project.modules[1].all_vars.keys()) == { + "int_public", + "real_public", + } + assert set(project.modules[1].pub_vars.keys()) == { + "int_public", + "real_public", + } + + +def test_module_use_everything_reexport(copy_fortran_file): + data = """\ + module default_access + ! No access keyword + integer :: int_public, int_private + private :: int_private + real :: real_public + real, private :: real_private + + type :: type_public + complex :: component_public + complex, private :: component_private + end type type_public + + type :: type_private + character(len=1) :: string_public + character(len=1), private :: string_private + end type type_private + + private :: sub_private, func_private, type_private + + contains + subroutine sub_public + end subroutine sub_public + + subroutine sub_private + end subroutine sub_private + + integer function func_public() + end function func_public + + integer function func_private() + end function func_private + end module default_access + + module use_everything + use default_access + end module use_everything + + module reexport + use use_everything + end module reexport + """ + + settings = copy_fortran_file(data) + project = Project(settings) + project.correlate() + + # Double-check we're looking at the right module + assert project.modules[2].name == "reexport" + assert set(project.modules[2].all_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(project.modules[2].pub_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(project.modules[2].all_types.keys()) == { + "type_public", + } + assert set(project.modules[2].pub_types.keys()) == { + "type_public", + } + assert set(project.modules[2].all_vars.keys()) == { + "int_public", + "real_public", + } + assert set(project.modules[2].pub_vars.keys()) == { + "int_public", + "real_public", + } diff --git a/test/test_sourceform.py b/test/test_sourceform.py index 9eb412e4..4cfc2a46 100644 --- a/test/test_sourceform.py +++ b/test/test_sourceform.py @@ -273,3 +273,207 @@ def test_module_get_used_entities_rename(): assert interfaces == {} assert types == {} assert variables == {"x": mod_variables["x"]} + + +def test_module_default_access(parse_fortran_file): + data = """\ + module default_access + ! No access keyword + integer :: int_public, int_private + private :: int_private + real :: real_public + real, private :: real_private + + type :: type_public + complex :: component_public + complex, private :: component_private + end type type_public + + type :: type_private + character(len=1) :: string_public + character(len=1), private :: string_private + end type type_private + + private :: sub_private, func_private, type_private + + contains + subroutine sub_public + end subroutine sub_public + + subroutine sub_private + end subroutine sub_private + + integer function func_public() + end function func_public + + integer function func_private() + end function func_private + end module default_access + """ + + fortran_file = parse_fortran_file(data) + fortran_file.modules[0].correlate(None) + + assert set(fortran_file.modules[0].all_procs.keys()) == { + "sub_public", + "func_public", + "sub_private", + "func_private", + } + assert set(fortran_file.modules[0].pub_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(fortran_file.modules[0].all_types.keys()) == { + "type_public", + "type_private", + } + assert set(fortran_file.modules[0].pub_types.keys()) == { + "type_public", + } + assert set(fortran_file.modules[0].all_vars.keys()) == { + "int_public", + "int_private", + "real_public", + "real_private", + } + assert set(fortran_file.modules[0].pub_vars.keys()) == { + "int_public", + "real_public", + } + + +def test_module_public_access(parse_fortran_file): + data = """\ + module public_access + public + integer :: int_public, int_private + private :: int_private + real :: real_public + real, private :: real_private + + type :: type_public + complex :: component_public + complex, private :: component_private + end type type_public + + type :: type_private + character(len=1) :: string_public + character(len=1), private :: string_private + end type type_private + + private :: sub_private, func_private, type_private + + contains + subroutine sub_public + end subroutine sub_public + + subroutine sub_private + end subroutine sub_private + + integer function func_public() + end function func_public + + integer function func_private() + end function func_private + end module public_access + """ + + fortran_file = parse_fortran_file(data) + fortran_file.modules[0].correlate(None) + + assert set(fortran_file.modules[0].all_procs.keys()) == { + "sub_public", + "func_public", + "sub_private", + "func_private", + } + assert set(fortran_file.modules[0].pub_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(fortran_file.modules[0].all_types.keys()) == { + "type_public", + "type_private", + } + assert set(fortran_file.modules[0].pub_types.keys()) == { + "type_public", + } + assert set(fortran_file.modules[0].all_vars.keys()) == { + "int_public", + "int_private", + "real_public", + "real_private", + } + assert set(fortran_file.modules[0].pub_vars.keys()) == { + "int_public", + "real_public", + } + + +def test_module_private_access(parse_fortran_file): + data = """\ + module private_access + private + integer :: int_public, int_private + public :: int_public + real :: real_private + real, public :: real_public + + type :: type_public + complex :: component_public + complex, private :: component_private + end type type_public + + type :: type_private + character(len=1) :: string_public + character(len=1), private :: string_private + end type type_private + + public :: sub_public, func_public, type_public + + contains + subroutine sub_public + end subroutine sub_public + + subroutine sub_private + end subroutine sub_private + + integer function func_public() + end function func_public + + integer function func_private() + end function func_private + end module private_access + """ + + fortran_file = parse_fortran_file(data) + fortran_file.modules[0].correlate(None) + + assert set(fortran_file.modules[0].all_procs.keys()) == { + "sub_public", + "func_public", + "sub_private", + "func_private", + } + assert set(fortran_file.modules[0].pub_procs.keys()) == { + "sub_public", + "func_public", + } + assert set(fortran_file.modules[0].all_types.keys()) == { + "type_public", + "type_private", + } + assert set(fortran_file.modules[0].pub_types.keys()) == { + "type_public", + } + assert set(fortran_file.modules[0].all_vars.keys()) == { + "int_public", + "int_private", + "real_public", + "real_private", + } + assert set(fortran_file.modules[0].pub_vars.keys()) == { + "int_public", + "real_public", + } From 500a0bedabd95bd8e83e48046ca37f66a27af27d Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 8 Sep 2021 17:12:18 +0100 Subject: [PATCH 03/10] Fix enumerator initial values with kind suffixes Fixes #243 --- ford/sourceform.py | 26 ++++++++++++++++++++++++-- test/test_sourceform.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/ford/sourceform.py b/ford/sourceform.py index c3953b33..12ecd8d9 100644 --- a/ford/sourceform.py +++ b/ford/sourceform.py @@ -45,6 +45,8 @@ VAR_TYPE_STRING = r"^integer|real|double\s*precision|character|complex|double\s*complex|logical|type|class|procedure|enumerator" VARKIND_RE = re.compile(r"\((.*)\)|\*\s*(\d+|\(.*\))") KIND_RE = re.compile(r"kind\s*=\s*", re.IGNORECASE) +KIND_SUFFIX_RE = re.compile(r"(?P.*)_(?P[a-z]\w*)", re.IGNORECASE) +CHAR_KIND_SUFFIX_RE = re.compile(r"(?P[a-z]\w*)_(?P.*)", re.IGNORECASE) LEN_RE = re.compile(r"len\s*=\s*", re.IGNORECASE) ATTRIBSPLIT_RE = re.compile(r",\s*(\w.*?)::\s*(.*)\s*") ATTRIBSPLIT2_RE = re.compile(r"\s*(::)?\s*(.*)\s*") @@ -2030,10 +2032,19 @@ def _cleanup(self): for var in self.variables: if not var.initial: var.initial = prev_val + 1 + + initial = ( + remove_kind_suffix(var.initial) + if isinstance(var.initial, str) + else var.initial + ) + try: - prev_val = int(var.initial) + prev_val = int(initial) except ValueError: - raise Exception("Non-integer assigned to enumerator.") + raise ValueError( + f"Non-integer ('{var.initial}') assigned to enumerator '{var.name}'." + ) class FortranInterface(FortranContainer): @@ -2677,6 +2688,17 @@ def lines_description(self, total, total_all=0): ] +def remove_kind_suffix(literal, is_character: bool = False): + """Return the literal without the kind suffix of a numerical literal, + or the kind prefix of a character literal""" + + kind_re = CHAR_KIND_SUFFIX_RE if is_character else KIND_SUFFIX_RE + kind_suffix = kind_re.match(literal) + if kind_suffix: + return kind_suffix.group("initial") + return literal + + def line_to_variables(source, line, inherit_permission, parent): """ Returns a list of variables declared in the provided line of code. The diff --git a/test/test_sourceform.py b/test/test_sourceform.py index 4cfc2a46..3c2ff6a7 100644 --- a/test/test_sourceform.py +++ b/test/test_sourceform.py @@ -213,6 +213,36 @@ def test_format_statement(parse_fortran_file): assert fortran_file.programs[0].calls == [] +def test_enumerator_with_kind(parse_fortran_file): + """Checking enumerators with specified kind, issue #293""" + + data = """\ + module some_enums + use, intrinsic :: iso_fortran_env, only : int32 + enum, bind(c) + enumerator :: item1, item2 + enumerator :: list1 = 100_int32, list2 + enumerator :: fixed_item1 = 0, fixed_item2 + end enum + end module some_enums + """ + + fortran_file = parse_fortran_file(data) + enum = fortran_file.modules[0].enums[0] + assert enum.variables[0].name == "item1" + assert enum.variables[0].initial == 0 + assert enum.variables[1].name == "item2" + assert enum.variables[1].initial == 1 + assert enum.variables[2].name == "list1" + assert enum.variables[2].initial == "100_int32" + assert enum.variables[3].name == "list2" + assert enum.variables[3].initial == 101 + assert enum.variables[4].name == "fixed_item1" + assert enum.variables[4].initial == "0" + assert enum.variables[5].name == "fixed_item2" + assert enum.variables[5].initial == 1 + + class FakeModule(FortranModule): def __init__( self, procedures: dict, interfaces: dict, types: dict, variables: dict From 556164e5a6b53534cf68ddc058baf1f2aef8e04d Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 8 Sep 2021 17:12:58 +0100 Subject: [PATCH 04/10] Add docstring for test --- test/test_sourceform.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_sourceform.py b/test/test_sourceform.py index 3c2ff6a7..42d34e85 100644 --- a/test/test_sourceform.py +++ b/test/test_sourceform.py @@ -201,6 +201,9 @@ def test_component_access(parse_fortran_file): def test_format_statement(parse_fortran_file): + """No function calls in `format` statements are allowed, so don't + confuse them with format specifiers. Issue #350""" + data = """\ program test_format_statement implicit none From d3cbd6a5368719d7f32adcfe97d871f8cdaa6069 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 8 Sep 2021 18:36:13 +0100 Subject: [PATCH 05/10] Ignore docmarks inside string literals Fixes #320 --- ford/reader.py | 65 +++++++++++++++++++++++++++++------------ test/conftest.py | 11 +++++++ test/test_reader.py | 65 +++++++++++++++++++++++++++++++++-------- test/test_sourceform.py | 6 ++-- 4 files changed, 113 insertions(+), 34 deletions(-) create mode 100644 test/conftest.py diff --git a/ford/reader.py b/ford/reader.py index 70ec8aa6..852c4c17 100644 --- a/ford/reader.py +++ b/ford/reader.py @@ -37,6 +37,44 @@ from ford.fixed2free2 import convertToFree +def _contains_unterminated_string(string: str) -> bool: + """Return True if `string` contains an unterminated quote""" + in_quote = False + current_quote = None + previous_char = None + for char in string: + # Non-quote characters don't bother us + if not (char == "'" or char == '"'): + previous_char = char + continue + # Doubling-up a quote character doesn't make us leave a quote + if char == previous_char: + previous_char = char + continue + # If the current character is the same as the starting quote character + # then we have left the quote (as we've dealt with doubled-quotes) + if char == current_quote: + in_quote = False + current_quote = None + previous_char = char + continue + # If we weren't in a quote before, we are now + if not in_quote: + current_quote = char + in_quote = True + previous_char = char + return in_quote + + +def _match_docmark(docmark, line: str, in_quote: bool): + """If docmark exists, and we're not in a string literal, try to match it""" + if in_quote: + return None + if not docmark: + return None + return docmark.match(line) + + class FortranReader(object): """ An iterator which will convert a free-form Fortran source file into @@ -188,23 +226,20 @@ def __next__(self): # Python 3 reading_predoc = False reading_predoc_alt = 0 linebuffer = "" + while not done: - if sys.version_info[0] > 2: - line = self.reader.__next__() - else: # Python 2 - line = self.reader.next() + line = next(self.reader) self.line_number += 1 + in_quote = _contains_unterminated_string(linebuffer) + if len(line.strip()) > 0 and line.strip()[0] == "#": continue # Capture any preceding documenation comments - if self.predoc_re: - match = self.predoc_re.match(line) - else: - match = False + match = _match_docmark(self.predoc_re, line, in_quote) if match: # Switch to predoc: following comment lines are predoc until the end of the block reading_predoc = True @@ -222,10 +257,7 @@ def __next__(self): # Python 3 ) # Check for alternate preceding documentation - if self.predoc_alt_re: - match = self.predoc_alt_re.match(line) - else: - match = False + match = _match_docmark(self.predoc_alt_re, line, in_quote) if match: # Switch to doc_alt: following comment lines are documentation until end of the block reading_predoc_alt = 1 @@ -244,10 +276,7 @@ def __next__(self): # Python 3 ) # Check for alternate succeeding documentation - if self.doc_alt_re: - match = self.doc_alt_re.match(line) - else: - match = False + match = _match_docmark(self.doc_alt_re, line, in_quote) if match: # Switch to doc_alt: following comment lines are documentation until end of the block self.reading_alt = 1 @@ -265,7 +294,7 @@ def __next__(self): # Python 3 ) # Capture any documentation comments - match = self.doc_re.match(line) + match = _match_docmark(self.doc_re, line, in_quote) if match: self.reading_alt = 0 reading_predoc_alt = 0 @@ -279,7 +308,7 @@ def __next__(self): # Python 3 reading_predoc_alt = 0 # Remove any regular comments, unless following an alternative (pre)docmark - match = self.COM_RE.match(line) + match = _match_docmark(self.COM_RE, line, in_quote) if match: if (reading_predoc_alt > 1 or self.reading_alt > 1) and len( line[0 : match.start(4)].strip() diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..c12cca2f --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.fixture +def copy_fortran_file(tmp_path): + def copy_file(data): + filename = tmp_path / "test.f90" + with open(filename, "w") as f: + f.write(data) + return filename + return copy_file diff --git a/test/test_reader.py b/test/test_reader.py index 2a072ff9..30fdcca1 100644 --- a/test/test_reader.py +++ b/test/test_reader.py @@ -9,6 +9,7 @@ import re import ford.reader as reader +from ford.reader import _contains_unterminated_string RE_WHITE = re.compile(r"\s+") @@ -49,7 +50,7 @@ def test_reader_test_data(): assert lines == elines -def test_reader_continuation(tmp_path): +def test_reader_continuation(copy_fortran_file): """Checks that line continuations are handled correctly""" data = """\ @@ -61,15 +62,13 @@ def test_reader_continuation(tmp_path): end """ - filename = tmp_path / "test.f90" - with open(filename, "w") as f: - f.write(data) + filename = copy_fortran_file(data) lines = list(reader.FortranReader(filename, docmark="!")) assert lines == ["program foo", "!! some docs", "integer :: bar = 4", "end"] -def test_type(tmp_path): +def test_type(copy_fortran_file): """Check that types can be read""" data = """\ @@ -93,15 +92,13 @@ def test_type(tmp_path): "end type", ] - filename = tmp_path / "test.f90" - with open(filename, "w") as f: - f.write(data) + filename = copy_fortran_file(data) lines = list(reader.FortranReader(filename, docmark="!")) assert lines == expected -def test_unknown_include(tmp_path): +def test_unknown_include(copy_fortran_file): """Check that `include "file.h"` ignores unknown files""" data = """\ @@ -116,9 +113,53 @@ def test_unknown_include(tmp_path): "end program test", ] - filename = tmp_path / "test.f90" - with open(filename, "w") as f: - f.write(data) + filename = copy_fortran_file(data) lines = list(reader.FortranReader(filename, docmark="!")) assert lines == expected + + +def test_unterminated_strings(): + """Check the utility function works""" + assert _contains_unterminated_string(""" bad "quote """) + assert _contains_unterminated_string(""" bad 'quote """) + assert _contains_unterminated_string(""" bad "'quote """) + assert _contains_unterminated_string(""" bad 'quote" """) + assert _contains_unterminated_string(""" "multiple" bad "'quote """) + assert _contains_unterminated_string(""" bad 'quote" """) + + assert not _contains_unterminated_string(""" good "quote" """) + assert not _contains_unterminated_string(""" good 'quote' """) + assert not _contains_unterminated_string(""" good "'quote" """) + assert not _contains_unterminated_string(""" good 'quote"' """) + assert not _contains_unterminated_string(""" "multiple" good "'quote" """) + assert not _contains_unterminated_string(""" good 'quote"' """) + + +def test_multiline_string(copy_fortran_file): + """Check that we can continue string literals including exclamation + marks over multiple lines. Issue #320""" + + data = '''\ + program multiline_string + implicit none + print*, 'dont''t', " get ""!>quotes!@""", " '""!quotes!@""", " '""! Date: Thu, 9 Sep 2021 17:19:41 +0100 Subject: [PATCH 06/10] Check subroutine attributes case insensitively Fixes #353 --- ford/sourceform.py | 78 +++++++++++++++++------------------------ test/test_sourceform.py | 48 +++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 45 deletions(-) diff --git a/ford/sourceform.py b/ford/sourceform.py index 12ecd8d9..e20ee653 100644 --- a/ford/sourceform.py +++ b/ford/sourceform.py @@ -26,6 +26,7 @@ import re import os.path import copy +from typing import List # Python 2 or 3: if sys.version_info[0] > 2: @@ -1558,6 +1559,31 @@ def _cleanup(self): self.all_procs[proc.name.lower()] = proc +def _list_of_procedure_attributes(attribute_string: str) -> List[str]: + """Convert a string of attributes into a list of attributes""" + if not attribute_string: + return [], "" + + attribute_list = [] + attribute_string = attribute_string.lower() + + for attribute in [ + "impure", + "pure", + "elemental", + "non_recursive", + "recursive", + "module", + ]: + if attribute in attribute_string: + attribute_list.append(attribute) + attribute_string = re.sub( + attribute, "", attribute_string, flags=re.IGNORECASE + ) + + return attribute_list, attribute_string.replace(" ", "") + + class FortranSubroutine(FortranCodeUnit): """ An object representing a Fortran subroutine and holding all of said @@ -1570,29 +1596,9 @@ def _initialize(self, line): attribstr = line.group(1) self.module = False self.mp = False - if not attribstr: - attribstr = "" - self.attribs = [] - if attribstr.find("impure") >= 0: - self.attribs.append("impure") - attribstr = attribstr.replace("impure", "", 1) - if attribstr.find("pure") >= 0: - self.attribs.append("pure") - attribstr = attribstr.replace("pure", "", 1) - if attribstr.find("elemental") >= 0: - self.attribs.append("elemental") - attribstr = attribstr.replace("elemental", "", 1) - if attribstr.find("non_recursive") >= 0: - self.attribs.append("non_recursive") - attribstr = attribstr.replace("non_recursive", "", 1) - if attribstr.find("recursive") >= 0: - self.attribs.append("recursive") - attribstr = attribstr.replace("recursive", "", 1) - if attribstr.find("module") >= 0: - self.module = True - attribstr = attribstr.replace("module", "", 1) - attribstr = re.sub(" ", "", attribstr) - # ~ self.name = line.group(2) + self.attribs, attribstr = _list_of_procedure_attributes(attribstr) + self.module = "module" in self.attribs + self.args = [] if line.group(3): if self.SPLIT_RE.split(line.group(3)[1:-1]): @@ -1674,28 +1680,10 @@ def _initialize(self, line): attribstr = line.group(1) self.module = False self.mp = False - if not attribstr: - attribstr = "" - self.attribs = [] - if attribstr.lower().find("impure") >= 0: - self.attribs.append("impure") - attribstr = re.sub("impure", "", attribstr, 0, re.IGNORECASE) - if attribstr.lower().find("pure") >= 0: - self.attribs.append("pure") - attribstr = re.sub("pure", "", attribstr, 0, re.IGNORECASE) - if attribstr.lower().find("elemental") >= 0: - self.attribs.append("elemental") - attribstr = re.sub("elemental", "", attribstr, 0, re.IGNORECASE) - if attribstr.lower().find("non_recursive") >= 0: - self.attribs.append("non_recursive") - attribstr = re.sub("non_recursive", "", attribstr, 0, re.IGNORECASE) - if attribstr.lower().find("recursive") >= 0: - self.attribs.append("recursive") - attribstr = re.sub("recursive", "", attribstr, 0, re.IGNORECASE) - if attribstr.lower().find("module") >= 0: - self.module = True - attribstr = re.sub("module", "", attribstr, 0, re.IGNORECASE) - attribstr = re.sub(" ", "", attribstr) + + self.attribs, attribstr = _list_of_procedure_attributes(attribstr) + self.module = "module" in self.attribs + if line.group(4): self.retvar = line.group(4) else: diff --git a/test/test_sourceform.py b/test/test_sourceform.py index d06732cd..16653e62 100644 --- a/test/test_sourceform.py +++ b/test/test_sourceform.py @@ -508,3 +508,51 @@ def test_module_private_access(parse_fortran_file): "int_public", "real_public", } + + +def test_module_procedure_case(parse_fortran_file): + """Check that submodule procedures in interface blocks are parsed correctly. Issue #353""" + data = """\ + module a + implicit none + interface + MODULE SUBROUTINE square( x ) + integer, intent(inout):: x + END SUBROUTINE square + module subroutine cube( x ) + integer, intent(inout):: x + end subroutine cube + MODULE FUNCTION square_func( x ) + integer, intent(in):: x + END FUNCTION square_func + module function cube_func( x ) + integer, intent(inout):: x + end function cube_func + end interface + end module a + + submodule (a) b + implicit none + contains + MODULE PROCEDURE square + x = x * x + END PROCEDURE square + module PROCEDURE cube + x = x * x * x + END PROCEDURE cube + MODULE PROCEDURE square_func + square_func = x * x + END PROCEDURE square_func + module procedure cube_func + cube_func = x * x * x + end procedure cube_func + end submodule b + """ + + fortran_file = parse_fortran_file(data) + module = fortran_file.modules[0] + assert len(module.interfaces) == 4 + assert module.interfaces[0].procedure.module + assert module.interfaces[1].procedure.module + assert module.interfaces[2].procedure.module + assert module.interfaces[3].procedure.module From d206d9d6802fffcb0bb8c75d61886cac5e216ef5 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 9 Sep 2021 18:20:01 +0100 Subject: [PATCH 07/10] Start adding test for sourceform.parse_type --- test/test_sourceform.py | 103 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/test/test_sourceform.py b/test/test_sourceform.py index 16653e62..f26c10aa 100644 --- a/test/test_sourceform.py +++ b/test/test_sourceform.py @@ -1,4 +1,4 @@ -from ford.sourceform import FortranSourceFile, FortranModule +from ford.sourceform import FortranSourceFile, FortranModule, parse_type from collections import defaultdict @@ -556,3 +556,104 @@ def test_module_procedure_case(parse_fortran_file): assert module.interfaces[1].procedure.module assert module.interfaces[2].procedure.module assert module.interfaces[3].procedure.module + + +def test_parse_type(): + vartype, kind, strlen, proto, rest = parse_type( + "integer i", [], {"extra_vartypes": []} + ) + assert vartype == "integer" + assert kind is None + assert strlen is None + assert proto is None + assert rest == "i" + + vartype, kind, strlen, proto, rest = parse_type( + "integer :: i", [], {"extra_vartypes": []} + ) + assert vartype == "integer" + assert kind is None + assert strlen is None + assert proto is None + assert rest == ":: i" + + vartype, kind, strlen, proto, rest = parse_type( + "real(real64) r", [], {"extra_vartypes": []} + ) + assert vartype == "real" + assert kind == "real64" + assert strlen is None + assert proto is None + assert rest == "r" + + vartype, kind, strlen, proto, rest = parse_type( + "REAL( KIND = 8) :: r, x, y", [], {"extra_vartypes": []} + ) + assert vartype == "real" + assert kind == "8" + assert strlen is None + assert proto is None + assert rest == ":: r, x, y" + + vartype, kind, strlen, proto, rest = parse_type( + "character(len=*) :: string", [], {"extra_vartypes": []} + ) + assert vartype == "character" + assert kind is None + assert strlen == "*" + assert proto is None + assert rest == ":: string" + + vartype, kind, strlen, proto, rest = parse_type( + "character(len=:) :: string", [], {"extra_vartypes": []} + ) + assert vartype == "character" + assert kind is None + assert strlen == ":" + assert proto is None + assert rest == ":: string" + + vartype, kind, strlen, proto, rest = parse_type( + "character(12) :: string", [], {"extra_vartypes": []} + ) + assert vartype == "character" + assert kind is None + assert strlen == "12" + assert proto is None + assert rest == ":: string" + + vartype, kind, strlen, proto, rest = parse_type( + "character(LEN=12) :: string", [], {"extra_vartypes": []} + ) + assert vartype == "character" + assert kind is None + assert strlen == "12" + assert proto is None + assert rest == ":: string" + + vartype, kind, strlen, proto, rest = parse_type( + "CHARACTER(KIND=kanji, len =12) :: string", [], {"extra_vartypes": []} + ) + assert vartype == "character" + # assert kind == "kanji" + # assert strlen == "12" + # assert proto is None + assert rest == ":: string" + + vartype, kind, strlen, proto, rest = parse_type( + "CHARACTER( len = 12,KIND=kanji) :: string", [], {"extra_vartypes": []} + ) + assert vartype == "character" + # assert kind == "kanji" + # assert strlen == "12" + assert proto is None + assert rest == ":: string" + + vartype, kind, strlen, proto, rest = parse_type( + "character(kind= kanji) :: string", [], {"extra_vartypes": []} + ) + assert vartype == "character" + assert kind == "kanji" + assert strlen is None + assert proto is None + assert rest == ":: string" From 29df73d316b3c114b927385c5dc840e90510531d Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 9 Sep 2021 18:20:20 +0100 Subject: [PATCH 08/10] Invert some conditionals to reduce indentation in parse_type --- ford/sourceform.py | 91 +++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/ford/sourceform.py b/ford/sourceform.py index e20ee653..a98b0775 100644 --- a/ford/sourceform.py +++ b/ford/sourceform.py @@ -2838,55 +2838,56 @@ def parse_type(string, capture_strings, settings): and not kindstr.startswith("*") ): return (vartype, None, None, None, rest) + match = VARKIND_RE.search(kindstr) - if match: - if match.group(1): - star = False - args = match.group(1).strip() - else: - star = True - args = match.group(2).strip() - if args.startswith("("): - args = args[1:-1].strip() - - args = re.sub(r"\s", "", args) - if vartype == "type" or vartype == "class" or vartype == "procedure": - PROTO_RE = re.compile(r"(\*|\w+)\s*(?:\((.*)\))?") + if not match: + raise ValueError( + "Bad declaration of variable type {}: {}".format(vartype, string) + ) + + if match.group(1): + star = False + args = match.group(1).strip() + else: + star = True + args = match.group(2).strip() + if args.startswith("("): + args = args[1:-1].strip() + + args = re.sub(r"\s", "", args) + if vartype == "type" or vartype == "class" or vartype == "procedure": + PROTO_RE = re.compile(r"(\*|\w+)\s*(?:\((.*)\))?") + try: + proto = list(PROTO_RE.match(args).groups()) + if not proto[1]: + proto[1] = "" + except AttributeError: + raise Exception( + "Bad type, class, or procedure prototype specification: {}".format(args) + ) + return (vartype, None, None, proto, rest) + elif vartype == "character": + if star: + return (vartype, None, args, None, rest) + + kind = None + length = None + if KIND_RE.search(args): + kind = KIND_RE.sub("", args) try: - proto = list(PROTO_RE.match(args).groups()) - if not proto[1]: - proto[1] = "" + match = QUOTES_RE.search(kind) + num = int(match.group()[1:-1]) + kind = QUOTES_RE.sub(capture_strings[num], kind) except AttributeError: - raise Exception( - "Bad type, class, or procedure prototype specification: {}".format( - args - ) - ) - return (vartype, None, None, proto, rest) - elif vartype == "character": - if star: - return (vartype, None, args, None, rest) - else: - kind = None - length = None - if KIND_RE.search(args): - kind = KIND_RE.sub("", args) - try: - match = QUOTES_RE.search(kind) - num = int(match.group()[1:-1]) - kind = QUOTES_RE.sub(capture_strings[num], kind) - except AttributeError: - pass - elif LEN_RE.search(args): - length = LEN_RE.sub("", args) - else: - length = args - return (vartype, kind, length, None, rest) + pass + elif LEN_RE.search(args): + length = LEN_RE.sub("", args) else: - kind = KIND_RE.sub("", args) - return (vartype, kind, None, None, rest) - - raise Exception("Bad declaration of variable type {}: {}".format(vartype, string)) + length = args + return (vartype, kind, length, None, rest) + else: + kind = KIND_RE.sub("", args) + return (vartype, kind, None, None, rest) def set_base_url(url): From 516dd0e3ad64f6bc9e6b8f99d0972fdb93c39beb Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 9 Sep 2021 21:12:06 +0100 Subject: [PATCH 09/10] Fix for both len and kind type parameters in character --- ford/sourceform.py | 41 ++++++---- test/test_sourceform.py | 167 +++++++++++++++++----------------------- 2 files changed, 94 insertions(+), 114 deletions(-) diff --git a/ford/sourceform.py b/ford/sourceform.py index a98b0775..651027b5 100644 --- a/ford/sourceform.py +++ b/ford/sourceform.py @@ -45,10 +45,10 @@ VAR_TYPE_STRING = r"^integer|real|double\s*precision|character|complex|double\s*complex|logical|type|class|procedure|enumerator" VARKIND_RE = re.compile(r"\((.*)\)|\*\s*(\d+|\(.*\))") -KIND_RE = re.compile(r"kind\s*=\s*", re.IGNORECASE) +KIND_RE = re.compile(r"kind\s*=\s*(\w+)", re.IGNORECASE) KIND_SUFFIX_RE = re.compile(r"(?P.*)_(?P[a-z]\w*)", re.IGNORECASE) CHAR_KIND_SUFFIX_RE = re.compile(r"(?P[a-z]\w*)_(?P.*)", re.IGNORECASE) -LEN_RE = re.compile(r"len\s*=\s*", re.IGNORECASE) +LEN_RE = re.compile(r"(?:len\s*=\s*(\w+|\*|:|\d+)|(\d+))", re.IGNORECASE) ATTRIBSPLIT_RE = re.compile(r",\s*(\w.*?)::\s*(.*)\s*") ATTRIBSPLIT2_RE = re.compile(r"\s*(::)?\s*(.*)\s*") ASSIGN_RE = re.compile(r"(\w+\s*(?:\([^=]*\)))\s*=(?!>)(?:\s*([^\s]+))?") @@ -2870,23 +2870,30 @@ def parse_type(string, capture_strings, settings): if star: return (vartype, None, args, None, rest) - kind = None - length = None - if KIND_RE.search(args): - kind = KIND_RE.sub("", args) - try: - match = QUOTES_RE.search(kind) - num = int(match.group()[1:-1]) - kind = QUOTES_RE.sub(capture_strings[num], kind) - except AttributeError: - pass - elif LEN_RE.search(args): - length = LEN_RE.sub("", args) - else: - length = args + args = args.split(",") + + for arg in args: + kind = KIND_RE.match(arg) + if kind: + kind = kind.group(1) + try: + match = QUOTES_RE.search(kind) + num = int(match.group()[1:-1]) + kind = QUOTES_RE.sub(capture_strings[num], kind) + except AttributeError: + pass + break + + for arg in args: + length = LEN_RE.match(arg) + if length: + length = length.group(1) or length.group(2) + break + return (vartype, kind, length, None, rest) else: - kind = KIND_RE.sub("", args) + kind = KIND_RE.match(args) + kind = kind.group(1) if kind else args return (vartype, kind, None, None, rest) diff --git a/test/test_sourceform.py b/test/test_sourceform.py index f26c10aa..9a7de3a2 100644 --- a/test/test_sourceform.py +++ b/test/test_sourceform.py @@ -1,6 +1,8 @@ from ford.sourceform import FortranSourceFile, FortranModule, parse_type from collections import defaultdict +from dataclasses import dataclass +from typing import Union import pytest @@ -558,102 +560,73 @@ def test_module_procedure_case(parse_fortran_file): assert module.interfaces[3].procedure.module -def test_parse_type(): +@dataclass +class ParsedType: + vartype: str + kind: Union[None, str] + strlen: Union[None, str] + proto: Union[None, str] + rest: str + + +@pytest.mark.parametrize( + ["variable_decl", "expected"], + [ + ("integer i", ParsedType("integer", None, None, None, "i")), + ("integer :: i", ParsedType("integer", None, None, None, ":: i")), + ("integer ( int32 ) :: i", ParsedType("integer", "int32", None, None, ":: i")), + ("real r", ParsedType("real", None, None, None, "r")), + ("real(real64) r", ParsedType("real", "real64", None, None, "r")), + ( + "REAL( KIND = 8) :: r, x, y", + ParsedType("real", "8", None, None, ":: r, x, y"), + ), + ( + "REAL( 8 ) :: r, x, y", + ParsedType("real", "8", None, None, ":: r, x, y"), + ), + ( + "complex*16 znum", + ParsedType("complex", "16", None, None, "znum"), + ), + ( + "character(len=*) :: string", + ParsedType("character", None, "*", None, ":: string"), + ), + ( + "character(len=:) :: string", + ParsedType("character", None, ":", None, ":: string"), + ), + ( + "character(12) :: string", + ParsedType("character", None, "12", None, ":: string"), + ), + ( + "character(LEN=12) :: string", + ParsedType("character", None, "12", None, ":: string"), + ), + ( + "CHARACTER(KIND=kanji, len =12) :: string", + ParsedType("character", "kanji", "12", None, ":: string"), + ), + ( + "CHARACTER( len = 12,KIND=kanji) :: string", + ParsedType("character", "kanji", "12", None, ":: string"), + ), + ( + "CHARACTER( kind= kanji) :: string", + ParsedType("character", "kanji", None, None, ":: string"), + ), + ("double PRECISION dp", ParsedType("double precision", None, None, None, "dp")), + ("DOUBLE complex dc", ParsedType("double complex", None, None, None, "dc")), + ], +) +def test_parse_type(variable_decl, expected): vartype, kind, strlen, proto, rest = parse_type( - "integer i", [], {"extra_vartypes": []} + variable_decl, [], {"extra_vartypes": []} ) - assert vartype == "integer" - assert kind is None - assert strlen is None - assert proto is None - assert rest == "i" - - vartype, kind, strlen, proto, rest = parse_type( - "integer :: i", [], {"extra_vartypes": []} - ) - assert vartype == "integer" - assert kind is None - assert strlen is None - assert proto is None - assert rest == ":: i" - - vartype, kind, strlen, proto, rest = parse_type( - "real(real64) r", [], {"extra_vartypes": []} - ) - assert vartype == "real" - assert kind == "real64" - assert strlen is None - assert proto is None - assert rest == "r" - - vartype, kind, strlen, proto, rest = parse_type( - "REAL( KIND = 8) :: r, x, y", [], {"extra_vartypes": []} - ) - assert vartype == "real" - assert kind == "8" - assert strlen is None - assert proto is None - assert rest == ":: r, x, y" - - vartype, kind, strlen, proto, rest = parse_type( - "character(len=*) :: string", [], {"extra_vartypes": []} - ) - assert vartype == "character" - assert kind is None - assert strlen == "*" - assert proto is None - assert rest == ":: string" - - vartype, kind, strlen, proto, rest = parse_type( - "character(len=:) :: string", [], {"extra_vartypes": []} - ) - assert vartype == "character" - assert kind is None - assert strlen == ":" - assert proto is None - assert rest == ":: string" - - vartype, kind, strlen, proto, rest = parse_type( - "character(12) :: string", [], {"extra_vartypes": []} - ) - assert vartype == "character" - assert kind is None - assert strlen == "12" - assert proto is None - assert rest == ":: string" - - vartype, kind, strlen, proto, rest = parse_type( - "character(LEN=12) :: string", [], {"extra_vartypes": []} - ) - assert vartype == "character" - assert kind is None - assert strlen == "12" - assert proto is None - assert rest == ":: string" - - vartype, kind, strlen, proto, rest = parse_type( - "CHARACTER(KIND=kanji, len =12) :: string", [], {"extra_vartypes": []} - ) - assert vartype == "character" - # assert kind == "kanji" - # assert strlen == "12" - # assert proto is None - assert rest == ":: string" - - vartype, kind, strlen, proto, rest = parse_type( - "CHARACTER( len = 12,KIND=kanji) :: string", [], {"extra_vartypes": []} - ) - assert vartype == "character" - # assert kind == "kanji" - # assert strlen == "12" - assert proto is None - assert rest == ":: string" - - vartype, kind, strlen, proto, rest = parse_type( - "character(kind= kanji) :: string", [], {"extra_vartypes": []} - ) - assert vartype == "character" - assert kind == "kanji" - assert strlen is None - assert proto is None - assert rest == ":: string" + assert vartype == expected.vartype + assert kind == expected.kind + assert strlen == expected.strlen + assert proto == expected.proto + assert rest == expected.rest From b7a66f4cd816ea3470b8db65cc72a1ab94df4dad Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 9 Sep 2021 21:25:43 +0100 Subject: [PATCH 10/10] Add more tests for parse_type --- test/test_sourceform.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_sourceform.py b/test/test_sourceform.py index 9a7de3a2..22e30fac 100644 --- a/test/test_sourceform.py +++ b/test/test_sourceform.py @@ -619,6 +619,18 @@ class ParsedType: ), ("double PRECISION dp", ParsedType("double precision", None, None, None, "dp")), ("DOUBLE complex dc", ParsedType("double complex", None, None, None, "dc")), + ( + "type(something) :: thing", + ParsedType("type", None, None, ["something", ""], ":: thing"), + ), + ( + "class(foo) :: thing", + ParsedType("class", None, None, ["foo", ""], ":: thing"), + ), + ( + "procedure(bar) :: thing", + ParsedType("procedure", None, None, ["bar", ""], ":: thing"), + ), ], ) def test_parse_type(variable_decl, expected):