Repository: stfc/fparser

Issue:
format-specifier does not match if '=' and concat are used
Sorry for finding the next edge case (narrowing down the last frontend failures and they are getting increasingly ugly). After simplifying the original offending line, this seems to be the minimum example to produce the problem:
```python
>>> from fparser.common.readfortran import FortranStringReader
>>> from fparser.two.parser import ParserFactory 
>>> fcode = '''
... PROGRAM FOO
... WRITE (6, '("write some=""'//'text'//'""")')
... END PROGRAM FOO
... '''
>>> reader = FortranStringReader(fcode)
>>> parser = ParserFactory().create(std='f2008')
>>> ast = parser(reader)
Traceback (most recent call last):
  File "/.../src/fparser/two/Fortran2003.py", line 237, in __new__
    return Base.__new__(cls, string)
  File "/.../src/fparser/two/utils.py", line 407, in __new__
    raise NoMatchError(errmsg)
fparser.two.utils.NoMatchError: at line 3
>>>WRITE (6, '("write some=""'//'text'//'""")')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../src/fparser/two/Fortran2003.py", line 241, in __new__
    raise FortranSyntaxError(string, "")
fparser.two.utils.FortranSyntaxError: at line 3
>>>WRITE (6, '("write some=""'//'text'//'""")')
```

It works fine as soon as you either replace `=` by whitespace or remove the string concatenation.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd2e1f3..b61dc4a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,9 +15,12 @@ Modifications by (in alphabetical order):
 * P. Vitt, University of Siegen, Germany
 * A. Voysey, UK Met Office
 
+21/04/2021 PR #260 for #253. Bug fix to improve Io_Control_Spec and
+	   Io_Control_Spec_List matching.
+
 11/03/2021 PR #269 for #268. Removed recognition of f2py directives in
-	   the reader. The original code is still there if needed in
-	   the future.
+	   the reader. The original code is still there if needed in the
+	   future.
 
 20/01/2021 PR #289 for #288. Bug fix for matching expressions containing
            real literals with signed exponents. This bug was introduced
diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py
index a0f0c58..206506b 100644
--- a/src/fparser/two/Fortran2003.py
+++ b/src/fparser/two/Fortran2003.py
@@ -6973,9 +6973,9 @@ class Io_Unit(StringBase):  # R901
     """
     subclass_names = ['File_Unit_Number', 'Internal_File_Variable']
 
+    @staticmethod
     def match(string):
         return StringBase.match('*', string)
-    match = staticmethod(match)
 
 
 class File_Unit_Number(Base):  # R902
@@ -7276,51 +7276,179 @@ items : (Format, Output_Item_List)
         return 'PRINT %s, %s' % tuple(self.items)
 
 
-class Io_Control_Spec_List(SequenceBase):  # R913-list
-    """
-    <io-control-spec-list> is a list taking into account C910, C917, C918
-    """
+class Io_Control_Spec_List(SequenceBase):
+    '''
+    Rule 913 - Control information list.
+
+    io-control-spec-list is a list of io-control-spec items.
+
+    Subject to the following constraints:
+
+    C909 No specifier shall appear more than once in a given
+         io-control-spec-list.
+    C910 An io-unit shall be specified; if the optional characters UNIT= are
+         omitted, the io-unit shall be the first item in the
+         io-control-spec-list.
+    C911 A DELIM= or SIGN= specifier shall not appear in a read-stmt.
+    C912 A BLANK=, PAD=, END=, EOR=, or SIZE=specifier shall not appear in a
+         write-stmt.
+    C913 The label in the ERR=, EOR=, or END= specifier shall be the statement
+         label of a branch target statement that appears in the same scoping
+         unit as the data transfer statement.
+    C914 A namelist-group-name shall be the name of a namelist group.
+    C915 A namelist-group-name shall not appear if an input-item-list or an
+         output-item-list appears in the data transfer statement.
+    C916 An io-control-spec-list shall not contain both a format and a
+         namelist-group-name.
+    C917 If format appears without a preceding FMT=, it shall be the second
+         item in the iocontrol-spec-list and the first item shall be io-unit.
+    C918 If namelist-group-name appears without a preceding NML=, it shall be
+         the second item in the io-control-spec-list and the first item shall
+         be io-unit.
+    C919 If io-unit is not a file-unit-number, the io-control-spec-list shall
+         not contain a REC= specifier or a POS= specifier.
+    C920 If the REC= specifier appears, an END= specifier shall not appear, a
+         namelist-groupname shall not appear, and the format, if any, shall not
+         be an asterisk.
+    C921 An ADVANCE= specifier may appear only in a formatted sequential or
+         stream input/output statement with explicit format specification
+         (10.1) whose control information list does not contain an
+         internal-file-variable as the io-unit.
+    C922 If an EOR= specifier appears, an ADVANCE= specifier also shall appear.
+    C923 If a SIZE= specifier appears, an ADVANCE= specifier also shall appear.
+    C924 The scalar-char-initialization-expr in an ASYNCHRONOUS= specifier
+         shall be of type default character and shall have the value YES or NO.
+    C925 An ASYNCHRONOUS= specifier with a value YES shall not appear unless
+         io-unit is a file-unit-number.
+    C926 If an ID= specifier appears, an ASYNCHRONOUS= specifier with the value
+         YES shall also appear.
+    C927 If a POS= specifier appears, the io-control-spec-list shall not
+         contain a REC= specifier.
+    C928 If a DECIMAL=, BLANK=, PAD=, SIGN=, or ROUND= specifier appears, a
+         format or namelist-group-name shall also appear.
+    C929 If a DELIM= specifier appears, either format shall be an asterisk or
+         namelist-group-name shall appear.
+
+    TODO #267. Of these constraints, only C910 & C916-918 are currently
+    enforced.
+
+    '''
     subclass_names = []
-    use_names = ['Io_Control_Spec']
+    use_names = ['Io_Control_Spec', 'Namelist_Group_Name', 'Format']
 
     @staticmethod
     def match(string):
+        '''
+        Attempts to match the supplied string with a list of Io_Control_Spec
+        items. We have to override the base implementation because the first
+        two items in the list have specific meanings if they are not explictly
+        named: the first must be the unit number and the second may be either
+        a format specifier *or* a namelist-group-name.
+
+        :param str string: the string that is checked for a match.
+
+        :returns: a tuple of Io_Control_Spec objects if the match is \
+                  successful, None otherwise.
+        :rtype: tuple of :py:class:`fparser.two.Fortran2003.Io_Control_Spec` \
+                objects or NoneType
+
+        '''
         line, repmap = string_replace_map(string)
         splitted = line.split(',')
         lst = []
-        unit_is_positional = False
-        for idx in range(len(splitted)):
-            spec_orig = splitted[idx].strip()
-            spec = repmap(spec_orig)
-            if idx == 0 and "=" not in spec_orig:
-                # Must be a unit number. However, we do not prepend "UNIT="
-                # to it in case the following Io_Control_Spec is positional
-                # (and therefore either a Format or Namelist spec).
-                lst.append(Io_Control_Spec(repmap(spec)))
-                unit_is_positional = True
-            elif idx == 1 and "=" not in spec_orig:
-                if not unit_is_positional:
-                    # Cannot have a positional argument following a
-                    # named argument
-                    return
-                # Without knowing the type of the variable named in spec
-                # we have no way of knowing whether this is a format or
-                # a namelist specifier. However, if it is a character
-                # constant or "*" then it must be a Format spec and we can
-                # prepend "FMT=" to it.
-                spec = spec.lstrip().rstrip()
-                if Char_Literal_Constant.match(spec) or \
-                   StringBase.match("*", spec):
-                    spec = "FMT={0}".format(spec)
-                lst.append(Io_Control_Spec(spec))
-            else:
+
+        # Examine the first entry in the list. If it is not named then it must
+        # be a unit number (C910).
+        have_unit = False
+        have_unnamed_nml_or_fmt = False
+        spec = splitted.pop(0).strip()
+        spec = repmap(spec)
+
+        try:
+
+            try:
+                Io_Unit(spec)
+                # We matched an unamed unit number. We now need to construct an
+                # Io_Control_Spec for it. In order to do so we have to
+                # temporarily name it so that Io_Control_Spec matches it.
+                io_spec = Io_Control_Spec("unit="+spec)
+                # Remove the name from the new object
+                io_spec.items = (None, io_spec.items[1])
+                lst.append(io_spec)
+                # Record that we have found a unit number for the purpose of
+                # performing validation checks.
+                have_unit = True
+
+                if not splitted:
+                    # The list only has one entry and it is an IO unit
+                    return ',', tuple(lst)
+
+                # Since the unit-number was not named, the following item may
+                # also not be named if it is a format specifier or namelist
+                # group name.
+                spec = splitted.pop(0).strip()
+                spec = repmap(spec)
+                for cls, name in [(Namelist_Group_Name, 'nml'),
+                                  (Format, 'fmt')]:
+                    try:
+                        if cls(spec):
+                            # We have a match on an un-named entry. We
+                            # temporarily add the name so that Io_Control_Spec
+                            # matches the correct one.
+                            io_spec = Io_Control_Spec(name+"="+spec)
+                            # Remove the name from the new object
+                            io_spec.items = (None, io_spec.items[1])
+                            lst.append(io_spec)
+                            have_unnamed_nml_or_fmt = True
+                            break
+                    except NoMatchError:
+                        pass
+                else:
+                    raise NoMatchError("Not an un-named nml-group-name or fmt")
+
+            except NoMatchError:
+                # If we get here we failed to match an un-named spec so from
+                # here on, they must all be named.
                 lst.append(Io_Control_Spec(spec))
+
+            # Deal with the remainder of the list entries. These must all be
+            # named.
+            for spec in splitted:
+                mapped_spec = repmap(spec.strip())
+                lst.append(Io_Control_Spec(mapped_spec))
+
+        except NoMatchError:
+            return None
+
+        # At this point we need to check the list and apply constraints.
+        # TODO #267 enforce remaining constraints.
+        have_nml = False
+        have_fmt = False
+        for spec in lst:
+            if spec.children[0] == 'UNIT':
+                have_unit = True
+            elif spec.children[0] == 'NML':
+                have_nml = True
+            elif spec.children[0] == 'FMT':
+                have_fmt = True
+        # C910: An io-unit shall be specified
+        if not have_unit:
+            return None
+        # C916: an io-control-spec-list shall not contain both a format
+        # and a namelist-group-name
+        if have_nml and have_fmt:
+            return None
+        if have_unnamed_nml_or_fmt and (have_nml or have_fmt):
+            return None
+
         return ',', tuple(lst)
 
 
-class Io_Control_Spec(KeywordValueBase):  # R913
+class Io_Control_Spec(KeywordValueBase):
     """
