summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/errors.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansiblelint/errors.py')
-rw-r--r--src/ansiblelint/errors.py162
1 files changed, 162 insertions, 0 deletions
diff --git a/src/ansiblelint/errors.py b/src/ansiblelint/errors.py
new file mode 100644
index 0000000..c8458b8
--- /dev/null
+++ b/src/ansiblelint/errors.py
@@ -0,0 +1,162 @@
+"""Exceptions and error representations."""
+from __future__ import annotations
+
+import functools
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, Any
+
+from ansiblelint._internal.rules import BaseRule, RuntimeErrorRule
+from ansiblelint.config import options
+from ansiblelint.file_utils import Lintable
+
+if TYPE_CHECKING:
+ from ansiblelint.utils import Task
+
+
+class LintWarning(Warning):
+ """Used by linter."""
+
+
+@dataclass
+class WarnSource:
+ """Container for warning information, so we can later create a MatchError from it."""
+
+ filename: Lintable
+ lineno: int
+ tag: str
+ message: str | None = None
+
+
+class StrictModeError(RuntimeError):
+ """Raise when we encounter a warning in strict mode."""
+
+ def __init__(
+ self,
+ message: str = "Warning treated as error due to --strict option.",
+ ):
+ """Initialize a StrictModeError instance."""
+ super().__init__(message)
+
+
+# pylint: disable=too-many-instance-attributes
+@dataclass(unsafe_hash=True)
+@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.
+ """
+
+ # order matters for these:
+ message: str = field(init=True, repr=False, default="")
+ lintable: Lintable = field(init=True, repr=False, default=Lintable(name=""))
+ filename: str = field(init=True, repr=False, default="")
+
+ tag: str = field(init=True, repr=False, default="")
+ lineno: int = 1
+ details: str = ""
+ column: int | None = None
+ # rule is not included in hash because we might have different instances
+ # of the same rule, but we use the 'tag' to identify the rule.
+ rule: BaseRule = field(hash=False, default=RuntimeErrorRule())
+ ignored: bool = False
+ fixed: bool = False # True when a transform has resolved this MatchError
+
+ def __post_init__(self) -> None:
+ """Can be use by rules that can report multiple errors type, so we can still filter by them."""
+ if not self.lintable and self.filename:
+ self.lintable = Lintable(self.filename)
+ elif self.lintable and not self.filename:
+ self.filename = self.lintable.name
+
+ # We want to catch accidental MatchError() which contains no useful
+ # information. When no arguments are passed, the '_message' field is
+ # set to 'property', only if passed it becomes a string.
+ if self.rule.__class__ is RuntimeErrorRule:
+ # so instance was created without a rule
+ if not self.message:
+ msg = f"{self.__class__.__name__}() missing a required argument: one of 'message' or 'rule'"
+ raise TypeError(msg)
+ if not isinstance(self.tag, str):
+ msg = "MatchErrors must be created with either rule or tag specified."
+ raise TypeError(msg)
+ if not self.message:
+ self.message = self.rule.shortdesc
+
+ self.match_type: str | None = None
+ # for task matches, save the normalized task object (useful for transforms)
+ self.task: Task | None = None
+ # path to the problem area, like: [0,"pre_tasks",3] for [0].pre_tasks[3]
+ self.yaml_path: list[int | str] = []
+
+ if not self.tag:
+ self.tag = self.rule.id
+
+ # Safety measure to ensure we do not end-up with incorrect indexes
+ if self.lineno == 0: # pragma: no cover
+ msg = "MatchError called incorrectly as line numbers start with 1"
+ raise RuntimeError(msg)
+ if self.column == 0: # pragma: no cover
+ msg = "MatchError called incorrectly as column numbers start with 1"
+ raise RuntimeError(msg)
+
+ @functools.cached_property
+ def level(self) -> str:
+ """Return the level of the rule: error, warning or notice."""
+ if not self.ignored and {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.lineno,
+ self.details,
+ )
+
+ @property
+ def position(self) -> str:
+ """Return error positioning, with column number if available."""
+ if self.column:
+ return f"{self.lineno}:{self.column}"
+ return str(self.lineno)
+
+ @property
+ def _hash_key(self) -> Any:
+ # line attr is knowingly excluded, as dict is not hashable
+ return (
+ self.filename,
+ self.lineno,
+ 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 __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__()