1# -*- coding: utf-8 -*- 2############################################################################### 3# This file is part of fprettify. 4# Copyright (C) 2016-2019 Patrick Seewald, CP2K developers group 5# 6# fprettify is free software: you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation, either version 3 of the License, or 9# (at your option) any later version. 10# 11# fprettify is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with fprettify. If not, see <http://www.gnu.org/licenses/>. 18############################################################################### 19 20"""This is a collection of Fortran parsing utilities.""" 21 22from __future__ import (absolute_import, division, 23 print_function, unicode_literals) 24import re 25from collections import deque 26 27RE_FLAGS = re.IGNORECASE | re.UNICODE 28 29# FIXME bad ass regex! 30VAR_DECL_RE = re.compile( 31 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_FLAGS) 32 33OMP_COND_RE = re.compile(r"^\s*(!\$ )", RE_FLAGS) 34 35# supported preprocessors 36FYPP_LINE_STR = r"^(#!|#:|\$:|@:)" 37CPP_STR = r"^#[^!:{}]" 38COMMENT_LINE_STR = r"^!" 39FYPP_OPEN_STR = r"(#{|\${|@{)" 40FYPP_CLOSE_STR = r"(}#|}\$|}@)" 41NOTFORTRAN_LINE_RE = re.compile(r"("+FYPP_LINE_STR+r"|"+CPP_STR+r"|"+COMMENT_LINE_STR+r")", RE_FLAGS) 42FYPP_LINE_RE = re.compile(FYPP_LINE_STR, RE_FLAGS) 43FYPP_OPEN_RE = re.compile(FYPP_OPEN_STR, RE_FLAGS) 44FYPP_CLOSE_RE = re.compile(FYPP_CLOSE_STR, RE_FLAGS) 45 46STR_OPEN_RE = re.compile(r"("+FYPP_OPEN_STR+r"|"+r"'|\"|!)", RE_FLAGS) 47CPP_RE = re.compile(CPP_STR, RE_FLAGS) 48 49class FprettifyException(Exception): 50 """Base class for all custom exceptions""" 51 52 def __init__(self, msg, filename, line_nr): 53 super(FprettifyException, self).__init__(msg) 54 self.filename = filename 55 self.line_nr = line_nr 56 57 58class FprettifyParseException(FprettifyException): 59 """Exception for unparseable Fortran code (user's fault).""" 60 61 pass 62 63 64class FprettifyInternalException(FprettifyException): 65 """Exception for potential internal errors (fixme's).""" 66 67 pass 68 69 70class CharFilter(object): 71 """ 72 An iterator to wrap the iterator returned by `enumerate(string)` 73 and ignore comments and characters inside strings 74 """ 75 76 def __init__(self, string, filter_comments=True, filter_strings=True): 77 self._content = string 78 self._it = enumerate(self._content) 79 self._instring = '' 80 self._infypp = False 81 self._incomment = '' 82 self._instring = '' 83 self._filter_comments = filter_comments 84 self._filter_strings = filter_strings 85 86 def update(self, string, filter_comments=True, filter_strings=True): 87 self._content = string 88 self._it = enumerate(self._content) 89 self._filter_comments = filter_comments 90 self._filter_strings = filter_strings 91 92 def __iter__(self): 93 return self 94 95 def next(self): # pragma: no cover 96 """ Python 2 compatibility """ 97 return self.__next__() 98 99 def __next__(self): 100 101 pos, char = next(self._it) 102 103 char2 = self._content[pos:pos+2] 104 105 if not self._instring: 106 if not self._incomment: 107 if FYPP_OPEN_RE.search(char2): 108 self._instring = char2 109 self._infypp = True 110 elif (NOTFORTRAN_LINE_RE.search(char2)): 111 self._incomment = char 112 elif char in ['"', "'"]: 113 self._instring = char 114 else: 115 if self._infypp: 116 if FYPP_CLOSE_RE.search(char2): 117 self._instring = '' 118 self._infypp = False 119 if self._filter_strings: 120 self.__next__() 121 return self.__next__() 122 123 elif char in ['"', "'"]: 124 if self._instring == char: 125 self._instring = '' 126 if self._filter_strings: 127 return self.__next__() 128 129 if self._filter_comments: 130 if self._incomment: 131 raise StopIteration 132 133 if self._filter_strings: 134 if self._instring: 135 return self.__next__() 136 137 return (pos, char) 138 139 def filter_all(self): 140 filtered_str = '' 141 for pos, char in self: 142 filtered_str += char 143 return filtered_str 144 145class InputStream(object): 146 """Class to read logical Fortran lines from a Fortran file.""" 147 148 def __init__(self, infile, orig_filename=None): 149 if not orig_filename: 150 orig_filename = infile.name 151 self.line_buffer = deque([]) 152 self.infile = infile 153 self.line_nr = 0 154 self.filename = orig_filename 155 self.endpos = deque([]) 156 self.what_omp = deque([]) 157 158 def next_fortran_line(self): 159 """Reads a group of connected lines (connected with &, separated by newline or semicolon) 160 returns a touple with the joined line, and a list with the original lines. 161 Doesn't support multiline character constants! 162 """ 163 joined_line = "" 164 comments = [] 165 lines = [] 166 continuation = 0 167 fypp_cont = 0 168 instring = '' 169 170 string_iter = CharFilter('') 171 fypp_cont = 0 172 while 1: 173 if not self.line_buffer: 174 line = self.infile.readline().replace("\t", 8 * " ") 175 self.line_nr += 1 176 # convert OMP-conditional fortran statements into normal fortran statements 177 # but remember to convert them back 178 179 what_omp = OMP_COND_RE.search(line) 180 181 if what_omp: 182 what_omp = what_omp.group(1) 183 else: 184 what_omp = '' 185 186 if what_omp: 187 line = line.replace(what_omp, '', 1) 188 line_start = 0 189 190 pos = -1 191 192 # update instead of CharFilter(line) to account for multiline strings 193 string_iter.update(line) 194 for pos, char in string_iter: 195 if char == ';' or pos + 1 == len(line): 196 self.endpos.append(pos - line_start) 197 self.line_buffer.append(line[line_start:pos + 1]) 198 self.what_omp.append(what_omp) 199 what_omp = '' 200 line_start = pos + 1 201 202 if pos + 1 < len(line): 203 if fypp_cont: 204 self.endpos.append(-1) 205 self.line_buffer.append(line) 206 self.what_omp.append(what_omp) 207 else: 208 for pos_add, char in CharFilter(line[pos+1:], filter_comments=False): 209 char2 = line[pos+1+pos_add:pos+3+pos_add] 210 if NOTFORTRAN_LINE_RE.search(char2): 211 self.endpos.append(pos + pos_add - line_start) 212 self.line_buffer.append(line[line_start:]) 213 self.what_omp.append(what_omp) 214 break 215 216 if not self.line_buffer: 217 self.endpos.append(len(line)) 218 self.line_buffer.append(line) 219 self.what_omp.append('') 220 221 222 line = self.line_buffer.popleft() 223 endpos = self.endpos.popleft() 224 what_omp = self.what_omp.popleft() 225 226 if not line: 227 break 228 229 lines.append(what_omp + line) 230 231 line_core = line[:endpos + 1] 232 233 if NOTFORTRAN_LINE_RE.search(line[endpos+1:endpos+3]) or fypp_cont: 234 line_comments = line[endpos + 1:] 235 else: 236 line_comments = '' 237 238 if line_core: 239 newline = (line_core[-1] == '\n') 240 else: 241 newline = False 242 243 line_core = line_core.strip() 244 245 if line_core: 246 continuation = 0 247 if line_core.endswith('&'): 248 continuation = 1 249 250 if line_comments: 251 if (FYPP_LINE_RE.search(line[endpos+1:endpos+3]) or fypp_cont) and line_comments.strip()[-1] == '&': 252 fypp_cont = 1 253 else: 254 fypp_cont = 0 255 256 line_core = line_core.strip('&') 257 258 comments.append(line_comments.rstrip('\n')) 259 if joined_line.strip(): 260 joined_line = joined_line.rstrip( 261 '\n') + line_core + '\n' * newline 262 else: 263 joined_line = what_omp + line_core + '\n' * newline 264 265 if not (continuation or fypp_cont): 266 break 267 268 return (joined_line, comments, lines) 269