Repository: stfc/fparser

Issue:
`open` statement missing `newunit` keyword
The `newunit` keyword was added in F2008. The following causes a syntax error: `open(file='filename', newunit=unit)`

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b6df97..21bf47c 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
 
+18/10/2022 PR #369 for #332. Add support for F2008 open intrinsic arguments.
+
 13/10/2022 PR #381 for #298. Fix F2008 allocate statement with arguments.
 
 20/09/2022 PR #376 for #349. Add support for use association to the
diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py
index 6d5f8ea..6d69497 100644
--- a/src/fparser/two/Fortran2008.py
+++ b/src/fparser/two/Fortran2008.py
@@ -71,18 +71,25 @@
 # pylint: disable=arguments-differ
 # pylint: disable=undefined-variable
 # pylint: disable=eval-used
+# These warnings are due to the auto-generation of classes when this
+# module is first imported.
 # pylint: disable=exec-used
 # pylint: disable=unused-import
-from fparser.common.splitline import string_replace_map
+# pylint: disable=no-name-in-module
+from fparser.common.splitline import string_replace_map, splitparen
+
 from fparser.two import pattern_tools as pattern
 
 from fparser.two.utils import (
-    STRINGBase,
     BracketBase,
-    WORDClsBase,
+    CALLBase,
+    KeywordValueBase,
+    NoMatchError,
     SeparatorBase,
-    Type_Declaration_StmtBase,
     StmtBase,
+    STRINGBase,
+    Type_Declaration_StmtBase,
+    WORDClsBase,
 )
 from fparser.two.Fortran2003 import (
     EndStmtBase,
@@ -93,13 +100,16 @@ from fparser.two.Fortran2003 import (
     Stat_Variable,
     Errmsg_Variable,
     Source_Expr,
-    KeywordValueBase,
     Module_Subprogram_Part,
     Implicit_Part,
     Implicit_Part_Stmt,
     Declaration_Construct,
     Use_Stmt,
+    File_Name_Expr,
+    File_Unit_Number,
     Import_Stmt,
+    Iomsg_Variable,
+    Label,
     Declaration_Type_Spec,
     Entity_Decl_List,
     Component_Decl_List,
@@ -108,21 +118,23 @@ from fparser.two.Fortran2003 import (
 
 # Import of F2003 classes that are updated in this standard.
 from fparser.two.Fortran2003 import (
-    Program_Unit as Program_Unit_2003,
-    Attr_Spec as Attr_Spec_2003,
-    Alloc_Opt as Alloc_Opt_2003,
-    Allocate_Stmt as Allocate_Stmt_2003,
-    Type_Declaration_Stmt as Type_Declaration_Stmt_2003,
-    Component_Attr_Spec as Component_Attr_Spec_2003,
-    Data_Component_Def_Stmt as Data_Component_Def_Stmt_2003,
     Action_Stmt as Action_Stmt_2003,
     Action_Stmt_C201 as Action_Stmt_C201_2003,
     Action_Stmt_C802 as Action_Stmt_C802_2003,
     Action_Stmt_C824 as Action_Stmt_C824_2003,
-    If_Stmt as If_Stmt_2003,
+    Alloc_Opt as Alloc_Opt_2003,
+    Allocate_Stmt as Allocate_Stmt_2003,
+    Attr_Spec as Attr_Spec_2003,
+    Component_Attr_Spec as Component_Attr_Spec_2003,
+    Connect_Spec as Connect_Spec_2003,
+    Data_Component_Def_Stmt as Data_Component_Def_Stmt_2003,
     Do_Term_Action_Stmt as Do_Term_Action_Stmt_2003,
     Executable_Construct as Executable_Construct_2003,
     Executable_Construct_C201 as Executable_Construct_C201_2003,
+    If_Stmt as If_Stmt_2003,
+    Open_Stmt as Open_Stmt_2003,
+    Program_Unit as Program_Unit_2003,
+    Type_Declaration_Stmt as Type_Declaration_Stmt_2003,
 )
 
 
@@ -1011,8 +1023,6 @@ class Submodule_Stmt(Base):  # R1117
             return None
         # "SUBMODULE is found so strip it out and split the remaining
         # line by parenthesis
-        from fparser.common.splitline import splitparen
-
         splitline = splitparen(fstring[len(name) :].lstrip())
         # We expect 2 entries, the first being parent_identifier with
         # brackets and the second submodule_name. However for some
@@ -1041,7 +1051,7 @@ class Submodule_Stmt(Base):  # R1117
     def tostr(self):
         """return the fortran representation of this object"""
         # return self.string  # this returns the original code
-        return "SUBMODULE ({0}) {1}".format(self.items[0], self.items[1])
+        return f"SUBMODULE ({self.items[0]}) {self.items[1]}"
 
     def get_name(self):  # C1114
         """Fortran 2008 constraint C1114
@@ -1122,7 +1132,7 @@ class Parent_Identifier(Base):  # R1118 (C1113)
         lhs_name = split_string[0].lstrip().rstrip()
         if len_split_string == 1:
             return Ancestor_Module_Name(lhs_name), None
-        elif len_split_string == 2:
+        if len_split_string == 2:
             rhs_name = split_string[1].lstrip().rstrip()
             return Ancestor_Module_Name(lhs_name), Parent_SubModule_Name(rhs_name)
         # we expect at most one ':' in our input so the match fails
@@ -1132,10 +1142,168 @@ class Parent_Identifier(Base):  # R1118 (C1113)
         """return the fortran representation of this object"""
         # return self.string  # this returns the original code
         if self.items[1]:
-            return "{0}:{1}".format(self.items[0], self.items[1])
+            return f"{self.items[0]}:{self.items[1]}"
         return str(self.items[0])
 
 
+class Open_Stmt(Open_Stmt_2003):  # R904
+    """
+    Fortran2008 Rule R904.
+
+    open-stmt is OPEN ( connect-spec-list )
+
+    """
+
+    subclass_names = []
+    use_names = ["Connect_Spec_List"]
+
+    @staticmethod
+    def match(string):
+        """
+        Attempts to match the supplied string as an Open_Stmt.
+
+        :param str string: the string to attempt to match.
+
+        :returns: a new Open_Stmt object if the match is successful, None otherwise.
+        :rtype: Optional[:py:class:`fparser.two.Fortran2008.Open_Stmt]
+
+        """
+        # The Connect_Spec_List class is generated automatically
+        # by code at the end of this module
+        obj = CALLBase.match("OPEN", Connect_Spec_List, string, require_rhs=True)
+        if not obj:
+            return None
+
+        # Apply constraints now that we have the full Connect_Spec_List.
+        have_unit = False
+        have_newunit = False
+        connect_specs = []
+        spec_list = obj[1].children
+        for spec in spec_list:
+            if spec.children[0] in connect_specs:
+                # C903 - no specifier can appear more than once.
+                return None
+            connect_specs.append(spec.children[0])
+            if spec.children[0] == "UNIT":
+                have_unit = True
+            elif spec.children[0] == "NEWUNIT":
+                have_newunit = True
+            if have_unit and have_newunit:
+                # C906 - cannot have both UNIT and NEWUNIT
+                return None
+        if not (have_unit or have_newunit):
+            # C904 - a file unit number must be specified.
+            return None
+        return obj
+
+
+class Connect_Spec(Connect_Spec_2003):
+    """
+    Fortran2008 rule R905.
+
+    connect-spec is [ UNIT = ] file-unit-number
+                     or ACCESS = scalar-default-char-expr
+                     or ACTION = scalar-default-char-expr
+                     or ASYNCHRONOUS = scalar-default-char-expr
+                     or BLANK = scalar-default-char-expr
+                     or DECIMAL = scalar-default-char-expr
+                     or DELIM = scalar-default-char-expr
+                     or ENCODING = scalar-default-char-expr
+                     or ERR = label
+                     or FILE = file-name-expr
+                     or FORM = scalar-default-char-expr
+                     or IOMSG = iomsg-variable
+                     or IOSTAT = scalar-int-variable
+                     or NEWUNIT = scalar-int-variable
+                     or PAD = scalar-default-char-expr
+                     or POSITION = scalar-default-char-expr
+                     or RECL = scalar-int-expr
+                     or ROUND = scalar-default-char-expr
+                     or SIGN = scalar-default-char-expr
+                     or STATUS = scalar-default-char-expr
+
+    R906 file-name-expr is scalar-default-char-expr
+    R907 iomsg-variable is scalar-default-char-variable
+    C903 No specifier shall appear more than once in a given connect-spec-list.
+
+    C904 (R904) If the NEWUNIT= specifier does not appear, a file-unit-number
+         shall be specified; if the optional characters UNIT= are omitted, the
+         file-unit-number shall be the first item in the connect-spec-list.
+
+    C905 (R904) The label used in the ERR= specifier shall be the statement label
+         of a branch target statement that appears in the same inclusive scope as
+         the OPEN statement.
+
+    C906 (R904) If a NEWUNIT= specifier appears, a file-unit-number shall not
+         appear.
+
+    The constraints listed above are checked for in the Open_Stmt.match() method
+    as we don't have access to the full list of Connect_Spec elements here.
+    The exceptions are the second part of C904 (un-named file-unit-number must
+    be first in the list) and C905: these are not currently checked.
+
+    """
+
+    subclass_names = []
+    use_names = [
+        "File_Unit_Number",
+        "Scalar_Default_Char_Expr",
+        "Label",
+        "File_Name_Expr",
+        "Iomsg_Variable",
+        "Scalar_Int_Expr",
+        "Scalar_Int_Variable",
+    ]
+
+    @staticmethod
+    def match(string):
+        """
+        :param str string: Fortran code to check for a match
+
+        :returns: 2-tuple containing the keyword and value or None if the
+                  supplied string is not a match
+        :rtype: Optional[Tuple[str, Any]]
+        """
+        if "=" not in string:
+            # The only argument which need not be named is the unit number
+            return "UNIT", File_Unit_Number(string)
+        # We have a keyword-value pair. Check whether it is valid...
+        for (keyword, value) in [
+            (
+                [
+                    "ACCESS",
+                    "ACTION",
+                    "ASYNCHRONOUS",
+                    "BLANK",
+                    "DECIMAL",
+                    "DELIM",
+                    "ENCODING",
+                    "FORM",
+                    "PAD",
+                    "POSITION",
+                    "ROUND",
+                    "SIGN",
+                    "STATUS",
+                ],
+                Scalar_Default_Char_Expr,
+            ),
+            ("ERR", Label),
+            ("FILE", File_Name_Expr),
+            ("IOSTAT", Scalar_Int_Variable),
+            ("IOMSG", Iomsg_Variable),
+            ("RECL", Scalar_Int_Expr),
+            ("UNIT", File_Unit_Number),
+            ("NEWUNIT", File_Unit_Number),
+        ]:
+            try:
+                obj = KeywordValueBase.match(keyword, value, string, upper_lhs=True)
+            except NoMatchError:
+                obj = None
+            if obj is not None:
+                return obj
+        return None
+
+
 #
 # GENERATE Scalar_, _List, _Name CLASSES
 #


Test patch:
diff --git a/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py b/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py
new file mode 100644
index 0000000..554bd3e
--- /dev/null
+++ b/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py
@@ -0,0 +1,152 @@
+# Copyright (c) 2022 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.
+
+"""Test Fortran 2008 rule R904
+
+   open-stmt is OPEN ( connect-spec-list )
+
+   R905 connect-spec is [ UNIT = ] file-unit-number
+                    or ACCESS = scalar-default-char-expr
+                    or ACTION = scalar-default-char-expr
+                    or ASYNCHRONOUS = scalar-default-char-expr
+                    or BLANK = scalar-default-char-expr
+                    or DECIMAL = scalar-default-char-expr
+                    or DELIM = scalar-default-char-expr
+                    or ENCODING = scalar-default-char-expr
+                    or ERR = label
+                    or FILE = file-name-expr
+                    or FORM = scalar-default-char-expr
+                    or IOMSG = iomsg-variable
+                    or IOSTAT = scalar-int-variable
+                    or NEWUNIT = scalar-int-variable
+                    or PAD = scalar-default-char-expr
+                    or POSITION = scalar-default-char-expr
+                    or RECL = scalar-int-expr
+                    or ROUND = scalar-default-char-expr
+                    or SIGN = scalar-default-char-expr
+                    or STATUS = scalar-default-char-expr
+
+R906 file-name-expr is scalar-default-char-expr
+R907 iomsg-variable is scalar-default-char-variable
+
+C903 No specifier shall appear more than once in a given connect-spec-list.
+C904 (R904) If the NEWUNIT= specifier does not appear, a file-unit-number
+     shall be specified; if the optional characters UNIT= are omitted, the
+     file-unit-number shall be the first item in the connect-spec-list.
+C905 (R904) The label used in the ERR= specifier shall be the statement label
+     of a branch target statement that appears in the same inclusive scope as
+     the OPEN statement.
+C906 (R904) If a NEWUNIT= specifier appears, a file-unit-number shall not
+     appear.
+
+"""
+
+# pylint: disable=no-member
+import pytest
+from fparser.api import get_reader
+from fparser.two import Fortran2008
+from fparser.two.utils import NoMatchError, walk
+
+
+@pytest.mark.parametrize(
+    "open_args",
+    [
+        "unit=23, file='hello'",
+        "23, file='hello', err=36",
+        "23, file='hello', status='OLD'",
+        "unit=23, file='hello', action='read'",
+    ],
+)
+@pytest.mark.usefixtures("f2008_parser")
+def test_open_f2003_args(open_args):
+    """Check that the Fortran2008 version of Open_Stmt still supports the
+    various arguments defined in 2003."""
+    obj = Fortran2008.Open_Stmt(f"open({open_args})")
+    assert isinstance(obj, Fortran2008.Open_Stmt)
+
+
+def test_open_newunit(f2008_parser):
+    """Test that NEWUNIT is a valid argument to OPEN."""
+    tree = f2008_parser(
+        get_reader(
+            """\
+subroutine myopen( unit, file )
+  integer, intent(out):: unit
+  character(len=*), intent(in):: file
+  open( newunit=unit, file=file )
+endsubroutine myopen
+    """
+        )
+    )
+    open_stmts = walk(tree, Fortran2008.Open_Stmt)
+    assert open_stmts
+    assert len(open_stmts[0].children) == 2
+    assert str(open_stmts[0]) == "OPEN(NEWUNIT = unit, FILE = file)"
+
+
+@pytest.mark.usefixtures("f2008_parser")
+def test_constraint_903():
+    """Check that Constraint 903 (no specifier shall appear more than once)
+    is applied."""
+    with pytest.raises(NoMatchError):
+        Fortran2008.Open_Stmt("open(23, unit=24, file='hello')")
+    with pytest.raises(NoMatchError):
+        Fortran2008.Open_Stmt("open(23, file='hello', file='another')")
+
+
+@pytest.mark.usefixtures("f2008_parser")
+def test_constraint_904():
+    """Check that a unit number is specified. We cannot currently check that
+    if no UNIT= appears that it is the first argument to open() because the
+    Connect_Spec.match() method always adds a UNIT= if one is missing."""
+    with pytest.raises(NoMatchError):
+        Fortran2008.Open_Stmt("open(file='hello')")
+
+
+@pytest.mark.usefixtures("f2008_parser")
+def test_constraint_906():
+    """Check that Constraint 906 is applied (no unit number may appear if
+    NEWUNIT is specified)."""
+    with pytest.raises(NoMatchError):
+        Fortran2008.Open_Stmt("open(23, newunit=10, file='hello')")
+    with pytest.raises(NoMatchError):
+        Fortran2008.Open_Stmt("open(unit=23, newunit=10, file='hello')")
+
+
+@pytest.mark.usefixtures("f2008_parser")
+def test_open_invalid_arg():
+    """Check that there is no match if an invalid argument is supplied."""
+    with pytest.raises(NoMatchError) as err:
+        Fortran2008.Open_Stmt("open(newunit=10, file='hello', andy='yes')")
+    assert "andy='yes'" in str(err)
diff --git a/src/fparser/two/tests/fortran2008/test_submodule_stmt_r1117.py b/src/fparser/two/tests/fortran2008/test_submodule_stmt_r1117.py
index bd870b9..c7ffc55 100644
--- a/src/fparser/two/tests/fortran2008/test_submodule_stmt_r1117.py
+++ b/src/fparser/two/tests/fortran2008/test_submodule_stmt_r1117.py
@@ -1,25 +1,25 @@
-# Copyright (c) 2018 Science and Technology Facilities Council
-
+# Copyright (c) 2018-2022 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
@@ -42,55 +42,55 @@ from fparser.two.utils import NoMatchError
 from fparser.two.Fortran2008 import Submodule_Stmt
 
 
-def test_simple(f2008_create):
+def test_simple():
     """Test the parsing of a submodule statement"""
     result = Submodule_Stmt("submodule (id) name")
     assert str(result) == "SUBMODULE (id) name"
 
 
-def test_simple_error1(f2008_create):
+def test_simple_error1():
     """Test the parsing of a submodule statement"""
     with pytest.raises(NoMatchError) as excinfo:
         dummy_ = Submodule_Stmt("submod (id) name")
     assert "Submodule_Stmt: 'submod (id) name" in str(excinfo.value)
 
 
-def test_simple_error2(f2008_create):
+def test_simple_error2():
     """Test the parsing of a submodule statement"""
     with pytest.raises(NoMatchError) as excinfo:
         dummy_ = Submodule_Stmt("submodule name")
     assert "Submodule_Stmt: 'submodule name'" in str(excinfo.value)
 
 
-def test_simple_error3(f2008_create):
+def test_simple_error3():
     """Test the parsing of a submodule statement"""
     with pytest.raises(NoMatchError) as excinfo:
         dummy_ = Submodule_Stmt("submodule () name")
     assert "Submodule_Stmt: 'submodule () name'" in str(excinfo.value)
 
 
-def test_simple_error4(f2008_create):
+def test_simple_error4():
     """Test the parsing of a submodule statement"""
     with pytest.raises(NoMatchError) as excinfo:
         dummy_ = Submodule_Stmt("submodule (id)")
     assert "Submodule_Stmt: 'submodule (id)'" in str(excinfo.value)
 
 
-def test_simple_error5(f2008_create):
+def test_simple_error5():
     """Test the parsing of a submodule statement"""
     with pytest.raises(NoMatchError) as excinfo:
         dummy_ = Submodule_Stmt("submodule name (id)")
     assert "Submodule_Stmt: 'submodule name (id)'" in str(excinfo.value)
 
 
-def test_simple_error6(f2008_create):
+def test_simple_error6():
     """Test the parsing of a submodule statement"""
     with pytest.raises(NoMatchError) as excinfo:
         dummy_ = Submodule_Stmt("submodule (id) (name)")
     assert "Submodule_Stmt: 'submodule (id) (name)'" in str(excinfo.value)
 
 
-def test_simple_error7(f2008_create, monkeypatch):
+def test_simple_error7(monkeypatch):
     """Test the parsing of a submodule statement when there is a single
     right hand bracket. The error generated here is unreachable if
     splitparen works correctly so we need to monkeypatch.
@@ -105,7 +105,7 @@ def test_simple_error7(f2008_create, monkeypatch):
     assert "Submodule_Stmt: 'submodule id) name'" in str(excinfo.value)
 
 
-def test_simple_error8(f2008_create, monkeypatch):
+def test_simple_error8(monkeypatch):
     """Test the parsing of a submodule statement when there is a single
     left hand bracket. The error generated here is unreachable if
     splitparen works correctly so we need to monkeypatch.
@@ -125,16 +125,15 @@ def test_splitparen_error(monkeypatch):
     an error is returned.
 
     """
-
-    monkeypatch.setattr(
-        "fparser.common.splitline.splitparen", lambda x: ["XXX", "", ""]
-    )
+    # We must monkeypatch the splitparen function that has already been
+    # imported into the F2008 module.
+    monkeypatch.setattr("fparser.two.Fortran2008.splitparen", lambda x: ["XXX", "", ""])
     with pytest.raises(NoMatchError) as excinfo:
         dummy_ = Submodule_Stmt("submodule (id) name")
     assert "Submodule_Stmt: 'submodule (id) name'" in str(excinfo.value)
 
 
-def test_simple_error9(f2008_create):
+def test_simple_error9():
     """Test the parsing of a submodule statement"""
     with pytest.raises(NoMatchError) as excinfo:
         dummy_ = Submodule_Stmt("submodule (id) name :")