suoto/hdl_checker

View on GitHub
hdl_checker/diagnostics.py

Summary

Maintainability
C
1 day
Test Coverage
# This file is part of HDL Checker.
#
# Copyright (c) 2015 - 2019 suoto (Andre Souto)
#
# HDL Checker is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# HDL Checker is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with HDL Checker.  If not, see <http://www.gnu.org/licenses/>.
"Diagnostics holders for checkers"

from typing import Iterable, Optional

from hdl_checker.parsers.elements.dependency_spec import (  # pylint: disable=unused-import
    BaseDependencySpec,
    RequiredDesignUnit,
)
from hdl_checker.parsers.elements.identifier import (  # pylint: disable=unused-import
    Identifier,
)
from hdl_checker.path import Path  # pylint: disable=unused-import
from hdl_checker.types import Location  # pylint: disable=unused-import
from hdl_checker.utils import HashableByKey

# pylint: disable=useless-object-inheritance

CHECKER_NAME = "HDL Checker"
STATIC_CHECKER_NAME = "HDL Checker/static"


class DiagType(object):  # pylint: disable=too-few-public-methods
    """
    Enum-like class for error types
    """

    NONE = "None"
    INFO = "Info"
    WARNING = "Warning"
    ERROR = "Error"
    STYLE_INFO = "Info (style)"
    STYLE_WARNING = "Warning (style)"
    STYLE_ERROR = "Error (style)"


class CheckerDiagnostic(HashableByKey):  # pylint: disable=too-many-instance-attributes
    """
    Base container for diagnostics
    """

    def __init__(  # pylint: disable=too-many-arguments
        self,
        text,  # type: str
        checker=None,  # type: Optional[str]
        filename=None,  # type: Optional[Path]
        line_number=None,  # type: Optional[int]
        column_number=None,  # type: Optional[int]
        error_code=None,
        severity=None,
    ):

        # Checker can't be changed
        self._checker = CHECKER_NAME if checker is None else checker

        # Modifiable attributes
        self._filename = filename  # type: Optional[Path]
        self._error_code = error_code
        self._text = str(text)

        # Modifiable with rules
        self._line_number = None if line_number is None else int(line_number)
        self._column = None if column_number is None else int(column_number)
        self._severity = DiagType.ERROR if severity is None else severity

    def copy(self, **kwargs):
        """
        Returns a copy of the object replacing __init__ arguments for values
        for kwargs keys.
        """
        return CheckerDiagnostic(
            checker=kwargs.get("checker", getattr(self, "checker", None)),
            text=kwargs.get("text", getattr(self, "text", None)),
            filename=kwargs.get("filename", getattr(self, "filename", None)),
            line_number=kwargs.get("line_number", getattr(self, "line_number", None)),
            column_number=kwargs.get(
                "column_number", getattr(self, "column_number", None)
            ),
            error_code=kwargs.get("error_code", getattr(self, "error_code", None)),
            severity=kwargs.get("severity", getattr(self, "severity", None)),
        )

    def __repr__(self):
        return (
            '{}(checker="{}", filename={}, line_number={}, column_number={}, '
            "error_code={}, severity={}, text={})".format(
                self.__class__.__name__,
                repr(self.checker),
                repr(self.filename),
                repr(self.line_number),
                repr(self.column_number),
                repr(self.error_code),
                repr(self.severity),
                repr(self.text),
            )
        )

    @property
    def __hash_key__(self):
        return (
            self.filename,
            self.checker,
            self.column_number,
            self.error_code,
            self.line_number,
            self.severity,
            self.text,
        )

    def toDict(self):
        """Returns a dict representation of the object. All keys are always
        present but not values are necessearily set, in which case their values
        will be 'None'. Dict has the folowwing format:

            - checker: (string) Checker name
            - filename: (string)
            - error_code: (string)
            - text: (string)
            - line_number: (int or None)
            - column_number: (int or None)
            - severity: (string) Values taken from DiagType
        """
        return {
            "checker": self.checker,
            "filename": str(self.filename),
            "error_code": self.error_code,
            "text": self.text,
            "line_number": self.line_number,
            "column_number": self.column_number,
            "severity": self.severity,
        }

    @classmethod
    def fromDict(cls, state):
        "Creates a diagnostics objects from state dict"
        return cls(
            checker=state["checker"],
            filename=state["filename"],
            error_code=state["error_code"],
            text=state["text"],
            line_number=state["line_number"],
            column_number=state["column_number"],
            severity=state["severity"],
        )

    @property
    def checker(self):
        "Full checker name"
        return self._checker

    @property
    def filename(self):
        "Full checker name"
        return self._filename

    @property
    def text(self):
        "Full checker name"
        return self._text

    @property
    def error_code(self):
        "Full checker name"
        return self._error_code

    @property
    def line_number(self):
        "Diagnostics line number"
        if self._line_number is not None:
            return int(self._line_number)
        return self._line_number

    @property
    def column_number(self):
        "Diagnostics column_number"
        if self._column is not None:
            return int(self._column)
        return self._column

    @property
    def severity(self):
        "Diagnostics severity (use diagnostics.DiagType for consistency)"
        return self._severity


