diff --git a/lint_rules/lint_rules/ifs_arpege_coding_standards.py b/lint_rules/lint_rules/ifs_arpege_coding_standards.py index 8257d4b38..cfe93b9c5 100644 --- a/lint_rules/lint_rules/ifs_arpege_coding_standards.py +++ b/lint_rules/lint_rules/ifs_arpege_coding_standards.py @@ -14,6 +14,7 @@ from collections import defaultdict import re +import difflib try: from fparser.two.Fortran2003 import Intrinsic_Name @@ -22,16 +23,235 @@ _intrinsic_fortran_names = () from loki import ( - FindInlineCalls, FindNodes, GenericRule, Module, RuleType + FindInlineCalls, FindNodes, GenericRule, Module, RuleType, + ExpressionFinder, ExpressionRetriever, FloatLiteral, + SubstituteExpressions ) -from loki import ir - +from loki import ir, fgen +from loki.frontend.util import read_file __all__ = [ 'MissingImplicitNoneRule', 'OnlyParameterGlobalVarRule', 'MissingIntfbRule', + 'MissingKindSpecifierRealLiterals' ] +class FindFloatLiterals(ExpressionFinder): + """ + A visitor to collects :any:`FloatLiteral` used in an IR tree. + + See :class:`ExpressionFinder` + """ + retriever = ExpressionRetriever(lambda e: isinstance(e, (FloatLiteral,))) + +class MissingKindSpecifierRealLiterals(GenericRule): + """ + ... + """ + + type = RuleType.SERIOUS + fixable = True + + docs = { + 'id': 'L0', + 'title': ( + 'Real Literals must have a kind specifier. ' + ), + } + + + @classmethod + def check_subroutine(cls, subroutine, rule_report, config, **kwargs): + """ + ... + """ + literal_nodes = FindFloatLiterals(with_ir_node=True).visit(subroutine.body) + for node, literals in literal_nodes: + for literal in literals: + if literal.kind is None: + rule_report.add(f'Real/Float literal without kind specifier "{literal}"', node) + + @classmethod + def fix_subroutinei_test_2(cls, subroutine, rule_report, config, sourcefile=None): + """ + ... + """ + for node, literals in literal_nodes: + literal_map = {} + for literal in literals: + if literal.kind is None and 'e' not in literal.value.lower() and 'd' not in literal.value.lower(): + literal_map[literal] = FloatLiteral(value=literal.value, kind='JPRB') + if literal_map: + # fixed_node = SubstituteExpressions(literal_map).visit(node) + # for key in literal_map: + # fixed_node = re.sub(rf'{re.escape()}', + # , content, flags = re.S) + indent = int((len(node.source.string) - len(node.source.string.lstrip(' ')))/2) + fixed_node_str = fgen(fixed_node, depth=indent) + with open (f'loki_lint_{subroutine.name}_new_file_fixed_node_str.F90', 'w') as f: + f.write(fixed_node_str) + content_new = re.sub(rf'{re.escape(node.source.string)}', + fixed_node_str, content, flags = re.S) + content = content_new + with open (f'loki_lint_{subroutine.name}_new_file.F90', 'w') as f: + f.write(content_new) + diff = difflib.unified_diff(original_content.splitlines(), content_new.splitlines(), + f'a/{sourcefile.path}', f'b/{sourcefile.path}', lineterm='') + diff_str = '\n'.join(list(diff)) + # print(f"---{sourcefile.path}------") + # print(diff_str) + # print(f"--------------------------") + with open (f'loki_lint_{subroutine.name}.approach_2.patch', 'w') as f: + f.write(diff_str) + f.write('\n') + + @classmethod + def fix_subroutine_test(cls, subroutine, rule_report, config, sourcefile=None): + """ + ... + """ + # sourcefile = subroutine.source.file + print(f"fix_subroutine: subroutine: {subroutine} | subroutine.source: {subroutine.source} | subroutine.source.file: {subroutine.source.file}") + original_content = read_file(str(sourcefile.path)) + content = original_content + literals = FindFloatLiterals(with_ir_node=False).visit(subroutine.body) + literal_map = {} + for literal in literals: + if literal.kind is None: + literal_map[literal] = FloatLiteral(value=literal.value, kind='JPRB') + # content_new = content + if literal_map: + for key in literal_map: + # print(f"replace ") + # content_new = re.sub(rf'{re.escape(str(key))}', rf'{re.escape(str(literal_map[key]))}', content, flags = re.S) + content_new = re.sub(rf'{re.escape(str(key))}', str(literal_map[key]), content, flags = re.S) + content = content_new + diff = difflib.unified_diff(original_content.splitlines(), content_new.splitlines(), + f'a/{sourcefile.path}', f'b/{sourcefile.path}', lineterm='') + diff_str = '\n'.join(list(diff)) + # print(f"---{sourcefile.path}------") + # print(diff_str) + # print(f"--------------------------") + with open (f'loki_lint_{subroutine.name}.approach_2.patch', 'w') as f: + f.write(diff_str) + f.write('\n') + + """ + for node, literals in literal_nodes: + literal_map = {} + for literal in literals: + if literal.kind is None: + literal_map[literal] = FloatLiteral(value=literal.value, kind='JPRB') + if literal_map: + # fixed_node = SubstituteExpressions(literal_map).visit(node) + # indent = int((len(node.source.string) - len(node.source.string.lstrip(' ')))/2) + # fixed_node_str = fgen(fixed_node, depth=indent) + # with open (f'loki_lint_{subroutine.name}_new_file_fixed_node_str.F90', 'w') as f: + #  f.write(fixed_node_str) + # content_new = re.sub(rf'{re.escape(node.source.string)}', + # fixed_node_str, content, flags = re.S) + # content = content_new + with open (f'loki_lint_{subroutine.name}_new_file.F90', 'w') as f: + f.write(content_new) + diff = difflib.unified_diff(original_content.splitlines(), content_new.splitlines(), + f'a/{sourcefile.path}', f'b/{sourcefile.path}', lineterm='') + diff_str = '\n'.join(list(diff)) + # print(f"---{sourcefile.path}------") + # print(diff_str) + # print(f"--------------------------") + with open (f'loki_lint_{subroutine.name}.approach_2.patch', 'w') as f: + f.write(diff_str) + f.write('\n') + """ + + @classmethod + def fix_subroutine(cls, subroutine, rule_report, config, sourcefile=None): + """ + ... + """ + # sourcefile = subroutine.source.file + print(f"fix_subroutine: subroutine: {subroutine} | subroutine.source: {subroutine.source} | subroutine.source.file: {subroutine.source.file}") + original_content = read_file(str(sourcefile.path)) + content = original_content + literal_nodes = FindFloatLiterals(with_ir_node=True).visit(subroutine.body) + content_new = None + for node, literals in literal_nodes: + # print(f"node.source: {node.source.string} | {type(node.source.string)}") + literal_map = {} + for literal in literals: + if literal.kind is None and 'e' not in str(literal.value).lower() and 'd' not in str(literal.value).lower(): + literal_map[literal] = FloatLiteral(value=literal.value, kind='JPRB') + if literal_map: + # fixed_node = SubstituteExpressions(literal_map).visit(node) + fixed_node = node.source.string + # if hasattr(node, 'comment') and node.comment is not None: + # comment = node.comment + # fixed_node._update(comment=None) + # else: + # comment = None + for key in literal_map: + fixed_node = re.sub(rf'{re.escape(str(key))}', + str(literal_map[key]), fixed_node, flags = re.S) + # indent = int((len(node.source.string) - len(node.source.string.lstrip(' ')))/2) + # fixed_node_str = fgen(fixed_node, depth=indent) + # if comment is not None: + # fixed_node._update(comment=comment) + fixed_node_str = str(fixed_node) + # with open (f'loki_lint_{subroutine.name}_new_file_fixed_node_str.F90', 'w') as f: + # f.write(fixed_node_str) + content_new = re.sub(rf'{re.escape(node.source.string)}', + fixed_node_str, content, flags = re.S) + content = content_new + # with open (f'loki_lint_{subroutine.name}_new_file.F90', 'w') as f: + # f.write(content_new) + if content_new is not None: + diff = difflib.unified_diff(original_content.splitlines(), content_new.splitlines(), + f'a/{sourcefile.path}', f'b/{sourcefile.path}', lineterm='') + diff_str = '\n'.join(list(diff)) + # print(f"---{sourcefile.path}------") + # print(diff_str) + # print(f"--------------------------") + with open (f'loki_lint_{subroutine.name}.patch', 'w') as f: + f.write(diff_str) + f.write('\n') + + @classmethod + def fix_subroutine_working(cls, subroutine, rule_report, config, sourcefile=None): + """ + ... + """ + # sourcefile = subroutine.source.file + print(f"fix_subroutine: subroutine: {subroutine} | subroutine.source: {subroutine.source} | subroutine.source.file: {subroutine.source.file}") + original_content = read_file(str(sourcefile.path)) + content = original_content + literal_nodes = FindFloatLiterals(with_ir_node=True).visit(subroutine.body) + for node, literals in literal_nodes: + # print(f"node.source: {node.source.string} | {type(node.source.string)}") + literal_map = {} + for literal in literals: + if literal.kind is None: + literal_map[literal] = FloatLiteral(value=literal.value, kind='JPRB') + if literal_map: + fixed_node = SubstituteExpressions(literal_map).visit(node) + indent = int((len(node.source.string) - len(node.source.string.lstrip(' ')))/2) + fixed_node_str = fgen(fixed_node, depth=indent) + with open (f'loki_lint_{subroutine.name}_new_file_fixed_node_str.F90', 'w') as f: + f.write(fixed_node_str) + content_new = re.sub(rf'{re.escape(node.source.string)}', + fixed_node_str, content, flags = re.S) + content = content_new + with open (f'loki_lint_{subroutine.name}_new_file.F90', 'w') as f: + f.write(content_new) + diff = difflib.unified_diff(original_content.splitlines(), content_new.splitlines(), + f'a/{sourcefile.path}', f'b/{sourcefile.path}', lineterm='') + diff_str = '\n'.join(list(diff)) + # print(f"---{sourcefile.path}------") + # print(diff_str) + # print(f"--------------------------") + with open (f'loki_lint_{subroutine.name}.patch', 'w') as f: + f.write(diff_str) + f.write('\n') + class MissingImplicitNoneRule(GenericRule): """ ``IMPLICIT NONE`` must be present in all scoping units but may be omitted diff --git a/loki/expression/mappers.py b/loki/expression/mappers.py index 26ffc39df..cc24fd094 100644 --- a/loki/expression/mappers.py +++ b/loki/expression/mappers.py @@ -173,7 +173,7 @@ def map_quotient(self, expr, enclosing_prec, *args, **kwargs): numerator = self.rec_with_force_parens_around(expr.numerator, PREC_PRODUCT, *args, **kwargs) kwargs['force_parens_around'] = self.multiplicative_primitives denominator = self.rec_with_force_parens_around(expr.denominator, PREC_PRODUCT, *args, **kwargs) - return self.parenthesize_if_needed(self.format('%s / %s', numerator, denominator), + return self.parenthesize_if_needed(self.format('%s/%s', numerator, denominator), enclosing_prec, PREC_PRODUCT) def map_parenthesised_add(self, expr, enclosing_prec, *args, **kwargs): @@ -206,7 +206,7 @@ def map_inline_do(self, expr, enclosing_prec, *args, **kwargs): def map_array_subscript(self, expr, enclosing_prec, *args, **kwargs): name_str = self.rec(expr.aggregate, PREC_NONE, *args, **kwargs) - index_str = self.join_rec(', ', expr.index_tuple, PREC_NONE, *args, **kwargs) + index_str = self.join_rec(',', expr.index_tuple, PREC_NONE, *args, **kwargs) return f'{name_str}({index_str})' map_string_subscript = map_array_subscript diff --git a/loki/frontend/tests/test_frontends.py b/loki/frontend/tests/test_frontends.py index de2427b8b..94ac13bbe 100644 --- a/loki/frontend/tests/test_frontends.py +++ b/loki/frontend/tests/test_frontends.py @@ -22,7 +22,7 @@ from loki import ( Module, Subroutine, FindVariables, BasicType, config, Sourcefile, RawSource, RegexParserClass, ProcedureType, DerivedType, - PreprocessorDirective, config_override + PreprocessorDirective, config_override, FindLiterals ) from loki.build import jit_compile, clean_test from loki.expression import symbols as sym @@ -53,7 +53,23 @@ def fixture_reset_regex_frontend_timeout(): yield config['regex-frontend-timeout'] = original_timeout +@pytest.mark.parametrize('frontend', available_frontends()) +def test_literals_kind(frontend): + """ + """ + fcode = """ +SUBROUTINE SOME_SUBROUTINE() +REAL :: A +A = 0.d0 +END SUBROUTINE + """.strip() + routine = Subroutine.from_source(fcode, frontend=frontend) + literals = FindLiterals().visit(routine.body) + # print(f"literals: {literals}") + for literal in literals: + print(f"literal: '{literal}' | {literal.kind}") + @pytest.mark.parametrize('frontend', available_frontends()) def test_check_alloc_opts(here, frontend): """ diff --git a/loki/ir/pprint.py b/loki/ir/pprint.py index 1a771fd45..0d0ca3a6b 100644 --- a/loki/ir/pprint.py +++ b/loki/ir/pprint.py @@ -150,7 +150,8 @@ def format_line(self, *items, comment=None, no_wrap=False, no_indent=False): required to observe the line width limit. :rtype: str """ - if not no_indent: + # print(f"items: {items}") + if not no_indent and items != ('',): items = [self.indent, *items] if no_wrap: # Simply concatenate items and append the comment diff --git a/loki/lint/rules.py b/loki/lint/rules.py index 6ed1b5f59..52aa3f40d 100644 --- a/loki/lint/rules.py +++ b/loki/lint/rules.py @@ -175,7 +175,7 @@ def check(cls, ast, rule_report, config, **kwargs): cls.check(member, rule_report, config, **kwargs) @classmethod - def fix_module(cls, module, rule_report, config): + def fix_module(cls, module, rule_report, config, sourcefile=None): """ Fix rule violations on module level @@ -183,7 +183,7 @@ def fix_module(cls, module, rule_report, config): """ @classmethod - def fix_subroutine(cls, subroutine, rule_report, config): + def fix_subroutine(cls, subroutine, rule_report, config, sourcefile=None): """ Fix rule violations on subroutine level diff --git a/loki/lint/utils.py b/loki/lint/utils.py index eb150a402..a53187602 100644 --- a/loki/lint/utils.py +++ b/loki/lint/utils.py @@ -23,7 +23,7 @@ class Fixer: """ @classmethod - def fix_module(cls, module, reports, config): # pylint: disable=unused-argument + def fix_module(cls, module, reports, config, sourcefile): # pylint: disable=unused-argument """ Call `fix_module` for all rules and apply the transformations. """ @@ -33,14 +33,14 @@ def fix_module(cls, module, reports, config): # pylint: disable=unused-argument return module @classmethod - def fix_subroutine(cls, subroutine, reports, config): + def fix_subroutine(cls, subroutine, reports, config, sourcefile): """ Call `fix_subroutine` for all rules and apply the transformations. """ mapper = {} for report in reports: rule_config = config[report.rule.__name__] - mapper.update(report.rule.fix_subroutine(subroutine, report, rule_config) or {}) + mapper.update(report.rule.fix_subroutine(subroutine, report, rule_config, sourcefile) or {}) if mapper: # Apply the changes and invalidate source objects @@ -87,10 +87,10 @@ def fix(cls, ast, reports, config): # Depth-first traversal if hasattr(ast, 'subroutines') and ast.subroutines is not None: for routine in ast.subroutines: - cls.fix_subroutine(routine, reports, config) + cls.fix_subroutine(routine, reports, config, ast) if hasattr(ast, 'modules') and ast.modules is not None: for module in ast.modules: - cls.fix_module(module, reports, config) + cls.fix_module(module, reports, config, ast) cls.fix_sourcefile(ast, reports, config) @@ -99,18 +99,18 @@ def fix(cls, ast, reports, config): # Depth-first traversal if hasattr(ast, 'subroutines') and ast.subroutines is not None: for routine in ast.subroutines: - cls.fix_subroutine(routine, reports, config) + cls.fix_subroutine(routine, reports, config, ast) - cls.fix_module(ast, reports, config) + cls.fix_module(ast, reports, config, ast) # Fix on subroutine level elif isinstance(ast, Subroutine): # Depth-first traversal if hasattr(ast, 'members') and ast.members is not None: for routine in ast.members: - cls.fix_subroutine(routine, reports, config) + cls.fix_subroutine(routine, reports, config, ast) - cls.fix_subroutine(ast, reports, config) + cls.fix_subroutine(ast, reports, config, ast) return ast