yala/linters.py

Summary

Maintainability
C
1 day
Test Coverage
"""Module for linters."""
# The less we need to code, the better!
import re

from .base import Linter, LinterOutput


class Flake8(Linter):
    """Parser for flake8."""

    name = "flake8"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines."""
        pattern = re.compile(
            r"""
                ^(?P<path>.+?)
                :(?P<line_nr>\d+?)
                :(?P<col>\d+?)
                :\ (?P<msg>.+)$
            """,
            re.VERBOSE,
        )
        return self._parse_by_pattern(stdout_lines, pattern), stderr_lines


class Isort(Linter):
    """Isort parser."""

    name = "isort"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines."""
        # E.g. "ERROR: /my/path/main.py Imports are incorrectly sorted."
        pattern = re.compile(
            r"""
                ^.+?
                :\ (?P<full_path>.+\.py)
                \ (?P<msg>.+)$
            """,
            re.VERBOSE,
        )
        return self._parse_by_pattern(stderr_lines, pattern), stdout_lines

    def _create_output_from_match(self, match_result):
        """As isort outputs full path, we change it to relative path."""
        full_path = match_result["full_path"]
        path = self._get_relative_path(full_path)
        return LinterOutput(self.name, path, match_result["msg"])


class Pycodestyle(Linter):
    """Pycodestyle parser."""

    name = "pycodestyle"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines."""
        pattern = re.compile(
            r"""
                ^(?P<path>.+?)
                :(?P<line_nr>\d+?)
                :(?P<col>\d+?)
                :\ (?P<msg>.+)$
            """,
            re.VERBOSE,
        )
        return self._parse_by_pattern(stdout_lines, pattern), stderr_lines


class Mypy(Linter):
    """Mypy parser."""

    name = "mypy"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines."""
        pattern = re.compile(
            r"""
                ^(?P<path>.+?)
                :(?P<line_nr>\d+?)
                :\ (?P<msg>.+)$
            """,
            re.VERBOSE,
        )
        return self._parse_by_pattern(stdout_lines, pattern), stderr_lines


class Pydocstyle(Linter):
    """Pydocstyle parser."""

    name = "pydocstyle"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines."""
        return self._parse(stdout_lines), stderr_lines

    def _parse(self, lines):
        """Get :class:`base.LinterOutput` parameters using regex.

        There are 2 lines for each pydocstyle result:
            1. Filename and line number;
            2. Message for the problem found.
        """
        patterns = [re.compile(r"^(.+?):(\d+)"), re.compile(r"^\s+(.+)$")]
        for i, line in enumerate(lines):
            if i % 2 == 0:
                path, line_nr = patterns[0].match(line).groups()
            else:
                msg = patterns[1].match(line).group(1)
                yield LinterOutput(self.name, path, msg, line_nr)


class Pyflakes(Linter):
    """Pyflakes parser."""

    name = "pyflakes"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines."""
        pattern = re.compile(
            r"""
                ^(?P<path>.+?)
                :(?P<line_nr>\d+?)
                :(?P<col>\d+?)?
                :\ (?P<msg>.+)$
            """,
            re.VERBOSE,
        )
        return self._parse_by_pattern(stdout_lines, pattern), stderr_lines


class Pylint(Linter):
    """Pylint parser."""

    name = "pylint"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines."""
        pattern = re.compile(
            r"""
                .*?^(?P<path>[^\n]+?)
                :(?P<msg>.+)
                :(?P<line_nr>\d+?)
                :(?P<col>\d+?)$
            """,
            re.X | re.M | re.S,
        )
        return self._parse_by_pattern(stdout_lines, pattern), stderr_lines


class RadonCC(Linter):
    """Parser for radon ciclomatic complexity."""

    name = "radon cc"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines."""
        return self._parse(stdout_lines), stderr_lines

    def _parse(self, lines):
        """Get :class:`base.LinterOutput` parameters using regex.

        The output has one line with the file path followed by others with
        one problem per line.
        """
        # E.g. 'relative/path/to/file.py'
        pattern_path = re.compile(r"^(\S.*$)")
        # E.g. '    C 19:0 RadonCC - A'
        pattern_result = re.compile(r"\s+\w (\d+):(\d+) (.+)$")
        path = None
        for line in lines:
            match = pattern_path.match(line)
            if match:
                # We have the file path. Skip the remainder of the loop
                path = line
                continue
            match = pattern_result.match(line)
            if match:
                # Output found for the file stored in ``path``.
                line_nr, col, msg = match.groups()
                yield LinterOutput(self.name, path, msg, line_nr, col)


class RadonMI(Linter):
    """Parser for radon maintainability index."""

    name = "radon mi"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines."""
        pattern = re.compile(
            r"""
                ^(?P<path>.+)
                \ -\ (?P<msg>[A-F])$
            """,
            re.VERBOSE,
        )
        return self._parse_by_pattern(stdout_lines, pattern), stderr_lines


class Black(Linter):
    """Parser for black code formatter lint check."""

    name = "black"
    command = "black --check"

    def parse(self, stdout_lines, stderr_lines):
        """Parse linter stdout and stderr lines.

        Expected error message:

        would reformat file1.py
        would reformat file2.py
        Oh no! 💥 💔 💥
        2 files would be reformatted.
        """
        pattern = re.compile(
            r"""
                ^.*?(?P<msg>would\sreformat)\s
                (?P<path>.+\.py)$
            """,
            re.VERBOSE,
        )
        return self._parse_by_pattern(stderr_lines, pattern), stdout_lines


#: dict: All Linter subclasses indexed by class name
LINTERS = {cls.name: cls for cls in Linter.__subclasses__()}