diff --git a/CHANGELOG.md b/CHANGELOG.md index 66071843..96b152b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Modifications by (in alphabetical order): * P. Vitt, University of Siegen, Germany * A. Voysey, UK Met Office +03/04/2023 PR #392 for #326. Add support for F2008 block and critical constructs. + 30/03/2023 PR #396 for #395. Fix trailing whitespace bug in CallBase. 13/03/2023 PR #391 for #324. Add GH workfow to automate a pypi upload during diff --git a/doc/source/developers_guide.rst b/doc/source/developers_guide.rst index 76c0cd2b..f1cf2308 100644 --- a/doc/source/developers_guide.rst +++ b/doc/source/developers_guide.rst @@ -170,9 +170,11 @@ returned. An example of a simple choice rule is `R202`. See the :ref:`program-unit-class` section for a description of its implementation. -.. note:: - - A `use_names` description, explanation and example needs to be added. +The `use_names` list should contain any classes that are referenced by the +implementation of the current class. These lists of names are aggregated +(along with `subclass_names`) and used to ensure that all necessary `Scalar_`, +`_List` and `_Name` classes are generated (in code at the end of the +`Fortran2003` and `Fortran2008` modules - see :ref:`class-generation`). When the rule is not a simple choice the developer needs to supply a static `match` method. An example of this is rule `R201`. See the @@ -294,10 +296,13 @@ there is no name associated with such a program, the corresponding symbol table is given the name "fparser2:main_program", chosen so as to prevent any clashes with other Fortran names. -Those classes taken to define scoping regions are stored as -a list within the `SymbolTables` instance. This list is populated -after the class hierarchy has been constructed for the parser (since -this depends on which Fortran standard has been chosen). +Those classes which define scoping regions must subclass the +`ScopingRegionMixin` class: + +.. autoclass:: fparser.two.utils.ScopingRegionMixin + + +.. _class-generation: Class Generation ++++++++++++++++ diff --git a/src/fparser/.pylintrc b/src/fparser/.pylintrc index 7a937382..4d9c1bef 100644 --- a/src/fparser/.pylintrc +++ b/src/fparser/.pylintrc @@ -3,9 +3,12 @@ # Maximum number of characters on a single line. Black's default is 88. max-line-length=88 -# fparser dynamically generates *_List classes so pylint can't -# find them. -generated-members=Fortran2003.*_List,Fortran2008.*_List +[TYPECHECK] + +# fparser generates *_List classes at runtime so pylint can't +# find them (as it's a static checker). +ignored-modules=fparser.two.Fortran2003,fparser.two.Fortran2008 +generated-members=fparser.two.Fortran2003.*_List,fparser.two.Fortran2008.*_List [DESIGN] # Maximum number of parents for a class (see R0901) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 6c913a23..d314d1e4 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -93,6 +93,7 @@ CALLBase, CallBase, KeywordValueBase, + ScopingRegionMixin, SeparatorBase, SequenceBase, UnaryOpBase, @@ -7036,7 +7037,13 @@ class If_Construct(BlockBase): # R802 """ subclass_names = [] - use_names = ["If_Then_Stmt", "Block", "Else_If_Stmt", "Else_Stmt", "End_If_Stmt"] + use_names = [ + "If_Then_Stmt", + "Execution_Part_Construct", + "Else_If_Stmt", + "Else_Stmt", + "End_If_Stmt", + ] @staticmethod def match(string): @@ -10882,7 +10889,7 @@ def match(reader): return result -class Program_Stmt(StmtBase, WORDClsBase): # R1102 +class Program_Stmt(StmtBase, WORDClsBase, ScopingRegionMixin): # R1102 """ Fortran 2003 rule R1102:: @@ -10973,7 +10980,7 @@ def match(reader): ) -class Module_Stmt(StmtBase, WORDClsBase): # R1105 +class Module_Stmt(StmtBase, WORDClsBase, ScopingRegionMixin): # R1105 """ = MODULE """ @@ -12472,7 +12479,7 @@ def match(reader): ) -class Function_Stmt(StmtBase): # R1224 +class Function_Stmt(StmtBase, ScopingRegionMixin): # R1224 """ :: @@ -12790,7 +12797,7 @@ def c1242_valid(prefix, binding_spec): return True -class Subroutine_Stmt(StmtBase): # R1232 +class Subroutine_Stmt(StmtBase, ScopingRegionMixin): # R1232 """ Fortran2003 rule R1232:: diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index 1db267af..ddea5401 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -1,5 +1,5 @@ -# Modified work Copyright (c) 2018-2022 Science and Technology -# Facilities Council +# Modified work Copyright (c) 2018-2023 Science and Technology +# Facilities Council. # Original work Copyright (c) 1999-2008 Pearu Peterson # All rights reserved. @@ -80,41 +80,44 @@ from fparser.common.splitline import string_replace_map, splitparen from fparser.two import pattern_tools as pattern - from fparser.two.utils import ( BracketBase, CALLBase, KeywordValueBase, NoMatchError, + ScopingRegionMixin, SeparatorBase, StmtBase, STRINGBase, Type_Declaration_StmtBase, WORDClsBase, ) + from fparser.two.Fortran2003 import ( - EndStmtBase, - BlockBase, - SequenceBase, Base, - Specification_Part, - Stat_Variable, - Errmsg_Variable, - Source_Expr, - Module_Subprogram_Part, - Implicit_Part, - Implicit_Part_Stmt, + BlockBase, + Component_Decl_List, Declaration_Construct, - Use_Stmt, + Declaration_Type_Spec, + EndStmtBase, + Entity_Decl_List, + Errmsg_Variable, + Execution_Part_Construct, File_Name_Expr, File_Unit_Number, + Implicit_Part, + Implicit_Part_Stmt, Import_Stmt, Iomsg_Variable, Label, - Declaration_Type_Spec, - Entity_Decl_List, - Component_Decl_List, + Module_Subprogram_Part, + Name, + SequenceBase, + Source_Expr, + Specification_Part, + Stat_Variable, Stop_Code, + Use_Stmt, ) # Import of F2003 classes that are updated in this standard. @@ -182,14 +185,14 @@ class Executable_Construct(Executable_Construct_2003): # R213 "C201 (R208) An execution-part shall not contain an end-function-stmt, end-mp-subprogram-stmt, end-program-stmt, or end-subroutine-stmt." - NB: The new block-construct and critical-construct are not yet implemented. - TODO: Implement missing F2008 executable-construct (#320) - """ + subclass_names = [ "Action_Stmt", "Associate_Construct", + "Block_Construct", "Case_Construct", + "Critical_Construct", "Do_Construct", "Forall_Construct", "If_Construct", @@ -894,13 +897,13 @@ def match(reader): param reader: the fortran file reader containing the line(s) of code that we are trying to match - :type reader: :py:class:`fparser.common.readfortran.FortranFileReader` - or - :py:class:`fparser.common.readfortran.FortranStringReader` - :return: `tuple` containing a single `list` which contains + :type reader: :py:class:`fparser.common.readfortran.FortranFileReader` \ + | :py:class:`fparser.common.readfortran.FortranStringReader` + + :returns: `tuple` containing a single `list` which contains instance of the classes that have matched if there is a match or `None` if there is no match - + :rtype: Optional[Tuple[List[:py:class:`fparser.two.utils.Base`]]] """ return BlockBase.match( None, @@ -1029,7 +1032,7 @@ def match(reader): return result -class Submodule_Stmt(Base): # R1117 +class Submodule_Stmt(Base, ScopingRegionMixin): # R1117 """ Fortran 2008 rule R1117:: @@ -1341,6 +1344,240 @@ def match(string): return None +class Block_Construct(BlockBase): + """ + Fortran 2008 Rule 807. + + block-construct is block-stmt + [ specification-part ] + block + end-block-stmt + + TODO #394: Should disallow COMMON, EQUIVALENCE, IMPLICIT, INTENT, + NAMELIST, OPTIONAL, VALUE, and statement functions (C806) (which are all + valid members of Specification_Part). + """ + + subclass_names = [] + use_names = [ + "Block_Stmt", + "Specification_Part", + "Execution_Part_Construct", + "End_Block_Stmt", + ] + + @staticmethod + def match(reader): + return BlockBase.match( + Block_Stmt, + [Specification_Part, Execution_Part_Construct], + End_Block_Stmt, + reader, + match_names=True, # C810 + strict_match_names=True, # C810 + ) + + +class Block_Stmt(StmtBase, WORDClsBase, ScopingRegionMixin): + """ + Fortran 2008 Rule 808. + + block-stmt is [ block-construct-name : ] BLOCK + + """ + + subclass_names = [] + use_names = ["Block_Construct_Name"] + counter = 0 + + @staticmethod + def match(string): + """ + Attempts to match the supplied text with this rule. + + :param str string: the text to match. + + :returns: a tuple of the matched node and instance of Counter or \ + None if there is no match. + :rtype: Tuple["BLOCK", \ + :py:class:`fparser.two.Fortran2008.Block_Stmt.Counter`] \ + | NoneType + """ + found = WORDClsBase.match("BLOCK", None, string) + if not found: + return None + block, _ = found + # Construct a unique name for this BLOCK (in case it isn't named). We + # ensure the name is not a valid Fortran name so that it can't clash + # with any regions named in the code. + scope_name = f"block:{Block_Stmt.counter}" + Block_Stmt.counter += 1 + # TODO #397. Ideally we'd have the name associated with the Block + # Construct here (if any) so that it could be displayed in repr. + # As it is, repr will show scope_name which will not be the same + # as any explicit name given to the Block. (This name *is* shown + # in the repr of the End_Block_Stmt.) This problem is common to + # other block constructs such as Block_Nonlabel_Do_Construct. + return block, scope_name + + def get_scope_name(self): + """ + :returns: the name of this scoping region. + :rtype: str + """ + if self.item.name: + return self.item.name + return self.items[1] + + def get_start_name(self): + """ + :returns: the name associated with this Block construct or None. + :rtype: str | NoneType + """ + return self.item.name + + def tostr(self): + """ + :returns: the string representation of this node. + :rtype: str + """ + return "BLOCK" + + +class End_Block_Stmt(EndStmtBase): # R809 + """ + Fortran 2008 Rule 809. + + end-block-stmt is END BLOCK [ block-construct-name ] + + """ + + subclass_names = [] + use_names = ["Block_Construct_Name"] + + @staticmethod + def match(string): + """ + :param str string: Fortran code to check for a match + + :return: 2-tuple containing "BLOCK" and, optionally, an associated \ + Name or None if no match. + :rtype: Optional[Tuple[ + str, + Optional[:py:class:`fparser.two.Fortran2003.Block_Construct_Name`]]] + + """ + return EndStmtBase.match( + "BLOCK", Block_Construct_Name, string, require_stmt_type=True + ) + + +class Critical_Construct(BlockBase): + """ + Fortran 2008 Rule 810. + + critical-construct is critical-stmt + block + end-critical-stmt + + TODO: Should disallow RETURN (C809) and CYCLE or EXIT to outside block (C811) + """ + + subclass_names = [] + use_names = ["Critical_Stmt", "Execution_Part_Construct", "End_Critical_Stmt"] + + @staticmethod + def match(reader): + """ + Attempt to match the supplied content with this Rule. + + :param reader: the fortran file reader containing the line(s) + of code that we are trying to match + :type reader: :py:class:`fparser.common.readfortran.FortranFileReader` \ + | :py:class:`fparser.common.readfortran.FortranStringReader` + + :returns: instance of class that has matched or `None` if no match. + :rtype: :py:class:`fparser.two.utils.BlockBase` | NoneType + + """ + return BlockBase.match( + Critical_Stmt, + [Execution_Part_Construct], + End_Critical_Stmt, + reader, + match_names=True, # C810 + strict_match_names=True, # C810 + ) + + +class Critical_Stmt(StmtBase, WORDClsBase): + """ + Fortran 2008 Rule R811. + + critical-stmt is [ critical-construct-name : ] CRITICAL + + """ + + subclass_names = [] + use_names = ["Critical_Construct_Name"] + + @staticmethod + def match(string): + """ + Attempts to match the supplied string as a CRITICAL statement. + + :param str string: the string to attempt to match. + + :returns: 2-tuple containing the matched word "CRITICAL" and None or \ + None if no match. + :rtype: Tuple[str, NoneType] or NoneType + + """ + return WORDClsBase.match("CRITICAL", None, string) + + def get_start_name(self): + """ + :returns: the name associated with the start of this CRITICAL region (if any) + :rtype: str | NoneType + """ + return self.item.name + + def tostr(self): + """ + :returns: the string representation of this node. + :rtype: str + """ + return "CRITICAL" + + +class End_Critical_Stmt(EndStmtBase): + """ + Fortran 2008 Rule 812. + + end-critical-stmt is END CRITICAL [ critical-construct-name ] + + """ + + subclass_names = [] + use_names = ["Critical_Construct_Name"] + + @staticmethod + def match(string): + """ + :param str string: Fortran code to check for a match + + :returns: 2-tuple containing "CRITICAL" and, optionally, an associated \ + Name or None if there is no match. + :rtype: Optional[Tuple[ + str, \ + Optional[:py:class:`fparser.two.Fortran2003.Critical_Construct_Name`]]] + + """ + return EndStmtBase.match( + "CRITICAL", Critical_Construct_Name, string, require_stmt_type=True + ) + + # # GENERATE Scalar_, _List, _Name CLASSES # @@ -1349,15 +1586,15 @@ def match(string): ClassType = type(Base) _names = dir() for clsname in _names: - cls = eval(clsname) + new_cls = eval(clsname) if not ( - isinstance(cls, ClassType) - and issubclass(cls, Base) - and not cls.__name__.endswith("Base") + isinstance(new_cls, ClassType) + and issubclass(new_cls, Base) + and not new_cls.__name__.endswith("Base") ): continue - names = getattr(cls, "subclass_names", []) + getattr(cls, "use_names", []) + names = getattr(new_cls, "subclass_names", []) + getattr(new_cls, "use_names", []) for n in names: if n in _names: continue @@ -1366,34 +1603,31 @@ def match(string): n = n[:-5] # Generate 'list' class exec( - """\ -class %s_List(SequenceBase): - subclass_names = [\'%s\'] + f"""\ +class {n}_List(SequenceBase): + subclass_names = [\'{n}\'] use_names = [] @staticmethod - def match(string): return SequenceBase.match(r\',\', %s, string) + def match(string): return SequenceBase.match(r\',\', {n}, string) """ - % (n, n, n) ) elif n.endswith("_Name"): _names.append(n) n = n[:-5] exec( - """\ -class %s_Name(Base): + f"""\ +class {n}_Name(Base): subclass_names = [\'Name\'] """ - % (n) ) elif n.startswith("Scalar_"): _names.append(n) n = n[7:] exec( - """\ -class Scalar_%s(Base): - subclass_names = [\'%s\'] + f"""\ +class Scalar_{n}(Base): + subclass_names = [\'{n}\'] """ - % (n, n) ) diff --git a/src/fparser/two/parser.py b/src/fparser/two/parser.py index d4404be1..6cf599be 100644 --- a/src/fparser/two/parser.py +++ b/src/fparser/two/parser.py @@ -1,4 +1,4 @@ -# Modified work Copyright (c) 2018-2022 Science and Technology +# Modified work Copyright (c) 2018-2023 Science and Technology # Facilities Council. # Original work Copyright (c) 1999-2008 Pearu Peterson @@ -65,30 +65,31 @@ """This file provides utilities to create a Fortran parser suitable for a particular standard.""" -# pylint: disable=eval-used import inspect +import logging import sys from fparser.two.symbol_table import SYMBOL_TABLES def get_module_classes(input_module): - """Return all classes local to a module. + """ + Return all classes local to a module. :param module input_module: the module containing the classes. - :return: a `list` of tuples each containing a class name and a \ - class. + + :returns: list of class names and types. + :rtype: List[Tuple[str, type]] """ module_cls_members = [] module_name = input_module.__name__ - # first find all classes in the module. This includes imported - # classes. + # First find all classes in the module. This includes imported classes. all_cls_members = inspect.getmembers(sys.modules[module_name], inspect.isclass) # next only keep classes that are specified in the module. - for cls_member in all_cls_members: - if cls_member[1].__module__ == module_name: - module_cls_members.append(cls_member) + for name, cls in all_cls_members: + if cls.__module__ == module_name: + module_cls_members.append((name, cls)) return module_cls_members @@ -137,16 +138,7 @@ def create(self, std=None): # we already have our required list of classes so call _setup # to setup our class hierarchy. self._setup(f2003_cls_members) - # We can now specify which classes are taken as defining new - # scoping regions. Programs without the optional program-stmt - # are handled separately in the Fortran2003.Main_Program0 class. - SYMBOL_TABLES.scoping_unit_classes = [ - Fortran2003.Module_Stmt, - Fortran2003.Subroutine_Stmt, - Fortran2003.Program_Stmt, - Fortran2003.Function_Stmt, - ] - # the class hierarchy has been set up so return the top + # The class hierarchy has been set up so return the top # level class that we start from when parsing Fortran code. return Fortran2003.Program if std == "f2008": @@ -159,6 +151,7 @@ def create(self, std=None): from fparser.two import Fortran2008 f2008_cls_members = get_module_classes(Fortran2008) + # next add in Fortran2003 classes if they do not already # exist as a Fortran2008 class. f2008_class_names = [i[0] for i in f2008_cls_members] @@ -168,17 +161,7 @@ def create(self, std=None): # we now have our required list of classes so call _setup # to setup our class hierarchy. self._setup(f2008_cls_members) - # We can now specify which classes are taken as defining new - # scoping regions. Programs without the optional program-stmt - # are handled separately in the Fortran2003.Main_Program0 class. - SYMBOL_TABLES.scoping_unit_classes = [ - Fortran2003.Module_Stmt, - Fortran2003.Subroutine_Stmt, - Fortran2003.Program_Stmt, - Fortran2003.Function_Stmt, - Fortran2008.Submodule_Stmt, - ] - # the class hierarchy has been set up so return the top + # The class hierarchy has been set up so return the top # level class that we start from when parsing Fortran # code. Fortran2008 does not extend the top level class so # we return the Fortran2003 one. @@ -197,106 +180,102 @@ def _setup(self, input_classes): class name and a class. """ + # pylint: disable=import-outside-toplevel + from fparser.two import Fortran2003 - __autodoc__ = [] - base_classes = {} - - import logging - import fparser.two.Fortran2003 - - class_type = type(fparser.two.Fortran2003.Base) + class_type = type(Fortran2003.Base) # Reset subclasses dictionary in case this function has been # called before. If this is not done then multiple calls to # the ParserFactory create method may not work correctly. - fparser.two.Fortran2003.Base.subclasses = {} + Fortran2003.Base.subclasses = {} + base_classes = {} - for clsinfo in input_classes: - clsname = "{0}.{1}".format(clsinfo[1].__module__, clsinfo[0]) - cls = eval(clsname) + for _, cls in input_classes: # ?? classtype is set to Base so why have issubclass? if ( isinstance(cls, class_type) - and issubclass(cls, fparser.two.Fortran2003.Base) + and issubclass(cls, Fortran2003.Base) and not cls.__name__.endswith("Base") ): base_classes[cls.__name__] = cls - if len(__autodoc__) < 10: - __autodoc__.append(cls.__name__) - # # OPTIMIZE subclass_names tree. # - - if 1: # Optimize subclass tree: - - def _rpl_list(clsname): - if clsname not in base_classes: - error_string = "Not implemented: {0}".format(clsname) - logging.getLogger(__name__).debug(error_string) - return [] - # remove this code when all classes are implemented. - cls = base_classes[clsname] - if hasattr(cls, "match"): - return [clsname] - bits = [] - for names in getattr(cls, "subclass_names", []): - list1 = _rpl_list(names) - for names1 in list1: - if names1 not in bits: - bits.append(names1) - return bits - - for cls in list(base_classes.values()): - if not hasattr(cls, "subclass_names"): - continue - opt_subclass_names = [] - for names in cls.subclass_names: - for names1 in _rpl_list(names): - if names1 not in opt_subclass_names: - opt_subclass_names.append(names1) - if not opt_subclass_names == cls.subclass_names: - cls.subclass_names[:] = opt_subclass_names - - # Initialize Base.subclasses dictionary: - for clsname, cls in list(base_classes.items()): - subclass_names = getattr(cls, "subclass_names", None) - if subclass_names is None: - message = "%s class is missing subclass_names list" % (clsname) + def _closest_descendants_with_match(clsname): + """ + Starting at the named class, searches down the tree defined by the + classes named in the `subclass_names` list to find the closest that + have `match` methods. If the current class does not have a + `match` method then this method is called again for each of + the classes in its `subclass_names` list. + + :param str clsname: The name of the class from which to search. + + :returns: names of 'nearest' subclasses with `match` methods. + :rtype: List[str | NoneType] + + """ + if clsname not in base_classes: + error_string = f"Not implemented: {clsname}" + logging.getLogger(__name__).debug(error_string) + return [] + # remove this code when all classes are implemented. + cls = base_classes[clsname] + if hasattr(cls, "match"): + # This class has a `match` method so no need to search further + # down the tree. + return [clsname] + # clsname doesn't have a `match` method so we look at each of its + # subclasses and find the nearest class in each that does have a + # `match` method. + bits = [] + for names in getattr(cls, "subclass_names", []): + list1 = _closest_descendants_with_match(names) + for names1 in list1: + if names1 not in bits: + bits.append(names1) + return bits + + # Dict in which to store optimised list of subclass names for each cls. + local_subclass_names = {} + + for cls in base_classes.values(): + if not hasattr(cls, "subclass_names"): + continue + # The optimised list of subclass names will only include subclasses + # that have `match` methods. + opt_subclass_names = [] + for names in cls.subclass_names: + for names1 in _closest_descendants_with_match(names): + if names1 not in opt_subclass_names: + opt_subclass_names.append(names1) + local_subclass_names[cls] = opt_subclass_names[:] + + # Now that we've optimised the list of subclass names for each class, + # use this information to initialise the Base.subclasses dictionary: + for clsname, cls in base_classes.items(): + if not hasattr(cls, "subclass_names"): + message = f"{clsname} class is missing subclass_names list" logging.getLogger(__name__).debug(message) continue + subclass_names = local_subclass_names.get(cls, []) try: - bits = fparser.two.Fortran2003.Base.subclasses[clsname] + bits = Fortran2003.Base.subclasses[clsname] except KeyError: - fparser.two.Fortran2003.Base.subclasses[clsname] = bits = [] + Fortran2003.Base.subclasses[clsname] = bits = [] for name in subclass_names: if name in base_classes: bits.append(base_classes[name]) else: - message = "{0} not implemented needed by {1}".format(name, clsname) + message = f"{name} not implemented needed by {clsname}" logging.getLogger(__name__).debug(message) - if 1: - for cls in list(base_classes.values()): - # subclasses = fparser.two.Fortran2003.Base.subclasses.get( - # cls.__name__, []) - # subclasses_names = [c.__name__ for c in subclasses] - subclass_names = getattr(cls, "subclass_names", []) - use_names = getattr(cls, "use_names", []) - # for name in subclasses_names: - # break - # if name not in subclass_names: - # message = ('%s needs to be added to %s ' - # 'subclasses_name list' - # % (name, cls.__name__)) - # logging.getLogger(__name__).debug(message) - # for name in subclass_names: - # break - # if name not in subclasses_names: - # message = '%s needs to be added to %s ' - # 'subclass_name list' % (name, cls.__name__) - # logging.getLogger(__name__).debug(message) - for name in use_names + subclass_names: - if name not in base_classes: - message = "%s not defined used " "by %s" % (name, cls.__name__) - logging.getLogger(__name__).debug(message) + # Double-check that all required classes have been constructed. + for cls in base_classes.values(): + subclass_names = local_subclass_names.get(cls, []) + use_names = getattr(cls, "use_names", []) + for name in use_names + subclass_names: + if name not in base_classes: + message = f"{name} not defined, used by {cls.__name__}" + logging.getLogger(__name__).debug(message) diff --git a/src/fparser/two/symbol_table.py b/src/fparser/two/symbol_table.py index 6b258932..fc2c32a4 100644 --- a/src/fparser/two/symbol_table.py +++ b/src/fparser/two/symbol_table.py @@ -56,8 +56,6 @@ class SymbolTables: def __init__(self): self._symbol_tables = {} - # Those classes that correspond to a new scoping unit - self._scoping_unit_classes = [] # The symbol table of the current scope self._current_scope = None # Whether or not we enable consistency checks in the symbol tables @@ -83,8 +81,7 @@ def enable_checks(self, value): def clear(self): """ - Deletes any stored SymbolTables but retains the stored list of - classes that define scoping units. + Deletes any stored SymbolTables. """ self._symbol_tables = {} @@ -126,36 +123,6 @@ def lookup(self, name): """ return self._symbol_tables[name.lower()] - @property - def scoping_unit_classes(self): - """ - :returns: the fparser2 classes that are taken to mark the start of \ - a new scoping region. - :rtype: list of types - - """ - return self._scoping_unit_classes - - @scoping_unit_classes.setter - def scoping_unit_classes(self, value): - """ - Set the list of fparser2 classes that are taken to mark the start of \ - a new scoping region. - - :param value: the list of fparser2 classes. - :type value: list of types - - :raises TypeError: if the supplied value is not a list of types. - - """ - if not isinstance(value, list): - raise TypeError( - f"Supplied value must be a list but got '{type(value).__name__}'" - ) - if not all(isinstance(item, type) for item in value): - raise TypeError(f"Supplied list must contain only classes but got: {value}") - self._scoping_unit_classes = value - @property def current_scope(self): """ diff --git a/src/fparser/two/tests/.pylintrc b/src/fparser/two/tests/.pylintrc index 7a937382..bf06335d 100644 --- a/src/fparser/two/tests/.pylintrc +++ b/src/fparser/two/tests/.pylintrc @@ -1,11 +1,34 @@ +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=useless-suppression + + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=suppressed-message,duplicate-code,too-many-locals,too-many-lines,protected-access,locally-disabled,too-few-public-methods,too-many-arguments,use-implicit-booleaness-not-comparison + [FORMAT] # Maximum number of characters on a single line. Black's default is 88. max-line-length=88 -# fparser dynamically generates *_List classes so pylint can't -# find them. -generated-members=Fortran2003.*_List,Fortran2008.*_List +[TYPECHECK] + +# fparser generates *_List classes at runtime so pylint can't +# find them (as it's a static checker). +ignored-modules=fparser.two.Fortran2003,fparser.two.Fortran2008 +generated-members=fparser.two.Fortran2003.*_List,fparser.two.Fortran2008.*_List [DESIGN] # Maximum number of parents for a class (see R0901) diff --git a/src/fparser/two/tests/fortran2008/test_block.py b/src/fparser/two/tests/fortran2008/test_block.py new file mode 100644 index 00000000..ba63e4a3 --- /dev/null +++ b/src/fparser/two/tests/fortran2008/test_block.py @@ -0,0 +1,224 @@ +# Copyright (c) 2022-2023 Science and Technology Facilities Council. + +# All rights reserved. + +# Modifications made as part of the fparser project are distributed +# under the following license: + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: + +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Module containing pytest tests for the support of the Fortran2008 +Block construct.""" + +import re +import pytest + +from fparser.api import get_reader +from fparser.two.Fortran2008 import Block_Construct, Block_Stmt +from fparser.two.symbol_table import SYMBOL_TABLES +from fparser.two.utils import FortranSyntaxError, ScopingRegionMixin, walk + + +def test_block(): + """Test that the Block_Construct matches as expected.""" + block = Block_Construct( + get_reader( + """\ + block + integer :: b = 4 + a = 1 + b + end block + """ + ) + ) + assert isinstance(block.children[0], Block_Stmt) + assert isinstance(block.children[0], ScopingRegionMixin) + name = block.children[0].get_scope_name() + assert re.match(r"block:[\d+]", name) + assert "BLOCK\n INTEGER :: b = 4\n a = 1 + b\nEND BLOCK" in str(block) + + +@pytest.mark.parametrize( + "before, after", [("", ""), ("b = 2.0 * b", ""), ("", "b = 2.0 * b")] +) +def test_block_new_scope(f2008_parser, before, after): + """Test that a Block_Construct creates a new scoping region.""" + block = f2008_parser( + get_reader( + f"""\ + program foo + integer :: b = 3 + {before} + block + integer :: b = 4 + a = 1 + b + end block + {after} + end program foo + """ + ) + ) + + assert "BLOCK\nINTEGER :: b = 4\na = 1 + b\nEND BLOCK" in str(block).replace( + " ", "" + ) + tables = SYMBOL_TABLES + assert list(tables._symbol_tables.keys()) == ["foo"] + table = SYMBOL_TABLES.lookup("foo") + assert len(table.children) == 1 + assert re.match(r"block:[\d+]", table.children[0].name) + + +def test_block_in_if(f2008_parser): + """Test that a Block may appear inside an IF.""" + ptree = f2008_parser( + get_reader( + """\ + program foo + integer :: b = 3 + if (b == 2) then + block + real :: tmp + tmp = ATAN(0.5) + b = NINT(tmp) + end block + end if + end program foo + """ + ) + ) + blocks = walk([ptree], Block_Construct) + assert len(blocks) == 1 + + +def test_named_block(): + """ + Test that a named block construct is correctly captured and also + reproduced. + + """ + block = Block_Construct( + get_reader( + """\ + foo: block + integer :: b = 4 + a = 1 + b + end block foo + """ + ) + ) + + assert "foo:BLOCK\n INTEGER :: b = 4\n a = 1 + b\nEND BLOCK foo" in str(block) + + +def test_end_block_missing_start_name(): # C808 + """ + Test Constraint 808 - that a name on the 'end block' must correspond + with the same name on the 'block'. + + """ + with pytest.raises(FortranSyntaxError) as err: + Block_Construct( + get_reader( + """\ + block + end block foo + """ + ) + ) + assert "Name 'foo' has no corresponding starting name" in str(err) + + +def test_end_block_missing_end_name(): # C808 + """ + Test that a named block that is missing a name on its 'end block' statement + results in a syntax error. + + """ + with pytest.raises(FortranSyntaxError) as err: + Block_Construct( + get_reader( + """\ + foo: block + end block + """ + ) + ) + assert "Expecting name 'foo' but none given" in str(err) + + +def test_end_block_wrong_name(): # C808 + """ + Test that an incorrect name on the end block statement results in a + syntax error. + + """ + with pytest.raises(FortranSyntaxError) as err: + Block_Construct( + get_reader( + """\ + foo: block + end block bar + """ + ) + ) + assert "Expecting name 'foo', got 'bar'" in str(err) + + +def test_block_in_subroutine(f2008_parser): + """ + Check that we get two, nested symbol tables when a routine contains + a Block construct. + + """ + code = """\ + program my_prog + real :: a + a = -1.0 + if (a < 0.0) then + rocking: block + real :: b + b = 42.0 + a = b + end block rocking + else + block + real :: c + c = 42.0 / 5.0 + a = 10.0 * c + end block + end if + end program my_prog + """ + _ = f2008_parser(get_reader(code)) + tables = SYMBOL_TABLES + assert list(tables._symbol_tables.keys()) == ["my_prog"] + table = SYMBOL_TABLES.lookup("my_prog") + assert len(table.children) == 2 + assert table.children[0].name == "rocking" + assert re.match(r"block:[\d+]", table.children[1].name) diff --git a/src/fparser/two/tests/fortran2008/test_critical.py b/src/fparser/two/tests/fortran2008/test_critical.py new file mode 100644 index 00000000..e9e6d6d2 --- /dev/null +++ b/src/fparser/two/tests/fortran2008/test_critical.py @@ -0,0 +1,122 @@ +# Copyright (c) 2022-2023 Science and Technology Facilities Council. + +# All rights reserved. + +# Modifications made as part of the fparser project are distributed +# under the following license: + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: + +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Module containing pytest tests for the support of the Fortran2008 +Critical construct.""" + + +import pytest + +from fparser.api import get_reader +from fparser.two.Fortran2003 import Assignment_Stmt +from fparser.two.Fortran2008 import Critical_Construct, Critical_Stmt, End_Critical_Stmt +from fparser.two.utils import FortranSyntaxError + + +def test_critical(): + """Test that a basic critical construct is correctly constructed.""" + critical = Critical_Construct( + get_reader( + """\ + critical + a = 1 + b + end critical + """ + ) + ) + assert isinstance(critical.children[0], Critical_Stmt) + assert isinstance(critical.children[1], Assignment_Stmt) + assert isinstance(critical.children[2], End_Critical_Stmt) + assert critical.children[0].get_start_name() is None + assert "CRITICAL\n a = 1 + b\nEND CRITICAL" in str(critical) + + +def test_named_critical(): + """Test that a named critical construct is matched correctly and that + its name can be queried.""" + critical = Critical_Construct( + get_reader( + """\ + foo: critical + a = 1 + b + end critical foo + """ + ) + ) + assert critical.children[0].get_start_name() == "foo" + assert "foo:CRITICAL\n a = 1 + b\nEND CRITICAL foo" in str(critical) + + +def test_end_critical_missing_start_name(): # C809 + """Check that a critical construct with an end name but no start name + results in a syntax error (C809).""" + with pytest.raises(FortranSyntaxError) as err: + Critical_Construct( + get_reader( + """\ + critical + end critical foo + """ + ) + ) + assert "Name 'foo' has no corresponding starting name" in str(err) + + +def test_end_critical_missing_end_name(): # C809 + """Test that a named critical construct with the name omitted from + the end critical results in a syntax error (C809).""" + with pytest.raises(FortranSyntaxError) as err: + Critical_Construct( + get_reader( + """\ + foo: critical + end critical + """ + ) + ) + assert "Expecting name 'foo' but none given" in str(err) + + +def test_end_critical_wrong_name(): # C809 + """Test that mismatched start and end names result in a syntax error (C809)""" + with pytest.raises(FortranSyntaxError) as err: + Critical_Construct( + get_reader( + """\ + foo: critical + end critical bar + """ + ) + ) + assert "Expecting name 'foo', got 'bar'" in str(err) diff --git a/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py b/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py index 554bd3ed..e928592b 100644 --- a/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py +++ b/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Science and Technology Facilities Council +# Copyright (c) 2022-2023 Science and Technology Facilities Council. # All rights reserved. @@ -72,7 +72,6 @@ """ -# pylint: disable=no-member import pytest from fparser.api import get_reader from fparser.two import Fortran2008 diff --git a/src/fparser/two/tests/test_bases.py b/src/fparser/two/tests/test_bases.py index ae9e9fdf..309dbeca 100644 --- a/src/fparser/two/tests/test_bases.py +++ b/src/fparser/two/tests/test_bases.py @@ -156,29 +156,3 @@ def test_blockbase_tofortran_non_ascii(): # Explicitly call tofortran() on the BlockBase class. out_str = BlockBase.tofortran(bbase) assert "for e1=1" in out_str - - -@pytest.mark.usefixtures("f2003_create") -def test_blockbase_symbol_table(monkeypatch): - """Check that the BlockBase.match method creates symbol-tables - for those classes that correspond to a scoping unit and not - otherwise.""" - # Monkeypatch the list of classes that are recognised as - # defining scoping regions. - monkeypatch.setattr( - SYMBOL_TABLES, "_scoping_unit_classes", [Fortran2003.Program_Stmt] - ) - code = "program my_test\n" "end program\n" - reader = FortranStringReader(code, ignore_comments=False) - obj = BlockBase.match( - Fortran2003.Program_Stmt, [], Fortran2003.End_Program_Stmt, reader - ) - # We should have a new symbol table named "my_test" - assert SYMBOL_TABLES.lookup("my_test") - code = "subroutine my_sub\n" "end subroutine\n" - reader = FortranStringReader(code, ignore_comments=False) - obj = BlockBase.match( - Fortran2003.Subroutine_Stmt, [], Fortran2003.End_Subroutine_Stmt, reader - ) - # There should be no new symbol table - assert "my_sub" not in SYMBOL_TABLES._symbol_tables diff --git a/src/fparser/two/tests/test_parser.py b/src/fparser/two/tests/test_parser.py index 9a8cc033..5d4a6e19 100644 --- a/src/fparser/two/tests/test_parser.py +++ b/src/fparser/two/tests/test_parser.py @@ -54,14 +54,6 @@ class do not affect current calls. with pytest.raises(FortranSyntaxError) as excinfo: _ = parser(reader) assert "at line 1\n>>>submodule (x) y\n" in str(excinfo.value) - # Check that the list of classes used to define scoping regions is - # correctly set. - assert SYMBOL_TABLES.scoping_unit_classes == [ - Fortran2003.Module_Stmt, - Fortran2003.Subroutine_Stmt, - Fortran2003.Program_Stmt, - Fortran2003.Function_Stmt, - ] parser = ParserFactory().create(std="f2003") reader = FortranStringReader(fstring) @@ -76,7 +68,6 @@ class do not affect current calls. assert "SUBMODULE (x) y\nEND" in code # Submodule_Stmt should now be included in the list of classes that define # scoping regions. - assert Fortran2008.Submodule_Stmt in SYMBOL_TABLES.scoping_unit_classes assert "y" in SYMBOL_TABLES._symbol_tables # Repeat f2003 example to make sure that a previously valid (f2008) @@ -86,7 +77,6 @@ class do not affect current calls. with pytest.raises(FortranSyntaxError) as excinfo: _ = parser(reader) assert "at line 1\n>>>submodule (x) y\n" in str(excinfo.value) - assert Fortran2008.Submodule_Stmt not in SYMBOL_TABLES.scoping_unit_classes # The previous symbol table entries should have been removed when # creating the new parser. assert "y" not in SYMBOL_TABLES._symbol_tables diff --git a/src/fparser/two/tests/test_symbol_tables.py b/src/fparser/two/tests/test_symbol_tables.py index 719e33d2..9fa34d17 100644 --- a/src/fparser/two/tests/test_symbol_tables.py +++ b/src/fparser/two/tests/test_symbol_tables.py @@ -80,23 +80,6 @@ def test_construction_addition_removal(): assert tables._symbol_tables == {} -def test_scoping_unit_classes_setter(): - """Check that the setter for the list of classes used to define scoping - regions works as expected.""" - tables = SymbolTables() - assert tables.scoping_unit_classes == [] - tables.scoping_unit_classes = [Fortran2003.Block_Data] - assert tables.scoping_unit_classes == [Fortran2003.Block_Data] - with pytest.raises(TypeError) as err: - tables.scoping_unit_classes = "hello" - assert "Supplied value must be a list but got 'str'" in str(err.value) - with pytest.raises(TypeError) as err: - tables.scoping_unit_classes = ["hello"] - assert "Supplied list must contain only classes but got: ['hello']" in str( - err.value - ) - - def test_str_method(): """Tests for the str() method.""" tables = SymbolTables() diff --git a/src/fparser/two/tests/test_utils.py b/src/fparser/two/tests/test_utils.py index 18f7217a..2803daf1 100644 --- a/src/fparser/two/tests/test_utils.py +++ b/src/fparser/two/tests/test_utils.py @@ -45,7 +45,8 @@ # test BlockBase -def test_blockbase_match_names(f2003_create): +@pytest.mark.usefixtures("f2003_create") +def test_blockbase_match_names(): """Test the blockbase name matching option in its match method. We use the Derived_Type_Def class (which subclasses BlockBase) for this as it sets match_names to True. @@ -78,7 +79,8 @@ def test_blockbase_match_names(f2003_create): ) in str(excinfo.value) -def test_blockbase_match_name_classes(f2003_create): +@pytest.mark.usefixtures("f2003_create") +def test_blockbase_match_name_classes(): """Test the blockbase name matching option in its match method. We use the If_Construct class (which subclasses BlockBase) for this as it sets match_names to True and provides match_name_classes. This is @@ -108,3 +110,34 @@ def test_blockbase_match_name_classes(f2003_create): assert ( "at line 2\n>>>endif label\nName 'label' has no corresponding " "starting name" ) in str(excinfo.value) + + +@pytest.mark.usefixtures("f2003_create") +def test_endstmtbase_match(): + """Tests for the EndStmtBase.match() method.""" + result = utils.EndStmtBase.match("critical", None, "hello") + assert result is None + # No statement type is required by default + result = utils.EndStmtBase.match("CRITICAL", None, "end") + assert result == (None, None) + # Missing statement type. + result = utils.EndStmtBase.match("CRITICAL", None, "end", require_stmt_type=True) + assert result is None + # Matching statement type. + result = utils.EndStmtBase.match( + "CRITICAL", None, "end critical", require_stmt_type=True + ) + assert result == ("CRITICAL", None) + # End construct with name but no class to match it with. + result = utils.EndStmtBase.match( + "SUBROUTINE", None, "end subroutine sub", require_stmt_type=True + ) + assert result is None + # End construct with name that matches with supplied class. + result = utils.EndStmtBase.match( + "SUBROUTINE", + Fortran2003.Subroutine_Name, + "end subroutine sub", + require_stmt_type=True, + ) + assert result == ("SUBROUTINE", Fortran2003.Name("sub")) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 2fa2d09f..3c711aee 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -1,5 +1,5 @@ # Modified work Copyright (c) 2017-2023 Science and Technology -# Facilities Council +# Facilities Council. # Original work Copyright (c) 1999-2008 Pearu Peterson # All rights reserved. @@ -373,15 +373,16 @@ class Base(ComparableMixin): :param type cls: the class of object to create. :param string: (source of) Fortran string to parse. - :type string: [Str | :py:class:`fparser.common.readfortran.FortranReaderBase`] + :type string: str | :py:class:`fparser.common.readfortran.FortranReaderBase` :param parent_cls: the parent class of this object. :type parent_cls: `type` """ # This dict of subclasses is populated dynamically by code at the end - # of this module. That code uses the entries in the + # of the fparser.two.parser module. That code uses the entries in the # 'subclass_names' list belonging to each class defined in this module. + # See Issue #191 for a discussion of a way of getting rid of this state. subclasses = {} def __init__(self, string, parent_cls=None): @@ -447,7 +448,8 @@ def __new__(cls, string, parent_cls=None): return result if result is None: # Loop over the possible sub-classes of this class and - # check for matches + # check for matches. This uses the list of subclasses calculated + # at runtime in fparser.two.parser. for subcls in Base.subclasses.get(cls.__name__, []): if subcls in parent_cls: # avoid recursion 2. continue @@ -562,6 +564,21 @@ def restore_reader(self, reader): reader.put_item(self.item) +class ScopingRegionMixin: + """ + Mixin class for use in all classes that represent a scoping region and + thus have an associated symbol table. + + """ + + def get_scope_name(self): + """ + :returns: the name of this scoping region. + :rtype: str + """ + return self.get_name().string + + class BlockBase(Base): """ Base class for matching all block constructs:: @@ -623,6 +640,9 @@ def match( # top-level due to circular dependencies). assert isinstance(reader, FortranReaderBase), repr(reader) content = [] + # This will store the name of the new SymbolTable if we match a + # scoping region. + table_name = None if startcls is not None: # Deal with any preceding comments, includes, and/or directives @@ -639,12 +659,12 @@ def match( for obj in reversed(content): obj.restore_reader(reader) return - if startcls in SYMBOL_TABLES.scoping_unit_classes: + if isinstance(obj, ScopingRegionMixin): # We are entering a new scoping unit so create a new # symbol table. # NOTE: if the match subsequently fails then we must # delete this symbol table. - table_name = str(obj.children[1]) + table_name = obj.get_scope_name() SYMBOL_TABLES.enter_scope(table_name) # Store the index of the start of this block proper (i.e. # excluding any comments) @@ -771,20 +791,20 @@ def match( except FortranSyntaxError as err: # We hit trouble so clean up the symbol table - if startcls in SYMBOL_TABLES.scoping_unit_classes: + if table_name: SYMBOL_TABLES.exit_scope() # Remove any symbol table that we created SYMBOL_TABLES.remove(table_name) raise err - if startcls in SYMBOL_TABLES.scoping_unit_classes: + if table_name: SYMBOL_TABLES.exit_scope() if not had_match or endcls and not found_end: # We did not get a match from any of the subclasses or # failed to find the endcls if endcls is not None: - if startcls in SYMBOL_TABLES.scoping_unit_classes: + if table_name: # Remove any symbol table that we created SYMBOL_TABLES.remove(table_name) for obj in reversed(content): @@ -1600,23 +1620,50 @@ class EndStmtBase(StmtBase): @staticmethod def match(stmt_type, stmt_name, string, require_stmt_type=False): + """ + Attempts to match the supplied string as a form of 'END xxx' statement. + + :param str stmt_type: the type of end statement (e.g. "do") that we \ + attempt to match. + :param type stmt_name: a class which should be used to match against \ + the name should this statement be named (e.g. end subroutine sub). + :param str string: the string to attempt to match. + :param bool require_stmt_type: whether or not the string must contain \ + the type of the block that is ending. + + :returns: 2-tuple containing the matched end-statement type (if any) \ + and, optionally, an associated name or None if there is no match. + :rtype: Optional[ + Tuple[Optional[str], + Optional[:py:class:`fparser.two.Fortran2003.Name`]]] + + """ start = string[:3].upper() if start != "END": - return + # string doesn't begin with 'END' + return None line = string[3:].lstrip() start = line[: len(stmt_type)].upper() if start: if start.replace(" ", "") != stmt_type.replace(" ", ""): - return + # Not the correct type of 'END ...' statement. + return None line = line[len(stmt_type) :].lstrip() else: if require_stmt_type: - return + # No type was found but one is required. + return None + # Got a bare "END" and that is a valid match. return None, None if line: if stmt_name is None: - return + # There is content after the 'end xxx' but this block isn't + # named so we fail to match. + return None + # Attempt to match the content after 'end xxx' with the supplied + # name class. return stmt_type, stmt_name(line) + # Successful match with an unnamed 'end xxx'. return stmt_type, None def init(self, stmt_type, stmt_name):