From 59c8ca43ba6898f3cdce007cd0b3d489cc5b2094 Mon Sep 17 00:00:00 2001 From: r-sharp Date: Tue, 5 May 2026 12:57:17 +0100 Subject: [PATCH 1/5] adding check for one statement per line --- .../tests/test_umdp3_rules_S3.py | 59 +++++++++++++++++++ script_umdp3_checker/umdp3_rules_S3.py | 24 ++++++++ 2 files changed, 83 insertions(+) diff --git a/script_umdp3_checker/tests/test_umdp3_rules_S3.py b/script_umdp3_checker/tests/test_umdp3_rules_S3.py index 495f7a61..f481b98f 100644 --- a/script_umdp3_checker/tests/test_umdp3_rules_S3.py +++ b/script_umdp3_checker/tests/test_umdp3_rules_S3.py @@ -17,6 +17,7 @@ r3_1_1_there_can_be_only_one, r3_2_1_check_crown_copyright, r3_3_2_line_too_long, + r3_3_3_one_statement_per_line, r3_4_1_capitalised_keywords, r3_4_2_no_full_uppercase_variable_names, ) @@ -299,6 +300,64 @@ def test_r3_3_2_line_too_long( # ================================================================= +@pytest.mark.parametrize( + "changes_list, expected_result, expected_errors", + [ + ( + [["add", 74, ["icode=0 ; icode= icode + 1"]]], + 1, + {"multiple statements on one line": [74]}, + ), + ( + [ + [ + "add", + 83, + [ + "IF ( l_unscale ) THEN ; icode=-300 ; ALLOCATE(field( 1,1 ) ) ;" + + " END IF" + ], + ], + [ + "replace", + 116, + ["IF ( l_unscale ) THEN ; DEALLOCATE(field) ; END IF"], + ], + ], + 2, + {"multiple statements on one line": [83, 117]}, + ), + ( + [], + 0, + {}, + ), # No changes, expect no errors + ], + ids=[ + "single multi-statements failure", + "multiple multi-statments failure", + "No error check", + ], +) +def test_3_3_3_one_statement_per_line( + example_fortran_lines, changes_list, expected_result, expected_errors +): + modified_fortran_lines = modify_fortran_lines(example_fortran_lines, changes_list) + result = r3_3_3_one_statement_per_line(modified_fortran_lines) + failure_count = result.failure_count + errors = result.errors + for error, lines_list in errors.items(): + assert error in expected_errors + for line_no in lines_list: + assert line_no in expected_errors[error] + assert len(lines_list) == len(expected_errors[error]) + assert len(errors) == len(expected_errors) + assert failure_count == expected_result + + +# ================================================================= + + @pytest.mark.parametrize( "changes_list, expected_result, expected_errors", [ diff --git a/script_umdp3_checker/umdp3_rules_S3.py b/script_umdp3_checker/umdp3_rules_S3.py index b7f03dfa..16563633 100644 --- a/script_umdp3_checker/umdp3_rules_S3.py +++ b/script_umdp3_checker/umdp3_rules_S3.py @@ -267,6 +267,29 @@ def r3_3_2_line_too_long(lines: List[str]) -> TestResult: """ * TODO: Never put more than one statement on a line. +""" +def r3_3_3_one_statement_per_line(lines: List[str]) -> TestResult: + """Check for more than one statement on a line, by looking for the presence of a ";" character outside of quoted strings and comments.""" + failures = 0 + error_log = {} + count = -1 + for count, line in enumerate(lines, 1): + # Remove quoted strings and comments + if line.lstrip(" ").startswith("!"): + continue + clean_line = remove_quoted(line) + clean_line = remove_comments(clean_line) + if ";" in clean_line: + failures += 1 + error_log = add_error_log(error_log, "multiple statements on one line", count) + return TestResult( + checker_name="Test 3.3 One Statement Per Line", + failure_count=failures, + passed=(failures == 0), + output=f"Checked {count + 1} lines, found {failures} failures.", + errors=error_log, + ) +""" * TODO: Write your program in UK English, unless you have a very good reason for not doing so.""" """3.4 Fortran style @@ -431,6 +454,7 @@ def r3_4_2_no_full_uppercase_variable_names(lines: List[str]) -> TestResult: r3_1_1_there_can_be_only_one, r3_2_1_check_crown_copyright, r3_3_2_line_too_long, + r3_3_3_one_statement_per_line, r3_4_1_capitalised_keywords, r3_4_2_no_full_uppercase_variable_names, ] From c0f2330838a17bd96997f3195b8c165d941088f7 Mon Sep 17 00:00:00 2001 From: r-sharp Date: Tue, 5 May 2026 14:39:39 +0100 Subject: [PATCH 2/5] ruff ruffling feathers and complaining one commit too late. --- script_umdp3_checker/umdp3_rules_S3.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/script_umdp3_checker/umdp3_rules_S3.py b/script_umdp3_checker/umdp3_rules_S3.py index 16563633..7325f943 100644 --- a/script_umdp3_checker/umdp3_rules_S3.py +++ b/script_umdp3_checker/umdp3_rules_S3.py @@ -268,6 +268,8 @@ def r3_3_2_line_too_long(lines: List[str]) -> TestResult: """ * TODO: Never put more than one statement on a line. """ + + def r3_3_3_one_statement_per_line(lines: List[str]) -> TestResult: """Check for more than one statement on a line, by looking for the presence of a ";" character outside of quoted strings and comments.""" failures = 0 @@ -281,7 +283,9 @@ def r3_3_3_one_statement_per_line(lines: List[str]) -> TestResult: clean_line = remove_comments(clean_line) if ";" in clean_line: failures += 1 - error_log = add_error_log(error_log, "multiple statements on one line", count) + error_log = add_error_log( + error_log, "multiple statements on one line", count + ) return TestResult( checker_name="Test 3.3 One Statement Per Line", failure_count=failures, @@ -289,6 +293,8 @@ def r3_3_3_one_statement_per_line(lines: List[str]) -> TestResult: output=f"Checked {count + 1} lines, found {failures} failures.", errors=error_log, ) + + """ * TODO: Write your program in UK English, unless you have a very good reason for not doing so.""" From 906a5c6a315319ff6ed640b2342525d86bca1685 Mon Sep 17 00:00:00 2001 From: r-sharp Date: Wed, 4 Nov 2026 16:08:55 +0000 Subject: [PATCH 3/5] chore: Move unseparated keywords test from old to ny style --- .../checker_dispatch_tables.py | 1 - .../tests/test_fortran_checks.py | 21 -------- .../tests/test_umdp3_rules_S3.py | 53 +++++++++++++++++++ script_umdp3_checker/umdp3_checker_rules.py | 27 ---------- script_umdp3_checker/umdp3_rules_S3.py | 34 ++++++++++-- 5 files changed, 84 insertions(+), 52 deletions(-) diff --git a/script_umdp3_checker/checker_dispatch_tables.py b/script_umdp3_checker/checker_dispatch_tables.py index e325f884..cb23793f 100644 --- a/script_umdp3_checker/checker_dispatch_tables.py +++ b/script_umdp3_checker/checker_dispatch_tables.py @@ -31,7 +31,6 @@ def get_diff_dispatch_table_fortran(self) -> list[Callable]: """Get dispatch table for Fortran diff tests""" return [ self.umdp3_checker.openmp_sentinels_in_column_one, - self.umdp3_checker.unseparated_keywords, self.umdp3_checker.go_to_other_than_9999, self.umdp3_checker.write_using_default_format, self.umdp3_checker.dimension_forbidden, diff --git a/script_umdp3_checker/tests/test_fortran_checks.py b/script_umdp3_checker/tests/test_fortran_checks.py index 0700bdd7..ca09c36a 100644 --- a/script_umdp3_checker/tests/test_fortran_checks.py +++ b/script_umdp3_checker/tests/test_fortran_checks.py @@ -77,27 +77,6 @@ def test_openmp_sentinels_in_column_one(lines, expected_result): assert result.failure_count == expected_result -unseparated_keywords_parameters = [ - (["ELSEIF", "ENDDO", "ENDSUBROUTINE"], 3, "All keywords unseparated"), - (["ELSE IF", "ENDMODULE", "ENDSUBROUTINE"], 2, "One keyword separated"), - (["ELSE IF", "END PARRALEL DO", "END IF"], 0, "All keywords separated"), - (["i=0", "i=i+1", "PRINT*,i"], 0, "No keywords"), - (["PROGRAM test", "i=0", "ENDIF"], 1, "One keyword unseparated"), - (["i=0", "ENDPARALLELDO", "END DO"], 1, "One keyword unseparated in middle"), -] - - -@pytest.mark.parametrize( - "lines, expected_result", - [data[:2] for data in unseparated_keywords_parameters], - ids=[data[2] for data in unseparated_keywords_parameters], -) -def test_unseparated_keywords(lines, expected_result): - checker = UMDP3Checker() - result = checker.unseparated_keywords(lines) - assert result.failure_count == expected_result - - go_to_other_than_9999_parameters = [ ( [" GO TO 1000", " GO TO 2000"], diff --git a/script_umdp3_checker/tests/test_umdp3_rules_S3.py b/script_umdp3_checker/tests/test_umdp3_rules_S3.py index f481b98f..ed709742 100644 --- a/script_umdp3_checker/tests/test_umdp3_rules_S3.py +++ b/script_umdp3_checker/tests/test_umdp3_rules_S3.py @@ -20,6 +20,7 @@ r3_3_3_one_statement_per_line, r3_4_1_capitalised_keywords, r3_4_2_no_full_uppercase_variable_names, + r3_4_3_no_unseparated_keywords, ) @@ -491,3 +492,55 @@ def test_r3_4_2_no_full_uppercase_variable_names( assert len(lines_list) == len(expected_errors[error]) for line_no in lines_list: assert line_no in expected_errors[error] +# ================================================================= + + +@pytest.mark.parametrize( + "changes_list, expected_result, expected_errors", + [ + ( + [ + ["replace", 71, ["ENDIF"]], + ["replace", 101, [" ENDDO"]], + ["replace", 102, ["ENDIF"]], + ], + 3, + { + "unseparated keyword at line: 71: ENDIF": [71], + "unseparated keyword at line: 101: ENDDO": [101], + "unseparated keyword at line: 102: ENDIF": [102], + }, + ), + ( + [ + ["replace", 119, ["ENDSUBROUTINE"]], + ["replace", 120, ["ENDMODULE"]], + ], + 2, + { + "unseparated keyword at line: 119: ENDSUBROUTINE": [119], + "unseparated keyword at line: 120: ENDMODULE": [120], + }, + ), + ([], 0, {}), # No changes, expect no errors + ], + ids=[ + "3 Unseparated Keywords Errors, ENDIF & ENDDO", + "2 Unseparated Keywords Errors, ENDSUBROUTINE & ENDMODULE", + "No Unseparated Keywords Errors", + ], +) +def test_r3_4_3_no_unseparated_keywords( + example_fortran_lines, changes_list, expected_result, expected_errors +): + modified_fortran_lines = modify_fortran_lines(example_fortran_lines, changes_list) + result = r3_4_3_no_unseparated_keywords(modified_fortran_lines) + failure_count = result.failure_count + assert failure_count == expected_result + errors = result.errors + assert len(errors) == len(expected_errors) + for error, lines_list in errors.items(): + assert error in expected_errors + assert len(lines_list) == len(expected_errors[error]) + for line_no in lines_list: + assert line_no in expected_errors[error] diff --git a/script_umdp3_checker/umdp3_checker_rules.py b/script_umdp3_checker/umdp3_checker_rules.py index ea469565..2c64ca2d 100644 --- a/script_umdp3_checker/umdp3_checker_rules.py +++ b/script_umdp3_checker/umdp3_checker_rules.py @@ -13,7 +13,6 @@ from typing import List, Dict from search_lists import ( obsolescent_intrinsics, - unseparated_keywords_list, retired_ifdefs, deprecated_c_identifiers, ) @@ -125,32 +124,6 @@ def openmp_sentinels_in_column_one(self, lines: List[str]) -> TestResult: errors=error_log, ) - def unseparated_keywords(self, lines: List[str]) -> TestResult: - """Check for omitted optional spaces in keywords""" - failures = 0 - error_log = {} - count = -1 - for count, line in enumerate(lines): - if line.lstrip(" ").startswith("!"): - continue - clean_line = self.remove_quoted(line) - for pattern in [f"\\b{kw}\\b" for kw in unseparated_keywords_list]: - if re.search(pattern, clean_line, re.IGNORECASE): - failures += 1 - error_log = self.add_error_log( - error_log, - f"unseparated keyword in line: {line.strip()}", - count + 1, - ) - output = f"Checked {count + 1} lines, found {failures} failures." - return TestResult( - checker_name="Unseparated Keywords", - failure_count=failures, - passed=(failures == 0), - output=output, - errors=error_log, - ) - def go_to_other_than_9999(self, lines: List[str]) -> TestResult: """Check for GO TO statements other than 9999""" failures = 0 diff --git a/script_umdp3_checker/umdp3_rules_S3.py b/script_umdp3_checker/umdp3_rules_S3.py index 7325f943..71218b05 100644 --- a/script_umdp3_checker/umdp3_rules_S3.py +++ b/script_umdp3_checker/umdp3_rules_S3.py @@ -14,12 +14,12 @@ """ToDo: This lot will need putting back as and when the tests that use them get imported/re-created""" # from fortran_keywords import fortran_keywords -# from search_lists import ( +from search_lists import ( # obsolescent_intrinsics, -# unseparated_keywords_list, + unseparated_keywords_list, # retired_ifdefs, # deprecated_c_identifiers, -# ) +) # from dataclasses import dataclass, field # from script_umdp3_checker import fortran_keywords @@ -383,6 +383,33 @@ def r3_4_2_no_full_uppercase_variable_names(lines: List[str]) -> TestResult: * TODO: To improve readability, you should always use the optional space to separate the Fortran keywords. This rule also applies to OpenMP keywords. (See: 3.15) +""" +def r3_4_3_no_unseparated_keywords(lines: List[str]) -> TestResult: + """Check for omitted optional spaces in keywords""" + failures = 0 + error_log = {} + count = -1 + for count, line in enumerate(lines, 1): + clean_line = remove_quoted(line) + clean_line = remove_comments(clean_line) + for kw in unseparated_keywords_list: + pattern = f"\\b{kw}\\b" + if re.search(pattern, clean_line, re.IGNORECASE): + failures += 1 + error_log = add_error_log( + error_log, + f"unseparated keyword at line: {count}: {kw}", + count, + ) + return TestResult( + checker_name="No Unseparated Keywords", + failure_count=failures, + passed=(failures == 0), + output=f"Checked {count} lines, found {failures} UPPERCASE variables.", + errors=error_log, + ) + +""" * TODO: The full version of END should be used at all times, eg END SUBROUTINE and END FUNCTION * TODO: New code should be written using Fortran 95/2003 features. Avoid non-portable @@ -463,4 +490,5 @@ def r3_4_2_no_full_uppercase_variable_names(lines: List[str]) -> TestResult: r3_3_3_one_statement_per_line, r3_4_1_capitalised_keywords, r3_4_2_no_full_uppercase_variable_names, + r3_4_3_no_unseparated_keywords, ] From 8f2c184e45d8aee75ea3f5714622680b4b85bd72 Mon Sep 17 00:00:00 2001 From: r-sharp Date: Wed, 4 Nov 2026 16:39:37 +0000 Subject: [PATCH 4/5] fix: Something went wrong with clash resolution on GH - fixing it here. --- script_umdp3_checker/tests/test_umdp3_rules_S3.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/script_umdp3_checker/tests/test_umdp3_rules_S3.py b/script_umdp3_checker/tests/test_umdp3_rules_S3.py index 24f5fea1..434fcfb2 100644 --- a/script_umdp3_checker/tests/test_umdp3_rules_S3.py +++ b/script_umdp3_checker/tests/test_umdp3_rules_S3.py @@ -622,6 +622,10 @@ def test_r3_4_2_no_full_uppercase_variable_names( assert error in expected_errors for line_no in lines_list: assert line_no in expected_errors[error] + assert len(lines_list) == len(expected_errors[error]) + assert len(errors) == len(expected_errors) + assert failure_count == expected_result + # ================================================================= @@ -671,6 +675,8 @@ def test_r3_4_3_no_unseparated_keywords( assert len(errors) == len(expected_errors) for error, lines_list in errors.items(): assert error in expected_errors - assert len(lines_list) == len(expected_errors[error]) for line_no in lines_list: assert line_no in expected_errors[error] + assert len(lines_list) == len(expected_errors[error]) + assert len(errors) == len(expected_errors) + assert failure_count == expected_result From 4c60d7cf55a2da5857f914d248868c8612d5d1ee Mon Sep 17 00:00:00 2001 From: r-sharp Date: Fri, 6 Nov 2026 16:16:27 +0000 Subject: [PATCH 5/5] chore: Why are ruff format and the linters in VScode always at loggerheads. --- script_umdp3_checker/umdp3_rules_S3.py | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/script_umdp3_checker/umdp3_rules_S3.py b/script_umdp3_checker/umdp3_rules_S3.py index 237dff18..111e258f 100644 --- a/script_umdp3_checker/umdp3_rules_S3.py +++ b/script_umdp3_checker/umdp3_rules_S3.py @@ -15,10 +15,10 @@ """ToDo: This lot will need putting back as and when the tests that use them get imported/re-created""" # from fortran_keywords import fortran_keywords from search_lists import ( -# obsolescent_intrinsics, + # obsolescent_intrinsics, unseparated_keywords_list, -# retired_ifdefs, -# deprecated_c_identifiers, + # retired_ifdefs, + # deprecated_c_identifiers, ) # from dataclasses import dataclass, field # from script_umdp3_checker import fortran_keywords @@ -28,11 +28,11 @@ word_splitter = re.compile(r"\b\w+\b") -def add_error_log(error_log: Dict, key: str = "no key", value: int = 0) -> Dict: +def add_error_log(error_log: dict, key: str = "no key", value: int = 0) -> dict: """Add error information to the dictionary. - This adds the error statement, some are more unique than others, along with the - line number the error occurs on. Line number(s) are stored in a list in case the - same error occurs multiple times in a file.""" + This adds the error statement, some are more unique than others, along + with the line number the error occurs on. Line number(s) are stored in a + list in case the same error occurs multiple times in a file.""" if key not in error_log: error_log[key] = [] error_log[key].append(value) @@ -57,13 +57,15 @@ def remove_quoted(line: str) -> str: return result -"""This is in case I return to the idea of replacing the quoted sections from above -with something unique and storing them in order to put them back at some stage...""" +"""This is in case I return to the idea of replacing the quoted sections from +above with something unique and storing them in order to put them back at some +stage...""" def create_unique_random_string(storage_set, length: int = 7) -> str: """Create a unique random string of a given length. - Creates a random string of given length and ensures it hasn't been used before.""" + Creates a random string of given length and ensures it hasn't been used + before.""" import random import string @@ -78,8 +80,9 @@ def create_unique_random_string(storage_set, length: int = 7) -> str: def remove_comments(line: str) -> str: """Remove comments from the lines : - There is a bit of an assumption here that quoted text has already been removed, so - that we don't accidentally remove text after an "!" in a string.""" + There is a bit of an assumption here that quoted text has already been + removed, so that we don't accidentally remove text after an "!" in a + string.""" return comment_line.sub("", line).rstrip() @@ -206,12 +209,13 @@ def find_last(lines: List[str], unit_type: str, unit_name: str) -> tuple[bool, s * All programming units require a suitable copyright header.""" -def r3_2_1_check_crown_copyright(lines: List[str]) -> TestResult: +def r3_2_1_check_crown_copyright(lines: list[str]) -> TestResult: """Check for crown copyright statement""" """ - TODO: This is a very simplistic check and will not detect many cases which break UMDP3. - It will pass if the word copyright appears on a commented out line. This could - include passing : + TODO: This is a very simplistic check and will not detect many cases which + break UMDP3. + It will pass if the word copyright appears on a commented out line. This + could include passing : ! I should put a copyright statement here... I suspect the Perl Predeccessor @@ -277,7 +281,7 @@ def r3_3_2_line_too_long(lines: List[str]) -> TestResult: """ -* TODO: Never put more than one statement on a line. +* Never put more than one statement on a line. """ @@ -324,7 +328,7 @@ def r3_4_1_capitalised_keywords(lines: List[str]) -> TestResult: if line.lstrip(" ").startswith("!"): continue clean_line = remove_quoted(line) - clean_line = comment_line.sub("", clean_line) + clean_line = remove_comments(clean_line) clean_line = remove_cpp_commands(clean_line) # Check for lowercase keywords for word in word_splitter.findall(clean_line): @@ -403,11 +407,6 @@ def r3_4_2_no_full_uppercase_variable_names(lines: List[str]) -> TestResult: ) -""" -* TODO: To improve readability, you should always use the optional space to separate - the Fortran keywords. - This rule also applies to OpenMP keywords. (See: 3.15) -""" def r3_4_3_no_unseparated_keywords(lines: List[str]) -> TestResult: """Check for omitted optional spaces in keywords""" failures = 0 @@ -433,6 +432,7 @@ def r3_4_3_no_unseparated_keywords(lines: List[str]) -> TestResult: errors=error_log, ) + """ * TODO: The full version of END should be used at all times, eg END SUBROUTINE and END FUNCTION