lintian.py 5.21 KB
Newer Older
1
#   lintian.py - lintian plugin
Jonny Lamb's avatar
Jonny Lamb committed
2
#
Baptiste Beauplat's avatar
Baptiste Beauplat committed
3
4
#   This file is part of debexpo -
#   https://salsa.debian.org/mentors.debian.net-team/debexpo
Jonny Lamb's avatar
Jonny Lamb committed
5
#
Jonny Lamb's avatar
Jonny Lamb committed
6
#   Copyright © 2008 Jonny Lamb <jonny@debian.org>
7
#   Copyright © 2012 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
8
#   Copyright © 2020 Baptiste Beauplat <lyknode@cilg.org>
Jonny Lamb's avatar
Jonny Lamb committed
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#
#   Permission is hereby granted, free of charge, to any person
#   obtaining a copy of this software and associated documentation
#   files (the "Software"), to deal in the Software without
#   restriction, including without limitation the rights to use,
#   copy, modify, merge, publish, distribute, sublicense, and/or sell
#   copies of the Software, and to permit persons to whom the
#   Software is furnished to do so, subject to the following
#   conditions:
#
#   The above copyright notice and this permission notice shall be
#   included in all copies or substantial portions of the Software.
#
#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
#   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
#   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
#   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
#   OTHER DEALINGS IN THE SOFTWARE.

31
32
from bisect import insort
from os.path import dirname
33
from collections import defaultdict
34
from subprocess import CalledProcessError, TimeoutExpired
35
36

from debexpo.plugins.models import BasePlugin, PluginSeverity
37
from debexpo.tools.proc import debexpo_exec
38
39
40


class PluginLintian(BasePlugin):
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    levels = {
        'X': {'name': 'Experimental', 'severity': PluginSeverity.info,
              'outcome': 'Package has lintian experimental tags'},
        'P': {'name': 'Pedantic', 'severity': PluginSeverity.info,
              'outcome': 'Package has lintian pedantic tags'},
        'O': {'name': 'Override', 'severity': PluginSeverity.info,
              'outcome': 'Package has overridden lintian tags'},
        'I': {'name': 'Info', 'severity': PluginSeverity.info,
              'outcome': 'Package has lintian informational tags'},
        'W': {'name': 'Warning', 'severity': PluginSeverity.warning,
              'outcome': 'Package has lintian warnings'},
        'E': {'name': 'Error', 'severity': PluginSeverity.error,
              'outcome': 'Package has lintian errors'},
    }
55
56
57
58
59
60
61

    @property
    def name(self):
        return 'lintian'

    def _run_lintian(self, changes, source):
        try:
62
63
            output = debexpo_exec("lintian",
                                  ["-E",
64
65
66
                                   "-I",
                                   "--pedantic",
                                   "--show-overrides",
67
68
                                   # To avoid warnings in the testsuite when
                                   # run as root in CI.
69
                                   "--allow-root",
70
                                   str(changes)],
71
                                  cwd=dirname(changes.filename))
72
73
        except FileNotFoundError:  # pragma: no cover
            self.failed('lintian not found')
74
75
        except TimeoutExpired:
            self.failed('lintian took too much time to run')
76
        except CalledProcessError as e:
77
            self.failed(f'lintian failed to run: {e.stderr}')
78
79
80
81
82

        return output.split('\n')

    def run(self, changes, source):
        tags = self._run_lintian(changes, source)
Jonny Lamb's avatar
Jonny Lamb committed
83

84
85
86
87
88
89
        # Yes, three levels of defaultdict and one of list...
        def defaultdict_defaultdict_list():
            def defaultdict_list():
                return defaultdict(list)
            return defaultdict(defaultdict_list)

90
        lintian_warnings = defaultdict(defaultdict_defaultdict_list)
91
92
93
        lintian_severities = set()
        override_comments = []

94
95
        for tag in tags:
            if not tag:
96
97
                continue

Baptiste Beauplat's avatar
Baptiste Beauplat committed
98
99
            # lintian output is of the form """SEVERITY: package: lintian_tag
            # [lintian tag arguments]""" or """N: Override comment"""
100
101
            if tag.startswith("N: "):
                override_comments.append(tag[3:].strip())
102
                continue
103
104

            severity, package, rest = tag.split(': ', 2)
105
106
107
            lintian_severities.add(severity)
            lintian_tag_data = rest.split()
            lintian_tag = lintian_tag_data[0]
108
            lintian_data = ' '.join(lintian_tag_data[1:])
109
            severity = f'{severity}-{self.levels[severity]["name"]}'
110

111
            if override_comments:
112
113
                comments = ' '.join(override_comments)
                lintian_data += f' (override comment: {comments})'
114
                override_comments = []
115
116
117
118
119
120
121
122

            insort(lintian_warnings[package][severity][lintian_tag],
                   lintian_data)

        lintian_warnings = dict(sorted(lintian_warnings.items()))
        severity = PluginSeverity.info
        outcome = 'Package is lintian clean'

123
124
        for name, level in self.levels.items():
            if name in lintian_severities:
125
126
127
128
129
                severity = level['severity']
                outcome = level['outcome']

        self.add_result('lintian-tags', outcome, lintian_warnings,
                        severity)