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