Extension to prettify script,

not yet activated in Makefile (i.e. make pretty command remains unchanged):
- auto indentation according to scopes and subunits of a Fortran file.
- auto alignment of continuation after line break.
- some white space formatting, removal of trailing white spaces and removal of consecutive blank lines.
The extended prettify is activated with command-line option --extended of prettify script.


svn-origin-rev: 16667
This commit is contained in:
Patrick Seewald 2016-03-07 16:44:38 +00:00
parent 84592240ff
commit 25b76bee5c
3 changed files with 942 additions and 53 deletions

View file

@ -0,0 +1,807 @@
#!/usr/bin/env python
"""
Impose white space conventions and indentation based on scopes / subunits
normalization of white spaces supported for following operators:
- relational operators (convention ' op '):
.EQ. .NE. .LT. .LE. .GT. .GE.
== /= < <= > >=
- logical operators (convention ' op ', exception '.NOT. '):
.AND. .OR. .EQV. .NEQV.
.NOT.
- bracket delimiters (context dependent convention)
- commas and semicolons (convention ', '):
- arithmetic operators convention 'op':
* / **
- arithmetic operators convention ' op ':
+ -
- other operators convention 'op':
% - (sign) = (function argument)
- other operators convention ' op ':
= (assignment) => (pointer assignment)
supported criteria for alignment / indentation:
Fortran lines:
- if, else, endif
- do, enddo
- select case, case, end select
- subroutine, end subroutine
- function, end function
- module, end module
- program, end program
- interface, end interface
- type, end type
Actual lines (parts of Fortran lines separated by linebreaks):
- bracket delimiters (.), (/./), and [.]
- assignments by value = and pointer =>.
LIMITATIONS
- assumes that all subunits are explicitly ended within same file, no treatment of #include statements
- can not deal with f77 constructs (files are ignored)
"""
import re
import sys
from formatting.normalizeFortranFile import useParseRe, typeRe, InputStream, CharFilter
#=========================================================================
# constants, mostly regular expressions
RE_FLAGS = re.IGNORECASE # all regex should be case insensitive
DEFAULT_ERROR_MESSAGE = " Syntax error - this script can not handle invalid Fortran files."
EOL_STR = r"\s*;?\s*$" # end of fortran line
EOL_SC = r"\s*;\s*$" # whether line is ended with semicolon
SOL_STR = r"^\s*" # start of fortran line
# special cases (f77 constructs and omp statements not formatted)
F77_STYLE = re.compile(r"^\s*\d", RE_FLAGS)
OMP_RE = re.compile(r"^\s*!\$", RE_FLAGS)
# regular expressions for parsing statements that start, continue or end a
# subunit:
IF_RE = re.compile(
SOL_STR + r"(\w+\s*:)?\s*IF\s*\(.+\)\s*THEN" + EOL_STR, RE_FLAGS)
ELSE_RE = re.compile(
SOL_STR + r"ELSE(\s*IF\s*\(.+\)\s*THEN)?" + EOL_STR, RE_FLAGS)
ENDIF_RE = re.compile(SOL_STR + r"END\s*IF(\s+\w+)?" + EOL_STR, RE_FLAGS)
DO_RE = re.compile(SOL_STR + r"(\w+\s*:)?\s*DO(" + EOL_STR + r"|\s)", RE_FLAGS)
ENDDO_RE = re.compile(SOL_STR + r"END\s*DO(\s+\w+)?" + EOL_STR, RE_FLAGS)
SELCASE_RE = re.compile(
SOL_STR + r"SELECT\s*CASE\s*\(.+\)" + EOL_STR, RE_FLAGS)
CASE_RE = re.compile(SOL_STR + r"CASE\s*(\(.+\)|DEFAULT)" + EOL_STR, RE_FLAGS)
ENDSEL_RE = re.compile(SOL_STR + r"END\s*SELECT" + EOL_STR, RE_FLAGS)
SUBR_RE = re.compile(
r"^[^\"'!]*SUBROUTINE\s+\w+\s*(\(.*\))?" + EOL_STR, RE_FLAGS)
ENDSUBR_RE = re.compile(
SOL_STR + r"END\s*SUBROUTINE(\s+\w+)?" + EOL_STR, RE_FLAGS)
FCT_RE = re.compile(
r"^[^\"'!]*FUNCTION\s+\w+\s*(\(.*\))?(\s*RESULT\s*\(\w+\))?" + EOL_STR, RE_FLAGS)
ENDFCT_RE = re.compile(
SOL_STR + r"END\s*FUNCTION(\s+\w+)?" + EOL_STR, RE_FLAGS)
MOD_RE = re.compile(SOL_STR + r"MODULE\s+\w+" + EOL_STR, RE_FLAGS)
ENDMOD_RE = re.compile(SOL_STR + r"END\s*MODULE(\s+\w+)?" + EOL_STR, RE_FLAGS)
TYPE_RE = re.compile(
SOL_STR + r"TYPE(\s*,\s*BIND\s*\(\s*C\s*\))?(\s*::\s*|\s+)\w+" + EOL_STR, RE_FLAGS)
ENDTYPE_RE = re.compile(SOL_STR + r"END\s*TYPE(\s+\w+)?" + EOL_STR, RE_FLAGS)
PROG_RE = re.compile(SOL_STR + r"PROGRAM\s+\w+" + EOL_STR, RE_FLAGS)
ENDPROG_RE = re.compile(
SOL_STR + r"END\s*PROGRAM(\s+\w+)?" + EOL_STR, RE_FLAGS)
INTERFACE_RE = re.compile(SOL_STR + r"INTERFACE(\s+\w+)?" + EOL_STR, RE_FLAGS)
ENDINTERFACE_RE = re.compile(
SOL_STR + r"END\s*INTERFACE(\s+\w+)?" + EOL_STR, RE_FLAGS)
CONTAINS_RE = re.compile(SOL_STR + r"CONTAINS" + EOL_STR, RE_FLAGS)
PUBLIC_RE = re.compile(SOL_STR + r"PUBLIC\s*::")
# intrinsic statements with parenthesis notation that are not functions
INTR_STMTS_PAR = "(ALLOCATE|DEALLOCATE|REWIND|BACKSPACE|INQUIRE|OPEN|CLOSE|WRITE|READ|FORALL|WHERE|NULLIFY)"
# regular expressions for parsing linebreaks (it implicitly ignores
# ampersands in strings but does not work for comments with ampersand)
LINEBREAK_STR = r"(&)[\s]*(?:!.*)?$"
# regular expressions for parsing operators:
# Note: exclusion of +/- in real literals and sign operator
PLUSMINUS_RE = re.compile(
r"((?<=[\w\)\]])|^)(?<![\d\.]\w)\s*(\+|-)\s*", RE_FLAGS)
REL_OP_RE = re.compile(
r"\s*(\.(?:EQ|NE|LT|LE|GT|GE)\.|(?:==|\/=|<(?!=)|<=|(?<!=)>(?!=)|>=))\s*", RE_FLAGS)
LOG_OP_RE = re.compile(r"\s*(\.(?:AND|OR|EQV|NEQV)\.)\s*", RE_FLAGS)
# regular expressions for parsing delimiters
DEL_OPEN_STR = r"(\(\/?|\[)"
DEL_OPEN_RE = re.compile(DEL_OPEN_STR, RE_FLAGS)
DEL_CLOSE_STR = r"(\/?\)|\])"
DEL_CLOSE_RE = re.compile(DEL_CLOSE_STR, RE_FLAGS)
# empty line regex, treats omp statements as exception
EMPTY_RE = re.compile(SOL_STR + r"(![^\$].*)?$", RE_FLAGS)
# two-sided operators
LR_OPS_RE = [REL_OP_RE, LOG_OP_RE, PLUSMINUS_RE]
# combine regex that define subunits
NEW_SCOPE_RE = [IF_RE, DO_RE, SELCASE_RE, SUBR_RE,
FCT_RE, MOD_RE, PROG_RE, INTERFACE_RE, TYPE_RE]
CONTINUE_SCOPE_RE = [ELSE_RE, None, CASE_RE, CONTAINS_RE,
CONTAINS_RE, CONTAINS_RE, CONTAINS_RE, None, None]
END_SCOPE_RE = [ENDIF_RE, ENDDO_RE, ENDSEL_RE, ENDSUBR_RE,
ENDFCT_RE, ENDMOD_RE, ENDPROG_RE, ENDINTERFACE_RE, ENDTYPE_RE]
#=========================================================================
class F90Indenter(object):
"""
Parses encapsulation of subunits / scopes line by line and updates the indentation.
"""
def __init__(self, filename):
self._scope_storage = []
self._indent_storage = [0]
self._filename = filename
self._line_indents = []
self._aligner = F90Aligner(filename)
def process_lines_of_fline(self, f_line, lines, rel_ind, rel_ind_con, line_nr):
"""
Process all lines that belong to a Fortran line `f_line`, impose a relative indent of `rel_ind`
(and `rel_ind_con` for line continuation).
"""
self._line_indents = [0] * len(lines)
line_indents = self._line_indents
scopes = self._scope_storage
indents = self._indent_storage
filename = self._filename
# check statements that start new scope
is_new = False
valid_new = False
debug = False
for new_n, newre in enumerate(NEW_SCOPE_RE):
if newre.search(f_line) and not END_SCOPE_RE[new_n].search(f_line):
what_new = new_n
is_new = True
valid_new = True
scopes.append(what_new)
if debug:
print f_line
break
# check statements that continue scope
is_con = False
valid_con = False
for con_n, conre in enumerate(CONTINUE_SCOPE_RE):
if conre is not None and conre.search(f_line):
what_con = con_n
is_con = True
if len(scopes) > 0:
what = scopes[-1]
if what == what_con:
valid_con = True
if debug:
print f_line
break
# check statements that end scope
is_end = False
valid_end = False
for end_n, endre in enumerate(END_SCOPE_RE):
if endre.search(f_line):
what_end = end_n
is_end = True
if len(scopes) > 0:
what = scopes.pop()
if what == what_end:
valid_end = True
if debug:
print f_line
break
# deal with line breaks
br_indent_list = [0] # separate indents list for linebreaks
self._aligner.process_lines_of_fline(f_line, lines, rel_ind_con, line_nr)
br_indent_list = self._aligner.get_lines_indent()
for pos in range(0, len(lines)):
if pos + 1 < len(lines):
line_indents[pos + 1] = br_indent_list[pos + 1]
if is_new:
if not valid_new:
raise SyntaxError(filename + ':' + str(line_nr) +
':' + DEFAULT_ERROR_MESSAGE)
else:
line_indents = [ind + indents[-1] for ind in line_indents]
old_ind = indents[-1]
rel_ind += old_ind # prevent originally unindented do / if blocks from being indented
indents.append(rel_ind)
elif is_con:
if not valid_con:
raise SyntaxError(filename + ':' + str(line_nr) +
':' + DEFAULT_ERROR_MESSAGE)
else:
line_indents = [ind + indents[-2] for ind in line_indents]
elif is_end:
if not valid_end:
raise SyntaxError(filename + ':' + str(line_nr) +
':' + DEFAULT_ERROR_MESSAGE)
else:
line_indents = [ind + indents[-2] for ind in line_indents]
indents.pop()
else:
line_indents = [ind + indents[-1] for ind in line_indents]
self._line_indents = line_indents
def get_fline_indent(self):
"""
after processing, retrieve the indentation of the full Fortran line.
"""
return self._indent_storage[-1]
def get_lines_indent(self):
"""
after processing, retrieve the indents of all line parts.
"""
return self._line_indents
#=========================================================================
class F90Aligner(object):
"""
Alignment of continuations of a broken line, based on the following heuristics
if line break in brackets: We are parsing the level of nesting and align to most inner bracket delimiter.
else if line is an assignment: alignment to '=' or '=>'.
note: assignment operator recognized as any '=' that is not
part of another operator and that is not enclosed in bracket
else if line is a declaration: alignment to '::'
else default indent
"""
def __init__(self, filename):
self._filename = filename
self.__init_line(0)
def __init_line(self, line_nr):
self._line_nr = line_nr
self._line_indents = [0]
self._level = 0
self._br_indent_list = [0]
def process_lines_of_fline(self, f_line, lines, rel_ind, line_nr):
"""
process all lines that belong to a Fortran line `f_line`.
"""
self.__init_line(line_nr)
is_decl = typeRe.match(f_line) or PUBLIC_RE.match(f_line)
for pos, line in enumerate(lines):
self.__align_linebreaks(
line, is_decl, rel_ind, self._line_nr + pos)
if pos + 1 < len(lines):
self._line_indents.append(self._br_indent_list[-1])
if len(self._br_indent_list) > 2 or self._level:
raise SyntaxError(self._filename + ':' + str(self._line_nr) +
':' + DEFAULT_ERROR_MESSAGE)
def get_lines_indent(self):
"""
after processing, retrieve the indents of all line parts.
"""
return self._line_indents
def __align_linebreaks(self, line, is_decl, indent_size, line_nr):
indent_list = self._br_indent_list
level = self._level
filename = self._filename
pos_eq = 0
pos_ldelim = []
pos_rdelim = []
ldelim = []
rdelim = []
# find delimiters that are not ended on this line.
# find proper alignment to most inner delimiter
# or alignment to assignment operator
rel_ind = indent_list[-1] # indentation of prev. line
instring = ''
end_of_delim = -1
for pos, char in CharFilter(enumerate(line)):
what_del_open = None
what_del_close = None
if not pos == end_of_delim:
what_del_open = DEL_OPEN_RE.match(line[pos:pos + 2])
what_del_close = DEL_CLOSE_RE.match(line[pos:pos + 2])
if not instring and what_del_open:
what_del_open = what_del_open.group()
end_of_delim = pos + len(what_del_open) - 1
level += 1
indent_list.append(pos + len(what_del_open) + rel_ind)
pos_ldelim.append(pos)
ldelim.append(what_del_open)
if not instring and what_del_close:
what_del_close = what_del_close.group()
end_of_delim = pos + len(what_del_close) - 1
level += -1
indent_list.pop()
if level < 0:
raise SyntaxError(filename + ':' + str(line_nr) +
':' + DEFAULT_ERROR_MESSAGE)
if pos_ldelim:
pos_ldelim.pop()
what_del_open = ldelim.pop()
valid = False
if what_del_open == r"(":
valid = what_del_close == r")"
if what_del_open == r"(/":
valid = what_del_close == r"/)"
if what_del_open == r"[":
valid = what_del_close == r"]"
if not valid:
raise SyntaxError(
filename + ':' + str(line_nr) + ':' + DEFAULT_ERROR_MESSAGE)
else:
pos_rdelim.append(pos)
rdelim.append(what_del_close)
if not instring and not level:
if not is_decl and char == '=':
if not REL_OP_RE.match(line[max(0, pos - 1):min(pos + 2, len(line))]):
if pos_eq > 0:
raise SyntaxError(
filename + ':' + str(line_nr) + ':' + DEFAULT_ERROR_MESSAGE)
is_pointer = line[pos + 1] == '>'
pos_eq = pos + 1
# don't align if assignment operator directly before
# line break
if not re.search(r"=>?\s*" + LINEBREAK_STR, line, RE_FLAGS):
indent_list.append(
pos_eq + 1 + is_pointer + indent_list[-1])
elif is_decl and line[pos:pos + 2] == '::':
if not re.search(r"::\s*" + LINEBREAK_STR, line, RE_FLAGS):
indent_list.append(pos + 3 + indent_list[-1])
# Don't align if delimiter opening directly before line break
if level and re.search(DEL_OPEN_STR + r"\s*" + LINEBREAK_STR, line, RE_FLAGS):
if len(indent_list) > 1:
indent_list[-1] = indent_list[-2]
else:
indent_list[-1] = 0
if not indent_list[-1]:
indent_list[-1] = indent_size
self._level = level
#=========================================================================
def inspect_ffile_format(infile, indent_size):
"""
Get some structural information of the Fortran file (original indentation,
check if it has f77 constructs).
"""
adopt = indent_size <= 0
is_f90 = True
indents = []
stream = InputStream(infile)
prev_offset = 0
while 1:
f_line, _, lines = stream.nextFortranLine()
if not lines:
break
offset = len(lines[0]) - len(lines[0].lstrip(' '))
indents.append(offset - prev_offset)
if not adopt: # do not adopt indentations but impose fixed rel. ind.
# but don't impose indentation for blocked do/if constructs
if prev_offset != offset or (not IF_RE.search(f_line) and not DO_RE.search(f_line)):
indents[-1] = indent_size
prev_offset = offset
# can not handle f77 style constructs
if F77_STYLE.search(f_line):
is_f90 = False
return indents, is_f90
#=========================================================================
def format_single_fline(f_line, linebreak_pos, filename, line_nr):
"""
format a single Fortran line. Takes a logical Fortran line as input
as well as the positions of the linebreaks. Filename and line_nr just
for error messages. Imposes white space formatting and inserts linebreaks.
"""
level = 0
lines_out = []
line = f_line
line_orig = line
line_ftd = line # copy to be formatted
offset = 0
for pos, char in CharFilter(enumerate(line)):
if char in ['+', '-', '*', '/', '%']:
offset = len(line_ftd) - len(line)
# strip whitespaces from elementary operators
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 1 + offset:]
line_ftd = lhs.rstrip(' ') + char + rhs.lstrip(' ')
line = line_ftd
pos_eq = []
end_of_delim = -1
for pos, char in CharFilter(enumerate(line)):
# offset w.r.t. unformatted line
offset = len(line_ftd) - len(line)
# format delimiters
what_del_open = None
what_del_close = None
if pos > end_of_delim:
what_del_open = DEL_OPEN_RE.match(
line[pos:pos + 2]) # opening delimiter token
what_del_close = DEL_CLOSE_RE.match(
line[pos:pos + 2]) # closing delimiter token
if what_del_open or what_del_close:
sep1 = 0
sep2 = 0
if what_del_open:
delim = what_del_open.group()
else:
delim = what_del_close.group()
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + len(delim) + offset:]
# format opening delimiters
if what_del_open:
level += 1 # new scope
# add separating whitespace before opening delimiter
# with some exceptions:
if (not re.search(r"(" + DEL_OPEN_STR + r"|[\w\*/=\+\-:])\s*$", line[:pos], RE_FLAGS) and
not EMPTY_RE.search(line[:pos])) or \
re.search(SOL_STR + r"(\w+\s*:)?(ELSE)?\s*IF\s*$", line[:pos], RE_FLAGS) or \
re.search(SOL_STR + r"(\w+\s*:)?\s*DO\s+WHILE\s*$", line[:pos], RE_FLAGS) or \
re.search(SOL_STR + r"(SELECT)?\s*CASE\s*", line[:pos], RE_FLAGS) or \
re.search(r"\b" + INTR_STMTS_PAR + r"\s*$", line[:pos], RE_FLAGS):
sep1 = 1
# format closing delimiters
else:
level += -1 # close scope
# add separating whitespace after closing delimiter
# with some exceptions:
if not re.search(r"^\s*(" + DEL_CLOSE_STR + r"|[,%:/\*])", line[pos + 1:], RE_FLAGS):
sep2 = 1
elif re.search(r"^\s*::", line[pos + 1:], RE_FLAGS):
sep2 = len(rhs) - len(rhs.lstrip(' '))
# where delimiter token ends
end_of_delim = pos + len(delim) - 1
line_ftd = lhs.rstrip(' ') + ' ' * sep1 + \
delim + ' ' * sep2 + rhs.lstrip(' ')
# format commas and semicolons
if char == ',' or char == ';':
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 1 + offset:]
line_ftd = lhs.rstrip(' ') + char + ' ' + rhs.lstrip(' ')
line_ftd = line_ftd.rstrip(' ')
# format .NOT.
if re.match(r"\.NOT\.", line[pos:pos + 5], RE_FLAGS):
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 5 + offset:]
line_ftd = lhs.rstrip(
' ') + line[pos:pos + 5] + ' ' + rhs.lstrip(' ')
# strip whitespaces from '=' and prepare assignment operator
# formatting
if char == '=':
if not REL_OP_RE.search(line[pos - 1:pos + 2]):
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 1 + offset:]
line_ftd = lhs.rstrip(' ') + '=' + rhs.lstrip(' ')
if not level: # remember position of assignment operator
pos_eq.append(len(lhs.rstrip(' ')))
line = line_ftd
# format assignments
for pos in pos_eq:
offset = len(line_ftd) - len(line)
is_pointer = line[pos + 1] == '>'
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 1 + is_pointer + offset:]
if is_pointer:
assign_op = ' => ' # pointer assignment
else:
assign_op = ' = ' # assignment
line_ftd = lhs.rstrip(' ') + assign_op + rhs.lstrip(' ')
# offset w.r.t. unformatted line
line = line_ftd
# for more advanced replacements we separate comments and strings
line_parts = []
str_end = -1
instring = ''
for pos, char in enumerate(line):
if not instring and char == '!': # skip comments
line_parts.append(line[str_end + 1:pos])
line_parts.append(line[pos:])
break
if char == '"' or char == "'": # skip string
if not instring:
str_start = pos
line_parts.append(line[str_end + 1:str_start])
instring = char
elif instring == char:
str_end = pos
line_parts.append(line[str_start:str_end + 1])
instring = ''
if pos == len(line) - 1:
line_parts.append(line[str_end + 1:])
# Two-sided operators
for lr_re in LR_OPS_RE:
for pos, part in enumerate(line_parts):
if not re.match(r"['\"!]", part, RE_FLAGS): # exclude comments, strings
partsplit = lr_re.split(part)
partsplit = [p for p in partsplit if p.strip(' ')]
line_parts[pos] = ' '.join(partsplit)
line = ''.join(line_parts)
# Now it gets messy - we need to shift line break positions from original
# to reformatted line
pos_new = 0
pos_old = 0
linebreak_pos.sort(reverse=True)
linebreak_pos_ftd = []
while 1:
if pos_new == len(line) - 1 or pos_old == len(line_orig) - 1:
break
if linebreak_pos and pos_old > linebreak_pos[-1]:
linebreak_pos.pop()
linebreak_pos_ftd.append(pos_new)
continue
if line[pos_new] is line_orig[pos_old]:
dowhile = True
while dowhile:
pos_new += 1
dowhile = line[pos_new] is ' '
dowhile = True
while dowhile:
pos_old += 1
dowhile = line_orig[pos_old] is ' '
assert line[pos_new] is line_orig[pos_old]
elif line[pos_new] is ' ':
pos_new += 1
elif line_orig[pos_old] is ' ':
pos_old += 1
else:
assert False
linebreak_pos_ftd.insert(0, 0)
# We do not insert ampersands in empty lines and comments lines
lines_out = [line[l:r].rstrip(' ') + ' &' * min(1, r - l)
for l, r in zip(linebreak_pos_ftd[0:-1], linebreak_pos_ftd[1:])]
lines_out.append(line[linebreak_pos_ftd[-1]:])
if level != 0:
raise SyntaxError(filename + ':' + str(line_nr) +
':' + DEFAULT_ERROR_MESSAGE)
return lines_out
#=========================================================================
def format_extended_ffile(infile, outfile, logFile=sys.stdout, indent_size=2, orig_filename=None):
"""
main method to be invoked for formatting a Fortran file
"""
debug = False
adopt_indents = indent_size <= 0 # don't change original indentation if rel-indents set to 0
if not orig_filename:
orig_filename = infile.name
indenter = F90Indenter(orig_filename)
req_indents, is_f90 = inspect_ffile_format(infile, indent_size)
infile.seek(0)
if not is_f90:
logFile.write("*** " + orig_filename +
": formatter can not handle f77 constructs. ***\n")
outfile.write(infile.read()) # does not handle f77 constructs
return
nfl = 0 # fortran line counter
do_indent = True
stream = InputStream(infile)
skip_blank = False
while 1:
f_line, comments, lines = stream.nextFortranLine()
if not lines:
break
# FIXME here we reconstruct comment positions from lines - this is a bad hack and should be
# fixed in nextFortranLine
if not comments:
comments = ''
comments_tmp = comments.split('\n')
comments_tmp = comments_tmp[::-1]
comment_lines = []
for line in lines:
if comments_tmp:
comment = comments_tmp[-1]
else:
comment = ''
comment_len = len(comment)
if comment and comment == line[-1 - comment_len:-1]:
sep = not comment.strip(' \n') == line.strip(' \n')
comment_lines.append(' ' * sep + comments_tmp.pop())
elif line.strip(' \n'):
comment_lines.append('')
orig_lines = lines
nfl += 1
indent = [0] * len(lines)
is_empty = EMPTY_RE.search(f_line) # blank line or comment only line
if useParseRe.match(f_line):
pass # do not touch use statements cleaned up by existing prettify
elif OMP_RE.match(f_line):
pass # do not touch OMP statements!
elif lines[0].startswith('#'): # preprocessor macros
assert len(lines) == 1
elif EMPTY_RE.search(f_line): # empty lines including comment lines
assert len(lines) == 1
if comments:
if lines[0].startswith('!'):
pass # don't indent unindented comment lines
else:
indent[0] = indenter.get_fline_indent()
elif skip_blank:
continue
lines = [l.strip(' ') for l in lines]
else:
# remove whitespaces on both sides, and '&' on left of line
lines = [l.strip(' ').lstrip('&') for l in lines]
f_line = f_line.strip(' ')
# find linebreak positions
linebreak_pos = []
for pos, line in enumerate(lines):
found = None
for char_pos, char in CharFilter(enumerate(line)):
if char == "&":
found = char_pos
if found:
linebreak_pos.append(found)
elif line.lstrip(' ').startswith('!'):
linebreak_pos.append(0)
linebreak_pos = [sum(linebreak_pos[0:_ + 1]) -
1 for _ in range(0, len(linebreak_pos))]
lines = format_single_fline(
f_line, linebreak_pos, orig_filename, stream.line_nr)
# we need to insert comments in formatted lines
for pos, (line, comment) in enumerate(zip(lines, comment_lines)):
if pos < len(lines) - 1:
has_nl = True
else:
has_nl = not re.search(EOL_SC, line)
lines[pos] = lines[pos].rstrip(' ') + comment + '\n' * has_nl
try:
rel_indent = req_indents[nfl]
except IndexError:
rel_indent = 0
indenter.process_lines_of_fline(f_line, lines, rel_indent, indent_size, stream.line_nr)
indent = indenter.get_lines_indent()
lines = [re.sub(r"\s+$", '\n', l, RE_FLAGS)
for l in lines] # deleting trailing whitespaces
for ind, line, orig_line in zip(indent, lines, orig_lines):
if do_indent:
ind_use = ind
else:
ind_use = 1
if ind_use + len(line) <= 133:
outfile.write(' ' * ind_use + line)
elif len(line) <= 133:
outfile.write(' ' * (133 - len(line)) + line)
logFile.write("*** " + orig_filename + ":" + str(stream.line_nr) +
": auto indentation failed due to 132 chars limit, please insert line break. ***\n")
else:
outfile.write(orig_line)
logFile.write("*** " + orig_filename + ":" + str(stream.line_nr) +
(": auto indentation and whitespace formatting failed due to 132 chars limit, "
"please insert line break. ***\n"))
if debug:
print ' ' * ind_use + line,
# no indentation of blank lines
if re.search(r";\s*$", f_line, RE_FLAGS):
do_indent = False
else:
do_indent = True
# rm subsequent blank lines
skip_blank = is_empty and not comments
#=========================================================================
if __name__ == '__main__':
import os.path
if len(sys.argv) < 2:
print "usage:", sys.argv[0], " out_dir file1 [file2 ...]"
sys.exit(1)
outDir = sys.argv[1]
if not os.path.isdir(outDir):
print "out_dir must be a directory"
print "usage:", sys.argv[0], " out_dir file1 [file2 ...]"
sys.exit(1)
for fileName in sys.argv[2:]:
try:
print "normalizing body of", fileName
with open(fileName, 'r') as infile, \
open(os.path.join(outDir, os.path.basename(fileName)), 'w') as outfile:
format_extended_ffile(infile, outfile)
except:
print "error for file", fileName
raise

View file

@ -5,6 +5,7 @@ import re
import string
from sys import argv
from cStringIO import StringIO
from collections import deque
rUse=0
rVar=0
@ -14,41 +15,102 @@ useParseRe=re.compile(
flags=re.IGNORECASE)
commonUsesRe=re.compile("^#include *\"([^\"]*(cp_common_uses.f90|base_uses.f90))\"")
localNameRe=re.compile(" *(?P<localName>[a-zA-Z_0-9]+)(?: *= *> *[a-zA-Z_0-9]+)? *$")
typeRe=re.compile(r" *(?P<type>integer(?: *\* *[0-9]+)?|logical|character(?: *\* *[0-9]+)?|real(?: *\* *[0-9]+)?|complex(?: *\* *[0-9]+)?|type) *(?P<parameters>\((?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*\))? *(?P<attributes>(?: *, *[a-zA-Z_0-9]+(?: *\((?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*\))?)+)? *(?P<dpnt>::)?(?P<vars>[^\n]+)\n?",re.IGNORECASE)#$
def readFortranLine(infile):
"""Reads a group of connected lines (connected with &)
returns a touple with the joined line, and a list with the original lines.
Doesn't support multiline character constants!"""
lineRe=re.compile(
r"(?:(?P<preprocessor>#.*\n?)| *(&)?(?P<core>(?:!\$|[^&!\"']+|\"[^\"]*\"|'[^']*')*)(?P<continue>&)? *(?P<comment>!.*)?\n?)",#$
re.IGNORECASE)
joinedLine=""
comments=None
lines=[]
continuation=0
while 1:
line=infile.readline().replace("\t",8*" ")
if not line: break
lines.append(line)
m=lineRe.match(line)
if not m or m.span()[1]!=len(line):
raise SyntaxError("unexpected line format:"+repr(line))
if m.group("preprocessor"):
if len(lines)>1:
raise SyntaxError("continuation to a preprocessor line not supported "+repr(line))
comments=line
break
coreAtt=m.group("core")
joinedLine = joinedLine.rstrip("\n") + coreAtt
if coreAtt and not coreAtt.isspace(): continuation=0
if m.group("continue"): continuation=1
if m.group("comment"):
if comments:
comments+="\n"+m.group("comment")
else:
comments=m.group("comment")
if not continuation: break
return (joinedLine,comments,lines)
class CharFilter(object):
"""
An iterator to wrap the iterator returned by `enumerate`
and ignore comments and characters inside strings
"""
def __init__(self, it):
self._it = it
self._instring = ''
def __iter__(self):
return self
def __next__(self):
pos, char = self._it.next()
if not self._instring and char == '!':
raise StopIteration
# detect start/end of a string
if char == '"' or char == "'":
if self._instring == char:
self._instring = ''
elif not self._instring:
self._instring = char
if self._instring:
return self.__next__()
return (pos, char)
def next(self):
return self.__next__()
class InputStream(object):
"""
Class to read logical Fortran lines from a Fortran file.
"""
def __init__(self, infile):
self.line_buffer = deque([])
self.infile = infile
self.line_nr = 0
def nextFortranLine(self):
"""Reads a group of connected lines (connected with &, separated by newline or semicolon)
returns a touple with the joined line, and a list with the original lines.
Doesn't support multiline character constants!
"""
lineRe=re.compile(
r"(?:(?P<preprocessor>#.*\n?)| *(&)?(?P<core>(?:!\$|[^&!\"']+|\"[^\"]*\"|'[^']*')*)(?P<continue>&)? *(?P<comment>!.*)?\n?)",#$
re.IGNORECASE)
joinedLine=""
comments=None
lines=[]
continuation=0
while 1:
if not self.line_buffer:
line=self.infile.readline().replace("\t",8*" ")
self.line_nr += 1
line_start = 0
for pos, char in CharFilter(enumerate(line)):
if char == ';' or pos + 1 == len(line):
self.line_buffer.append(line[line_start:pos+1])
line_start = pos+1
if(line_start < len(line)): self.line_buffer.append(line[line_start:]) # append comment
if self.line_buffer:
line = self.line_buffer.popleft()
if not line: break
lines.append(line)
m=lineRe.match(line)
if not m or m.span()[1]!=len(line):
raise SyntaxError("unexpected line format:"+repr(line))
if m.group("preprocessor"):
if len(lines)>1:
raise SyntaxError("continuation to a preprocessor line not supported "+repr(line))
comments=line
break
coreAtt=m.group("core")
joinedLine = joinedLine.rstrip("\n") + coreAtt
if coreAtt and not coreAtt.isspace(): continuation=0
if m.group("continue"): continuation=1
if m.group("comment"):
if comments:
comments+="\n"+m.group("comment")
else:
comments=m.group("comment")
if not continuation: break
return (joinedLine,comments,lines)
def parseRoutine(inFile):
"""Parses a routine"""
@ -57,7 +119,6 @@ def parseRoutine(inFile):
startRoutineRe=re.compile(r" *(?:recursive +|pure +|elemental +)*(?P<kind>subroutine|function) +(?P<name>[a-zA-Z_][a-zA-Z_0-9]*) *(?:\((?P<arguments>[^()]*)\))? *(?:result *\( *(?P<result>[a-zA-Z_][a-zA-Z_0-9]*) *\))? *(?:bind *\([^()]+\))? *\n?",re.IGNORECASE)#$
typeBeginRe=re.compile(r" *(?P<type>integer(?: *\* *[0-9]+)?|logical|character(?: *\* *[0-9]+)?|real(?: *\* *[0-9]+)?|complex(?: *\* *[0-9]+)?|type)[,( ]",
re.IGNORECASE)
typeRe=re.compile(r" *(?P<type>integer(?: *\* *[0-9]+)?|logical|character(?: *\* *[0-9]+)?|real(?: *\* *[0-9]+)?|complex(?: *\* *[0-9]+)?|type) *(?P<parameters>\((?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*\))? *(?P<attributes>(?: *, *[a-zA-Z_0-9]+(?: *\((?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*\))?)+)? *(?P<dpnt>::)?(?P<vars>[^\n]+)\n?",re.IGNORECASE)#$
attributeRe=re.compile(r" *, *(?P<attribute>[a-zA-Z_0-9]+) *(?:\( *(?P<param>(?:[^()]+|\((?:[^()]+|\([^()]*\))*\))*)\))? *",re.IGNORECASE)
ignoreRe=re.compile(r" *(?:|implicit +none *)$",re.IGNORECASE)
interfaceStartRe=re.compile(r" *interface *$",re.IGNORECASE)
@ -77,8 +138,9 @@ def parseRoutine(inFile):
'use':[]
}
includeRe=re.compile(r"#? *include +[\"'](?P<file>.+)[\"'] *$",re.IGNORECASE)
stream = InputStream(inFile)
while 1:
(jline,comments,lines)=readFortranLine(inFile)
(jline,comments,lines)=stream.nextFortranLine()
if len(lines)==0: break
if startRe.match(jline):break
routine['preRoutine'].extend(lines)
@ -86,8 +148,9 @@ def parseRoutine(inFile):
if m:
try:
subF=file(m.group('file'))
subStream = InputStream(subF)
while 1:
(subjline,subcomments,sublines)=readFortranLine(subF)
(subjline,subcomments,sublines)=subStream.nextFortranLine()
if not sublines:
break
routine['strippedCore'].append(subjline)
@ -112,7 +175,7 @@ def parseRoutine(inFile):
if (not routine['result'])and(routine['kind'].lower()=="function"):
routine['result']=routine['name']
while 1:
(jline,comments,lines)=readFortranLine(inFile)
(jline,comments,lines)=stream.nextFortranLine()
if len(lines)==0: break
if lines[0].lower().startswith("#include"): break
if not ignoreRe.match(jline):
@ -162,7 +225,7 @@ def parseRoutine(inFile):
istart=lines
interfaceDeclFile=StringIO()
while 1:
(jline,comments,lines)=readFortranLine(inFile)
(jline,comments,lines)=stream.nextFortranLine()
if interfaceEndRe.match(jline):
iend=lines
break
@ -201,6 +264,7 @@ def parseRoutine(inFile):
elif comments:
routine['declComments'].append(comments)
containsRe=re.compile(r" *contains *$",re.IGNORECASE)
while len(lines)>0:
if endRe.match(jline):
routine['end']=lines
@ -213,8 +277,9 @@ def parseRoutine(inFile):
if m:
try:
subF=file(m.group('file'))
subStream = InputStream(subF)
while 1:
(subjline,subcomments,sublines)=readFortranLine(subF)
(subjline,subcomments,sublines)=subStream.nextFortranLine()
if not sublines:
break
routine['strippedCore'].append(subjline)
@ -224,7 +289,7 @@ def parseRoutine(inFile):
print "error trying to follow include ",m.group('file')
print "warning this might lead to the removal of used variables"
traceback.print_exc()
(jline,comments,lines)=readFortranLine(inFile)
(jline,comments,lines)=stream.nextFortranLine()
return routine
def findWord(word,text,options=re.IGNORECASE):
@ -676,8 +741,9 @@ def parseUse(inFile):
modules=[]
origLines=[]
commonUses=""
stream = InputStream(inFile)
while 1:
(jline,comments,lines)=readFortranLine(inFile)
(jline,comments,lines)=stream.nextFortranLine()
lineNr=lineNr+len(lines)
if not lines: break
origLines.append("".join(lines))

