diff options
Diffstat (limited to 'src/ansiblelint/errors.py')
-rw-r--r-- | src/ansiblelint/errors.py | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/src/ansiblelint/errors.py b/src/ansiblelint/errors.py new file mode 100644 index 0000000..c5a1895 --- /dev/null +++ b/src/ansiblelint/errors.py @@ -0,0 +1,144 @@ +"""Exceptions and error representations.""" +from __future__ import annotations + +import functools +from typing import Any + +from ansiblelint._internal.rules import BaseRule, RuntimeErrorRule +from ansiblelint.config import options +from ansiblelint.file_utils import Lintable, normpath + + +# pylint: disable=too-many-instance-attributes +@functools.total_ordering +class MatchError(ValueError): + """Rule violation detected during linting. + + It can be raised as Exception but also just added to the list of found + rules violations. + + Note that line argument is not considered when building hash of an + instance. + """ + + tag = "" + + # IMPORTANT: any additional comparison protocol methods must return + # IMPORTANT: `NotImplemented` singleton to allow the check to use the + # IMPORTANT: other object's fallbacks. + # Ref: https://docs.python.org/3/reference/datamodel.html#object.__lt__ + + # pylint: disable=too-many-arguments + def __init__( + self, + message: str | None = None, + # most linters report use (1,1) base, including yamllint and flake8 + # we should never report line 0 or column 0 in output. + linenumber: int = 1, + column: int | None = None, + details: str = "", + filename: Lintable | None = None, + rule: BaseRule = RuntimeErrorRule(), + tag: str | None = None, # optional fine-graded tag + ) -> None: + """Initialize a MatchError instance.""" + super().__init__(message) + + if rule.__class__ is RuntimeErrorRule and not message: + raise TypeError( + f"{self.__class__.__name__}() missing a " + "required argument: one of 'message' or 'rule'", + ) + + self.message = str(message or getattr(rule, "shortdesc", "")) + + # Safety measure to ensure we do not end-up with incorrect indexes + if linenumber == 0: # pragma: no cover + raise RuntimeError( + "MatchError called incorrectly as line numbers start with 1" + ) + if column == 0: # pragma: no cover + raise RuntimeError( + "MatchError called incorrectly as column numbers start with 1" + ) + + self.linenumber = linenumber + self.column = column + self.details = details + self.filename = "" + if filename: + if isinstance(filename, Lintable): + self.lintable = filename + self.filename = normpath(str(filename.path)) + else: + self.filename = normpath(filename) + self.lintable = Lintable(self.filename) + self.rule = rule + self.ignored = False # If set it will be displayed but not counted as failure + # This can be used by rules that can report multiple errors type, so + # we can still filter by them. + self.tag = tag or rule.id + + # optional indicator on how this error was found + self.match_type: str | None = None + # for task matches, save the normalized task object (useful for transforms) + self.task: dict[str, Any] | None = None + # path to the problem area, like: [0,"pre_tasks",3] for [0].pre_tasks[3] + self.yaml_path: list[int | str] = [] + # True when a transform has resolved this MatchError + self.fixed = False + + @functools.cached_property + def level(self) -> str: + """Return the level of the rule: error, warning or notice.""" + if {self.tag, self.rule.id, *self.rule.tags}.isdisjoint(options.warn_list): + return "error" + return "warning" + + def __repr__(self) -> str: + """Return a MatchError instance representation.""" + formatstr = "[{0}] ({1}) matched {2}:{3} {4}" + # note that `rule.id` can be int, str or even missing, as users + # can defined their own custom rules. + _id = getattr(self.rule, "id", "000") + + return formatstr.format( + _id, self.message, self.filename, self.linenumber, self.details + ) + + @property + def position(self) -> str: + """Return error positioning, with column number if available.""" + if self.column: + return f"{self.linenumber}:{self.column}" + return str(self.linenumber) + + @property + def _hash_key(self) -> Any: + # line attr is knowingly excluded, as dict is not hashable + return ( + self.filename, + self.linenumber, + str(getattr(self.rule, "id", 0)), + self.message, + self.details, + # -1 is used here to force errors with no column to sort before + # all other errors. + -1 if self.column is None else self.column, + ) + + def __lt__(self, other: object) -> bool: + """Return whether the current object is less than the other.""" + if not isinstance(other, self.__class__): + return NotImplemented + return bool(self._hash_key < other._hash_key) + + def __hash__(self) -> int: + """Return a hash value of the MatchError instance.""" + return hash(self._hash_key) + + def __eq__(self, other: object) -> bool: + """Identify whether the other object represents the same rule match.""" + if not isinstance(other, self.__class__): + return NotImplemented + return self.__hash__() == other.__hash__() |