diff --git a/CHANGELOG.md b/CHANGELOG.md index e079f6c9..2d064a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ Modifications by (in alphabetical order): * A. R. Porter, Science & Technology Facilities Council, UK * P. Vitt, University of Siegen, Germany +23/11/2018 PR #122 for issue #118. Bug fix for reporting invalid + Fortran when parsing `use module_name, only:` in fparser2. + 21/11/2018 PR #127 for #126. Adds get_child function to help AST traversal. 19/11/2018 PR #124 for #112. Bug fix - spaces within names are now diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index c839d582..edf7d43b 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -7247,14 +7247,14 @@ class Module_Subprogram(Base): # R1108 class Use_Stmt(StmtBase): # pylint: disable=invalid-name - """ - R1109 + ''' + Fortran 2003 rule R1109 - = USE [ [ , ] :: ] - [ , ] - | USE [ [ , ] :: ] , - ONLY: [ ] - """ + use-stmt is USE [ [ , module-nature ] :: ] module-name [ , rename-list ] + or USE [ [ , module-nature ] :: ] module-name , + ONLY : [ only-list ] + + ''' subclass_names = [] use_names = ['Module_Nature', 'Module_Name', 'Rename_List', 'Only_List'] @@ -7268,10 +7268,11 @@ def match(string): "ONLY" specification and optional "Rename" or "Only" list) :rtype: 5-tuple of objects (module name and 4 optional) ''' - # Incorrect 'USE' statement - if string[:3].upper() != 'USE': + line = string.strip() + # Incorrect 'USE' statement or line too short + if line[:3].upper() != 'USE': return - line = string[3:] + line = line[3:] # Empty string after 'USE' if not line: return @@ -7308,27 +7309,32 @@ def match(string): if nature is not None: return - i = line.find(',') - if i == -1: + position = line.find(',') + if position == -1: return nature, dcolon, Module_Name(line), '', None - name = line[:i].rstrip() + name = line[:position].rstrip() # Missing Module_Name before Only_List if not name: return name = Module_Name(name) - line = line[i+1:].lstrip() + line = line[position+1:].lstrip() # Missing 'ONLY' specification after 'USE Module_Name,' if not line: return if line[:4].upper() == 'ONLY': line = line[4:].lstrip() - # Missing ':' after ', ONLY' specification + if not line: + # Expected ':' but there is nothing after the 'ONLY' + # specification + return if line[0] != ':': + # Expected ':' but there is a different character + # after the 'ONLY' specification return line = line[1:].lstrip() - # Missing Only_List/Rename_List after 'USE Module_Name, ONLY:' if not line: - return + # Missing Only_List after 'USE Module_Name, ONLY:' + return nature, dcolon, name, ', ONLY:', None return nature, dcolon, name, ', ONLY:', Only_List(line) return nature, dcolon, name, ',', Rename_List(line) @@ -7336,21 +7342,36 @@ def tostr(self): ''' :return: parsed representation of "USE" statement :rtype: string + :raises InternalError: if items array is not the expected size + :raises InternalError: if items array[2] is not a string or is an \ + empty string + :raises InternalError: if items array[3] is 'None' as it should be \ + a string ''' + if len(self.items) != 5: + raise InternalError( + "Use_Stmt.tostr(). 'Items' should be of size 5 but found " + "'{0}'.".format(len(self.items))) + if not self.items[2]: + raise InternalError("Use_Stmt.tostr(). 'Items' entry 2 should " + "be a module name but it is empty") + if self.items[3] is None: + raise InternalError("Use_Stmt.tostr(). 'Items' entry 3 should " + "be a string but found 'None'") usestmt = 'USE' # Add optional Module_Nature ("INTRINSIC" or "NON_INTRINSIC") # followed by a double colon to "USE" statement - if self.items[0] is not None and self.items[1] is not None: - usestmt += ', %s %s' % (self.items[0], self.items[1]) + if self.items[0] and self.items[1]: + usestmt += ", {0} {1}".format(self.items[0], self.items[1]) # Add optional double colon after "USE" statement without # Module_Nature (valid Fortran) - elif self.items[0] is None and self.items[1] is not None: - usestmt += ' %s' % (self.items[1]) + elif not self.items[0] and self.items[1]: + usestmt += " {0}".format(self.items[1]) # Add Module_Name and optional "ONLY" specifier if present - usestmt += ' %s%s' % (self.items[2], self.items[3]) + usestmt += " {0}{1}".format(self.items[2], self.items[3]) # Add optional Only_List or Rename_List if present if self.items[4] is not None: - usestmt += ' %s' % (self.items[4]) + usestmt += " {0}".format(self.items[4]) return usestmt diff --git a/src/fparser/two/tests/fortran2003/test_usestmt_r1109.py b/src/fparser/two/tests/fortran2003/test_usestmt_r1109.py new file mode 100644 index 00000000..337c22ba --- /dev/null +++ b/src/fparser/two/tests/fortran2003/test_usestmt_r1109.py @@ -0,0 +1,221 @@ +# Copyright (c) 2018 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 2003 rule R1109 : This file tests the support for the +Use statement. + +''' + +import pytest +from fparser.api import get_reader +from fparser.two.Fortran2003 import Use_Stmt +from fparser.two.utils import NoMatchError, InternalError + +# match() use ... + + +# 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 +def test_use(f2003_create): + '''Check that a basic use is parsed correctly. Input separately as a + string and as a reader object + + ''' + def check_use(reader): + '''Internal helper function to avoid code replication.''' + ast = Use_Stmt(reader) + assert "USE my_model" in str(ast) + assert repr(ast) == "Use_Stmt(None, None, Name('my_model'), '', None)" + + line = "use my_model" + check_use(line) + reader = get_reader(line) + check_use(reader) + + +# match() 'use :: x' +def test_use_colons(f2003_create): + '''Check that a basic use with '::' is parsed correctly.''' + line = "use :: my_model" + ast = Use_Stmt(line) + assert "USE :: my_model" in str(ast) + assert repr(ast) == "Use_Stmt(None, '::', Name('my_model'), '', None)" + + +# match() 'use, nature :: x' +def test_use_nature(f2003_create): + '''Check that a use with a 'nature' specification is parsed correctly.''' + line = "use, intrinsic :: my_model" + ast = Use_Stmt(line) + assert "USE, INTRINSIC :: my_model" in str(ast) + assert repr(ast) == ( + "Use_Stmt(Module_Nature('INTRINSIC'), '::', Name('my_model'), " + "'', None)") + + +# match() 'use x, rename' +def test_use_rename(f2003_create): + '''Check that a use with a nename clause is parsed correctly.''' + line = "use my_module, name=>new_name" + ast = Use_Stmt(line) + assert "USE my_module, name => new_name" in str(ast) + assert repr(ast) == ( + "Use_Stmt(None, None, Name('my_module'), ',', " + "Rename(None, Name('name'), Name('new_name')))") + + +# match() 'use x, only: y' +def test_use_only(f2003_create): + '''Check that a use statement is parsed correctly when there is an + only clause. + + ''' + line = "use my_model, only: name" + ast = Use_Stmt(line) + assert "USE my_model, ONLY: name" in str(ast) + assert repr(ast) == ( + "Use_Stmt(None, None, Name('my_model'), ', ONLY:', Name('name'))") + + +# match() 'use x, only:' +def test_use_only_empty(f2003_create): + '''Check that a use statement is parsed correctly when there is an + only clause without any content. + + ''' + line = "use my_model, only:" + ast = Use_Stmt(line) + assert "USE my_model, ONLY:" in str(ast) + assert repr(ast) == ( + "Use_Stmt(None, None, Name('my_model'), ', ONLY:', None)") + + +# match() ' use , nature :: x , name=>new_name' +def test_use_spaces_1(f2003_create): + '''Check that a use statement with spaces works correctly with + renaming. + + ''' + line = " Use , intrinsic :: my_model , name=>new_name " + ast = Use_Stmt(line) + assert "USE, INTRINSIC :: my_model, name => new_name" in str(ast) + assert repr(ast) == ( + "Use_Stmt(Module_Nature('INTRINSIC'), '::', Name('my_model'), ',', " + "Rename(None, Name('name'), Name('new_name')))") + + +# match() ' use , nature :: x , only : name' +def test_use_spaces_2(f2003_create): + '''Check that a use statement with spaces works correctly with an only + clause. + + ''' + line = " use , intrinsic :: my_model , only : name " + ast = Use_Stmt(line) + assert "USE, INTRINSIC :: my_model, ONLY: name" in str(ast) + assert repr(ast) == ("Use_Stmt(Module_Nature('INTRINSIC'), '::', " + "Name('my_model'), ', ONLY:', Name('name'))") + + +# match() mixed case +def test_use_mixed_case(f2003_create): + '''Check that a use statement with mixed case keywords ('use' and + 'only') works as expected. + + ''' + line = "UsE my_model, OnLy: name" + ast = Use_Stmt(line) + assert "USE my_model, ONLY: name" in str(ast) + assert repr(ast) == ("Use_Stmt(None, None, Name('my_model'), ', ONLY:', " + "Name('name'))") + +# match() Syntax errors + + +def test_syntaxerror(f2003_create): + '''Test that NoMatchError is raised for various syntax errors.''' + for line in ["us", "ust", "use", "usemy_model", "use, ", "use, ::", + "use, intrinsic", "use, intrinsic::", + "use, intrinsic my_module", "use,", "use my_model,", + "use my_model, only", "use my_model, only ;", + "use my_model, only name"]: + with pytest.raises(NoMatchError) as excinfo: + _ = Use_Stmt(line) + assert "Use_Stmt: '{0}'".format(line) in str(excinfo) + +# match() Internal errors + + +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. + + ''' + line = "use my_model" + ast = Use_Stmt(line) + ast.items = (None, None, None, None) + with pytest.raises(InternalError) as excinfo: + str(ast) + assert "should be of size 5 but found '4'" in str(excinfo) + + +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 + a string with content. + + ''' + line = "use my_model" + ast = Use_Stmt(line) + for content in [None, ""]: + ast.items = (None, None, content, None, None) + with pytest.raises(InternalError) as excinfo: + str(ast) + assert ("entry 2 should be a module name but it is " + "empty") in str(excinfo) + + +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) + string. + + ''' + line = "use my_model" + ast = Use_Stmt(line) + ast.items = (None, None, "my_module", None, None) + with pytest.raises(InternalError) as excinfo: + str(ast) + assert "entry 3 should be a string but found 'None'" in str(excinfo) diff --git a/src/fparser/two/tests/test_fortran2003.py b/src/fparser/two/tests/test_fortran2003.py index b8a9e9ad..a2ff1538 100644 --- a/src/fparser/two/tests/test_fortran2003.py +++ b/src/fparser/two/tests/test_fortran2003.py @@ -3607,134 +3607,6 @@ def test_module_subprogram_part(): # R1107 '\n a = 1.0\nEND SUBROUTINE foo') -def test_use_stmt(): - ''' Tests that USE statement is parsed correctly (R1109). ''' - tcls = Use_Stmt - obj = tcls('use a') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE a' - assert repr(obj) == "Use_Stmt(None, None, Name('a'), '', None)" - - obj = tcls('use :: a') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE :: a' - assert repr(obj) == "Use_Stmt(None, '::', Name('a'), '', None)" - - obj = tcls('use a, only: b') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE a, ONLY: b' - assert repr(obj) == ( - "Use_Stmt(None, None, Name('a'), ', ONLY:', Name('b'))") - - obj = tcls('use :: a, only: b') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE :: a, ONLY: b' - assert repr(obj) == ( - "Use_Stmt(None, '::', Name('a'), ', ONLY:', Name('b'))") - - obj = tcls('use a, ONLY : b') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE a, ONLY: b' - assert repr(obj) == ( - "Use_Stmt(None, None, Name('a'), ', ONLY:', Name('b'))") - - obj = tcls('use, intrinsic :: a, ONLY: b') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE, INTRINSIC :: a, ONLY: b' - assert repr(obj) == ( - "Use_Stmt(Module_Nature('INTRINSIC'), '::', Name('a'), " - "', ONLY:', Name('b'))") - - obj = tcls('use, non_intrinsic :: a, ONLY: b, c, d') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE, NON_INTRINSIC :: a, ONLY: b, c, d' - assert repr(obj) == ( - "Use_Stmt(Module_Nature('NON_INTRINSIC'), '::', Name('a'), " - "', ONLY:', Only_List(',', (Name('b'), Name('c'), Name('d'))))") - - obj = tcls('use a, c=>d') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE a, c => d' - assert repr(obj) == ( - "Use_Stmt(None, None, Name('a'), " - "',', Rename(None, Name('c'), Name('d')))") - - obj = tcls('use :: a, operator(.hey.)=>operator(.hoo.)') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE :: a, OPERATOR(.HEY.) => OPERATOR(.HOO.)' - assert repr(obj) == ( - "Use_Stmt(None, '::', Name('a'), ',', " - "Rename('OPERATOR', Defined_Op('.HEY.'), Defined_Op('.HOO.')))") - - obj = tcls('use, intrinsic :: a, operator(.hey.)=>operator(.hoo.), c=>g') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == ( - 'USE, INTRINSIC :: a, OPERATOR(.HEY.) => OPERATOR(.HOO.), c => g') - assert repr(obj) == ( - "Use_Stmt(Module_Nature('INTRINSIC'), '::', Name('a'), " - "',', Rename_List(',', (" - "Rename('OPERATOR', Defined_Op('.HEY.'), Defined_Op('.HOO.')), " - "Rename(None, Name('c'), Name('g')))))") - - obj = tcls('use, non_intrinsic :: a, ONLY: b => c') - assert isinstance(obj, tcls), repr(obj) - assert str(obj) == 'USE, NON_INTRINSIC :: a, ONLY: b => c' - assert repr(obj) == ( - "Use_Stmt(Module_Nature('NON_INTRINSIC'), '::', Name('a'), " - "', ONLY:', Rename(None, Name('b'), Name('c')))") - - # Checks that no match is found for incorrect 'USE' statement contructs - # Incorrect 'USE' statement - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('8se') - assert "Use_Stmt: '8se'" in str(excinfo) - - # Empty string after 'USE' - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('use') - assert "Use_Stmt: 'use'" in str(excinfo) - - # No separation between 'USE' statement and its specifiers - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('usemodulename') - assert "Use_Stmt: 'usemodulename'" in str(excinfo) - - # Missing Module_Nature between ',' and '::' - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('use, ::') - assert "Use_Stmt: 'use, ::'" in str(excinfo) - - # No Module_Name after 'USE, Module_Nature ::' - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('use, intrinsic ::') - assert "Use_Stmt: 'use, intrinsic ::'" in str(excinfo) - - # Missing '::' after Module_Nature - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('use, intrinsic a') - assert "Use_Stmt: 'use, intrinsic a'" in str(excinfo) - - # Missing Module_Name before Only_List - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('use , only: b') - assert "Use_Stmt: 'use , only: b'" in str(excinfo) - - # Missing 'ONLY' specification after 'USE Module_Name,' - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('use a,') - assert "Use_Stmt: 'use a,'" in str(excinfo) - - # Missing ':' after ', ONLY' specification - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('use a, only b') - assert "Use_Stmt: 'use a, only b" in str(excinfo) - - # Missing Only_List/Rename_List after 'USE Module_Name, ONLY:' - with pytest.raises(NoMatchError) as excinfo: - _ = tcls('use a, only:') - assert "Use_Stmt: 'use a, only:" in str(excinfo) - - def test_module_nature(): ''' Tests that a module nature statement is parsed correctly (INTRINSIC or NON_INTRINSIC allowed, R1110). '''