View file

@ -6,6 +6,7 @@ import os, os.path
from formatting import normalizeFortranFile
from formatting import replacer
from formatting import addSynopsis
from formatting import normalizeExtended
from sys import argv
operatorsStr=r"\.(?:and|eqv?|false|g[et]|l[et]|n(?:e(?:|qv)|ot)|or|true)\."
@ -51,7 +52,7 @@ def upcaseKeywords(infile,outfile,logFile=sys.stdout):
e.text=line
raise
def prettifyFile(infile,normalize_use=1, upcase_keywords=1,
def prettifyFile(infile, normalize_use=1, normalize_extended=0, indent=2, upcase_keywords=1,
interfaces_dir=None,replace=None,logFile=sys.stdout):
"""prettifyes the fortran source in infile into a temporary file that is
returned. It can be the same as infile.
@ -75,6 +76,15 @@ def prettifyFile(infile,normalize_use=1, upcase_keywords=1,
tmpfile.close()
tmpfile=tmpfile2
ifile=tmpfile
if normalize_extended: # extended needs to be done first
tmpfile2=os.tmpfile()
normalizeExtended.format_extended_ffile(ifile,tmpfile2,logFile=logFile,
indent_size=indent,orig_filename=orig_filename)
tmpfile2.seek(0)
if tmpfile:
tmpfile.close()
tmpfile=tmpfile2
ifile=tmpfile
if normalize_use:
tmpfile2=os.tmpfile()
normalizeFortranFile.rewriteFortranFile(ifile,tmpfile2,logFile,
@ -119,7 +129,7 @@ def prettifyFile(infile,normalize_use=1, upcase_keywords=1,
raise
def prettfyInplace(fileName,bkDir="preprettify",normalize_use=1,
upcase_keywords=1, interfaces_dir=None,
normalize_extended=0,indent=2,upcase_keywords=1, interfaces_dir=None,
replace=None,logFile=sys.stdout):
"""Same as prettify, but inplace, replaces only if needed"""
if not os.path.exists(bkDir):
@ -127,8 +137,8 @@ def prettfyInplace(fileName,bkDir="preprettify",normalize_use=1,
if not os.path.isdir(bkDir):
raise Error("bk-dir must be a directory, was "+bkDir)
infile=open(fileName,'r')
outfile=prettifyFile(infile, normalize_use,
upcase_keywords, interfaces_dir, replace)
outfile=prettifyFile(infile, normalize_use, normalize_extended,
indent, upcase_keywords, interfaces_dir, replace)
if (infile==outfile):
return
infile.seek(0)
@ -168,12 +178,12 @@ def prettfyInplace(fileName,bkDir="preprettify",normalize_use=1,
if __name__ == '__main__':
defaultsDict={'upcase':1,'normalize-use':1,'replace':1,
defaultsDict={'upcase':1,'normalize-use':1,'extended':0,'indent':2,'replace':1,
'interface-dir':None,
'backup-dir':'preprettify'}
usageDesc=("usage:\n"+sys.argv[0]+ """
[--[no-]upcase] [--[no-]normalize-use] [--[no-]replace]
[--interface-dir=~/cp2k/obj/platform/target] [--help]
[--[no-]extended] --indent=2 [--interface-dir=~/cp2k/obj/platform/target] [--help]
[--backup-dir=bk_dir] file1 [file2 ...]
replaces file1,... with their prettified version after performing on
@ -189,16 +199,20 @@ if __name__ == '__main__':
sys.exit(0)
args=[]
for arg in sys.argv[1:]:
m=re.match(r"--(no-)?(normalize-use|upcase|replace)",arg)
m=re.match(r"--(no-)?(normalize-use|upcase|replace|extended)",arg)
if m:
defaultsDict[m.groups()[1]]=not m.groups()[0]
else:
m=re.match(r"--(interface-dir|backup-dir)=(.*)",arg)
m=re.match(r"--(indent)=(.*)",arg)
if m:
path=os.path.abspath(os.path.expanduser(m.groups()[1]))
defaultsDict[m.groups()[0]]=path
defaultsDict[m.groups()[0]]=int(m.groups()[1])
else:
args.append(arg)
m=re.match(r"--(interface-dir|backup-dir)=(.*)",arg)
if m:
path=os.path.abspath(os.path.expanduser(m.groups()[1]))
defaultsDict[m.groups()[0]]=path
else:
args.append(arg)
if len(args)<1:
print usageDesc
else:
@ -221,6 +235,8 @@ if __name__ == '__main__':
try:
prettfyInplace(fileName,bkDir,
normalize_use=defaultsDict['normalize-use'],
normalize_extended=defaultsDict['extended'],
indent=defaultsDict['indent'],
upcase_keywords=defaultsDict['upcase'],
interfaces_dir=defaultsDict['interface-dir'],
replace=defaultsDict['replace'])