Repository: stfc/fparser

Issue:
Allow for matching of isolated code fragments (i.e. with no associated scoping region)
Testing PSyclone with master of fparser has revealed that the new symbol-table functionality causes problems - whenever fparser is used to parse just a fragment (e.g. by calling `match` directly) then there is no associated scoping region and `SYMBOL_TABLES.current_scope` (correctly) returns None. We just need to allow for this in the three places where `current_scope` is used.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dfd3451..5aca068 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,8 @@ Modifications by (in alphabetical order):
 * P. Vitt, University of Siegen, Germany
 * A. Voysey, UK Met Office
 
+15/03/2022 PR #311 for #310. Allows symbol table scope to be None.
+
 08/12/2021 PR #293 towards #201. Initial symbol-table support added.
 
 02/12/2021 PR #305 for #304. Fix failing build of documentation on RTD.
diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py
index 02f15c2..d0e1d53 100644
--- a/src/fparser/two/Fortran2003.py
+++ b/src/fparser/two/Fortran2003.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 
-# Modified work Copyright (c) 2017-2021 Science and Technology
+# Modified work Copyright (c) 2017-2022 Science and Technology
 # Facilities Council.
 # Original work Copyright (c) 1999-2008 Pearu Peterson
 
@@ -2668,7 +2668,7 @@ class Type_Declaration_Stmt(Type_Declaration_StmtBase):  # R501
         '''
         Attempts to match the supplied string as a type declaration. If the
         match is successful the declared symbols are added to the symbol table
-        of the current scope.
+        of the current scope (if there is one).
 
         :param str string: the string to match.
 
@@ -2684,7 +2684,7 @@ class Type_Declaration_Stmt(Type_Declaration_StmtBase):  # R501
             # symbol table of the current scoping region.
             table = SYMBOL_TABLES.current_scope
 
-            if isinstance(result[0], Intrinsic_Type_Spec):
+            if table and isinstance(result[0], Intrinsic_Type_Spec):
                 # We have a definition of symbol(s) of intrinsic type
                 decl_list = walk(result, Entity_Decl)
                 for decl in decl_list:
@@ -9293,7 +9293,8 @@ class Use_Stmt(StmtBase):  # pylint: disable=invalid-name
     def match(string):
         '''
         Wrapper for the match method that captures any successfully-matched
-        use statements in the symbol table associated with the current scope.
+        use statements in the symbol table associated with the current scope
+        (if there is one).
 
         :param str string: Fortran code to check for a match.
 
@@ -9308,12 +9309,13 @@ class Use_Stmt(StmtBase):  # pylint: disable=invalid-name
         result = Use_Stmt._match(string)
         if result:
             table = SYMBOL_TABLES.current_scope
-            only_list = None
-            # TODO #201 we currently ignore any symbol renaming here
-            if isinstance(result[4], Only_List):
-                names = walk(result[4], Name)
-                only_list = [name.string for name in names]
-            table.add_use_symbols(str(result[2]), only_list)
+            if table:
+                only_list = None
+                # TODO #201 we currently ignore any symbol renaming here
+                if isinstance(result[4], Only_List):
+                    names = walk(result[4], Name)
+                    only_list = [name.string for name in names]
+                table.add_use_symbols(str(result[2]), only_list)
 
         return result
 
@@ -10281,7 +10283,9 @@ class Intrinsic_Function_Reference(CallBase):  # No explicit rule
                 table.lookup(function_name)
                 # We found a matching name so refuse to match this intrinsic.
                 return None
-            except KeyError:
+            except (KeyError, AttributeError):
+                # There is either no matching name in the table or we have
+                # no current scoping region.
                 pass
 
             # This if/else will not be needed once issue #170 has been


Test patch:
diff --git a/src/fparser/two/tests/fortran2003/test_intrinsics.py b/src/fparser/two/tests/fortran2003/test_intrinsics.py
index 5e42c1b..92d8f29 100644
--- a/src/fparser/two/tests/fortran2003/test_intrinsics.py
+++ b/src/fparser/two/tests/fortran2003/test_intrinsics.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2019-2021, Science and Technology Facilities Council.
+# Copyright (c) 2019-2022, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -105,18 +105,26 @@ def test_intrinsic_name_case_insensitive(f2003_create):
 # class intrinsic_function_reference
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
-def test_intrinsic_function_reference_generic(f2003_create):
+@pytest.mark.usefixtures("f2003_create")
+def test_intrinsic_function_reference_generic():
     '''Test that class Intrinsic_Function_Reference correctly matches a
-    generic intrinsic with a valid number of arguments.
+    generic intrinsic with a valid number of arguments. We test both
+    with and without the existance of a symbol table.
 
     '''
     result = Intrinsic_Function_Reference("SIN(A)")
     assert isinstance(result, Intrinsic_Function_Reference)
     assert str(result) == "SIN(A)"
