OpenMC/tests/check_source.py

230 lines
8.5 KiB
Python
Executable file

#!/usr/bin/env python3
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 is 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)