From 83a2be6ec51f793a1ac300ecf2c940f7a6327476 Mon Sep 17 00:00:00 2001 From: Sterling Harper Date: Tue, 21 Jul 2015 22:10:15 -0600 Subject: [PATCH] Make source code style checking script --- tests/check_source.py | 235 ++++++++++++++++++++++++++++++++++++++++++ tests/travis.sh | 1 + 2 files changed, 236 insertions(+) create mode 100755 tests/check_source.py diff --git a/tests/check_source.py b/tests/check_source.py new file mode 100755 index 0000000000..490eb9c7c3 --- /dev/null +++ b/tests/check_source.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import glob +from string import whitespace +from sys import exit +from textwrap import TextWrapper + + +################################################################################ +# Fortran reading/parsing backend. +################################################################################ + + +class LineOfCode(object): + """Contains and provides basic info about a line of Fortran 90 code.""" + def __init__(self, number, content, is_continuation=False, + string_terminator=None): + # Initialize member variables. + self.number = number + self.content = content + self.is_continuation = is_continuation + self.is_continued = False + assert (string_terminator == None or + string_terminator == "'" or + string_terminator == '"') + self.initial_string_terminator = string_terminator + self.final_string_terminator = None + self.is_comment_only = False + + # Parse the string. + self.parse() + + def __repr__(self): + rep = 'LineOfCode: line number = {0:d}\n'.format(self.number) + rep += '\tis_continuation = ' + str(self.is_continuation) + '\n' + rep += '\tis_continued = ' + str(self.is_continued) + '\n' + rep += ('\tinitial_string_terminator = ' + + str(self.initial_string_terminator) + '\n') + rep += ('\tfinal_string_terminator = ' + + str(self.final_string_terminator) + '\n') + rep += '\tis_comment_only = ' + str(self.is_comment_only) + '\n' + rep += '\tContent:\n' + self.content + return rep + + def __str__(self): + return repr(self) + + def parse(self): + # Initialize the string variables. + if self.initial_string_terminator == "'": + in_a_single_quote = True + in_a_double_quote = False + elif self.initial_string_terminator == '"': + in_a_single_quote = False + in_a_double_quote = True + else: + in_a_single_quote = False + in_a_double_quote = False + + # Initialize other variables. + in_a_comment = False + ends_with_ampersand = False + has_real_content = False + + # Parse through the line. + for char in self.content: + # Check for the start or end of strings. + if char == "'" and in_a_single_quote: + in_a_single_quote = False + elif char == "'" and not in_a_double_quote: + in_a_single_quote = True + elif char == '"' and in_a_double_quote: + in_a_double_quote = False + elif char == '"' and not in_a_single_quote: + in_a_double_quote = True + + # Check for the start of a comment. + if (char == "!" and not in_a_single_quote + and not in_a_double_quote): + in_a_comment = True + break # Don't care about anything in a comment. + + # Check for a continuation ampersand. + if char == "&": + ends_with_ampersand = True + elif all([char != w for w in whitespace]): + ends_with_ampersand = False + + # Check to see if there is any real content (non-whitespace, not in + # a comment) + if (not in_a_comment) and all([char != w for w in whitespace]): + has_real_content = True + + # Make sure nothing went terribly wrong. + assert not (in_a_single_quote and in_a_double_quote) + assert not (in_a_single_quote and in_a_comment) + assert not (in_a_double_quote and in_a_comment) + + # Is this line continued onto the next? + if ends_with_ampersand: + self.is_continued = True + + # Is a multiline string continued on the next line? + if in_a_single_quote: + assert self.is_continued + self.final_string_terminator = "'" + if in_a_double_quote: + assert self.is_continued + self.final_string_terminator = '"' + + # Is this line a comment-only line? + if (not has_real_content) and in_a_comment: + self.is_comment_only = True + + + def get_indent(self): + """Return the number of indentation spaces prefixing this line.""" + return len(self.content) - len(self.content.lstrip(' ')) + + + def contains_whitespace_only(self): + """Return True/False if all characters in the line are whitespace.""" + if len(self.content.strip('\n')) == 0: return False + is_ws = [ any([char == ws for ws in whitespace]) + for char in self.content.strip('\n') ] + return all(is_ws) + + + def has_trailing_whitespace(self): + """Return True/False if this line ends with a whitespace character.""" + stripped = self.content.strip('\n') + if len(stripped) == 0: return False + return any([stripped[-1] == ws for ws in whitespace]) + + def contains_tab(self): + """Return True/False if a tab character appears in the line.""" + return '\t' in self.content + + +def read_lines_of_code(fname): + line_num = 0 + cont = False + str_term = None + with open(fname) as fin: + for line in fin: + loc = LineOfCode(line_num, line, is_continuation=cont, + string_terminator=str_term) + cont = loc.is_continued + str_term = loc.final_string_terminator + line_num += 1 + yield loc + + +################################################################################ +# Error checking. +################################################################################ + + +def print_error(fname, line_number, err_msg): + header = "Error in file {0}, line {1}:".format(fname, line_number + 1) + + tw = TextWrapper(width=80, initial_indent=' '*4, subsequent_indent=' '*4) + body = '\n'.join(tw.wrap(err_msg)) + + print(header + '\n' + body + '\n') + + +def check_source(fname): + """Make sure the given Fortran source file meets OpenMC standards. + + If errors are found, messages will be printed to the screen describing the + error. The function will return True if no errors were found or False + otherwise. + """ + good_code = True + base_indent = 0 + + for loc in read_lines_of_code(fname): + # Check for extra whitespace errors. + if loc.contains_whitespace_only(): + good_code = False + print_error(fname, loc.number, "Line contains whitespace " \ + "characters but no content. Please remove whitespace.") + elif loc.has_trailing_whitespace(): + good_code = False + print_error(fname, loc.number, "Line has trailing whitespace after"\ + " the content. Please remove trailing whitespace.") + if loc.contains_tab(): + good_code = False + print_error(fname, loc.number, "Line contains a tab character. " \ + "Please replace with single whitespace characters.") + + # Check indentation. + current_indent = loc.get_indent() + if ((not loc.is_continuation) and (not loc.is_comment_only) and + (not loc.contains_whitespace_only()) and current_indent % 2 != 0): + good_code = False + print_error(fname, loc.number, "Line is indented an odd number of "\ + "spaces. All non-continuation lines should be indented an "\ + "even number of spaces.") + if loc.is_continuation and current_indent < base_indent + 5: + good_code = False + print_error(fname, loc.number, "Continuation lines must be "\ + "indented by at least 5 spaces, but this line is indented {0}"\ + " spaces.".format(current_indent - base_indent)) + + # Set base indentation for next lines. + if not loc.is_continuation: + base_indent = current_indent + + return good_code + + +################################################################################ +# Main loop. +################################################################################ + + +if __name__ == '__main__': + # Get a list of the F90 source files. + f90_files = glob.glob('../src/*.F90') + + # Make sure we found the source files. + assert len(f90_files) != 0, 'No .F90 source files found.' + + # Make sure all the F90 source files meet our standards. + good_code = [check_source(fname) for fname in f90_files] + if not all(good_code): + print("ERROR: The Fortran source code does not meet OpenMC's standards") + exit(-1) + else: + print("SUCCESS! Looks like the Fortran source meets our standards") + exit(0) diff --git a/tests/travis.sh b/tests/travis.sh index d591428686..fac8d793a6 100755 --- a/tests/travis.sh +++ b/tests/travis.sh @@ -3,6 +3,7 @@ set -ev # Run all debug tests +./check_source.py if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then ./run_tests.py -C "^basic-debug$|^hdf5-debug$|^mpi-omp-debug$|^phdf5-omp-debug$" -j 2 -s else