class PathNotInProjectFile(CheckerDiagnostic):
    """
    Reports a check request on a file whose path in not present on the project
    file
    """

    def __init__(self, path):
        super(PathNotInProjectFile, self).__init__(
            checker=CHECKER_NAME,
            filename=path,
            severity=DiagType.WARNING,
            text='Path "{}" not found in project file'.format(path),
        )


class StaticCheckerDiag(CheckerDiagnostic):
    "Base diagnostics issues from static checks"

    def __init__(  # pylint: disable=too-many-arguments
        self,
        text,
        severity,
        filename=None,
        line_number=None,
        column_number=None,
        error_code=None,
    ):

        assert severity in (
            DiagType.STYLE_INFO,
            DiagType.STYLE_WARNING,
            DiagType.STYLE_ERROR,
        ), "Static checker diags should only carry style error types"

        super(StaticCheckerDiag, self).__init__(
            checker=STATIC_CHECKER_NAME,
            text=text,
            severity=severity,
            filename=filename,
            line_number=line_number,
            column_number=column_number,
            error_code=error_code,
        )


class LibraryShouldBeOmited(StaticCheckerDiag):
    "Library declaration should be ommited"

    def __init__(self, library, filename=None, line_number=None, column_number=None):
        super(LibraryShouldBeOmited, self).__init__(
            line_number=line_number,
            column_number=column_number,
            filename=filename,
            severity=DiagType.STYLE_INFO,
            text="Declaration of library '{library}' can be omitted".format(
                library=library
            ),
        )


class ObjectIsNeverUsed(StaticCheckerDiag):
    "Reports an object that was created but never used"

    def __init__(  # pylint: disable=too-many-arguments
        self,
        filename=None,
        line_number=None,
        column_number=None,
        object_name=None,
        object_type=None,
    ):
        super(ObjectIsNeverUsed, self).__init__(
            filename=filename,
            line_number=line_number,
            column_number=column_number,
            severity=DiagType.STYLE_WARNING,
            text="{} '{}' is never used".format(
                str(object_type).capitalize(), object_name
            ),
        )


class BuilderDiag(CheckerDiagnostic):
    """
    Reports issues when checking a file whose path in not present on the
    project file
    """

    def __init__(  # pylint: disable=too-many-arguments
        self,
        builder_name,
        text,
        filename=None,
        line_number=None,
        column_number=None,
        error_code=None,
        severity=None,
    ):
        super(BuilderDiag, self).__init__(
            checker="{}/{}".format(CHECKER_NAME, builder_name),
            text=text,
            filename=filename,
            severity=severity,
            line_number=line_number,
            column_number=column_number,
            error_code=error_code,
        )


class FailedToCreateProject(CheckerDiagnostic):
    """
    Reports problems when reading the project file
    """

    def __init__(self, exception):
        text = "Exception while creating server: {}"

        super(FailedToCreateProject, self).__init__(
            checker=CHECKER_NAME,
            severity=DiagType.ERROR,
            text=text.format(str(exception)),
        )


class DependencyNotUnique(CheckerDiagnostic):
    """
    Searching for a dependency should yield a single source file
    """

    def __init__(  # pylint: disable=too-many-arguments
        self, filename, dependency, choices, line_number=None, column_number=None
    ):
        # Revert to str and not Paths for the ease for sorting, which helps esp
        # when testing (order of sets depend on their hash)
        _choices = sorted(list(map(str, choices)))

        if isinstance(dependency, RequiredDesignUnit):
            text = (
                "Dependency '{}' (library={}) has {} definitions (files are {}). "
                "The selected option may not be the correct one".format(
                    dependency.name,
                    dependency.library,
                    len(_choices),
                    ", ".join(('"%s"' % x for x in _choices)),
                )
            )
        else:
            text = (
                "Inclue path '{}' has {} definitions (files are {}). "
                "The selected option may not be the correct one".format(
                    dependency.name,
                    len(_choices),
                    ", ".join(('"%s"' % x for x in _choices)),
                )
            )

        super(DependencyNotUnique, self).__init__(
            filename=filename,
            severity=DiagType.STYLE_WARNING,
            line_number=line_number,
            column_number=column_number,
            text=text,
        )


class PathLibraryIsNotUnique(CheckerDiagnostic):
    """
    Searching for a dependency should yield a single source file
    """

    def __init__(self, filename, actual, choices):
        # type: (Path, Identifier, Iterable[Identifier]) -> None
        _choices = list(choices)

        msg = []
        for library in set(_choices):
            msg.append("'{}' (x{})".format(library, _choices.count(library)))

        text = (
            "Using library '{}' for this file but its units are referenced in "
            "multiple libraries: {}".format(actual, ", ".join(msg))
        )

        super(PathLibraryIsNotUnique, self).__init__(
            filename=filename, severity=DiagType.WARNING, text=text
        )


class UnresolvedDependency(CheckerDiagnostic):
    """
    Marks dependencies that could not be resolved for a file
    """

    def __init__(self, dependency, location):
        # type: (BaseDependencySpec, Location) -> None
        if isinstance(dependency, RequiredDesignUnit):
            reference = "%s.%s" % (dependency.library or "work", dependency.name)
        else:
            reference = str(dependency.name)

        super(UnresolvedDependency, self).__init__(
            filename=dependency.owner,
            severity=DiagType.STYLE_ERROR,
            line_number=location.line,
            column_number=location.column,
            text="Unable to resolve '{}' to a path".format(reference),
        )