diff --git a/CHANGELOG.md b/CHANGELOG.md index 4771430c..e8182f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ Modifications by (in alphabetical order): * A. R. Porter, Science & Technology Facilities Council, UK * P. Vitt, University of Siegen, Germany +03/07/2019 PR #200 for #171. Disable Statement Function support + in fparser2. + 02/07/2019 PR #205 for #204. Corrects the minimum number of arguments for the SELECTED_REAL_KIND intrinsic. diff --git a/doc/fparser2.rst b/doc/fparser2.rst index d8705ec6..7bfe9ed3 100644 --- a/doc/fparser2.rst +++ b/doc/fparser2.rst @@ -160,6 +160,27 @@ number. For example: fparser.two.Fortran2003.FortranSyntaxError: at line 2 >>>en +Unsupported Features +-------------------- + +Statement Functions ++++++++++++++++++++ + +Fparser2 is currently not able to distinguish between statement +functions and array assignments when one or more array assignment +statements are the first statements after a declaration section. This +limitation leads to these particular array assignments being +incorrectly parsed as statement functions. + +To avoid this incorrect behaviour, support for statement functions has +been temporarily removed from fparser2. However, with this change, +statement functions will be incorrectly parsed as array assignments +when one or more statement function statements are the last statements +in a declaration section. + +Whilst any incorrect behaviour should be avoided, the behaviour of +this temporary change is considered preferable to the former case, as +array assigments are more common than statement functions. Extensions ---------- diff --git a/src/fparser/scripts/fparser2.py b/src/fparser/scripts/fparser2.py index 3f9bf29a..613557e0 100755 --- a/src/fparser/scripts/fparser2.py +++ b/src/fparser/scripts/fparser2.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Modified work Copyright (c) 2017-2018 Science and Technology +# Modified work Copyright (c) 2017-2019 Science and Technology # Facilities Council # Original work Copyright (c) 1999-2008 Pearu Peterson @@ -83,23 +83,26 @@ def runner(_, options, args): from fparser.two.Fortran2003 import FortranSyntaxError, InternalError from fparser.common.readfortran import FortranFileReader if not args: - print ("Error: No fortran files specified") + print("Error: No fortran files specified") raise SystemExit(1) for filename in args: try: reader = FortranFileReader(filename, ignore_comments=False) except IOError as error: - print (error) + print(error) return if options.mode != 'auto': reader.format.from_mode(options.mode) try: f2003_parser = ParserFactory().create() program = f2003_parser(reader) - print (program) + if options.task == "show": + print(program) + if options.task == "repr": + print(repr(program)) except FortranSyntaxError as msg: - print ("Syntax error: {0}".format(str(msg))) + print("Syntax error: {0}".format(str(msg))) try: # protect the access to fifo_item[-1] in case the fifo # buffer is empty @@ -110,7 +113,7 @@ def runner(_, options, args): pass raise SystemExit(1) except InternalError as msg: - print ("Internal error in fparser: {0}".format(str(msg))) + print("Internal error in fparser: {0}".format(str(msg))) raise SystemExit(1) @@ -120,7 +123,6 @@ def main(): set_fparser_options(parser) options, args = parser.parse_args() runner(parser, options, args) - return if __name__ == "__main__": diff --git a/src/fparser/scripts/script_options.py b/src/fparser/scripts/script_options.py index 8ccf7840..2093db69 100644 --- a/src/fparser/scripts/script_options.py +++ b/src/fparser/scripts/script_options.py @@ -93,15 +93,22 @@ def set_parse_options(parser): parser.add_option_group(get_fortran_code_group(parser)) def set_fparser_options(parser): + ''' Command line options used by the fparser2 script. + + :param parser: The OptionParser object. + :type parser: :py:class:`optparse.OptionParser` + + ''' + parser.set_usage('''\ %prog [options] Description: %prog parses Fortran code.''') parser.add_option('--task', - default = 'show', - choices = ['show', 'none'], - help = 'Specify parsing result task. Default: %default.' + default='show', + choices=['show', 'repr', 'none'], + help='Specify parsing result task. Default: %default.' ) parser.add_option('--std', default = 'f2003', diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 60f12001..243b3445 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -419,23 +419,31 @@ class Implicit_Part_Stmt(Base): # R206 class Declaration_Construct(Base): # R207 - """ -:F03R:`207`:: - = - | - | - | - | - | - | - | - | - | - """ - subclass_names = ['Comment', 'Derived_Type_Def', 'Entry_Stmt', 'Enum_Def', + '''Fortran 2003 rule R207 + + declaration-construct is derived-type-def + or entry-stmt + or enum-def + or format-stmt + or interface-block + or parameter-stmt + or procedure-declaration-stmt + or specification-stmt + or type-declaration-stmt + or stmt-function-stmt + + Note, stmt-function-stmt is not currently matched. + + ''' + # Commented out Stmt_Function_Stmt as it can falsely match an + # access to an array or function. Reintroducing statement + # functions is captured in issue #202. + + # 'Type_Declaration_Stmt', 'Stmt_Function_Stmt'] + subclass_names = ['Derived_Type_Def', 'Entry_Stmt', 'Enum_Def', 'Format_Stmt', 'Interface_Block', 'Parameter_Stmt', 'Procedure_Declaration_Stmt', 'Specification_Stmt', - 'Type_Declaration_Stmt', 'Stmt_Function_Stmt'] + 'Type_Declaration_Stmt'] class Execution_Part(BlockBase): # R208 diff --git a/src/fparser/two/Fortran2008.py b/src/fparser/two/Fortran2008.py index e1c14c47..917c1fa8 100644 --- a/src/fparser/two/Fortran2008.py +++ b/src/fparser/two/Fortran2008.py @@ -192,7 +192,10 @@ class Declaration_Construct_C1112(Declaration_Construct): # C1112 subclass_names = Declaration_Construct.subclass_names[:] subclass_names.remove('Format_Stmt') subclass_names.remove('Entry_Stmt') - subclass_names.remove('Stmt_Function_Stmt') + # Commented out Stmt_Function_Stmt as it can falsely match an + # access to an array or function. Reintroducing statement + # functions is captured in issue #202. + # subclass_names.remove('Stmt_Function_Stmt') class Submodule(BlockBase): # R1116 [C1112,C1114] diff --git a/src/fparser/two/tests/fortran2003/test_declaration_construct_r207.py b/src/fparser/two/tests/fortran2003/test_declaration_construct_r207.py new file mode 100644 index 00000000..e25fb8d0 --- /dev/null +++ b/src/fparser/two/tests/fortran2003/test_declaration_construct_r207.py @@ -0,0 +1,173 @@ +# Copyright (c) 2019 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 R207 : This file tests support for the +Declaration_Construct class. + +As this class just uses Base to match with a choice of subclasses we +test that an instance of each subclass can be succesfully +parsed. Detailed checking of the subclass rules are performed by the +subclass tests. + +''' + +import pytest +from fparser.two.Fortran2003 import Declaration_Construct +from fparser.api import get_reader + + +def test_derived_type_def(f2003_create): + '''Test a derived type definition statement is supported by the + declaration construct class. + + ''' + code = ("TYPE :: my_type\n" + "END TYPE my_type") + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Derived_Type_Def" in repr(result) + + +def test_entry_stmt(f2003_create): + '''Test an entry statement is supported by the declaration construct + class. + + ''' + code = "ENTRY my_function()" + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Entry_Stmt" in repr(result) + + +def test_enum_def(f2003_create): + '''Test an enum definition is supported by the declaration construct + class. + + ''' + code = ("ENUM, BIND(C)\n" + " ENUMERATOR :: a = 1\n" + "END ENUM") + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Enum_Def" in repr(result) + + +def test_format_statement(f2003_create): + '''Test a format statement is supported by the declaration construct + class. + + ''' + code = "FORMAT('(x)')" + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Format_Stmt" in repr(result) + + +def test_interface_block(f2003_create): + '''Test an interface block statement is supported by the declaration + construct class. + + ''' + code = ("INTERFACE\n" + "END INTERFACE") + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Interface_Block" in repr(result) + + +def test_parameter_stmt(f2003_create): + '''Test a parameter statement is supported by the declaration + construct class. + + ''' + code = "PARAMETER(A = 2)" + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Parameter_Stmt" in repr(result) + + +def test_procedure_declaration_stmt(f2003_create): + '''Test a procedure declaration statement is supported by the + declaration construct class. + + ''' + code = "PROCEDURE(REAL) FUNC" + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Procedure_Declaration_Stmt" in repr(result) + + +def test_specification_stmt(f2003_create): + '''Test a specification statement is supported by the declaration + construct class. An access statement is a specification statement, + so check for this. + + ''' + code = "PUBLIC :: A" + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Access_Stmt" in repr(result) + + +def test_type_declaration_stmt(f2003_create): + '''Test a type declaration statement is supported by the declaration + construct class. + + ''' + code = "INTEGER :: X" + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Type_Declaration_Stmt" in repr(result) + + +@pytest.mark.xfail(reason="#202. Statement Function support removed.") +def test_stmt_function_stmt(f2003_create): + '''Test a statement function statement is supported by the declaration + construct class. + + ''' + code = "C(F) = 5.0*(F - 32.0)/9.0" + reader = get_reader(code) + result = Declaration_Construct(reader) + assert str(result) == code + assert "Stmt_Function_Stmt" in repr(result) diff --git a/src/fparser/two/tests/test_scripts.py b/src/fparser/two/tests/test_scripts.py index 3818110f..ea1d417a 100644 --- a/src/fparser/two/tests/test_scripts.py +++ b/src/fparser/two/tests/test_scripts.py @@ -50,6 +50,7 @@ class DummyArgs(object): ''' dummy object pretending to be the argument options ''' mode = "auto" + task = "show" def test_runner_no_files(capsys): @@ -86,6 +87,7 @@ def test_runner_set_mode(tmpdir, capsys): class DummyArgsFree(object): ''' dummy object pretending to be the argument options ''' mode = "free" + task = "show" # run the relevant script method (runner()) fparser2.runner(None, DummyArgsFree(), [my_file.strpath]) # capture the output and check that the code has been output @@ -155,18 +157,71 @@ def dummy_parser(_): assert "Internal error in fparser: '{0}'".format(error_string) in stdout -def test_runner_output(tmpdir, capsys): - '''Test that the script outputs the code it has parsed ''' - # Create a temporary file containing Fortran code to pass into runner() +def test_runner_output_task_show(tmpdir, capsys): + '''Test that the script outputs the code it has parsed with the + 'task' option set to "show". + + ''' + class DummyArgsTask(object): + ''' dummy object pretending to be the argument options ''' + mode = "free" + task = "show" + + # Create a temporary file containing Fortran code to pass into + # runner(). my_file = tmpdir.mkdir("sub").join("hello.f90") my_file.write("program hello\nend program hello\n") - # run the relevant script method (runner()) - fparser2.runner(None, DummyArgs(), [my_file.strpath]) - # capture the output and check that the code has been output + # Run the relevant script method (runner()). + fparser2.runner(None, DummyArgsTask(), [my_file.strpath]) + # Capture the output and check that the code has been output. stdout, _ = capsys.readouterr() assert "PROGRAM hello\nEND PROGRAM hello\n" in stdout +def test_runner_output_task_repr(tmpdir, capsys): + '''Test that the script outputs the repr representation of the code it + has parsed with the 'task' option set to "repr". + + ''' + class DummyArgsTask(object): + ''' dummy object pretending to be the argument options ''' + mode = "free" + task = "repr" + + # Create a temporary file containing Fortran code to pass into + # runner(). + my_file = tmpdir.mkdir("sub").join("hello.f90") + my_file.write("program hello\nend program hello\n") + # Run the relevant script method (runner()). + fparser2.runner(None, DummyArgsTask(), [my_file.strpath]) + # Capture the output and check that the repr of the code has been + # produced. + stdout, _ = capsys.readouterr() + assert (stdout == "Program(Main_Program(Program_Stmt('PROGRAM'," + " Name('hello')), End_Program_Stmt('PROGRAM', Name('hello'))))\n") + + +def test_runner_output_task_none(tmpdir, capsys): + '''Test that the script outputs nothing when the 'task' option is set + to "none". + + ''' + class DummyArgsTask(object): + ''' dummy object pretending to be the argument options ''' + mode = "free" + task = "none" + + # Create a temporary file containing Fortran code to pass into + # runner(). + my_file = tmpdir.mkdir("sub").join("hello.f90") + my_file.write("program hello\nend program hello\n") + # Run the relevant script method (runner()). + fparser2.runner(None, DummyArgsTask(), [my_file.strpath]) + # Capture the output and check that nothig has been produced. + stdout, _ = capsys.readouterr() + assert stdout == "" + + def test_runner_multi_output(tmpdir, capsys): '''Test that the script outputs the code it has parsed when there are multiple files specified @@ -185,8 +240,9 @@ def test_runner_multi_output(tmpdir, capsys): # fparser2.py script function main() -def test_main_output(tmpdir, capsys, monkeypatch): - '''Test that the script main() function outputs the code it has parsed''' +def test_main_output_task_default(tmpdir, capsys, monkeypatch): + '''Test that the script main() function outputs the code it has parsed + by default.''' import sys # Create a temporary file containing Fortran code to pass into runner() my_file = tmpdir.mkdir("sub").join("hello.f90") @@ -197,7 +253,92 @@ def test_main_output(tmpdir, capsys, monkeypatch): fparser2.main() # capture the output and check that the code has been output stdout, _ = capsys.readouterr() - assert "PROGRAM hello\nEND PROGRAM hello\n" in stdout + assert stdout == "PROGRAM hello\nEND PROGRAM hello\n" + + +def test_main_output_task_show(tmpdir, capsys, monkeypatch): + '''Test that the script main() function outputs the code it has parsed + when --task=show. + + ''' + import sys + # Create a temporary file containing Fortran code to pass into + # runner(). + my_file = tmpdir.mkdir("sub").join("hello.f90") + my_file.write("program hello\nend program hello\n") + # Use monkeypatch to spoof the command-line argument. + monkeypatch.setattr(sys, "argv", ["fparser2", "--task=show", + my_file.strpath]) + # Run the relevant script method (main()). + fparser2.main() + # Capture the output and check that the code has been output. + stdout, _ = capsys.readouterr() + assert stdout == "PROGRAM hello\nEND PROGRAM hello\n" + + +def test_main_output_task_repr(tmpdir, capsys, monkeypatch): + '''Test that the script main() function outputs the 'repr' of the code + it has parsed when --task=repr. + + ''' + import sys + # Create a temporary file containing Fortran code to pass into + # runner(). + my_file = tmpdir.mkdir("sub").join("hello.f90") + my_file.write("program hello\nend program hello\n") + # Use monkeypatch to spoof the command-line argument. + monkeypatch.setattr(sys, "argv", ["fparser2", "--task=repr", + my_file.strpath]) + # Run the relevant script method (main()). + fparser2.main() + # Capture the output and check that the code has been output. + stdout, _ = capsys.readouterr() + assert stdout == ("Program(Main_Program(Program_Stmt('PROGRAM'," + " Name('hello')), End_Program_Stmt('PROGRAM'," + " Name('hello'))))\n") + + +def test_main_output_task_none(tmpdir, capsys, monkeypatch): + '''Test that the script main() function outputs nothing when + --task=none. + + ''' + import sys + # Create a temporary file containing Fortran code to pass into + # runner(). + my_file = tmpdir.mkdir("sub").join("hello.f90") + my_file.write("program hello\nend program hello\n") + # Use monkeypatch to spoof the command-line argument. + monkeypatch.setattr(sys, "argv", ["fparser2", "--task=none", + my_file.strpath]) + # Run the relevant script method (main()). + fparser2.main() + # Capture the output and check that the code has been output. + stdout, _ = capsys.readouterr() + assert stdout == "" + + +def test_main_output_task_invalid(tmpdir, capsys, monkeypatch): + '''Test that the script main() function prints an error when an + invalid task option is provided. + + ''' + import sys + # Create a temporary file containing Fortran code to pass into + # runner(). + my_file = tmpdir.mkdir("sub").join("hello.f90") + my_file.write("program hello\nend program hello\n") + # Use monkeypatch to spoof the command-line argument. + monkeypatch.setattr(sys, "argv", ["fparser2", "--task=invalid", + my_file.strpath]) + with pytest.raises(SystemExit): + # Run the relevant script method (main()). + fparser2.main() + # Capture the output and check that the appropriate error has been + # written. + _, stderr = capsys.readouterr() + assert ("fparser2: error: option --task: invalid choice: 'invalid'" + "" in stderr) # read.py script function runner()