+    # Repeat when there is a scoping region.
+    SYMBOL_TABLES.enter_scope("test_scope")
+    result = Intrinsic_Function_Reference("SIN(A)")
+    assert isinstance(result, Intrinsic_Function_Reference)
+    assert str(result) == "SIN(A)"
+    table = SYMBOL_TABLES.current_scope
+    assert "sin" not in table._data_symbols
+    SYMBOL_TABLES.exit_scope()
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_intrinsic_function_reference(f2003_create):
     '''Test that class Intrinsic_Function_Reference correctly matches a
     specific intrinsic with a valid number of arguments.
@@ -127,7 +135,6 @@ def test_intrinsic_function_reference(f2003_create):
     assert str(result) == "DSIN(A)"
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_intrinsic_function_nomatch(f2003_create):
     '''Test that class Intrinsic_Function_Reference raises the expected
     exception if there is no match.
@@ -137,7 +144,6 @@ def test_intrinsic_function_nomatch(f2003_create):
         _ = Intrinsic_Function_Reference("NO_MATCH(A)")
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_intrinsic_function_reference_multi_args(f2003_create):
     '''Test that class Intrinsic_Function_Reference correctly matches a
     generic intrinsic which accepts more than one argument (two in
@@ -149,7 +155,6 @@ def test_intrinsic_function_reference_multi_args(f2003_create):
     assert str(result) == "MATMUL(A, B)"
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_intrinsic_function_reference_zero_args(f2003_create):
     '''Test that class Intrinsic_Function_Reference correctly matches a
     generic intrinsic which accepts zero arguments.
@@ -160,7 +165,6 @@ def test_intrinsic_function_reference_zero_args(f2003_create):
     assert str(result) == "COMMAND_ARGUMENT_COUNT()"
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_intrinsic_function_reference_range_args(f2003_create):
     '''Test that class Intrinsic_Function_Reference correctly matches a
     generic intrinsic which accepts a range of number of arguments.
@@ -172,7 +176,6 @@ def test_intrinsic_function_reference_range_args(f2003_create):
         assert str(result) == "SYSTEM_CLOCK({0})".format(args)
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_intrinsic_function_reference_unlimited_args(f2003_create):
     '''Test that class Intrinsic_Function_Reference correctly matches a
     generic intrinsic which accepts an unlimitednumber of arguments.
@@ -184,7 +187,6 @@ def test_intrinsic_function_reference_unlimited_args(f2003_create):
         assert str(result) == "MAX({0})".format(args)
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_intrinsic_function_reference_error1(f2003_create):
     '''Test that class Intrinsic_Function_Reference raises the expected
     exception when the valid min and max args are equal (2 in this case)
@@ -202,7 +204,6 @@ def test_intrinsic_function_reference_error1(f2003_create):
             "" in str(excinfo.value))
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_intrinsic_function_reference_error2(f2003_create):
     '''Test that class Intrinsic_Function_Reference raises the expected
     exception when the valid min args is less than the valid max args
