import AutoRPE.UtilsRPE.VariablePrecision as VariablePrecision
import AutoRPE.UtilsRPE.RegexPattern as RegexPattern
import AutoRPE.UtilsRPE.MaskCreator as MaskCreator
import AutoRPE.UtilsRPE.Error as Error
import AutoRPE.UtilsRPE.SourceManager as SourceManager
import AutoRPE.UtilsRPE.Classes.Module as Module
import re
# def is_in_reals_blacklist(blacklist, line, module, dt=''):
# for v in blacklist:
# if module == v['module'] and \
# dt.lower() == v['derived_type'].lower() and \
# re.search(RegexPattern.real_variable_declaration_name % v['var_name'], line, re.IGNORECASE):
# return True
# return False
[docs]
def load_source_file(_filename: str, extension: str, no_strip: bool=False):
"""
Loads a Fortran source file and creates a `SourceFile` object.
Parameters:
_filename (str): The name of the source file to load.
extension (str): The file extension (e.g., ".f90").
no_strip (bool): If True, preserves extra whitespace and comments while reading the file.
Returns:
SourceFile: An object representing the source file, with its content loaded.
"""
import AutoRPE.UtilsRPE.Classes.SourceFile as SourceFile
# Create a SourceFile object
file = SourceFile.SourceFile(_filename, extension)
# Load the file text into the object
file.read_file(_filename, no_strip)
return file
[docs]
def load_sources(input_path: str, extension: list[str]=[".f90"]):
"""
Recursively loads Fortran source files from the specified input path(s).
Parameters:
input_path (str | list[str]): A single directory path or a list of paths to scan for source files.
extension (list[str]): A list of allowed file extensions to load (default is [".f90"]).
Returns:
list: A list of `SourceFile` objects representing the loaded files.
Raises:
ValueError: If no files with the specified extensions are found in the input path.
"""
from warnings import warn
from os.path import join, abspath, splitext
from os import walk
file_list = []
# If the passed argument is not a list, convert to avoid splitting a string
if not isinstance(input_path, list):
input_path = [input_path]
# Reads files for top folder, and searches recursively in sub-folders
for path in input_path:
for dir_path, dir_name, file_name in walk(path):
for f_name in file_name:
# Include file with specified extensions and exclude hidden files (like .swp files)
file_extension = splitext(f_name)[1]
if file_extension in extension and f_name[0][0] != ".":
file_list.append(load_source_file(join(dir_path, f_name), file_extension, no_strip=True))
if not len(file_list):
raise ValueError(f"No files found in {abspath(input_path[0])} folder")
# If h90 extension was specified then append included files to including modules
if ".h90" in extension:
for file in file_list:
header_files = [l.lower().split("include")[1] for l in file.lines if
# remove comments and literals and search for the # include path
re.search(RegexPattern.name_occurrence % "include", remove_comments(MaskCreator.remove_literals(l)), re.I)]
for h_90 in header_files:
h_90 = h_90.replace("\"", "").strip()
h_90 = h_90.replace("\'", "").strip()
try:
h_90 = [f for f in file_list if f.filename.lower() == h_90.lower()][0]
except IndexError:
warn("Header file %s not found among input files" % h_90)
continue
file.header_files.append(h_90)
file.preprocess_header_file()
return file_list
[docs]
def load_intrinsics():
"""
Loads a dictionary of Fortran intrinsic functions and their corresponding types.
Returns:
dict: A dictionary where keys are intrinsic function names (lowercase) and values are their types.
"""
from os.path import abspath, dirname
package_directory = dirname(abspath(__file__))
# Hardcoded path to file.
with open(package_directory + "/../AdditionalFiles/fortran_intrinsics") as f:
intrinsics = dict((intrinsic.split(',')[0].lower(), intrinsic.split(',')[1]) for intrinsic in f)
return intrinsics
[docs]
def remove_unary_operator(string: str):
"""
Removes unary operators (e.g., `+` or `-`) from a string, if present.
Parameters:
string (str): The input string.
Returns:
str: The string with unary operators removed.
"""
import re
return re.sub(RegexPattern.unary_operator, "", string)
[docs]
def close_brackets(string: str):
"""
Trims a string to include only the portion up to the closing bracket of a function or array call.
Parameters:
string (str): The input string.
Returns:
str: The trimmed string ending at the first matched closing bracket.
Raises:
ExceptionNotManaged: If the brackets are unbalanced in the input string.
"""
# Check is a call with arguments
first_parenthesis = re.search(r'\(', string)
if not first_parenthesis:
return string
first_parenthesis = first_parenthesis.start()
# Index that mark if we are inside an array
bracket_balance = -1
# Start counting just after first bracket
for char_index, c in enumerate(string[first_parenthesis + 1:]):
if bracket_balance == 0:
string = string[:char_index + first_parenthesis+1]
break
elif c in ["(", "["]:
bracket_balance -= 1
elif c in [")", "]"]:
bracket_balance += 1
else:
continue
if bracket_balance != 0:
raise Error.ExceptionNotManaged("This should not happen, check arguments %s" % string)
return string
[docs]
def combine_types(list_of_types: list[str]):
"""
Combines a list of variable types into a single resulting type, based on a predefined dictionary.
Parameters:
list_of_types (list): A list of variable types to combine.
Returns:
str: The resulting combined type.
Raises:
ExceptionNotManaged: If the type combination is not defined in the dictionary.
"""
ret_val = list_of_types.pop()
while list_of_types:
next_type = list_of_types.pop()
try:
ret_val = VariablePrecision.dictionary_of_possible_combination[frozenset([ret_val, next_type])]
except KeyError:
raise Error.ExceptionNotManaged("Please add the combination" +
"[" + str(ret_val) + "," + str(next_type) + "]" +
" to dictionary_of_possible_combination ")
return ret_val
[docs]
def create_type_combinations(case_types: list):
"""
Generates all possible combinations of casting real types for a list of variable types.
Parameters:
case_types (list): A list of variable types.
Returns:
list: A list of all possible variations of the input variable types.
Raises:
AssertionError: If the number of arguments exceeds 10, which can cause combinatorial issues.
"""
# TODO: In some cases a big number of arguments can make this approach crash. Here we add a security check.
if len(case_types) > 10:
raise AssertionError("Too much arguments to use a combinatorial approach.")
import itertools
import itertools
syn_types = ["real" if x in VariablePrecision.real_id else x for x in case_types]
number_of_reals = syn_types.count("real")
a = ["sp", "dp"] * number_of_reals
combinations = list(set(itertools.permutations(a, number_of_reals)))
new_types = []
for comb in combinations:
comb_types = syn_types[:]
for i in range(number_of_reals):
comb_types[comb_types.index("real")] = comb[i]
new_types.append(comb_types)
return new_types
[docs]
def hash_from_list(string_list: list[str]):
"""
Computes a hash value from a list of strings.
Parameters:
string_list (list[str]): A list of strings.
Returns:
str: The MD5 hash value of the concatenated strings.
Raises:
ExceptionNotManaged: If the input is not a list of strings.
"""
import hashlib
if not isinstance(string_list, list):
raise Error.ExceptionNotManaged("This input should be a string list")
string_to_hash = ""
for i_str in string_list:
string_to_hash += i_str
hash_object = hashlib.md5(string_to_hash.encode())
return hash_object.hexdigest()
[docs]
def find_next_end(lines: list[str], _index: int, end_statement: str):
"""
Finds the next occurrence of an end statement in a block of lines.
Parameters:
lines (list[str]): A list of source code lines.
_index (int): The starting index to search from.
end_statement (str): The end statement to search for (e.g., "end subroutine").
Returns:
int: The index of the next line containing the end statement.
"""
_i = _index + 1
begin_statement = end_statement.replace('end', '')
nested_declaration = 0
while _i < len(lines):
_l = lines[_i]
if re.match(r'^\s*(' + begin_statement + ')', _l, re.I):
nested_declaration += 1
if _l.lower().count(end_statement):
if nested_declaration == 0:
return _i
else:
nested_declaration -= 1
_i += 1
return len(lines)
[docs]
def contain_literals(string: str, string_dictionary: dict=None):
"""
Determines if a string contains literals (e.g., quotes, concatenation symbols, or masks).
Parameters:
string (str): The input string to check.
string_dictionary (dict, optional): A dictionary of masked strings for additional checks.
Returns:
bool: True if the string contains literals or matches a masked string.
"""
if string_dictionary is None:
for i in ["'", "//", '"']:
if string.lower().count(i):
return True
else:
# Check if the string is a masked string
if string.count('@') or string in string_dictionary.keys():
return True
if re.search(RegexPattern.name_occurrence % "trim", string, re.I):
return True
return False
[docs]
def is_all_inside_parenthesis(string: str):
"""
Checks if the entire string is enclosed in parentheses.
Parameters:
string (str): The input string to check.
Returns:
bool: True if the string is fully enclosed in parentheses, False otherwise.
"""
if string.strip()[0] == "(":
try:
if "c%s" % string.strip() == close_brackets("c%s" % string.strip()):
return True
else:
return False
except ValueError:
return False
else:
return False
[docs]
def remove_if_condition(_line: str, return_if_condition: bool=False):
"""
Removes an `if` condition from a line, preserving the remaining content.
Parameters:
_line (str): The input line.
return_if_condition (bool): If True, also returns the removed condition.
Returns:
str | tuple[str, str]: The line without the `if` condition, and optionally the removed condition.
"""
if _line.replace(" ", "").lower().strip().find("if(") == 0:
condition = close_brackets(_line)
_line = _line.replace(condition, "")
else:
condition = ""
if not return_if_condition:
return _line
else:
return [condition, _line]
[docs]
def remove_else_condition(line: str):
"""
Removes an `else` condition from a line, preserving the remaining content.
Parameters:
line (str): The input line.
Returns:
str: The line without the `else` condition.
"""
import re
if line.replace(" ", "").lower().strip().find("else") == 0:
# This way we will also transform elseif condition in simple if
line = re.sub(r"^\s*else\s*", "", line, flags=re.I)
return line
[docs]
def clean_if_else_condition(line: str, function_name: str):
"""
Removes conditions from an `if` or `else` statement, returning the function call if present.
Parameters:
line (str): The input line of code.
function_name (str): The name of the function to search for.
Returns:
str: The function call if found, otherwise the remaining code.
"""
line = remove_else_condition(line)
condition, call = split_if_condition(line)
function_call = re.search(RegexPattern.name_occurrence % function_name, condition)
if function_call:
return close_brackets(condition[function_call.start():])
else:
return call
[docs]
def remove_keywords(arguments: list[str]):
"""
Removes keywords from a list of argument strings, returning only the values.
Parameters:
arguments (list[str]): A list of arguments, potentially containing keywords.
Returns:
list[str]: The list of arguments with keywords removed.
"""
patt = "[a-zA-Z_0-9]* *=(.*)"
for index, argument in enumerate(arguments):
m = re.match(patt, argument.strip())
if m:
arguments[index] = m.group(1).strip()
return arguments
[docs]
def remove_hardcoded_array_assign(string: str):
"""
Removes hardcoded array assignments from a string.
Parameters:
string (str): The input string containing the assignment.
Returns:
str: The string without hardcoded array assignments.
"""
hardcoded_array_assign = r'='+RegexPattern.hardcoded_array
m = re.search(hardcoded_array_assign, string)
if m:
string = string[:m.start()]
return string
[docs]
def split_if_condition(_line: str):
"""
Splits a line into its `if` condition and the remaining content.
Parameters:
_line (str): The input line.
Returns:
tuple[str, str]: The `if` condition and the remaining content.
"""
return remove_if_condition(_line, return_if_condition=True)
[docs]
def remove_outer_brackets(string: str):
"""
Removes the outermost parentheses from a string, if present.
Parameters:
string (str): The input string.
Returns:
str: The string without its outermost parentheses.
"""
string = string.strip()
if is_all_inside_parenthesis(string):
return string[1:-1].strip()
else:
return string
[docs]
def remove_spaces(string: str):
"""
Removes extra spaces from a string.
Parameters:
string (str): The input string.
Returns:
str: The string with multiple spaces reduced to a single space.
"""
return re.sub('\\s\\s+', r' ', string)
[docs]
def count_initial_spaces(string: str):
"""
Counts the number of leading spaces in a string.
Parameters:
string (str): The input string.
Returns:
int: The number of leading spaces.
"""
m = re.search(r'\w', string, re.I)
if m:
return m.start()
return 0
[docs]
def remove_indexing(content: str):
"""
Removes indexing (e.g., array brackets) from a variable or expression.
Parameters:
content (str): The input content string.
Returns:
str: The string without indexing or array brackets.
"""
import AutoRPE.UtilsRPE.NatureDeterminer as NatureDeterminer
# If there are not brackets, there is no indexing
if not content.count("("):
# Case of a recursive call
content = content.replace("XXX", "%")
return content
# If the variable is already a simple name
if re.match(RegexPattern.variable, content):
# Case of a recursive call
content = content.replace("XXX", "%")
return content
# If is a simple array with indexing
if NatureDeterminer.is_array(content):
# Case of a recursive call
return content[:content.index("(")].replace("XXX", "%")
# Avoid problem with structure used as index
content = content.replace("%", "XXX")
# Used to avoid recursion error
clean_content = content
# Replace hardcoded array
hard_code_array = re.search(RegexPattern.hardcoded_array, clean_content)
if hard_code_array:
clean_content = clean_content.replace(hard_code_array.group(), "")
# Remove possible container of hardcoded array
clean_content = clean_content.replace("()", "")
match_array_brackets = re.findall(RegexPattern.match_array_brackets, clean_content)
for brackets in match_array_brackets:
clean_content = clean_content.replace(brackets, "")
# Recursively remove brackets
if not re.search(RegexPattern.variable, clean_content) and not re.search(RegexPattern.structure, clean_content):
# This check avoid infinite recursion, if the function has not changed anything returns
if clean_content != content:
return remove_indexing(clean_content)
# Reinsert structure markers
clean_content = clean_content.replace("XXX", "%")
return clean_content
[docs]
def merge_line_range(lines: list[str], line_index: int):
"""
Merges a multi-line statement into a single line by resolving line continuations.
Parameters:
lines (list[str]): The list of lines in the source file.
line_index (int): The starting index of the line to merge.
Returns:
str: The merged line with all continuations resolved.
"""
n_of_spaces = count_initial_spaces(lines[line_index])
line = remove_comments(lines[line_index]).strip()
merge_line_range.number = 1
if line.count("&"):
while line and line.strip()[-1] == "&":
line1 = line
while remove_comments(lines[line_index + merge_line_range.number]).strip() == "":
merge_line_range.number = merge_line_range.number + 1
line2 = remove_comments(lines[line_index + merge_line_range.number])
line = line1.replace("&", "").rstrip() + line2
merge_line_range.number += 1
line = line.replace("&", "")
# To split a print statement extra char are needed: remove them and join it into a single string
line = re.sub(r'\"\s*//\s*\"', " ", line)
line = re.sub(", +", ", ", line)
line = re.sub(": +", ": ", line)
line = line.strip() + "\n"
return " " * n_of_spaces + line
[docs]
def merge_line_in_file(line_index: int, module: Module):
"""
Merges a split statement into a single line within a source file. Removes empty lines.
Parameters:
line_index (int): The index of the line to merge.
module: The module object containing the lines of source code.
"""
line = merge_line_range(module.lines, line_index)
module.lines[line_index] = line
# Nullify lines that have been merged
for j in range(1, merge_line_range.number):
module.lines[line_index + j] = ""
[docs]
def split_comparison(content: str) -> tuple[str, str]:
"""
Splits a string based on the first occurrence of a comparison operator.
Parameters:
content (str): The input string containing a comparison.
Returns:
tuple[str, str]: A tuple with the left-hand side and right-hand side of the comparison.
"""
# Iterate through the string to find the first occurrence of any operator
for operator in SourceManager.comparison_operators:
if operator in content:
# Split the string at the first occurrence of the operator
parts = content.split(operator, 1)
# Return the two parts as a tuple, stripping any leading or trailing whitespace
return parts[0].strip(), parts[1].strip()
# If no operator is found, return the original string as the first element and an empty string as the second element
return content, ""