-    <io-control-spec> = [ UNIT = ] <io-unit>
+    This class implements *partial* support for Rule 913:
+
+    <io-control-spec> is  [UNIT = ] <io-unit>
                         | [ FMT = ] <format>
                         | [ NML = ] <namelist-group-name>
                         | ADVANCE = <scalar-default-char-expr>
@@ -7340,6 +7468,11 @@ class Io_Control_Spec(KeywordValueBase):  # R913
                         | ROUND = <scalar-default-char-expr>
                         | SIGN = <scalar-default-char-expr>
                         | SIZE = <scalar-int-variable>
+
+    The support is partial because this class requires that every spec be
+    named. The specs that may not be named are explicitly handled in
+    Io_Control_Spec_List.match().
+
     """
     subclass_names = []
     use_names = ['Io_Unit', 'Format', 'Namelist_Group_Name',
@@ -7352,14 +7485,8 @@ class Io_Control_Spec(KeywordValueBase):  # R913
     def match(string):
         for (k, v) in [('UNIT', Io_Unit),
                        ('FMT', Format),
-                       ('NML', Namelist_Group_Name)]:
-            obj = KeywordValueBase.match(k, v, string,
-                                         require_lhs=False,
-                                         upper_lhs=True)
-            if obj:
-                return obj
-
-        for (k, v) in [(['ADVANCE', 'BLANK', 'DECIMAL', 'DELIM', 'PAD',
+                       ('NML', Namelist_Group_Name),
+                       (['ADVANCE', 'BLANK', 'DECIMAL', 'DELIM', 'PAD',
                          'ROUND', 'SIGN'], Scalar_Default_Char_Expr),
                        ('ASYNCHRONOUS', Scalar_Char_Initialization_Expr),
                        (['END', 'EOR', 'ERR'], Label),
@@ -7369,7 +7496,7 @@ class Io_Control_Spec(KeywordValueBase):  # R913
             obj = KeywordValueBase.match(k, v, string, upper_lhs=True)
             if obj:
                 return obj
-        return
+        return None
 
 
 class Format(StringBase):  # R914
diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py
index 918bbb9..89de8d7 100644
--- a/src/fparser/two/utils.py
+++ b/src/fparser/two/utils.py
@@ -70,7 +70,6 @@
 # First version created: Oct 2006
 
 import re
-import logging
 import six
 from fparser.common.splitline import string_replace_map
 from fparser.two import pattern_tools as pattern
@@ -374,7 +373,7 @@ class Base(ComparableMixin):
                     continue
                 try:
                     obj = subcls(string, parent_cls=parent_cls)
-                except NoMatchError as msg:
+                except NoMatchError:
                     obj = None
                 if obj is not None:
                     return obj
@@ -1038,33 +1037,46 @@ class SeparatorBase(Base):
 
 
 class KeywordValueBase(Base):
-    """
-::
-    <keyword-value-base> = [ <lhs> = ] <rhs>
-    """
+    '''
+
+    keyword-value-base is [ lhs = ] rhs
+
+    where:
+
+    R215 keyword is name.
+
+    '''
     @staticmethod
     def match(lhs_cls, rhs_cls, string, require_lhs=True, upper_lhs=False):
         '''
-        :param lhs_cls: list, tuple or single value of classes to attempt to
-                        match LHS against (in order), or string containing
-                        keyword to match
+        Attempts to match the supplied `string` with `lhs_cls` = `rhs_cls`.
+        If `lhs_cls` is a str then it is compared with the content to the
+        left of the first '=' character in `string`. If that content is a
+        valid Fortran name but does *not* match `lhs_cls` then the match
+        fails, irrespective of the setting of `require_lhs`.
+
+        :param lhs_cls: list, tuple or single value of classes to attempt to \
+                        match LHS against (in order), or string containing \
+                        keyword to match.
         :type lhs_cls: names of classes deriving from `:py:class:Base` or str
-        :param rhs_cls: name of class to match RHS against
+        :param rhs_cls: name of class to match RHS against.
         :type rhs_cls: name of a class deriving from `:py:class:Base`
-        :param str string: text to be matched
-        :param bool require_lhs: whether the expression to be matched must
-                                 contain a LHS that is assigned to
-        :param bool upper_lhs: whether or not to convert the LHS of the
-                               matched expression to upper case
-        :return: instances of the classes representing quantities on the LHS
-                 and RHS (LHS is optional) or nothing if no match is found
-        :rtype: 2-tuple of objects or nothing
+        :param str string: text to be matched.
+        :param bool require_lhs: whether the expression to be matched must \
+                                 contain a LHS that is assigned to.
+        :param bool upper_lhs: whether or not to convert the LHS of the \
+                               matched expression to upper case.
+
+        :return: instances of the classes representing quantities on the LHS \
+                 and RHS (LHS is optional) or None if no match is found.
+        :rtype: 2-tuple of objects or NoneType
+
         '''
         if require_lhs and '=' not in string:
-            return
+            return None
         if isinstance(lhs_cls, (list, tuple)):
-            for s in lhs_cls:
-                obj = KeywordValueBase.match(s, rhs_cls, string,
+            for cls in lhs_cls:
+                obj = KeywordValueBase.match(cls, rhs_cls, string,
                                              require_lhs=require_lhs,
                                              upper_lhs=upper_lhs)
                 if obj:
@@ -1072,28 +1084,37 @@ class KeywordValueBase(Base):
             return obj
         # We can't just blindly check whether 'string' contains an '='
         # character as it could itself hold a string constant containing
-        # an '=', e.g. FMT='("Hello = False")'
-        from fparser.two.Fortran2003 import Char_Literal_Constant
-        if not Char_Literal_Constant.match(string) and "=" in string:
-            lhs, rhs = string.split('=', 1)
-            lhs = lhs.rstrip()
-        else:
-            lhs = None
-            rhs = string.rstrip()
-        rhs = rhs.lstrip()
-        if not rhs:
-            return
+        # an '=', e.g. FMT='("Hello = False")'.
+        # Therefore we only split on the left-most '=' character
+        pieces = string.split('=', 1)
+        lhs = None
+        if len(pieces) == 2:
+            # It does contain at least one '='. Proceed to attempt to match
+            # the content on the LHS of it.
+            lhs = pieces[0].strip()
+            if isinstance(lhs_cls, str):
+                # lhs_cls is a keyword
+                if upper_lhs:
+                    lhs = lhs.upper()
+                if lhs != lhs_cls:
+                    # The content to the left of the '=' does not match the
+                    # supplied keyword
+                    lhs = None
+            else:
+                lhs = lhs_cls(lhs)
         if not lhs:
+            # We haven't matched the LHS and therefore proceed to treat the
+            # whole string as a RHS if the LHS is not strictly required.
             if require_lhs:
-                return
-            return None, rhs_cls(rhs)
-        if isinstance(lhs_cls, str):
-            if upper_lhs:
-                lhs = lhs.upper()
-            if lhs_cls != lhs:
-                return
-            return lhs, rhs_cls(rhs)
-        return lhs_cls(lhs), rhs_cls(rhs)
+                return None
+            rhs = string.strip()
+        else:
+            rhs = pieces[-1].strip()
+        if rhs:
+            rhs = rhs_cls(rhs)
+        if not rhs:
+            return None
+        return lhs, rhs
 
     def tostr(self):
         if self.items[0] is None:


Test patch:
diff --git a/src/fparser/two/tests/fortran2003/test_io_control_spec_r913.py b/src/fparser/two/tests/fortran2003/test_io_control_spec_r913.py
new file mode 100644
index 0000000..2d417cb
--- /dev/null
+++ b/src/fparser/two/tests/fortran2003/test_io_control_spec_r913.py
@@ -0,0 +1,261 @@
+# Copyright (c) 2020-2021 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 py.test tests for the Io_Control_Spec class and
+the list variant - r913.
+
+'''
+
+from __future__ import print_function
+import pytest
+from fparser.two import Fortran2003
+from fparser.two.parser import ParserFactory
+
+# this is required to setup the fortran2003 classes
+_ = ParserFactory().create(std="f2003")
+
+# Tests for the io_control_spec class.
+
+
+def test_io_control_spec():
+    ''' Test that we can construct an io-control-spec and that its str
+    and repr properties are correct. '''
+    tcls = Fortran2003.Io_Control_Spec
+    obj = tcls('end=123')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'END = 123'
+    assert repr(obj) == "Io_Control_Spec('END', Label('123'))"
+
+
+@pytest.mark.parametrize("string", ["6", "end 123"])
+def test_io_ctrl_spec_named_only(string):
+    ''' Check that io-control-spec only matches named entities. '''
+    tcls = Fortran2003.Io_Control_Spec
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        _ = tcls(string)
+    assert "Io_Control_Spec: '{0}'".format(string) in str(err.value)
+
+
+@pytest.mark.parametrize("lhs, rhs",
+                         [("unit", "num"), ("unit", "6"),
+                          ("fmt", "fmt_str"), ("fmt", "*"), ("fmt", "'(I3)'"),
+                          ("nml", "nml_name"),
+                          ("advance", "yes"), ("advance", "no"),
+                          ("asynchronous", "yes"), ("asynchronous", "no"),
+                          ("blank", "null"), ("blank", "zero"),
+                          ("decimal", "comma"), ("decimal", "point"),
+                          ("delim", "apostrophe"),
+                          ("end", "10"),
+                          ("eor", "10"),
+                          ("err", "10"),
+                          ("id", "var"),
+                          ("iomsg", "msg_var"),
+                          ("iostat", "var"),
+                          ("pad", "yes"), ("pad", "no"),
+                          ("pos", "posn"), ("pos", "10"),
+                          ("rec", "1"),
+                          ("round", "up"), ("round", "down"),
+                          ("round", "zero"), ("round", "nearest"),
+                          ("round", "compatible"),
+                          ("round", "processor_defined"),
+                          ("sign", "plus"), ("sign", "suppress"),
+                          ("sign", "processor_defined"),
+                          ("size", "var")])
+def test_io_ctrl_spec_named_entities(lhs, rhs):
+    ''' Check that all valid names are matched correctly. '''
+    obj = Fortran2003.Io_Control_Spec("=".join([lhs, rhs]))
+    assert isinstance(obj, Fortran2003.Io_Control_Spec)
+    assert obj.children[0] == lhs.upper()
+    obj = Fortran2003.Io_Control_Spec("=".join([lhs.upper(), rhs]))
+    assert isinstance(obj, Fortran2003.Io_Control_Spec)
+
+
+@pytest.mark.parametrize("lhs, rhs",
+                         [("asize", "var"), ("unit", "1.0"), ("size", "10")])
+def test_io_ctrl_spec_invalid_values(lhs, rhs):
+    ''' Check that io_ctrl_spec does not match if a keyword is unrecognised
+    or the value is invalid. '''
+    with pytest.raises(Fortran2003.NoMatchError):
+        _ = Fortran2003.Io_Control_Spec("=".join([lhs, rhs]))
+
+# Tests for the io_control_spec_list class.
+
+
+def test_io_control_spec_list():
+    ''' Test that we correctly parse and then generate various
+    forms of IO-control specification lists (R913-list). '''
+    tcls = Fortran2003.Io_Control_Spec_List
+    obj = tcls('23, end=123')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == '23, END = 123'
+    assert (repr(obj) == "Io_Control_Spec_List(',', (Io_Control_Spec("
+            "None, Int_Literal_Constant('23', None)), Io_Control_Spec('END', "
+            "Label('123'))))")
+
+    obj = tcls('123')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == '123'
+
+    obj = tcls('123,*')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == '123, *'
+    assert repr(obj) == ("Io_Control_Spec_List(',', (Io_Control_Spec("
+                         "None, Int_Literal_Constant('123', None)), "
+                         "Io_Control_Spec(None, Format('*'))))")
+
+    obj = tcls('123,fmt=a')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == '123, FMT = a'
+    assert repr(obj) == ("Io_Control_Spec_List(',', "
+                         "(Io_Control_Spec(None, "
+                         "Int_Literal_Constant('123', None)), "
+                         "Io_Control_Spec('FMT', Name('a'))))")
+
+    obj = tcls('123,nml=a')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == '123, NML = a'
+    assert repr(obj) == ("Io_Control_Spec_List(',', "
+                         "(Io_Control_Spec(None, "
+                         "Int_Literal_Constant('123', None)), "
+                         "Io_Control_Spec('NML', Name('a'))))")
+
+    obj = tcls('123, "(I3)"')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == '123, "(I3)"'
+
+    obj = tcls('123,a')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == '123, a'
+
+    # Unit named and not the first argument
+    obj = tcls('fmt=b, unit=123')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'FMT = b, UNIT = 123'
+
+
+def test_io_control_spec_list_3_or_more():
+    ''' Test the matching when there are more than 2 entries in list - 3rd
+    and subsequent entries must be named.
+    '''
+    tcls = Fortran2003.Io_Control_Spec_List
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        _ = tcls('123,a,b')
+    assert "Io_Control_Spec_List: '123,a,b'" in str(err.value)
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        _ = tcls('123,a,err=10,var')
+    assert "Io_Control_Spec_List: '123,a,err=10,var'" in str(err.value)
+
+    obj = tcls('123,a,err=10')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == '123, a, ERR = 10'
+
+    obj = tcls('123,a,err=10,end=12')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == '123, a, ERR = 10, END = 12'
+
+    obj = tcls('unit=123,err=10,end=12')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'UNIT = 123, ERR = 10, END = 12'
+
+
+def test_io_control_spec_list_invalid_first_entry():
+    ''' Test that the special handling for the first item in the list
+    rejects an invalid, unnamed entry. '''
+    tcls = Fortran2003.Io_Control_Spec_List
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls("'name'")
+    assert "Io_Control_Spec_List: ''name''" in str(err.value)
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls("1.0")
+    assert "Io_Control_Spec_List: '1.0'" in str(err.value)
+
+
+def test_io_control_spec_list_invalid_2nd_entry():
+    ''' Test that the special handling for the first two unnamed items in
+    the list rejects an invalid, unnamed second entry. '''
+    tcls = Fortran2003.Io_Control_Spec_List
+    # The second entry must be either a format specifier or a namelist-group-
+    # name. We therefore supply a real literal as that doesn't match either.
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls("unit_no, 1.0")
+    assert "Io_Control_Spec_List: 'unit_no, 1.0'" in str(err.value)
+
+
+def test_io_spec_list_c910():
+    ''' Check that various constraints are applied in the matching. Currently
+    only C910, C916-918 are implemented (#267). '''
+    tcls = Fortran2003.Io_Control_Spec_List
+    # C910 - must have a unit number
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls('fmt="some-fmt", end=10')
+    assert "Io_Control_Spec_List: 'fmt=\"some-fmt\", end=10'" in str(err.value)
+    # Again but when list contains only one entry
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls('fmt="some-fmt"')
+    assert "Io_Control_Spec_List: 'fmt=\"some-fmt\"'" in str(err.value)
+
+
+def test_io_spec_list_c916():
+    ''' C916 - cannot have both a namelist and a format. '''
+    tcls = Fortran2003.Io_Control_Spec_List
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls('123,nml=a,fmt=b')
+    assert "Io_Control_Spec_List: '123,nml=a,fmt=b'" in str(err.value)
+    # Check when we have an un-named second entry
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls('123, "(I3)", fmt=var')
+    assert "Io_Control_Spec_List: '123, \"(I3)\", fmt=var'" in str(err.value)
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls('123, nml_grp_name, fmt=var')
+    assert ("Io_Control_Spec_List: '123, nml_grp_name, fmt=var'" in
+            str(err.value))
+
+
+def test_io_spec_list_c917():
+    ''' C917, if format is second item and is un-named then first item must be
+    un-named unit number. '''
+    tcls = Fortran2003.Io_Control_Spec_List
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls('unit=10, "(\'a format\')"')
+    assert "Io_Control_Spec_List: 'unit=10" in str(err.value)
+
+
+def test_io_spec_list_c918():
+    ''' C918, if namelist group name is second item and is un-named then first
+    item must be un-named unit number. '''
+    tcls = Fortran2003.Io_Control_Spec_List
+    with pytest.raises(Fortran2003.NoMatchError) as err:
+        tcls('unit=10, nml_grp_name')
+    assert "Io_Control_Spec_List: 'unit=10" in str(err.value)
diff --git a/src/fparser/two/tests/fortran2003/test_write_stmt_r911.py b/src/fparser/two/tests/fortran2003/test_write_stmt_r911.py
new file mode 100644
index 0000000..4c77fba
--- /dev/null
+++ b/src/fparser/two/tests/fortran2003/test_write_stmt_r911.py
@@ -0,0 +1,117 @@
+# This Python file uses the following encoding: utf-8
+# Copyright (c) 2020 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.
+
+''' pytest module for the Fortran2003 Write Statement - R911. '''
+
+import pytest
+from fparser.two.utils import NoMatchError
+from fparser.two.Fortran2003 import Write_Stmt, Io_Control_Spec_List
+
+
+@pytest.mark.usefixtures("f2003_create")
+def test_write_stmt():
+    ''' Tests for various forms of WRITE statement (R911). '''
+    tcls = Write_Stmt
+    obj = tcls('write (123)"hey"')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'WRITE(123) "hey"'
+    assert repr(obj).replace("u'", "'") == (
+        "Write_Stmt(Io_Control_Spec_List(',', (Io_Control_Spec(None, "
+        "Int_Literal_Constant('123', None)),)), Output_Item_List(',', "
+        "(Char_Literal_Constant('\"hey\"', None),)))")
+
+    obj = tcls('WRITE (*,"(I3)") my_int')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'WRITE(*, "(I3)") my_int'
+    assert repr(obj).replace("u'", "'") == (
+        "Write_Stmt(Io_Control_Spec_List(',', (Io_Control_Spec(None, "
+        "Io_Unit('*')), Io_Control_Spec(None, "
+        "Char_Literal_Constant('\"(I3)\"', None)))), Output_Item_List(',', "
+        "(Name('my_int'),)))")
+
+    obj = tcls('WRITE (*,namtest)')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'WRITE(*, namtest)'
+    assert repr(obj).replace("u'", "'") == (
+        "Write_Stmt(Io_Control_Spec_List(',', "
+        "(Io_Control_Spec(None, Io_Unit('*')), Io_Control_Spec(None, "
+        "Name('namtest')))), None)")
+
+    # Test when format specifier contains an '=' character
+    iolist = Io_Control_Spec_List("*,'(5X,\"q_mesh =\",4F12.8)'")
+    assert isinstance(iolist, Io_Control_Spec_List)
+    obj = tcls("WRITE(*,'(5X,\"q_mesh =\",1F12.8)') 1.d0")
+    assert isinstance(obj, tcls)
+    assert repr(obj).replace("u'", "'") == (
+        "Write_Stmt(Io_Control_Spec_List(',', (Io_Control_Spec(None, "
+        "Io_Unit('*')), Io_Control_Spec(None, "
+        "Char_Literal_Constant('\\'(5X,\"q_mesh =\",1F12.8)\\'', None)))), "
+        "Output_Item_List(',', (Real_Literal_Constant('1.D0', None),)))")
+
+    obj = tcls("WRITE(*,FMT='(5X,\"q_mesh =\",1F12.8)') 1.d0")
+    assert isinstance(obj, tcls)
+    assert repr(obj).replace("u'", "'") == (
+        "Write_Stmt(Io_Control_Spec_List(',', (Io_Control_Spec(None, "
+        "Io_Unit('*')), Io_Control_Spec('FMT', "
+        "Char_Literal_Constant('\\'(5X,\"q_mesh =\",1F12.8)\\'', None)))), "
+        "Output_Item_List(',', (Real_Literal_Constant('1.D0', None),)))")
+
+    # Format specifier contains an '=' and is built using concatenation
+    obj = tcls('''WRITE (6, '("write = some=""'//'text'//'""")')''')
+    assert isinstance(obj, tcls)
+    assert str(obj) == '''WRITE(6, '("write = some=""' // 'text' // '""")')'''
+    obj_repr = repr(obj)
+    obj_repr = obj_repr.replace('u"', '"')
+    assert obj_repr.replace("u'", "'") == (
+        "Write_Stmt(Io_Control_Spec_List(',', "
+        "(Io_Control_Spec(None, Int_Literal_Constant('6', None)), "
+        "Io_Control_Spec(None, Level_3_Expr(Level_3_Expr("
+        "Char_Literal_Constant('\\'(\"write = some=\"\"\\'', None), '//', "
+        "Char_Literal_Constant(\"'text'\", None)), '//', "
+        "Char_Literal_Constant('\\'\"\"\")\\'', None))))), None)")
+
+
+@pytest.mark.usefixtures("f2003_create")
+def test_named_unit_before_fmt_error():
+    ''' Check that we reject a WRITE that names the io-unit argument but
+    still has a positional format argument (containing an '=').
+    TODO #267. This test needs expanding and probably moving to a file
+    dedicated to R913 and its (many) constraints.
+
+    '''
+    tcls = Write_Stmt
+    # Cannot have an un-named (positional) argument after a named argument
+    with pytest.raises(NoMatchError):
+        tcls('''WRITE (UNIT=6, '("write some=""'//'text'//'""")')''')
diff --git a/src/fparser/two/tests/test_bases.py b/src/fparser/two/tests/test_bases.py
index cd2fa9b..3dca5b7 100644
--- a/src/fparser/two/tests/test_bases.py
+++ b/src/fparser/two/tests/test_bases.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2017-2020 Science and Technology Facilities Council.
+# Copyright (c) 2017-2021 Science and Technology Facilities Council.
 
 # All rights reserved.
 
