• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rpm-software-management / rpmlint / 27743531775

18 Jun 2026 07:23AM UTC coverage: 88.143% (+0.2%) from 87.933%
27743531775

push

github

6096 of 6916 relevant lines covered (88.14%)

3.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.35
/rpmlint/filter.py
1
from pathlib import Path
4✔
2
import re
4✔
3
import textwrap
4✔
4

5
from rpmlint.color import Color
4✔
6
from rpmlint.helpers import print_warning
4✔
7

8
try:
4✔
9
    import tomllib
4✔
10
except ImportError:
×
11
    import tomli as tomllib
×
12

13

14
class Filter:
4✔
15
    """
16
    Handle all printing/formatting/filtering of the rpmlint output.
17

18
    Nothing gets printed out until the end of all runs and all errors are
19
    sorted and formatted based on the rules specified by the user/config
20
    """
21

22
    def __init__(self, config):
4✔
23
        """
24
        Initialize options from configuration and load rpmlint descriptions.
25

26
        Args:
27
            config: Config object with parsed rpmlint configuration.
28
        """
29
        # badness stuff
30
        self.badness_threshold = config.configuration['BadnessThreshold']
4✔
31
        self.badness = config.configuration['Scoring']
4✔
32
        self.strict = config.strict
4✔
33
        # list of filter regexes
34
        self.filters_regexes = [re.compile(f) for f in config.configuration['Filters']]
4✔
35
        self.filter_titles = set(config.configuration['FilterErrorTitles'])
4✔
36
        # list of blocked filters
37
        self.blocked_filters = set(config.configuration['BlockedFilters'])
4✔
38
        # set of filters that are actually used in add_info
39
        self.used_filters = set()
4✔
40
        self.rpmlintrc_filters = config.rpmlintrc_filters
4✔
41
        # informative or quiet
42
        self.info = config.info
4✔
43
        # How many bad hits we already collected while collecting issues
44
        self.score = 0
4✔
45
        # Dictionary containing mapped values of descriptions for the errors.
46
        self.error_details = {}
4✔
47
        # Load it up with the toml descriptions
48
        self.error_details.update(self._load_descriptions())
4✔
49
        # Counter of how many issues we encountered
4✔
50
        self.printed_messages = {'I': 0, 'W': 0, 'E': 0}
51
        # Number of promoted warnings and infos to errors
52
        self.promoted_to_error = 0
4✔
53
        # Number of messaged that are filtered out
54
        self.filtered_out = 0
4✔
55
        # Messages
56
        self.results = []
4✔
57

58
    @staticmethod
4✔
59
    def _load_descriptions():
60
        """
4✔
61
        Load rpmlint error/warning description texts from toml files.
4✔
62

63
        Detailed description for every rpmlint error/warning is stored in
64
        descriptions/<check_name>.toml file.
65

66
        Returns:
67
            A dictionary mapping error/warning/info names to their
68
            descriptions.
69
         """
70
        descriptions = {}
71
        descr_folder = Path(__file__).parent / 'descriptions'
72
        try:
4✔
73
            for description_file in sorted(descr_folder.glob('*.toml')):
4✔
74
                with open(description_file, 'rb') as f:
4✔
75
                    descriptions.update(tomllib.load(f))
4✔
76
        except tomllib.TOMLDecodeError as terr:
4✔
77
            print_warning(f'(none): W: unable to parse description files: {terr}')
4✔
78
        return descriptions
×
79

×
80
    def add_info(self, level, package, rpmlint_issue, *details):
4✔
81
        """
82
        Format rpmlint issue output and add it to self.results.
4✔
83

84
        It creates formatted and colored output consisting of all information
85
        about rpmlint issue given by the arguments.
86

87
        Args:
4✔
88
            level: A string with level of the rpmlint issue ('E' - Error,
89
                   'W' - Warning, 'I' - Info
4✔
90
            package: Pkg object representing processed package
4✔
91
            rpmlint_issue: A string representing the name of the rpmlint
4✔
92
                           issue
4✔
93
            *details: Details of the rpmlint issue
4✔
94
        """
4✔
95

4✔
96
        if ' ' in rpmlint_issue:
4✔
97
            raise ValueError(f'Space cannot be part of an issue name: "{rpmlint_issue}"')
4✔
98

4✔
99
        # filename in some cases can contain tmp paths and we don't need it
100
        # for the printout
4✔
101
        filename = Path(package.name).name
102
        # we can get badness treshold
103
        badness = None
104
        if rpmlint_issue in self.badness:
105
            badness = int(self.badness[rpmlint_issue])
106
            # If we have any badness configured then we 'stricten' and call the
107
            # result Error. Otherwise we downgrade the error to Warn.
108
            if badness > 0:
109
                level = 'E'
110
            elif level == 'E':
111
                level = 'W'
112
        # allow strict reporting where we override levels and treat everything
113
        # as an error
114
        if self.strict:
115
            if level != 'E':
