diff --git a/source/stylist/fortran.py b/source/stylist/fortran.py index 54e260c..4982032 100644 --- a/source/stylist/fortran.py +++ b/source/stylist/fortran.py @@ -8,9 +8,8 @@ """ import re from abc import ABC, abstractmethod -from collections import defaultdict from typing import (Container, Dict, List, Optional, Pattern, Sequence, Type, - Union) + Union, Any, cast) import fparser.two.Fortran2003 as Fortran2003 # type: ignore import fparser.two.Fortran2008 as Fortran2008 # type: ignore @@ -539,8 +538,8 @@ class KindPattern(FortranRule): " not fit the pattern /{pattern}/." def __init__(self, *, # There are no positional arguments. - integer: Union[str, Pattern], - real: Union[str, Pattern]): + integer: Optional[Union[str, Pattern]] = None, + real: Optional[Union[str, Pattern]] = None): """ Patterns are set only for integer and real data types however Fortran supports many more. Logical and Complex for example. For those cases a @@ -549,13 +548,16 @@ def __init__(self, *, # There are no positional arguments. :param integer: Regular expression which integer kinds must match. :param real: Regular expression which real kinds must match. """ - self._patterns: Dict[str, Pattern] \ - = defaultdict(lambda: re.compile(r'.*')) - if isinstance(integer, str): + self._patterns: Dict[str, Optional[Pattern]] = {'logical': None} + if integer is None: + pass + elif isinstance(integer, str): self._patterns['integer'] = re.compile(integer) else: self._patterns['integer'] = integer - if isinstance(real, str): + if real is None: + pass + elif isinstance(real, str): self._patterns['real'] = re.compile(real) else: self._patterns['real'] = real @@ -582,19 +584,34 @@ def examine_fortran(self, subject: FortranSource) -> List[Issue]: (Fortran2003.Data_Component_Def_Stmt, Fortran2003.Type_Declaration_Stmt)): type_spec: Fortran2003.Intrinsic_Type_Spec = candidate.items[0] + data_type: str = type_spec.items[0].lower() + + if self._patterns.get(data_type) is None: + continue + pattern = cast(Pattern[Any], self._patterns[data_type]) + kind_selector: Fortran2003.Kind_Selector = type_spec.items[1] + if kind_selector is None: + entity_declaration = candidate.items[2] + message = self._ISSUE_TEMPLATE.format( + type=data_type, + kind='', + name=entity_declaration, + pattern=pattern.pattern) + issues.append(Issue(message, + line=_line(candidate))) + continue if isinstance(kind_selector, Fortran2003.Kind_Selector): - data_type: str = type_spec.items[0].lower() kind: str = str(kind_selector.children[1]) - match = self._patterns[data_type].match(kind) + match = pattern.match(kind) if match is None: entity_declaration = candidate.items[2] message = self._ISSUE_TEMPLATE.format( type=data_type, kind=kind, name=entity_declaration, - pattern=self._patterns[data_type].pattern) + pattern=pattern.pattern) issues.append(Issue(message, line=_line(candidate))) diff --git a/unit-tests/fortran_kind_pattern_test.py b/unit-tests/fortran_kind_pattern_test.py index b0b4109..bd95c68 100644 --- a/unit-tests/fortran_kind_pattern_test.py +++ b/unit-tests/fortran_kind_pattern_test.py @@ -9,6 +9,8 @@ import re from textwrap import dedent +import pytest + from stylist.fortran import KindPattern from stylist.source import (FortranPreProcessor, FortranSource, @@ -238,8 +240,9 @@ def test_failing(self): assert len(issues) == 28 - def test_missing_kind(self): - case = dedent(""" + @pytest.fixture + def missing_kind_text(self) -> str: + return dedent(""" module passing_mod implicit none integer :: global_int @@ -273,9 +276,43 @@ def test_missing_kind(self): end module passing_mod """) - reader = SourceStringReader(case) + def test_missing_kind_expected(self, missing_kind_text): + reader = SourceStringReader(missing_kind_text) source = FortranSource(reader) - unit_under_test = KindPattern(integer=r'.+_beef', - real=re.compile(r'.+_cheese')) + unit_under_test = KindPattern() issues = unit_under_test.examine(source) - assert len(issues) == 0 + assert [str(issue) for issue in issues] == [] + + def test_missing_kind_integer(self, missing_kind_text): + reader = SourceStringReader(missing_kind_text) + source = FortranSource(reader) + unit_under_test = KindPattern(integer=re.compile('i_.*_var')) + issues = unit_under_test.examine(source) + assert [str(issue) for issue in issues] \ + == ["4: Kind '' found for integer variable 'global_int' " + "does not fit the pattern /i_.*_var/.", + "8: Kind '' found for integer variable 'member_int' " + "does not fit the pattern /i_.*_var/.", + "17: Kind '' found for integer variable 'arg_int' " + "does not fit the pattern /i_.*_var/.", + "20: Kind '' found for integer variable 'return_int' " + "does not fit the pattern /i_.*_var/.", + "27: Kind '' found for integer variable 'marg_int' " + "does not fit the pattern /i_.*_var/."] + + def test_missing_kind_float(self, missing_kind_text): + reader = SourceStringReader(missing_kind_text) + source = FortranSource(reader) + unit_under_test = KindPattern(real=re.compile('r_.*_var')) + issues = unit_under_test.examine(source) + assert [str(issue) for issue in issues] \ + == ["5: Kind '' found for real variable 'global_float' " + "does not fit the pattern /r_.*_var/.", + "9: Kind '' found for real variable 'member_float' " + "does not fit the pattern /r_.*_var/.", + "18: Kind '' found for real variable 'arg_float' " + "does not fit the pattern /r_.*_var/.", + "28: Kind '' found for real variable 'marg_float' " + "does not fit the pattern /r_.*_var/.", + "30: Kind '' found for real variable 'return_float' " + "does not fit the pattern /r_.*_var/."]