summaryrefslogtreecommitdiff
path: root/parabola_repolint/linter.py
blob: 4cd418f545b272bea9eaf86b8289ffc65ae8700d (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
'''
this module provides the linter orchestrator
'''

import importlib
import pkgutil
import logging
import datetime
import socket
import enum


class LinterCheckMeta(type):
    ''' a meta class for linter checks '''

    def __repr__(cls):
        ''' produce a string representation of the check '''
        return cls.name


class LinterCheckBase(metaclass=LinterCheckMeta):
    ''' a base class for linter checks '''

    def __init__(self, linter, cache):
        ''' a default constructor '''
        self._linter = linter
        self._cache = cache
        self._issues = []

    @property
    def issues(self):
        ''' produce the list of issues generated by this check '''
        return self._issues

    def format(self):
        ''' a default formatter for found issues '''
        res = []
        for issue in self._issues:
            res.append('    ' + issue[0] % issue[1:])
        return "\n".join(sorted(res))

    def fixhook_base(self, issue):
        ''' produce the default fixhook base path '''
        return issue[1]

    def fixhook_args(self, issue):
        ''' produce the default fixhook arguments '''
        return issue[2:]

    def __repr__(self):
        ''' produce a string representation of the check '''
        return str(type(self))


class LinterIssue(Exception):
    ''' raised by linter checks to indicate problems '''


class LinterCheckType(enum.Enum):
    ''' possible linter check types '''
    PKGBUILD = 1
    PKGFILE = 2
    PKGENTRY = 3
    SIGNING_KEY = 4
    MASTER_KEY = 5


def _is_linter_check(cls):
    ''' indicate whether a given anything is a linter check class '''
    return (isinstance(cls, type)
            and issubclass(cls, LinterCheckBase)
            and cls != LinterCheckBase)


def _load_linter_checks_from(package_name):
    ''' load a list of classes from the given package '''
    package = importlib.import_module(package_name)

    result = []
    for _, name, _ in pkgutil.walk_packages(package.__path__):
        name = package.__name__ + '.' + name

        logging.debug('loading linter checks from "%s"', name)
        module = importlib.import_module(name)

        for cls in module.__dict__.values():
            if _is_linter_check(cls):
                logging.debug('loaded linter check "%s"', cls)
                result.append(cls)

    return result


class Linter():
    ''' the master linter class '''

    def __init__(self, repo_cache):
        ''' constructor '''
        self._checks = _load_linter_checks_from('parabola_repolint.linter_checks')
        self._enabled_checks = []

        self._cache = repo_cache

        self._start_time = None
        self._end_time = None

    @property
    def checks(self):
        ''' return the names of all supported linter checks '''
        return self._checks

    def register_repo_cache(self, cache):
        ''' store a reference to the repo cache '''
        self._cache = cache

    def load_checks(self, checks):
        ''' initialize the set of enabled linter checks '''
        self._start_time = datetime.datetime.now()

        self._enabled_checks = [c(self, self._cache) for c in self._checks if c.name in checks]
        logging.debug('initialized enabled checks %s', self._enabled_checks)

    def run_checks(self):
        ''' run the previuosly initialized enabled checks '''
        check_funcs = {
            LinterCheckType.PKGBUILD: self._run_check_pkgbuild,
            LinterCheckType.PKGENTRY: self._run_check_pkgentry,
            LinterCheckType.PKGFILE: self._run_check_pkgfile,
            LinterCheckType.SIGNING_KEY: self._run_check_signing_key,
            LinterCheckType.MASTER_KEY: self._run_check_master_key,
        }

        for check_type in LinterCheckType:
            check_func = check_funcs[check_type]
            for check in [c for c in self._enabled_checks if c.check_type == check_type]:
                logging.info('running check %s', check)
                check_func(check)

        self._end_time = datetime.datetime.now()

    def _run_check_pkgbuild(self, check):
        ''' run a PKGBUILD type check '''
        for pkgbuild in self._cache.pkgbuilds:
            self._try_check(check, pkgbuild)

    def _run_check_pkgentry(self, check):
        ''' run a PKGENTRY type check '''
        for pkgentry in self._cache.pkgentries:
            self._try_check(check, pkgentry)

    def _run_check_pkgfile(self, check):
        ''' run a PKGFILE type check '''
        for pkgfile in self._cache.pkgfiles:
            self._try_check(check, pkgfile)

    def _run_check_signing_key(self, check):
        ''' run a SIGNING_KEY type check '''
        for key in self._cache.key_cache.values():
            self._try_check(check, key)

    def _run_check_master_key(self, check):
        ''' run a MASTER_KEY type check '''
        for key in self._cache.keyring:
            self._try_check(check, key)

    # pylint: disable=no-self-use
    def _try_check(self, check, *args, **kwargs):
        ''' run a check and catch any LinterIssue '''
        try:
            check.check(*args, **kwargs)
        except LinterIssue as i:
            check.issues.append(i.args)

    @property
    def triggered_checks(self):
        ''' produce a list of all checks with issues '''
        return [ check for check in self._enabled_checks if check.issues ]

    def format(self):
        ''' return a formatted string of the linter issues '''
        now = self._end_time.strftime("%Y-%m-%d %H:%M:%S")
        out = '''
==============================================================================
This is an auto-generated list of issues in the parabola package repository.
Generated by parabola-repolint on %s at %s
==============================================================================
''' % (socket.gethostname(), now)

        for check in self.triggered_checks:
            header = '%s:\n%s' % (check.header, '-' * (len(check.header) + 1))
            out += '\n\n\n%s\n%s\nissues:\n' % (header, check.__doc__)
            out += check.format()
        return out

    def short_format(self):
        ''' return a (short) formatted string of the linter issues '''
        now = self._end_time.strftime("%Y-%m-%d %H:%M:%S")
        out = 'repolint digest at %s' % now

        for check in self.triggered_checks:
            out += '\n  %s: %i' % (check, len(check.issues))
        out += '\ntotal issues: %i' % self.total_issues

        return out

    @property
    def total_issues(self):
        ''' produce the total number of found issues '''
        res = 0
        for check in self.triggered_checks:
            res += len(check.issues)
        return res

    @property
    def start_time(self):
        ''' produce the start time of the linter '''
        return self._start_time

    @property
    def end_time(self):
        ''' produce the end time of the linter '''
        return self._end_time