116
                self.promoted_to_error += 1
4✔
117
            level = 'E'
×
118

119
        if badness is None:
120
            badness = 1 if level == 'E' else 0
121
        # set coloring
4✔
122
        if level == 'E':
123
            lvl_color = Color.Red
4✔
124
        elif level == 'W':
4✔
125
            lvl_color = Color.Yellow
4✔
126
        else:
127
            lvl_color = Color.Bold
128
        # compile the message
4✔
129
        line = f'{package.current_linenum}:' if package.current_linenum else ''
4✔
130
        arch = f'.{package.arch}' if package.arch else ''
4✔
131
        bad_output = f' (Badness: {badness})' if badness > 1 else ''
4✔
132
        detail_output = ''
133
        for detail in details:
134
            if detail:
4✔
135
                detail_output += f' {detail}'
4✔
136
        result = f'{Color.Bold}{filename}{arch}:{line}{Color.Reset} {lvl_color}{level}: {rpmlint_issue}{Color.Reset}{bad_output}{detail_output}'
4✔
137

4✔
138
        # filter by the result message
139
        result_no_color = f'{filename}{arch}:{line} {level}: {rpmlint_issue}{detail_output}'
4✔
140
        # unused-rpmlintrc-filter warnings should be skipped
4✔
141
        if rpmlint_issue != 'unused-rpmlintrc-filter' and rpmlint_issue not in self.blocked_filters:
142
            if rpmlint_issue in self.filter_titles:
4✔
143
                self.filtered_out += 1
4✔
144
                return
4✔
145
            for f in self.filters_regexes:
4✔
146
                if f.search(result_no_color):
147
                    self.used_filters.add(f.pattern)
4✔
148
                    self.filtered_out += 1
149
                    return
4✔
150

4✔
151
        # raise the counters
4✔
152
        self.score += badness
4✔
153
        self.printed_messages[level] += 1
4✔
154

4✔
155
        self.results.append(result)
4✔
156

4✔
157
    def print_results(self, results, config=None):
158
        """
159
        Provide all the information about the specified package.
4✔
160

161
        If there is description to be provided it needs to be provided only
4✔
162
        once per rpmlint_issue.
4✔
163

×
164
        Args:
×
165
            results: A list with rpmlint messages.
4✔
166
            config: parsed configuration file that is used as a source for
4✔
167
                    new description strings
4✔
168

4✔
169
        Returns:
4✔
170
            A string with final rpmlint output.
171
        """
172
        output = ''
4✔
173
        results.sort(key=self.__diag_sortkey, reverse=True)
4✔
174
        last_issue = ''
175
        for diag in results:
4✔
176
            if self.info:
177
                rpmlint_issue = diag.split()[2].rstrip(Color.Reset)
4✔
178
                # print out details for each rpmlint_issue we had
179
                if rpmlint_issue != last_issue:
180
                    if last_issue:
181
                        output += self.get_description(last_issue, config)
182
                    last_issue = rpmlint_issue
183
            output += diag + '\n'
184
        if self.info and last_issue:
185
            output += self.get_description(last_issue, config)
186
        # normalize the output as rpm 4.15 uses surrogates
187
        output = output.encode('utf-8', errors='surrogateescape').decode('utf-8', errors='replace')
188

189
        return output
190

191
    def get_description(self, rpmlint_issue, config=None):
192
        """
4✔
193
        Get description for specified rpmlint issue (error, warning or info).
4✔
194

4✔
195
        Args:
4✔
196
            rpmlint_issue: A string with the rpmlint error/warning/info name
4✔
197
            config: parsed configuration file that is used as a source for
4✔
198
                    custom description strings ([Descriptions] table in toml
199
                    syntax)
4✔
200

4✔
201
        Returns:
4✔
202
            A string with description for specified rpmlint issue. Empty
4✔
203
            content does not cause an issue and we just return empty content
4✔
204
        """
4✔
205
        description = ''
4✔
206
        if rpmlint_issue in self.error_details:
207

4✔
208
            # Update rpmlint error descriptions from configuration file
209
            if config and config.configuration.get('Descriptions').get(rpmlint_issue):
4✔
210
                self.error_details[rpmlint_issue] = config.configuration['Descriptions'][rpmlint_issue]
211

4✔
212
            # we need 2 enters at the end for whitespace purposes
213
            description = textwrap.fill(self.error_details[rpmlint_issue], 78, break_on_hyphens=False) + '\n\n'
214
        return description
215

216
    def __diag_sortkey(self, x):
217
        """
218
        Sorting helper, xs[1] is packagename line architecture
219
                        xs[2] is the reason of the error
220
        """
221
        xs = x.split()
222
        return (xs[2], xs[1])
223

224
    def validate_filters(self, pkg):
225
        for f in self.rpmlintrc_filters:
4✔
226
            if f not in self.used_filters:
4✔
227
                self.add_info('E', pkg, 'unused-rpmlintrc-filter', f'"{f}"')
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc