PyForTool
Python-fortran-tool
Loading...
Searching...
No Matches
cosmetics.py
1"""
2Code formatting and style transformations.
3
4Provides the Cosmetics class for modifying FORTRAN code appearance including
5case, indentation, spacing, comments, and line continuation.
6
7Key Features
8------------
9- Case conversion (upper/lower FORTRAN keywords)
10- Indentation fixing with directive exclusion
11- Comment removal with directive preservation
12- Whitespace normalization
13- Line continuation alignment
14
15Classes
16-------
17Cosmetics : Mixin class providing code formatting methods
18
19Examples
20--------
21>>> pft = PYFT('input.F90')
22>>> pft.upperCase() # Convert keywords to UPPER CASE
23>>> pft.lowerCase() # Convert keywords to lower case
24>>> pft.indent() # Fix indentation
25>>> pft.removeComments() # Remove comments (keep directives)
26>>> pft.prettify() # Full formatting (indent + case + spaces)
27"""
28
29import re
30from pyfortool.util import debugDecor, nonCode, tag
31from pyfortool.expressions import createElem
32from pyfortool import NAMESPACE
33
34
35class Cosmetics():
36 """
37 Code formatting and style transformation methods.
38
39 Provides utilities for modifying code appearance including case,
40 indentation, spacing, and comment handling.
41 """
42
43 @debugDecor
44 def upperCase(self):
45 """
46 Convert all FORTRAN keywords to uppercase.
47
48 Examples
49 --------
50 >>> pft = PYFT('mixed.F90')
51 >>> pft.upperCase()
52 # WRITE -> write becomes WRITE
53 """
54 for elem in self.iter():
55 if (not nonCode(elem)) and elem is not None and elem.text is not None:
56 elem.text = elem.text.upper()
57
58 @debugDecor
59 def lowerCase(self):
60 """
61 Convert all FORTRAN keywords to lowercase.
62
63 Examples
64 --------
65 >>> pft = PYFT('MIXED.F90')
66 >>> pft.lowerCase()
67 # WRITE -> write
68 """
69 for elem in self.iter():
70 if (not nonCode(elem)) and elem is not None and elem.text is not None:
71 elem.text = elem.text.lower()
72
73 @debugDecor
74 def indent(self, nodeToUpdate=None, indentProgramunit=0, indentBranch=2,
75 exclDirectives=None):
76 """
77 Fix code indentation.
78
79 Parameters
80 ----------
81 nodeToUpdate : Element, optional
82 Specific node to indent. If None, indents entire scope.
83 indentProgramunit : int, optional
84 Number of spaces for program unit contents. Default is 0.
85 indentBranch : int, optional
86 Number of spaces for nested constructs (do, if, etc.). Default is 2.
87 exclDirectives : list, optional
88 Lines to exclude from indentation:
89 - None: Exclude '!$OMP' lines (default).
90 - []: Include all lines.
91 - ['!$acc', '!$mnh']: Custom directive list.
92
93 Returns
94 -------
95 self
96 Returns self for chaining.
97
98 Examples
99 --------
100 >>> pft = PYFT('input.F90')
101 >>> pft.indent() # Default indentation
102 >>> pft.indent(indentProgramunit=0, indentBranch=4) # Custom
103 """
104
105 if nodeToUpdate is None:
106 nodeToUpdate = self
107
108 if exclDirectives is None:
109 exclDirectives = ['!$OMP']
110
111 def setLevel(elem, level, nextElem):
112 """
113 :param elem: element whose tail must be modifies
114 :param level: level of indentation
115 :param nextElem: next element
116 """
117 if elem.tail is not None:
118 elem.tail = elem.tail.replace('\t', ' ')
119 excl = (nextElem is not None and
120 (tag(nextElem) == 'cpp' or
121 tag(nextElem) == 'C' and
122 any(nextElem.text.startswith(d) for d in exclDirectives)))
123 if not excl:
124 elem.tail = re.sub('\n[ ]*', '\n' + ' ' * level, elem.tail)
125
126 def indentRecur(elem, level, inConstruct):
127 """
128 :param elem: dom element
129 :param level: current level for elem
130 :param inConstruct: True if we are inside a construct
131 """
132 blocs = ['file', 'program-unit', 'if-block', 'where-block', 'selectcase-block']
133 progstmt = ['subroutine-stmt', 'program-stmt', 'module-stmt', 'function-stmt',
134 'submodule-stmt', 'procedure-stmt', 'interface-stmt']
135 endprogstmt = ['end-' + s for s in progstmt]
136 interbranchstmt = ['else-stmt', 'else-if-stmt', 'else-where-stmt']
137 branchstmt = ['if-then-stmt', 'where-construct-stmt'] + interbranchstmt
138 endbranchstmt = ['end-if-stmt', 'end-where-stmt']
139
140 currlevel = level
141 laste = None
142 firstnumselect = True
143 for ie, sElem in enumerate(elem):
144 # Indentation does not apply to these lines (eg SUBROUTINE statement, DO construct)
145 # but apply to the lines inside
146 if tag(sElem) in progstmt:
147 currlevel += indentProgramunit
148 elif tag(sElem) in branchstmt + [inConstruct + '-stmt']:
149 currlevel += indentBranch
150
151 # Add indentation *to the tail*, thus for the next line
152 setLevel(sElem, currlevel, elem[ie + 1] if ie + 1 < len(elem) else None)
153
154 if tag(elem) == 'selectcase-construct':
155 # Structure is:
156 # <selectcase-construct>
157 # <selectcase-block><select-case-stmt>SELECT
158 # CASE (...)</select-case-stmt> \n +2
159 # </selectcase-block>
160 # <selectcase-block><case-stmt>CASE<case-selector>(...)
161 # </case-selector></case-stmt> \n +4
162 # statement \n +4
163 # statement \n +2
164 # </selectcase-block>
165 # <selectcase-block><case-stmt>CASE<case-selector>(...)
166 # </case-selector></case-stmt> \n +4
167 # statement \n +4
168 # statement \n +0
169 # <end-select-case-stmt>END SELECT</end-select-case-stmt>
170 # </selectcase-block></selectcase-construct>
171 if firstnumselect:
172 firstnumselect = False
173 else:
174 # previous line was a CASE line, we must indent it only once
175 # pylint: disable-next=unsubscriptable-object
176 setLevel(laste[-1], level + indentBranch, sElem)
177 # statements are indented twice
178 indentRecur(sElem, level + indentBranch * 2, "")
179 if tag(sElem[-1]) == 'end-select-case-stmt':
180 setLevel(sElem[-2], level, sElem[-1])
181
182 elif tag(sElem) in blocs or tag(sElem).endswith('-construct'):
183 # This xml tag contains other tags, we iterate on them
184 if tag(sElem[0]) in interbranchstmt:
185 # Structure is <if-construct><if-block><if-then-stmt>IF...
186 # ...THEN</if-then-stmt>
187 # statement (the identation of the ELSE line is
188 # in the tail of this stetement)
189 # </if-block><if-block><else-stmt>ELSE</else-stmt>
190 # statement
191 # <end-if-stmt>ENDIF</end-if-stmt></if-block></if-construct>
192 # pylint: disable-next=unsubscriptable-object
193 setLevel(laste[-1], level, sElem)
194 construct = tag(sElem)[:-10] if tag(sElem).endswith('-construct') else ""
195 indentRecur(sElem, currlevel, construct)
196
197 # This line contains the end statement, we must remove the indentation contained
198 # in the tail of the previous item
199 if tag(sElem) in endprogstmt + endbranchstmt + ['end-' + inConstruct + '-stmt']:
200 setLevel(laste, level, sElem)
201 laste = sElem
202
203 indentRecur(nodeToUpdate, 0, "")
204 return nodeToUpdate
205
206 @debugDecor
208 """
209 Remove empty lines
210 """
211 elem = self.find('{*}file')
212 if elem is not None and elem.text is not None:
213 elem.text = elem.text.replace('\n', '')
214 for elem in self.iter():
215 if elem.tail is not None and '\n' in elem.tail:
216 elem.tail = elem.tail.replace('\t', ' ')
217 elem.tail = re.sub(r"\n[  \n]*\n", r"\n", elem.tail)
218
219 @debugDecor
220 def removeComments(self, exclDirectives=None, pattern=None):
221 """
222 Remove comments from the source code.
223
224 Parameters
225 ----------
226 exclDirectives : list, optional
227 Comments to preserve (directives to exclude):
228 - None: Preserve '!$OMP', '!$mnh', '!$ACC' lines (default).
229 - []: Remove all comments.
230 - ['!$acc']: Preserve only specific directives.
231 pattern : str or re.Pattern, optional
232 Only remove comments matching this pattern.
233
234 Examples
235 --------
236 >>> pft = PYFT('input.F90')
237 >>> pft.removeComments() # Keep OpenMP/directives
238 >>> pft.removeComments(exclDirectives=[]) # Remove all
239 >>> pft.removeComments(pattern=re.compile(r'!.*TODO'))
240 """
241 if exclDirectives is None:
242 exclDirectives = ['!$OMP', '!$mnh', '!$ACC', '!$acc']
243
244 if isinstance(pattern, str):
245 pattern = re.compile(pattern)
246
247 def recur(elem):
248 tailUpper = None
249 for ie in range(len(elem))[::-1]: # Loop from the end to the begining
250 sElem = elem[ie]
251 if tag(sElem) == 'C' and \
252 not any(sElem.text.startswith(d) for d in exclDirectives) and \
253 (pattern is None or pattern.match(sElem.text)):
254 # Don't loose the tail (containing new line character and indentation)
255 if ie != 0:
256 # It exists an element before,
257 # we add the current tail to this previsous element
258 if elem[ie - 1].tail is None:
259 elem[ie - 1].tail = sElem.tail
260 elif sElem.tail is not None:
261 elem[ie - 1].tail += sElem.tail
262 else:
263 # The's no previsous element, tail is givent back the container element
264 tailUpper = sElem.tail
265 elem.remove(sElem)
266 if len(sElem) >= 1:
267 tail = recur(sElem) # recursive call to inner elements
268 if tail is not None:
269 # The first element was a comment,
270 # its tail must be added to the text attribute
271 if sElem.text is None:
272 sElem.text = tail
273 else:
274 sElem.text += tail
275 return tailUpper
276 recur(self)
277
278 @debugDecor
279 def updateContinuation(self, nodeToUpdate=None, align=True,
280 removeALL=False, addBegin=True, removeBegin=False):
281 """
282 :param nodeToUpdate: if None, the entire xml is updated
283 :param align: True to align begin of continued lines
284 :param removeALL: True to suppress all the continuation line characters ('&')
285 :param addBegin: True to add missing continuation line characters ('&')
286 at the begining of lines
287 :param removeBegin: True to suppress continuation line characters ('&')
288 at the begining of lines
289
290 When suppressed, the '&' are replaced by a space character
291 Comments after a '&' are lost
292 """
293
294 assert not (align and removeALL), "We cannot remove and align at the same time"
295 assert not (addBegin and (removeALL or removeBegin)), \
296 "We cannot remove and add, at the same time, continuation characters"
297
298 if nodeToUpdate is None:
299 nodeToUpdate = self
300
301 parents = {} # cache to be used in recurDirect
302
303 def recurReverse(elem, tail):
304 for ie in range(len(elem))[::-1]: # Loop from the end to the begining
305 sElem = elem[ie]
306 parents[sElem] = elem
307 if tag(sElem) == 'cnt':
308 # Search for comments or cpp after the cnt node
309 commentsAfter = []
310 j = ie + 1
311 while j < len(elem) and tag(elem[j]) in ('C', 'cpp'):
312 commentsAfter.append(elem[j])
313 j += 1
314 nextNode = elem[j] if j < len(elem) else None
315
316 # Is it a '&' at the end of a line (or at the begining)?
317 isend = ((sElem.tail is not None and '\n' in sElem.tail) or
318 len(commentsAfter) > 0)
319
320 # Add missing continuation character at the begining of line
321 if isend and addBegin:
322 if sElem.tail is not None and \
323 sElem.tail.replace('\n', '').replace('\t', '').lstrip(' ') != '':
324 # tail contains text, probably an endding ')', after a carriage return
325 # Thus, there is no '&' to begin line
326 new = createElem('cnt', text='&')
327 # '&' must be put before any text on the following line containing code
328 i = 0
329 while sElem.tail[i] in (' ', '\n', '\t'):
330 i += 1
331 new.tail = ' ' + sElem.tail[i:]
332 sElem.tail = sElem.tail[:i]
333 elem.insert(ie + 1, new)
334 elif tag(nextNode) != 'cnt':
335 # There is no '&' to begin next line
336 new = createElem('cnt', text='&')
337 if len(commentsAfter) > 0:
338 # '&' must be put before any text on the following
339 # line containing code
340 i = 0
341 while i < len(commentsAfter[-1].tail) and \
342 commentsAfter[-1].tail[i] in (' ', '\n', '\t'):
343 i += 1
344 new.tail = ' ' + commentsAfter[-1].tail[i:]
345 commentsAfter[-1].tail = commentsAfter[-1].tail[:i]
346 else:
347 new.tail = ' '
348 elem.insert(ie + 1 + len(commentsAfter), new)
349
350 # Suppression
351 if removeALL or (removeBegin and not isend):
352 cpp = False
353 for com in commentsAfter[::-1]:
354 if tag(com) != 'cpp':
355 elem.remove(com)
356 else:
357 cpp = True
358 if not cpp:
359 # We cannot remove a continuation line followed by a cpp
360 elem.remove(sElem) # OK because we loop in reverse order
361 if sElem.tail is not None:
362 txt = sElem.tail.strip() + ' '
363 else:
364 txt = ' '
365 if ie != 0:
366 if elem[ie - 1].tail is None:
367 elem[ie - 1].tail = txt
368 else:
369 elem[ie - 1].tail += txt
370
371 # Recursively enter blocs
372 if len(sElem) >= 1:
373 recurReverse(sElem, tail)
374
375 def recurDirect(elem, ct, inCnt):
376 """
377 :param ct: current text
378 :param inCnt: -1 if we are not in a statement spanning several lines
379 elswhere contains the number of spaces to add
380 """
381 ignoreComment = False
382 if align:
383 for ie, sElem in enumerate(list(elem)):
384 # It is a '&' character marking the end of the line
385 isendcnt = tag(sElem) == 'cnt' and \
386 ((sElem.tail is not None and '\n' in sElem.tail) or
387 (ie + 1 < len(elem) and tag(elem[ie + 1]) == 'C'))
388 ignoreComment = (ignoreComment or
389 (isendcnt and
390 (ie + 1 < len(elem) and tag(elem[ie + 1]) == 'C')))
391
392 # REAL :: X1, & !comment 1
393 # !comment 2
394 # X2, &
395 # #ifdef XXX
396 # X3, &
397 # #endif
398 # X4
399 if isendcnt or ignoreComment or (inCnt != -1 and tag(sElem) == 'cpp'):
400 # Number of spaces for alignment not already determined
401 # (first line of the continuation)
402 if isendcnt and inCnt == -1:
403 # Search for the container statement
404 topstmt = elem
405 while not tag(topstmt).endswith('-stmt'):
406 topstmt = parents[topstmt]
407
408 # Character to align on
409 if tag(topstmt) == 'a-stmt':
410 patList = ('=>', '=', r'\‍(')
411 elif tag(topstmt) == 'call-stmt':
412 patList = (r'\‍(', r'call[ ]+\w', 'call ', 'call')
413 elif tag(topstmt) == 'if-stmt':
414 patList = (r'\‍(', r'\‍)', 'if ', 'if')
415 elif tag(topstmt) == 'where-stmt':
416 patList = (r'\‍(', r'\‍)', 'where ', 'where')
417 elif tag(topstmt) == 'forall-stmt':
418 patList = (r'\‍(', r'\‍)', 'forall ', 'forall')
419 elif tag(topstmt) == 'namelist-stmt':
420 patList = ('/.*/', '/', 'namelist')
421 elif tag(topstmt) == 'subroutine-stmt':
422 patList = (r'\‍(', r'subroutine[ ]+\w', 'subroutine ', 'subroutine')
423 elif tag(topstmt) == 'use-stmt':
424 patList = (':', r'use[ ]+\w', 'use ', 'use')
425 elif tag(topstmt) == 'T-decl-stmt':
426 patList = ('::', r'\w,', r'\w ', r'\w')
427 elif tag(topstmt) == 'print-stmt':
428 patList = ('print', )
429 elif tag(topstmt) == 'write-stmt':
430 patList = (r'\‍)', r'write[ ]*\‍(', 'write[ ]*', 'write')
431 elif tag(topstmt) == 'procedure-stmt':
432 patList = ('module[ ]+procedure[ ]*', 'module[ ]*', 'module')
433 else:
434 patList = ('::', ':', r'\‍(', '=>', '=', '[', ':', '/')
435
436 # Compute indentation value
437 inCnt = None
438 for pat in patList:
439 if inCnt is None:
440 mat = re.search(pat, ct, flags=re.IGNORECASE)
441 if mat is not None:
442 if ie + 1 < len(elem) and tag(elem[ie + 1]) != 'cnt':
443 # If there is no continuation character at the begining,
444 # align the text with the position after the delimiter
445 # found
446 inCnt = mat.end()
447 else:
448 inCnt = mat.end() - 1
449 if inCnt is None:
450 inCnt = 4
451
452 # Align the next line exept if it is a cpp line
453 if not (ie + 1 < len(elem) and tag(elem[ie + 1]) == 'cpp'):
454 if sElem.tail is not None:
455 sElem.tail = re.sub('\n[ ]*', '\n' + ' ' * inCnt, sElem.tail)
456 else:
457 sElem.tail = '\n' + ' ' * inCnt
458
459 if tag(sElem) not in ('C', 'cnt'):
460 ct += (sElem.text if sElem.text is not None else '')
461 ignoreComment = False
462
463 # Recursively enter the inner blocks
464 if len(sElem) >= 1:
465 ct, inCnt = recurDirect(sElem, ct, inCnt)
466
467 # Text after the end of block
468 ct += (sElem.tail if sElem.tail is not None else '')
469 if '\n' in ct:
470 ct = ct.split('\n')[-1]
471 if tag(sElem) not in ('cnt', 'C', 'cpp'):
472 inCnt = -1
473
474 return ct, inCnt
475
476 recurReverse(nodeToUpdate, 0)
477 recurDirect(nodeToUpdate, "", -1)
478 return nodeToUpdate
479
480 __NO_VALUE__ = '__NO_VALUE__'
481
482 @debugDecor
483 def updateSpaces(self, beforeOp=1, afterOp=1, inOperator=True,
484 beforeComma=0, afterComma=1,
485 beforeParenthesis=0, afterParenthesis=0,
486 beforeAffectation=1, afterAffectation=1, inAffectation=True,
487 beforeRangeDelim=0, afterRangeDelim=0,
488 beforeUseDelim=0, afterUseDelim=1,
489 beforeDeclDelim=1, afterDeclDelim=1,
490 inDeclDelim=True, afterTypeDecl=1,
491 beforeEqDo=0, afterEqDo=0,
492 beforeEqCall=0, afterEqCall=0,
493 beforeEqInit=0, afterEqInit=0,
494 beforeEndcnt=1, afterBegincnt=1,
495 afterIfwherecase=1, beforeThen=1, beforeIfaction=1,
496 afterProgunit=1,
497 endOfLine=True, afterName=0, inName=True,
498 beforeCmdsep=0, afterCmdsep=1,
499 adjacentKeywords=__NO_VALUE__, afterKeywords=__NO_VALUE__):
500 """
501 :param beforeOp, afterOp: number of spaces before and after operators
502 :param inOperator: True to suppress spaces in operators
503 :param beforeComma, afterComma: number of spaces before and after commas
504 :param beforeParenthesis, afterParenthesis: number of spaces before and after parenthesis
505 :param beforeAffectation, afterAffectation: number of spaces before and after
506 affectations or associations
507 :param inAffectation: True to suppress spaces in affectations and in association ('= >')
508 :param beforeRangeDelim, afterRangeDelim: number of spaces before and after range delimiters
509 :param beforeUseDelim, afterUseDelim: number of spaces before and after use delimiters (':')
510 :param beforeDeclDelim, afterDeclDelim: number of spaces before and after declaration and
511 enumerator delimiter ('::')
512 :param inDeclDelim: True to suppress spaces in declaration and enumerator delimiter (': :')
513 :param afterTypeDecl: number of spaces after the type in a declaration w/o '::'
514 (e.g. 'INTEGER I'); also for enumerators (minimum 1)
515 :param beforeEqDo, afterEqDo: number of spaces before and after '=' sign in DO and
516 FORALL statements
517 :param beforeEqCall, afterEqCall: number of spaces before and after '=' sign
518 in CALL statement
519 :param beforeEqInit, afterEqInit: number of spaces before and after '=' sign for init values
520 :param beforeEndcnt, afterBegincnt: number of spaces before a continuation chararcter at the
521 end of the line and after a continuation character
522 at the begining of a line
523 :param afterIfwherecase: number of spaces after the IF, ELSEIF, WHERE, ELSEWHERE,
524 SELECTCASE, CASE and FORALL keywords
525 :param beforeThen: number of spaces before the THEN keyword
526 :param beforeIfaction: number of spaces
527 between IF condition and action in one-line IF statement and
528 between FORALL specification and affectation in one-line FORALL
529 statement and
530 between WHERE mask and action in one-line WHERE statement
531 :param afterProgunit: between the program unit type (e.g. SUBROUTINE) and its name
532 :param endOfLine: True to suppress spaces at the end of the line
533 :param afterName: number of spaces after an indentifier, type or attribute name
534 :param inName: True to suppress spaces in identifier names
535 :param beforeCmdsep, afterCmdsep: number of spaces before and after command separator (';')
536 :param adjacentKeywords: describes the number of spaces to introduce between adjancent
537 keywords when this is legal (the list comes from the table
538 "6.2 Adjacent keywords where separating blanks are optional" of the
539 F2008 norm and has been complemented by "end select",
540 "implicit none" and "module procedure"; for the last two,
541 a minimum of 1 is required).
542 The allowed dictionnary keys are:
543 - block_data
544 - double_precision
545 - else_if
546 - else_where
547 - end_associate
548 - end_block
549 - end_block_data
550 - end_critical
551 - end_do
552 - end_enum
553 - end_file
554 - end_forall
555 - end_function
556 - end_if
557 - end_interface
558 - end_module
559 - end_procedure
560 - end_program
561 - end_selec
562 - end_select
563 - end_submodule
564 - end_subroutine
565 - end_team
566 - end_type
567 - end_where
568 - go_to
569 - in_out
570 - select_case
571 - select_type
572 - implicit_none
573 - module_procedure
574 For example, use {'end_do':1} to write 'END DO' or
575 {'end_do':0} to write 'ENDDO' or
576 {'end_do':None} to not update the writting
577 or use adjacentKeywords=None to disable everything
578 :param afterKeywords: describes the number of spaces to introduce after keywords.
579 Some keywords need a more sophisticated treatment and are controled
580 by specific keys (e.g. CASE).
581 The keys are the keyword in lowercase, some names can be tricky
582 to guess (e.g. the key for ENDFILE is 'end-file'). By default
583 only a few are defined.
584 Use afterKeywords=None to disable everything.
585
586 To not update spaces, put None instead of an integer and False in booleans.
587 For example, to not change number of spaces after a comma, use afterComma=None
588
589 Updates are done in the following order:
590 """
591
592 adjaKeyDesc = {
593 'block_data': (1, './/{*}block-data-stmt'),
594 'double_precision': (1, './/{*}intrinsic-T-spec/{*}T-N'),
595 'else_if': (1, './/{*}else-if-stmt'),
596 'else_where': (0, './/{*}else-where-stmt'),
597 'end_associate': (1, './/{*}end-associate-stmt'),
598 'end_block': (1, './/{*}end-block-stmt'),
599 'end_block_data': (1, './/{*}end-block-data-stmt'),
600 'end_critical': (1, './/{*}end-critical-stmt'),
601 'end_do': (1, './/{*}end-do-stmt'),
602 'end_enum': (1, './/{*}end-enum-stmt'),
603 'end_file': (1, './/{*}end-file-stmt'),
604 'end_forall': (1, './/{*}end-forall-stmt'),
605 'end_function': (1, './/{*}end-function-stmt'),
606 'end_if': (1, './/{*}end-if-stmt'),
607 'end_interface': (1, './/{*}end-interface-stmt'),
608 'end_module': (1, './/{*}end-module-stmt'),
609 'end_procedure': (1, './/{*}end-procedure-stmt'),
610 'end_program': (1, './/{*}end-program-stmt'),
611 'end_selec': (1, './/{*}end-select-case-stmt'),
612 'end_select': (1, './/{*}end-select-T-stmt'),
613 'end_submodule': (1, './/{*}end-submodule-stmt'),
614 'end_subroutine': (1, './/{*}end-subroutine-stmt'),
615 'end_team': (1, './/{*}end-change-team-stmt'),
616 'end_type': (1, './/{*}end-T-stmt'),
617 'end_where': (1, './/{*}end-where-stmt'),
618 'go_to': (0, './/{*}goto-stmt'),
619 'in_out': (0, './/{*}intent-spec'),
620 'select_case': (1, './/{*}select-case-stmt'),
621 'select_type': (1, './/{*}select-T-stmt'),
622 'implicit_none': (1, './/{*}implicit-none-stmt'),
623 'module_procedure': (1, './/{*}procedure-stmt'),
624 }
625
626 afterKey = {
627 'print': 0,
628 'call': 1,
629 'use': 1,
630 'do': 1,
631 'end-file': 1,
632 'save': 1,
633 }
634
635 assert adjacentKeywords is None or adjacentKeywords == self.__NO_VALUE__ or \
636 all(k in adjaKeyDesc
637 for k in adjacentKeywords), "Unknown key in **adjacentKeywords"
638
639 def getvalAdja(key):
640 if adjacentKeywords is None:
641 return None
642 if adjacentKeywords == self.__NO_VALUE__:
643 return adjaKeyDesc[key][0]
644 return adjacentKeywords.get(key, adjaKeyDesc[key][0])
645
646 def getvalAfter(key):
647 key = key[:-5]
648 if afterKeywords != self.__NO_VALUE__:
649 num = afterKeywords.get(key, afterKey.get(key, None))
650 else:
651 num = afterKey.get(key, None)
652 return num
653
654 assert afterProgunit is None or afterProgunit >= 1
655 assert afterTypeDecl is None or afterTypeDecl >= 1
656 for k in ('implicit_none', 'module_procedure'):
657 num = getvalAdja(k)
658 assert num is None or num >= 1, \
659 "adjacentKeywords['" + k + "'] must be at least 1 (is " + str(num) + ")"
660 for k in ('use', 'call', 'end-file', 'do'):
661 num = getvalAfter(k + '-stmt')
662 assert num is None or num >= 1, \
663 "afterKeywords['" + k + "'] must be at least 1 (is " + str(num) + ")"
664
665 for elem in self.iter():
666 isNotC = tag(elem) != 'C'
667 # security
668 if elem.tail is None:
669 elem.tail = ""
670 elem.tail = elem.tail.replace('\t', ' ')
671
672 # Around parenthesis
673 if beforeParenthesis is not None:
674 elem.tail = re.sub(r"[  ]*\‍(", " " * beforeParenthesis + r"(", elem.tail)
675 elem.tail = re.sub(r"[  ]*\‍)", " " * beforeParenthesis + r")", elem.tail)
676 if elem.text is not None and isNotC:
677 elem.text = re.sub(r"[  ]*\‍(", " " * beforeParenthesis + r"(", elem.text)
678 elem.text = re.sub(r"[  ]*\‍)", " " * beforeParenthesis + r")", elem.text)
679 if afterParenthesis is not None:
680 elem.tail = re.sub(r"\‍([  ]*", "(" + " " * afterParenthesis, elem.tail)
681 elem.tail = re.sub(r"\‍)[  ]*", ")" + " " * afterParenthesis, elem.tail)
682 if elem.text is not None and isNotC:
683 elem.text = re.sub(r"\‍([  ]*", "(" + " " * afterParenthesis, elem.text)
684 elem.text = re.sub(r"\‍)[  ]*", ")" + " " * afterParenthesis, elem.text)
685
686 # Around commas
687 if beforeComma is not None:
688 elem.tail = re.sub(r"[  ]*,", " " * beforeComma + r",", elem.tail)
689 if elem.text is not None and isNotC:
690 elem.text = re.sub(r"[  ]*,", " " * beforeComma + r",", elem.text)
691 if afterComma is not None:
692 elem.tail = re.sub(r",[  ]*", "," + " " * afterComma, elem.tail)
693 if elem.text is not None and isNotC:
694 elem.text = re.sub(r",[  ]*", "," + " " * afterComma, elem.text)
695
696 # End of line
697 if endOfLine:
698 elem.tail = re.sub(r"[  ]*\n", r"\n", elem.tail)
699
700 # In names or around names (identifier, type, attribute)
701 if tag(elem) in ('N', 'T-N', 'attribute-N'):
702 if inName:
703 for nnn in elem.findall('{*}n'):
704 if nnn.tail is not None:
705 nnn.tail = nnn.tail.strip(' ')
706 if elem.tail is not None and afterName is not None:
707 elem.tail = ' ' * afterName + elem.tail.lstrip(' ')
708
709 # Around range delimiter
710 elif tag(elem) == 'lower-bound' and elem.tail is not None and ':' in elem.tail:
711 if beforeRangeDelim is not None:
712 elem.tail = ' ' * beforeRangeDelim + elem.tail.lstrip(' ')
713 if afterRangeDelim is not None:
714 elem.tail = elem.tail.rstrip(' ') + ' ' * beforeRangeDelim
715
716 # Around ':' in USE statements
717 elif tag(elem) == 'module-N' and elem.tail is not None and ':' in elem.tail:
718 if beforeUseDelim is not None:
719 elem.tail = re.sub(r"[  ]*:", " " * beforeUseDelim + r":", elem.tail)
720 if afterUseDelim is not None:
721 elem.tail = re.sub(r":[  ]*", ":" + " " * afterUseDelim, elem.tail)
722
723 # Around and in '::' in declaration statements
724 # After the type in a declaration
725 elif tag(elem) in ('attribute', '_T-spec_') and elem.tail is not None:
726 if inDeclDelim:
727 elem.tail = re.sub(r":[  ]*:", r"::", elem.tail)
728 if beforeDeclDelim is not None:
729 elem.tail = re.sub(r"[ ]*(:[  ]*:)", ' ' * beforeDeclDelim + r"\1", elem.tail)
730 if afterDeclDelim is not None:
731 elem.tail = re.sub(r"(:[  ]*:)[ ]*", r"\1" + ' ' * afterDeclDelim, elem.tail)
732 if tag(elem) == '_T-spec_' and afterTypeDecl is not None:
733 elem.tail = elem.tail.rstrip(' ') + ' ' * afterTypeDecl
734
735 # Around and in '::' in enumerators
736 # After the enumerator keyword
737 elif tag(elem) == 'enumerator-stmt' and elem.text is not None:
738 if ':' in elem.text:
739 if inDeclDelim:
740 elem.text = re.sub(r":[  ]*:", r"::", elem.text)
741 if beforeDeclDelim is not None:
742 elem.text = re.sub(r"[ ]*(:[  ]*:)", ' ' * beforeDeclDelim + r"\1",
743 elem.text)
744 if afterDeclDelim is not None:
745 elem.text = re.sub(r"(:[  ]*:)[ ]*", r"\1" + ' ' * afterDeclDelim,
746 elem.text)
747 elif afterTypeDecl is not None:
748 elem.text = elem.text.rstrip(' ') + ' ' * afterTypeDecl
749
750 # Between the program unit type and its name
751 elif (tag(elem) in ('subroutine-stmt', 'program-stmt', 'module-stmt', 'function-stmt',
752 'submodule-stmt', 'procedure-stmt', 'interface-stmt',
753 'end-subroutine-stmt', 'end-program-stmt',
754 'end-module-stmt', 'end-function-stmt',
755 'end-submodule-stmt', 'end-procedure-stmt', 'end-interface-stmt')
756 and afterProgunit is not None):
757 if elem.text is not None:
758 elem.text = elem.text.rstrip(' ') + ' ' * afterProgunit
759
760 # Around '=' sign in DO and FORALL statements
761 elif tag(elem) in ('do-V', 'V') and elem.tail is not None and '=' in elem.tail:
762 if beforeEqDo is not None:
763 elem.tail = re.sub('[ ]*=', ' ' * beforeEqDo + '=', elem.tail)
764 if afterEqDo is not None:
765 elem.tail = re.sub('=[ ]*', '=' + ' ' * beforeEqDo, elem.tail)
766
767 # Around '=' sign in CALL statements
768 elif tag(elem) == 'arg-N' and elem.tail is not None and '=' in elem.tail:
769 if beforeEqCall is not None:
770 elem.tail = re.sub('[ ]*=', ' ' * beforeEqCall + '=', elem.tail)
771 if afterEqCall is not None:
772 elem.tail = re.sub('=[ ]*', '=' + ' ' * beforeEqCall, elem.tail)
773
774 # Around '=' sign for init values
775 elif (tag(elem) in ('EN-N', 'named-constant') and
776 elem.tail is not None and '=' in elem.tail):
777 if beforeEqInit is not None:
778 elem.tail = re.sub('[ ]*=', ' ' * beforeEqInit + '=', elem.tail)
779 if afterEqInit is not None:
780 elem.tail = re.sub('=[ ]*', '=' + ' ' * beforeEqInit, elem.tail)
781 # Around the command separator ';'
782 elif tag(elem) == 'smc':
783 if beforeCmdsep is not None:
784 prev = self.getSiblings(elem, after=False)
785 if len(prev) != 0 and prev[-1].tail is not None:
786 prev[-1].tail = ' ' * beforeCmdsep + prev[-1].tail.lstrip(' ')
787 if afterCmdsep is not None and elem.tail is not None:
788 elem.tail = elem.tail.rstrip(' ') + ' ' * afterCmdsep
789
790 # Around and in association operators (affectation case done after)
791 elif tag(elem) == 'associate-N' and elem.tail is not None and '=' in elem.tail:
792 if beforeAffectation is not None:
793 elem.tail = re.sub('[ ]*=', ' ' * beforeAffectation + '=', elem.tail)
794 if afterAffectation is not None:
795 elem.tail = re.sub('>[ ]*', '>' + ' ' * beforeAffectation, elem.tail)
796 if inAffectation:
797 elem.tail = re.sub(r'=[ ]*>', '=>', elem.tail)
798
799 # After a reserved keyword
800 # elif afterKeywords is not None and tag(elem).endswith('-stmt'):
801 # num = getvalAfter(tag(elem))
802 # if num is not None and elem.text is not None:
803 # elem.text = elem.text.rstrip(' ') + ' ' * num
804
805 # Another loop on elements
806 # All the transformations are not put in a single loop because the following one act
807 # on sub-elements. Putting them all in the same loop would prevent to control in which order
808 # the different transformations occur.
809 # For instance, the suppression on the space after the parenthesis must be done before
810 # the adding of a space before a THEN keyword
811 for elem in self.iter():
812 # Around and in operators
813 if tag(elem) == 'op-E': # op are always (?) in op-E nodes
814 for op in elem.findall('{*}op'):
815 if beforeOp is not None:
816 io = list(elem).index(op)
817 if io != 0:
818 prev = elem[io - 1]
819 if prev.tail is None:
820 prev.tail = ' ' * beforeOp
821 else:
822 prev.tail = prev.tail.rstrip(' ') + ' ' * beforeOp
823 if afterOp is not None:
824 if op.tail is None:
825 op.tail = ' ' * afterOp
826 else:
827 op.tail = op.tail.lstrip(' ') + ' ' * afterOp
828 if inOperator:
829 for oo in op.findall('{*}o'):
830 if oo.tail is not None:
831 oo.tail = oo.tail.strip(' ')
832
833 # Around and in affectation operators (association case done before)
834 elif tag(elem) in ('a-stmt', 'pointer-a-stmt'):
835 # a are always (?) in a-stmt or pointer-a-stmt nodes
836 for aff in elem.findall('{*}a'):
837 if beforeAffectation is not None:
838 prev = elem[list(elem).index(aff) - 1]
839 if prev.tail is None:
840 prev.tail = ' ' * beforeAffectation
841 else:
842 prev.tail = prev.tail.rstrip(' ') + ' ' * beforeAffectation
843 if afterAffectation is not None:
844 if aff.tail is None:
845 aff.tail = ' ' * afterAffectation
846 else:
847 aff.tail = aff.tail.lstrip(' ') + ' ' * afterAffectation
848 if inAffectation:
849 aff.text = aff.text.replace(' ', '')
850
851 # After a IF, WHERE, ELSEIF, ELSEWHERE, SELECTCASE, CASE and FORALL keyword,
852 # and before THEN keyword
853 elif tag(elem) in ('if-stmt', 'if-then-stmt', 'else-if-stmt',
854 'where-stmt', 'where-construct-stmt', 'else-where-stmt',
855 'select-case-stmt', 'case-stmt',
856 'forall-stmt', 'forall-construct-stmt'):
857 if afterIfwherecase is not None and elem.text is not None:
858 if tag(elem) == 'case-stmt':
859 # the (eventual) parenthesis is not in the text of the node
860 elem.text = elem.text.rstrip(' ') + ' ' * afterIfwherecase
861 else:
862 elem.text = re.sub(r'[ ]*\‍(', ' ' * afterIfwherecase + '(',
863 elem.text, count=1)
864 if tag(elem) in ('if-then-stmt', 'else-if-stmt') and beforeThen is not None:
865 cond = elem.find('{*}condition-E')
866 cond.tail = re.sub(r'\‍)[ ]*([a-zA-Z]*$)', ')' + ' ' * beforeThen + r'\1',
867 cond.tail)
868 elif tag(elem) == 'if-stmt' and beforeIfaction is not None:
869 cond = elem.find('{*}condition-E')
870 cond.tail = re.sub(r'\‍)[ ]*$', ')' + ' ' * beforeIfaction, cond.tail)
871 elif tag(elem) == 'where-stmt' and beforeIfaction is not None:
872 cond = elem.find('{*}mask-E')
873 cond.tail = re.sub(r'\‍)[ ]*$', ')' + ' ' * beforeIfaction, cond.tail)
874 elif tag(elem) == 'forall-stmt' and beforeIfaction is not None:
875 sub = elem.find('{*}forall-triplet-spec-LT')
876 sub.tail = re.sub(r'\‍)[ ]*$', ')' + ' ' * beforeIfaction, sub.tail)
877
878 # Direct search to prevent using the costly getParent function
879 if beforeEndcnt is not None or afterBegincnt is not None:
880 for elem in self.findall('.//{*}cnt/..'): # node containing continuation characters
881 for cnt in elem.findall('{*}cnt'): # continuation characters
882 ic = list(elem).index(cnt)
883 if ic == 0:
884 # the string before the continuation character is in the parent text
885 prev = elem
886 pstring = prev.text
887 else:
888 # the string before the continuation character is in previsous sibling tail
889 prev = elem[ic - 1]
890 pstring = prev.tail
891 if '\n' in pstring and '\n' in cnt.tail:
892 # continuation character alone on a line
893 pass
894 elif '\n' in pstring and afterBegincnt is not None:
895 # continuation character at the begining of a line
896 cnt.tail = ' ' * afterBegincnt + cnt.tail.lstrip(' ')
897 elif beforeEndcnt is not None:
898 # continuation character at the end of a line
899 # (eventually followed by a comment)
900 if prev == elem:
901 prev.text = prev.text.rstrip(' ') + ' ' * beforeEndcnt
902 else:
903 prev.tail = prev.tail.rstrip(' ') + ' ' * beforeEndcnt
904
905 # In adjacent keywords
906 for key, val in adjaKeyDesc.items():
907 num = getvalAdja(key)
908 if num is not None:
909 for node in self.findall(val[1]):
910 lf = "[ ]*".join(["(" + p + ")" for p in key.split('_')])
911 repl = (" " * num).join([r"\{i}".format(i=i + 1)
912 for i, _ in enumerate(key.split('_'))])
913 node.text = re.sub(lf, repl, node.text, flags=re.IGNORECASE)
914
915 @debugDecor
916 def changeIfStatementsInIfConstructs(self, singleItem=None):
917 """
918 Convert one-line IF statements to IF-THEN-ENDIF blocks.
919
920 Parameters
921 ----------
922 singleItem : Element, optional
923 Specific IF statement node to convert. If None, converts all.
924
925 Transformation
926 -------------
927 Before:
928 IF(A=B) print*, "C"
929
930 After:
931 IF(A=B) THEN
932 print*, "C"
933 END IF
934
935 Examples
936 --------
937 >>> pft = PYFT('input.F90')
938 >>> pft.changeIfStatementsInIfConstructs()
939 """
940 if singleItem is not None:
941 ifstmt = [singleItem]
942 else:
943 ifstmt = self.findall('.//{*}if-stmt')
944 for item in ifstmt:
945 cycleStmt = item.findall('.//{*}cycle-stmt')
946 if len(cycleStmt) == 0:
947 # Get indentation from last sibling
948 par = self.getParent(item)
949 ind = par[:].index(item)
950 if ind != 0 and par[ind - 1].tail is not None:
951 # if tail of previous sibling exists
952 currIndent = len(par[ind - 1].tail) - len(par[ind - 1].tail.rstrip(' '))
953 else:
954 # no tail = no indentation
955 currIndent = 0
956
957 # Convert if-stmt into if-construct
958 # <if-stmt>IF(<condition-E>...</condition-E>) <f:action-stmt>...
959 # ...</f:action-stmt></f:if-stmt>
960 # <if-construct><if-block><if-then-stmt>IF(<f:condition-E>...
961 # ...</condition-E>) THEN</f:if-then-stmt>
962 # ...
963 # <f:end-if-stmt>ENDIF</f:end-if-stmt></f:if-block></f:if-construct>
964 # 1 create missing blocks
965 item.tag = f'{{{NAMESPACE}}}if-construct'
966 ifBlock = createElem('if-block')
967 ifThenStmt = createElem('if-then-stmt')
968 endif = createElem('end-if-stmt')
969 ifBlock.append(ifThenStmt)
970 item.append(ifBlock)
971 # 2 move 'IF(' text
972 ifThenStmt.text = item.text # copy 'IF(' text
973 ifThenStmt.tail = '\n' + (2 + currIndent) * ' ' # indentation for main statement
974 item.text = None # remove olf 'IF(' text
975 # 3 move condition and add THEN
976 condition = item.find('{*}condition-E')
977 if not condition.tail.endswith(' '):
978 condition.tail += ' '
979 condition.tail += 'THEN'
980 ifThenStmt.append(condition)
981 item.remove(condition)
982 # 4 move action
983 action = item.find('{*}action-stmt')
984 action[0].tail = '\n' + currIndent * ' ' # indentation for the ENDIF
985 ifBlock.append(action[0])
986 item.remove(action)
987 # 5 add ENDIF
988 endif.text = 'END IF'
989 ifBlock.append(endif)
990 # 6 remove any cnt which was directly in the if-stmt node
991 # (replaced by '\n' after THEN)
992 for cnt in item.findall('./{*}cnt'):
993 item.remove(cnt)
994
995 @debugDecor
997 """
998 Remove the CONTAINS statement if this section is empty
999 """
1000 for contains in self.findall('.//{*}contains-stmt'):
1001 par = self.getParent(contains)
1002 index = list(par).index(contains)
1003 nextStmt = index + 1
1004 while tag(par[nextStmt]) == 'C':
1005 nextStmt += 1
1006 if tag(par[nextStmt]) in ('end-subroutine-stmt', 'end-function-stmt',
1007 'end-module-stmt'):
1008 # CONTAINS bloc is empty
1009 par.remove(contains)
updateContinuation(self, nodeToUpdate=None, align=True, removeALL=False, addBegin=True, removeBegin=False)
Definition cosmetics.py:280
removeComments(self, exclDirectives=None, pattern=None)
Definition cosmetics.py:220
updateSpaces(self, beforeOp=1, afterOp=1, inOperator=True, beforeComma=0, afterComma=1, beforeParenthesis=0, afterParenthesis=0, beforeAffectation=1, afterAffectation=1, inAffectation=True, beforeRangeDelim=0, afterRangeDelim=0, beforeUseDelim=0, afterUseDelim=1, beforeDeclDelim=1, afterDeclDelim=1, inDeclDelim=True, afterTypeDecl=1, beforeEqDo=0, afterEqDo=0, beforeEqCall=0, afterEqCall=0, beforeEqInit=0, afterEqInit=0, beforeEndcnt=1, afterBegincnt=1, afterIfwherecase=1, beforeThen=1, beforeIfaction=1, afterProgunit=1, endOfLine=True, afterName=0, inName=True, beforeCmdsep=0, afterCmdsep=1, adjacentKeywords=__NO_VALUE__, afterKeywords=__NO_VALUE__)
Definition cosmetics.py:499
indent(self, nodeToUpdate=None, indentProgramunit=0, indentBranch=2, exclDirectives=None)
Definition cosmetics.py:75
changeIfStatementsInIfConstructs(self, singleItem=None)
Definition cosmetics.py:916