1"""Exceptions and error representations."""
2import functools
3from typing import Any, Optional, Union
4
5from ansiblelint._internal.rules import BaseRule, RuntimeErrorRule
6from ansiblelint.file_utils import Lintable, normpath
7
8
9@functools.total_ordering
10class MatchError(ValueError):
11    """Rule violation detected during linting.
12
13    It can be raised as Exception but also just added to the list of found
14    rules violations.
15
16    Note that line argument is not considered when building hash of an
17    instance.
18    """
19
20    # IMPORTANT: any additional comparison protocol methods must return
21    # IMPORTANT: `NotImplemented` singleton to allow the check to use the
22    # IMPORTANT: other object's fallbacks.
23    # Ref: https://docs.python.org/3/reference/datamodel.html#object.__lt__
24
25    # pylint: disable=too-many-arguments
26    def __init__(
27        self,
28        message: Optional[str] = None,
29        # most linters report use (1,1) base, including yamllint and flake8
30        # we should never report line 0 or column 0 in output.
31        linenumber: int = 1,
32        column: Optional[int] = None,
33        details: str = "",
34        filename: Optional[Union[str, Lintable]] = None,
35        rule: BaseRule = RuntimeErrorRule(),
36        tag: Optional[str] = None,  # optional fine-graded tag
37    ) -> None:
38        """Initialize a MatchError instance."""
39        super().__init__(message)
40
41        if rule.__class__ is RuntimeErrorRule and not message:
42            raise TypeError(
43                f'{self.__class__.__name__}() missing a '
44                "required argument: one of 'message' or 'rule'",
45            )
46
47        self.message = message or getattr(rule, 'shortdesc', "")
48
49        # Safety measture to ensure we do not endup with incorrect indexes
50        if linenumber == 0:
51            raise RuntimeError(
52                "MatchError called incorrectly as line numbers start with 1"
53            )
54        if column == 0:
55            raise RuntimeError(
56                "MatchError called incorrectly as column numbers start with 1"
57            )
58
59        self.linenumber = linenumber
60        self.column = column
61        self.details = details
62        self.filename = ""
63        if filename:
64            if isinstance(filename, Lintable):
65                self.filename = normpath(str(filename.path))
66            else:
67                self.filename = normpath(filename)
68        self.rule = rule
69        self.ignored = False  # If set it will be displayed but not counted as failure
70        # This can be used by rules that can report multiple errors type, so
71        # we can still filter by them.
72        self.tag = tag
73
74    def __repr__(self) -> str:
75        """Return a MatchError instance representation."""
76        formatstr = u"[{0}] ({1}) matched {2}:{3} {4}"
77        # note that `rule.id` can be int, str or even missing, as users
78        # can defined their own custom rules.
79        _id = getattr(self.rule, "id", "000")
80
81        return formatstr.format(
82            _id, self.message, self.filename, self.linenumber, self.details
83        )
84
85    @property
86    def position(self) -> str:
87        """Return error positioniong, with column number if available."""
88        if self.column:
89            return f"{self.linenumber}:{self.column}"
90        return str(self.linenumber)
91
92    @property
93    def _hash_key(self) -> Any:
94        # line attr is knowingly excluded, as dict is not hashable
95        return (
96            self.filename,
97            self.linenumber,
98            str(getattr(self.rule, 'id', 0)),
99            self.message,
100            self.details,
101            # -1 is used here to force errors with no column to sort before
102            # all other errors.
103            -1 if self.column is None else self.column,
104        )
105
106    def __lt__(self, other: object) -> bool:
107        """Return whether the current object is less than the other."""
108        if not isinstance(other, self.__class__):
109            return NotImplemented
110        return bool(self._hash_key < other._hash_key)
111
112    def __hash__(self) -> int:
113        """Return a hash value of the MatchError instance."""
114        return hash(self._hash_key)
115
116    def __eq__(self, other: object) -> bool:
117        """Identify whether the other object represents the same rule match."""
118        if not isinstance(other, self.__class__):
119            return NotImplemented
120        return self.__hash__() == other.__hash__()
121