@@ -220,7 +221,6 @@ def test_intrinsic_function_reference_error2(f2003_create):
             "" in str(excinfo.value))
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_intrinsic_function_reference_error3(f2003_create):
     '''Test that class Intrinsic_Function_Reference raises the expected
     exception when the number of arguments is unlimited.
diff --git a/src/fparser/two/tests/fortran2003/test_type_decl_stmt_r501.py b/src/fparser/two/tests/fortran2003/test_type_decl_stmt_r501.py
new file mode 100644
index 0000000..9de7c08
--- /dev/null
+++ b/src/fparser/two/tests/fortran2003/test_type_decl_stmt_r501.py
@@ -0,0 +1,116 @@
+# 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.
+
+'''
+pytest tests for Fortran2003 rule R501 - Type Declaration Statement.
+
+TODO #318 - these tests need extending to fully cover the
+Fortran2003.Type_Declaration_Stmtclass. They will also need to be
+extended as part of #259 as that will add support for the various
+constraints that apply to R501.
+
+'''
+
+import pytest
+from fparser.two import Fortran2003
+from fparser.two.symbol_table import SYMBOL_TABLES
+
+
+@pytest.mark.usefixtures("f2003_create")
+@pytest.mark.parametrize("table_name", ["", "test_mod"])
+def test_type_declaration_stmt(table_name):
+    ''' Various tests for the type declaration statement (R501). We test both
+    with and without an existing scoping region. '''
+    if table_name:
+        SYMBOL_TABLES.enter_scope(table_name)
+    table = SYMBOL_TABLES.current_scope
+
+    tcls = Fortran2003.Type_Declaration_Stmt
+    obj = tcls('integer a')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'INTEGER :: a'
+    assert (repr(obj) ==
+            "Type_Declaration_Stmt(Intrinsic_Type_Spec('INTEGER', "
+            "None), None, Entity_Decl_List(',', (Entity_Decl(Name('a'), None, "
+            "None, None),)))")
+    if table:
+        assert "a" in table._data_symbols
+
+    obj = tcls('integer ,dimension(2):: b*3')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'INTEGER, DIMENSION(2) :: b*3'
+    if table:
+        assert "b" in table._data_symbols
+
+    obj = tcls('real c')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'REAL :: c'
+    assert (repr(obj) ==
+            "Type_Declaration_Stmt(Intrinsic_Type_Spec('REAL', None), None, "
+            "Entity_Decl_List(',', (Entity_Decl(Name('c'), None, None, "
+            "None),)))")
+    if table:
+        assert "c" in table._data_symbols
+
+    obj = tcls('REAL D( LDA, * ), E( LDB, * )')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'REAL :: D(LDA, *), E(LDB, *)'
+    if table:
+        assert "d" in table._data_symbols
+        assert "e" in table._data_symbols
+
+    obj = tcls('DOUBLE PRECISION   ALPHA, BETA')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'DOUBLE PRECISION :: ALPHA, BETA'
+    if table:
+        assert "alpha" in table._data_symbols
+        assert "beta" in table._data_symbols
+
+    obj = tcls('logical,parameter:: T=.true.')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'LOGICAL, PARAMETER :: T = .TRUE.'
+    if table:
+        assert "t" in table._data_symbols
+
+    obj = tcls('character(n),private:: x(n)')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'CHARACTER(LEN = n), PRIVATE :: x(n)'
+    if table:
+        assert "x" in table._data_symbols
+
+    obj = tcls('character(lenmax),private:: y(n)')
+    assert isinstance(obj, tcls), repr(obj)
+    assert str(obj) == 'CHARACTER(LEN = lenmax), PRIVATE :: y(n)'
+    if table:
+        assert "y" in table._data_symbols
diff --git a/src/fparser/two/tests/fortran2003/test_usestmt_r1109.py b/src/fparser/two/tests/fortran2003/test_usestmt_r1109.py
index 52ceba6..8c841a2 100644
--- a/src/fparser/two/tests/fortran2003/test_usestmt_r1109.py
+++ b/src/fparser/two/tests/fortran2003/test_usestmt_r1109.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2018-2021 Science and Technology Facilities Council
+# Copyright (c) 2018-2022 Science and Technology Facilities Council.
 
 # All rights reserved.
 
@@ -40,6 +40,7 @@ Use statement.
 import pytest
 from fparser.api import get_reader
 from fparser.two.Fortran2003 import Use_Stmt
+from fparser.two.symbol_table import SYMBOL_TABLES
 from fparser.two.utils import NoMatchError, InternalError
 
 # match() use ...
@@ -48,7 +49,6 @@ from fparser.two.utils import NoMatchError, InternalError
 # match() 'use x'. Use both string and reader input here, but from
 # here on we will just use string input as that is what is passed to
 # the match() method
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use(f2003_create):
     '''Check that a basic use is parsed correctly. Input separately as a
     string and as a reader object
@@ -67,7 +67,6 @@ def test_use(f2003_create):
 
 
 # match() 'use :: x'
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_colons(f2003_create):
     '''Check that a basic use with '::' is parsed correctly.'''
     line = "use :: my_model"
@@ -77,7 +76,6 @@ def test_use_colons(f2003_create):
 
 
 # match() 'use, nature :: x'
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_nature(f2003_create):
     '''Check that a use with a 'nature' specification is parsed correctly.'''
     line = "use, intrinsic :: my_model"
@@ -89,7 +87,6 @@ def test_use_nature(f2003_create):
 
 
 # match() 'use x, rename'
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_rename(f2003_create):
     '''Check that a use with a nename clause is parsed correctly.'''
     line = "use my_module, name=>new_name"
@@ -101,10 +98,9 @@ def test_use_rename(f2003_create):
 
 
 # match() 'use x, only: y'
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_only(f2003_create):
     '''Check that a use statement is parsed correctly when there is an
-    only clause.
+    only clause. Test both with and without a scoping region.
 
     '''
     line = "use my_model, only: name"
@@ -113,10 +109,16 @@ def test_use_only(f2003_create):
     assert repr(ast) == (
         "Use_Stmt(None, None, Name('my_model'), ', ONLY:', Only_List(',', "
         "(Name('name'),)))")
+    # Repeat when there is a scoping region.
+    SYMBOL_TABLES.enter_scope("test_scope")
+    ast = Use_Stmt(line)
+    table = SYMBOL_TABLES.current_scope
+    assert "my_model" in table._modules
+    assert table._modules["my_model"] == ["name"]
+    SYMBOL_TABLES.exit_scope()
 
 
 # match() 'use x, only:'
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_only_empty(f2003_create):
     '''Check that a use statement is parsed correctly when there is an
     only clause without any content.
@@ -130,7 +132,6 @@ def test_use_only_empty(f2003_create):
 
 
 # match() '  use  ,  nature  ::  x  ,  name=>new_name'
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_spaces_1(f2003_create):
     '''Check that a use statement with spaces works correctly with
     renaming.
@@ -145,7 +146,6 @@ def test_use_spaces_1(f2003_create):
 
 
 # match() '  use  ,  nature  ::  x  ,  only  :  name'
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_spaces_2(f2003_create):
     '''Check that a use statement with spaces works correctly with an only
     clause.
@@ -160,7 +160,6 @@ def test_use_spaces_2(f2003_create):
 
 
 # match() mixed case
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_mixed_case(f2003_create):
     '''Check that a use statement with mixed case keywords ('use' and
     'only') works as expected.
@@ -190,7 +189,6 @@ def test_syntaxerror(f2003_create):
 # match() Internal errors
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_internal_error1(f2003_create):
     '''Check that an internal error is raised if the length of the Items
     list is not 5 as the str() method assumes that it is.
@@ -204,7 +202,6 @@ def test_use_internal_error1(f2003_create):
     assert "should be of size 5 but found '4'" in str(excinfo.value)
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_internal_error2(f2003_create):
     '''Check that an internal error is raised if the module name (entry 2
     of Items) is empty or None as the str() method assumes that it is
@@ -221,7 +218,6 @@ def test_use_internal_error2(f2003_create):
                 "empty") in str(excinfo.value)
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_use_internal_error3(f2003_create):
     '''Check that an internal error is raised if entry 3 of Items is
     'None' as the str() method assumes it is a (potentially empty)
diff --git a/src/fparser/two/tests/test_fortran2003.py b/src/fparser/two/tests/test_fortran2003.py
index 90b3f1b..839f18a 100644
--- a/src/fparser/two/tests/test_fortran2003.py
+++ b/src/fparser/two/tests/test_fortran2003.py
@@ -1,4 +1,4 @@
-# Modified work Copyright (c) 2017-2021 Science and Technology
+# Modified work Copyright (c) 2017-2022 Science and Technology
 # Facilities Council.
 # Original work Copyright (c) 1999-2008 Pearu Peterson
 #
@@ -125,7 +125,6 @@ def _repr_utf(anobj):
 #
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
 def test_specification_part():
     ''' Tests for parsing specification-part (R204). '''
     reader = get_reader('''\
@@ -981,51 +980,6 @@ def test_ac_implied_do_control():  # R471
 #
 
 
-@pytest.mark.usefixtures("fake_symbol_table")
-def test_type_declaration_stmt():
-    ''' Various tests for the type declaration statement (R501). '''
-    tcls = Type_Declaration_Stmt
-    obj = tcls('integer a')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'INTEGER :: a'
-    assert (_repr_utf(obj) ==
-            "Type_Declaration_Stmt(Intrinsic_Type_Spec('INTEGER', "
-            "None), None, Entity_Decl_List(',', (Entity_Decl(Name('a'), None, "
-            "None, None),)))")
-
-    obj = tcls('integer ,dimension(2):: b*3')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'INTEGER, DIMENSION(2) :: b*3'
-
-    obj = tcls('real c')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'REAL :: c'
-    assert (_repr_utf(obj) ==
-            "Type_Declaration_Stmt(Intrinsic_Type_Spec('REAL', None), None, "
-            "Entity_Decl_List(',', (Entity_Decl(Name('c'), None, None, "
-            "None),)))")
-
-    obj = tcls('REAL D( LDA, * ), E( LDB, * )')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'REAL :: D(LDA, *), E(LDB, *)'
-
-    obj = tcls('DOUBLE PRECISION   ALPHA, BETA')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'DOUBLE PRECISION :: ALPHA, BETA'
-
-    obj = tcls('logical,parameter:: T=.true.')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'LOGICAL, PARAMETER :: T = .TRUE.'
-
-    obj = tcls('character(n),private:: x(n)')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'CHARACTER(LEN = n), PRIVATE :: x(n)'
-
-    obj = tcls('character(lenmax),private:: y(n)')
-    assert isinstance(obj, tcls), repr(obj)
-    assert str(obj) == 'CHARACTER(LEN = lenmax), PRIVATE :: y(n)'
-
-
 def test_declaration_type_spec():  # R502
 
     tcls = Declaration_Type_Spec