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