aboutsummaryrefslogtreecommitdiff
blob: 23d639fcca1d3f2b8c0d4668eda76fad4ebb6e3e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
"""Base classes for check results."""

from functools import total_ordering

from pkgcore.ebuild import cpv
from snakeoil import klass
from snakeoil.strings import pluralism

from . import base
from .packages import FilteredPkg, RawCPV


class InvalidResult(Exception):
    """Creating a result object failed in some fashion."""


@total_ordering
class Result:
    """Generic report result returned from a check."""

    # all results are shown by default
    _filtered = False
    # default to repository level results
    scope = base.repo_scope
    # priority level, color, name, and profile type
    level = None
    color = None
    _name = None
    _profile = None

    def __init_subclass__(cls, **kwargs):
        """Initialize result subclasses and set 'name' class attribute."""
        super().__init_subclass__(**kwargs)
        cls.name = cls._name if cls._name is not None else cls.__name__

    def __str__(self):
        return f"{self.name}: {self.desc}"

    @property
    def desc(self):
        """Result description."""

    @property
    def _attrs(self):
        """Return all public result attributes."""
        return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}

    @classmethod
    def _create(cls, **kwargs):
        """Create a new result object from a given attributes dict."""
        if issubclass(cls, CategoryResult):
            category = kwargs.pop("category", None)
            package = kwargs.pop("package", None)
            version = kwargs.pop("version", None)
            if "pkg" not in kwargs:
                # recreate pkg param from related, separated attributes
                if category is None:
                    raise InvalidResult("missing category")
                if issubclass(cls, PackageResult) and package is None:
                    raise InvalidResult("missing package")
                if issubclass(cls, VersionResult) and version is None:
                    raise InvalidResult("missing version")
                kwargs["pkg"] = RawCPV(category, package, version)
        return cls(**kwargs)

    def __eq__(self, other):
        return self.name == other.name and self._attrs == other._attrs

    def __hash__(self):
        return hash((self.name, tuple(sorted(self._attrs.items()))))

    def __lt__(self, other):
        if self.scope == other.scope:
            if self.name == other.name:
                return self.desc < other.desc
            return self.name < other.name
        return self.scope < other.scope


class AliasResult(Result):
    """Classes directly inheriting this class can be targeted as scannable keywords."""


class BaseLinesResult:
    """Base class for results of multiples lines."""

    def __init__(self, lines, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.lines = tuple(lines)

    @property
    def lines_str(self):
        s = pluralism(self.lines)
        lines = ", ".join(map(str, self.lines))
        return f"on line{s}: {lines}"


class Error(Result):
    """Result with an error priority level."""

    level = "error"
    color = "red"


class Warning(Result):
    """Result with a warning priority level."""

    level = "warning"
    color = "yellow"


class Style(Result):
    """Result with a coding style priority level."""

    level = "style"
    color = "cyan"


class Info(Result):
    """Result with an info priority level."""

    level = "info"
    color = "green"


class CommitResult(Result):
    """Result related to a specific git commit."""

    scope = base.commit_scope

    def __init__(self, commit, **kwargs):
        super().__init__(**kwargs)
        self.commit = str(commit)
        self._attr = "commit"

    def __lt__(self, other):
        try:
            # if hashes match, sort by name/desc
            if self.commit == other.commit:
                if self.name == other.name:
                    return self.desc < other.desc
                return self.name < other.name
        except AttributeError:
            pass
        return False


class ProfilesResult(Result):
    """Result related to profiles."""

    scope = base.profiles_scope


class EclassResult(Result):
    """Result related to a specific eclass."""

    scope = base.eclass_scope

    def __init__(self, eclass, **kwargs):
        super().__init__(**kwargs)
        self.eclass = str(eclass)
        self._attr = "eclass"

    def __lt__(self, other):
        try:
            # if eclasses match, sort by name/desc
            if self.eclass == other.eclass:
                if self.name == other.name:
                    return self.desc < other.desc
                return self.name < other.name
            return self.eclass < other.eclass
        except AttributeError:
            pass
        return False


class CategoryResult(Result):
    """Result related to a specific category."""

    scope = base.category_scope

    def __init__(self, pkg, **kwargs):
        super().__init__(**kwargs)
        self.category = pkg.category
        self._attr = "category"

    def __lt__(self, other):
        try:
            if self.category != other.category:
                return self.category < other.category
        except AttributeError:
            pass
        return super().__lt__(other)


class PackageResult(CategoryResult):
    """Result related to a specific package."""

    scope = base.package_scope

    def __init__(self, pkg, **kwargs):
        super().__init__(pkg, **kwargs)
        self.package = pkg.package
        self._attr = "package"

    def __lt__(self, other):
        try:
            if self.category == other.category and self.package != other.package:
                return self.package < other.package
        except AttributeError:
            pass
        return super().__lt__(other)


class VersionResult(PackageResult):
    """Result related to a specific version of a package."""

    scope = base.version_scope

    def __init__(self, pkg, **kwargs):
        if isinstance(pkg, FilteredPkg):
            self._filtered = True
            pkg = pkg._pkg
        super().__init__(pkg, **kwargs)
        self.version = pkg.fullver
        self._attr = "version"

    @klass.jit_attr
    def ver_rev(self):
        version, _, revision = self.version.partition("-r")
        revision = cpv.Revision(revision)
        return version, revision

    def __lt__(self, other, cmp=None):
        try:
            if self.category == other.category and self.package == other.package:
                if cmp is None:
                    cmp = cpv.ver_cmp(*(self.ver_rev + other.ver_rev))
                if cmp < 0:
                    return True
                elif cmp > 0:
                    return False
        except AttributeError:
            pass
        return super().__lt__(other)


class LinesResult(BaseLinesResult, VersionResult):
    """Result related to multiples lines of an ebuild."""


class LineResult(VersionResult):
    """Result related to a specific line of an ebuild."""

    def __init__(self, line, lineno, **kwargs):
        super().__init__(**kwargs)
        self.line = line
        self.lineno = lineno

    def __lt__(self, other):
        cmp = None
        try:
            if self.category == other.category and self.package == other.package:
                # sort by line number for matching versions
                cmp = cpv.ver_cmp(*(self.ver_rev + other.ver_rev))
                if cmp == 0:
                    if self.lineno < other.lineno:
                        return True
                    elif self.lineno > other.lineno:
                        return False
        except AttributeError:
            pass
        return super().__lt__(other, cmp=cmp)


class _LogResult(Result):
    """Message caught from a logger instance."""

    def __init__(self, msg):
        super().__init__()
        self.msg = str(msg)

    @property
    def desc(self):
        return self.msg


class LogWarning(_LogResult, Warning):
    """Warning caught from a logger instance."""


class LogError(_LogResult, Error):
    """Error caught from a logger instance."""


class MetadataError(Error):
    """Problem detected with a package's metadata."""

    # specific metadata attributes handled by the result class
    attr = None
    # mapping from data attributes to result classes
    results = {}

    def __init_subclass__(cls, **kwargs):
        """Register metadata attribute error results."""
        super().__init_subclass__(**kwargs)
        if cls.attr is not None:
            setting = cls.results.setdefault(cls.attr, cls)
            if setting != cls:
                raise ValueError(f"metadata attribute {cls.attr!r} already registered: {setting!r}")
        else:
            raise ValueError(f"class missing metadata attributes: {cls!r}")

    def __init__(self, attr, msg, **kwargs):
        super().__init__(**kwargs)
        self.attr = attr
        self.msg = str(msg)

    @property
    def desc(self):
        return self.msg