@@ -35,6 +35,7 @@
 ''' Module containing pytest tests for fparser2 base classes '''
 
 import pytest
+from fparser.two.utils import NoMatchError
 
 
 @pytest.mark.usefixtures("f2003_create")
@@ -69,6 +70,12 @@ def test_keywordvaluebase_errors():
                                  "'(\"my_var =  \", (A))'",
                                  require_lhs=True)
     assert obj is None
+    # Check with a valid Fortran name on the lhs but not what the match
+    # is requesting.
+    with pytest.raises(NoMatchError) as err:
+        _ = KeywordValueBase.match("FERMAT", Format, "FMT='(A)'",
+                                   require_lhs=False)
+    assert "FMT='(A)'" in str(err.value)
 
 
 @pytest.mark.usefixtures("f2003_create")
diff --git a/src/fparser/two/tests/test_fortran2003.py b/src/fparser/two/tests/test_fortran2003.py
index de88187..bcd82a6 100644
--- a/src/fparser/two/tests/test_fortran2003.py
+++ b/src/fparser/two/tests/test_fortran2003.py
@@ -104,13 +104,14 @@ def _repr_utf(anobj):
     :param object anobj: the object for which the repr() is required.
 
     :returns: the repr of the supplied object with any "u'" replaced
-              with "'".
+              with "'" and '(u"' with '("'.
     :rtype: str
 
     '''
     import six
     if six.PY2:
-        return repr(anobj).replace("u'", "'")
+        new_repr = repr(anobj).replace("u'", "'")
+        return new_repr.replace('(u"', '("')
     return repr(anobj)
 
 #
@@ -2369,54 +2370,6 @@ def test_read_stmt():
         "Name('a_namelist_or_format')))), None, None)")
 
 
-def test_write_stmt():
-    ''' Tests for various forms of WRITE statement (R911). '''
-    tcls = Write_Stmt
-    obj = tcls('write (123)"hey"')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'WRITE(123) "hey"'
-    assert _repr_utf(obj) == (
-        "Write_Stmt(Io_Control_Spec_List(',', (Io_Control_Spec(None, "
-        "Int_Literal_Constant('123', None)),)), Output_Item_List(',', "
-        "(Char_Literal_Constant('\"hey\"', None),)))")
-
-    obj = tcls('WRITE (*,"(I3)") my_int')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'WRITE(*, FMT = "(I3)") my_int'
-    assert _repr_utf(obj) == (
-        "Write_Stmt(Io_Control_Spec_List(',', (Io_Control_Spec(None, "
-        "Io_Unit('*')), Io_Control_Spec('FMT', "
-        "Char_Literal_Constant('\"(I3)\"', None)))), Output_Item_List(',', "
-        "(Name('my_int'),)))")
-
-    obj = tcls('WRITE (*,namtest)')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'WRITE(*, namtest)'
-    assert _repr_utf(obj) == (
-        "Write_Stmt(Io_Control_Spec_List(',', "
-        "(Io_Control_Spec(None, Io_Unit('*')), Io_Control_Spec(None, "
-        "Name('namtest')))), None)")
-
-    # Test when format specifier contains an '=' character
-    iolist = Io_Control_Spec_List("*,'(5X,\"q_mesh =\",4F12.8)'")
-    assert isinstance(iolist, Io_Control_Spec_List)
-    obj = tcls("WRITE(*,'(5X,\"q_mesh =\",1F12.8)') 1.d0")
-    assert isinstance(obj, tcls)
-    assert _repr_utf(obj) == (
-        "Write_Stmt(Io_Control_Spec_List(',', (Io_Control_Spec(None, "
-        "Io_Unit('*')), Io_Control_Spec('FMT', "
-        "Char_Literal_Constant('\\'(5X,\"q_mesh =\",1F12.8)\\'', None)))), "
-        "Output_Item_List(',', (Real_Literal_Constant('1.D0', None),)))")
-
-    obj = tcls("WRITE(*,FMT='(5X,\"q_mesh =\",1F12.8)') 1.d0")
-    assert isinstance(obj, tcls)
-    assert _repr_utf(obj) == (
-        "Write_Stmt(Io_Control_Spec_List(',', (Io_Control_Spec(None, "
-        "Io_Unit('*')), Io_Control_Spec('FMT', "
-        "Char_Literal_Constant('\\'(5X,\"q_mesh =\",1F12.8)\\'', None)))), "
-        "Output_Item_List(',', (Real_Literal_Constant('1.D0', None),)))")
-
-
 def test_print_stmt():  # R912
 
     tcls = Print_Stmt
@@ -2430,59 +2383,6 @@ def test_print_stmt():  # R912
     assert str(obj) == 'PRINT *, "a=", a'
 
 
-def test_io_control_spec():  # R913
-
-    tcls = Io_Control_Spec
-    obj = tcls('end=123')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'END = 123'
-    assert repr(obj) == "Io_Control_Spec('END', Label('123'))"
-
-
-def test_io_control_spec_list():
-    ''' Test that we correctly parse and then generate various
-    forms of IO-control specification lists (R913-list). '''
-    tcls = Io_Control_Spec_List
-    obj = tcls('end=123')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'END = 123'
-    assert repr(obj) == \
-        "Io_Control_Spec_List(',', (Io_Control_Spec('END', Label('123')),))"
-
-    obj = tcls('123')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == '123'
-
-    obj = tcls('123,*')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == '123, FMT = *'
-    assert repr(obj) == ("Io_Control_Spec_List(',', (Io_Control_Spec(None, "
-                         "Int_Literal_Constant('123', None)), "
-                         "Io_Control_Spec('FMT', Format('*'))))")
-
-    obj = tcls('123,fmt=a')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == '123, FMT = a'
-    assert repr(obj) == ("Io_Control_Spec_List(',', (Io_Control_Spec(None, "
-                         "Int_Literal_Constant('123', None)), "
-                         "Io_Control_Spec('FMT', Name('a'))))")
-
-    obj = tcls('123,nml=a')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == '123, NML = a'
-    assert repr(obj) == ("Io_Control_Spec_List(',', (Io_Control_Spec(None, "
-                         "Int_Literal_Constant('123', None)), "
-                         "Io_Control_Spec('NML', Name('a'))))")
-
-    obj = tcls('123, "(I3)"')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == '123, FMT = "(I3)"'
-
-    obj = tcls('123,a')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == '123, a'
-
-
 def test_format():  # R914
 
     tcls = Format