6f2c076c32907785a52a4b95bf62fe3390f5928c
[portage.git] / pym / repoman / checks.py
1 # repoman: Checks
2 # Copyright 2007 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4 # $Id$
5
6 """This module contains functions used in Repoman to ascertain the quality
7 and correctness of an ebuild."""
8
9 import os
10 import re
11 import time
12 import repoman.errors as errors
13
14 class LineCheck(object):
15         """Run a check on a line of an ebuild."""
16         """A regular expression to determine whether to ignore the line"""
17         ignore_line = False
18
19         def new(self, pkg):
20                 pass
21
22         def check(self, num, line):
23                 """Run the check on line and return error if there is one"""
24                 if self.re.match(line):
25                         return self.error
26
27         def end(self):
28                 pass
29
30 class EbuildHeader(LineCheck):
31         """Ensure ebuilds have proper headers
32                 Copyright header errors
33                 CVS header errors
34                 License header errors
35         
36         Args:
37                 modification_year - Year the ebuild was last modified
38         """
39
40         repoman_check_name = 'ebuild.badheader'
41
42         gentoo_copyright = r'^# Copyright ((1999|200\d)-)?%s Gentoo Foundation$'
43         # Why a regex here, use a string match
44         # gentoo_license = re.compile(r'^# Distributed under the terms of the GNU General Public License v2$')
45         gentoo_license = r'# Distributed under the terms of the GNU General Public License v2'
46         cvs_header = re.compile(r'^#\s*\$Header.*\$$')
47
48         def new(self, pkg):
49                 self.modification_year = str(time.gmtime(pkg.mtime)[0])
50                 self.gentoo_copyright_re = re.compile(
51                         self.gentoo_copyright % self.modification_year)
52
53         def check(self, num, line):
54                 if num > 2:
55                         return
56                 elif num == 0:
57                         if not self.gentoo_copyright_re.match(line):
58                                 return errors.COPYRIGHT_ERROR
59                 elif num == 1 and line.strip() != self.gentoo_license:
60                         return errors.LICENSE_ERROR
61                 elif num == 2:
62                         if not self.cvs_header.match(line):
63                                 return errors.CVS_HEADER_ERROR
64
65
66 class EbuildWhitespace(LineCheck):
67         """Ensure ebuilds have proper whitespacing"""
68
69         repoman_check_name = 'ebuild.minorsyn'
70
71         ignore_line = re.compile(r'(^$)|(^(\t)*#)')
72         leading_spaces = re.compile(r'^[\S\t]')
73         trailing_whitespace = re.compile(r'.*([\S]$)')  
74
75         def check(self, num, line):
76                 if not self.leading_spaces.match(line):
77                         return errors.LEADING_SPACES_ERROR
78                 if not self.trailing_whitespace.match(line):
79                         return errors.TRAILING_WHITESPACE_ERROR
80
81
82 class EbuildQuote(LineCheck):
83         """Ensure ebuilds have valid quoting around things like D,FILESDIR, etc..."""
84
85         repoman_check_name = 'ebuild.minorsyn'
86         _message_commands = ["die", "echo", "eerror",
87                 "einfo", "elog", "eqawarn", "ewarn"]
88         _message_re = re.compile(r'\s(' + "|".join(_message_commands) + \
89                 r')\s+"[^"]*"\s*$')
90         _ignored_commands = ["local", "export"] + _message_commands
91         ignore_line = re.compile(r'(^$)|(^\s*#.*)|(^\s*\w+=.*)' + \
92                 r'|(^\s*(' + "|".join(_ignored_commands) + r')\s+)')
93         var_names = ["D", "DISTDIR", "FILESDIR", "S", "T", "ROOT", "WORKDIR"]
94
95         # variables for games.eclass
96         var_names += ["Ddir", "dir", "GAMES_PREFIX_OPT", "GAMES_DATADIR",
97                 "GAMES_DATADIR_BASE", "GAMES_SYSCONFDIR", "GAMES_STATEDIR",
98                 "GAMES_LOGDIR", "GAMES_BINDIR"]
99
100         var_names = "(%s)" % "|".join(var_names)
101         var_reference = re.compile(r'\$(\{'+var_names+'\}|' + \
102                 var_names + '\W)')
103         missing_quotes = re.compile(r'(\s|^)[^"\'\s]*\$\{?' + var_names + \
104                 r'\}?[^"\'\s]*(\s|$)')
105         cond_begin =  re.compile(r'(^|\s+)\[\[($|\\$|\s+)')
106         cond_end =  re.compile(r'(^|\s+)\]\]($|\\$|\s+)')
107         
108         def check(self, num, line):
109                 if self.var_reference.search(line) is None:
110                         return
111                 # There can be multiple matches / violations on a single line. We
112                 # have to make sure none of the matches are violators. Once we've
113                 # found one violator, any remaining matches on the same line can
114                 # be ignored.
115                 pos = 0
116                 while pos <= len(line) - 1:
117                         missing_quotes = self.missing_quotes.search(line, pos)
118                         if not missing_quotes:
119                                 break
120                         # If the last character of the previous match is a whitespace
121                         # character, that character may be needed for the next
122                         # missing_quotes match, so search overlaps by 1 character.
123                         group = missing_quotes.group()
124                         pos = missing_quotes.end() - 1
125
126                         # Filter out some false positives that can
127                         # get through the missing_quotes regex.
128                         if self.var_reference.search(group) is None:
129                                 continue
130
131                         # Filter matches that appear to be an
132                         # argument to a message command.
133                         # For example: false || ewarn "foo $WORKDIR/bar baz"
134                         message_match = self._message_re.search(line)
135                         if message_match is not None and \
136                                 message_match.start() < pos and \
137                                 message_match.end() > pos:
138                                 break
139
140                         # This is an attempt to avoid false positives without getting
141                         # too complex, while possibly allowing some (hopefully
142                         # unlikely) violations to slip through. We just assume
143                         # everything is correct if the there is a ' [[ ' or a ' ]] '
144                         # anywhere in the whole line (possibly continued over one
145                         # line).
146                         if self.cond_begin.search(line) is not None:
147                                 continue
148                         if self.cond_end.search(line) is not None:
149                                 continue
150
151                         # Any remaining matches on the same line can be ignored.
152                         return errors.MISSING_QUOTES_ERROR
153
154
155 class EbuildAssignment(LineCheck):
156         """Ensure ebuilds don't assign to readonly variables."""
157
158         repoman_check_name = 'variable.readonly'
159
160         readonly_assignment = re.compile(r'^\s*(export\s+)?(A|CATEGORY|P|PV|PN|PR|PVR|PF|D|WORKDIR|FILESDIR|FEATURES|USE)=')
161         line_continuation = re.compile(r'([^#]*\S)(\s+|\t)\\$')
162         ignore_line = re.compile(r'(^$)|(^(\t)*#)')
163
164         def __init__(self):
165                 self.previous_line = None
166
167         def check(self, num, line):
168                 match = self.readonly_assignment.match(line)
169                 e = None
170                 if match and (not self.previous_line or not self.line_continuation.match(self.previous_line)):
171                         e = errors.READONLY_ASSIGNMENT_ERROR
172                 self.previous_line = line
173                 return e
174
175
176 class EbuildNestedDie(LineCheck):
177         """Check ebuild for nested die statements (die statements in subshells"""
178         
179         repoman_check_name = 'ebuild.nesteddie'
180         nesteddie_re = re.compile(r'^[^#]*\([^)]*\bdie\b')
181         
182         def check(self, num, line):
183                 if self.nesteddie_re.match(line):
184                         return errors.NESTED_DIE_ERROR
185
186
187 class EbuildUselessDodoc(LineCheck):
188         """Check ebuild for useless files in dodoc arguments."""
189         repoman_check_name = 'ebuild.minorsyn'
190         uselessdodoc_re = re.compile(
191                 r'^\s*dodoc(\s+|\s+.*\s+)(ABOUT-NLS|COPYING|LICENSE)($|\s)')
192
193         def check(self, num, line):
194                 match = self.uselessdodoc_re.match(line)
195                 if match:
196                         return "Useless dodoc '%s'" % (match.group(2), ) + " on line: %d"
197
198
199 class EbuildUselessCdS(LineCheck):
200         """Check for redundant cd ${S} statements"""
201         repoman_check_name = 'ebuild.minorsyn'
202         method_re = re.compile(r'^\s*src_(compile|install|test)\s*\(\)')
203         cds_re = re.compile(r'^\s*cd\s+("\$(\{S\}|S)"|\$(\{S\}|S))\s')
204
205         def __init__(self):
206                 self.check_next_line = False
207
208         def check(self, num, line):
209                 if self.check_next_line:
210                         self.check_next_line = False
211                         if self.cds_re.match(line):
212                                 return errors.REDUNDANT_CD_S_ERROR
213                 elif self.method_re.match(line):
214                         self.check_next_line = True
215
216 class EbuildPatches(LineCheck):
217         """Ensure ebuilds use bash arrays for PATCHES to ensure white space safety"""
218         repoman_check_name = 'ebuild.patches'
219         re = re.compile(r'^\s*PATCHES=[^\(]')
220         error = errors.PATCHES_ERROR
221
222 class EbuildQuotedA(LineCheck):
223         """Ensure ebuilds have no quoting around ${A}"""
224
225         repoman_check_name = 'ebuild.minorsyn'
226         a_quoted = re.compile(r'.*\"\$(\{A\}|A)\"')
227
228         def check(self, num, line):
229                 match = self.a_quoted.match(line)
230                 if match:
231                         return "Quoted \"${A}\" on line: %d"
232
233 class InheritAutotools(LineCheck):
234         """
235         Make sure appropriate functions are called in
236         ebuilds that inherit autotools.eclass.
237         """
238
239         repoman_check_name = 'inherit.autotools'
240         ignore_line = re.compile(r'(^|\s*)#')
241         _inherit_autotools_re = re.compile(r'^\s*inherit\s(.*\s)?autotools(\s|$)')
242         _autotools_funcs = (
243                 "eaclocal", "eautoconf", "eautoheader",
244                 "eautomake", "eautoreconf", "_elibtoolize")
245         _autotools_func_re = re.compile(r'\b(' + \
246                 "|".join(_autotools_funcs) + r')\b')
247         # Exempt eclasses:
248         # git - An EGIT_BOOTSTRAP variable may be used to call one of
249         #       the autotools functions.
250         # subversion - An ESVN_BOOTSTRAP variable may be used to call one of
251         #       the autotools functions.
252         _exempt_eclasses = frozenset(["git", "subversion"])
253
254         def new(self, pkg):
255                 self._inherit_autotools = None
256                 self._autotools_func_call = None
257                 self._disabled = self._exempt_eclasses.intersection(pkg.inherited)
258
259         def check(self, num, line):
260                 if self._disabled:
261                         return
262                 if self._inherit_autotools is None:
263                         self._inherit_autotools = self._inherit_autotools_re.match(line)
264                 if self._inherit_autotools is not None and \
265                         self._autotools_func_call is None:
266                         self._autotools_func_call = self._autotools_func_re.search(line)
267
268         def end(self):
269                 if self._inherit_autotools and self._autotools_func_call is None:
270                         yield 'no eauto* function called'
271
272 class IUseUndefined(LineCheck):
273         """
274         Make sure the ebuild defines IUSE (style guideline
275         says to define IUSE even when empty).
276         """
277
278         repoman_check_name = 'IUSE.undefined'
279         _iuse_def_re = re.compile(r'^IUSE=.*')
280
281         def new(self, pkg):
282                 self._iuse_def = None
283
284         def check(self, num, line):
285                 if self._iuse_def is None:
286                         self._iuse_def = self._iuse_def_re.match(line)
287
288         def end(self):
289                 if self._iuse_def is None:
290                         yield 'IUSE is not defined'
291
292 class EMakeParallelDisabled(LineCheck):
293         """Check for emake -j1 calls which disable parallelization."""
294         repoman_check_name = 'upstream.workaround'
295         re = re.compile(r'^\s*emake\s+-j\s*1\s')
296         error = errors.EMAKE_PARALLEL_DISABLED
297
298 class DeprecatedBindnowFlags(LineCheck):
299         """Check for calls to the deprecated bindnow-flags function."""
300         repoman_check_name = 'ebuild.minorsyn'
301         re = re.compile(r'.*\$\(bindnow-flags\)')
302         error = errors.DEPRECATED_BINDNOW_FLAGS
303
304 _constant_checks = tuple((c() for c in (
305         EbuildHeader, EbuildWhitespace, EbuildQuote,
306         EbuildAssignment, EbuildUselessDodoc,
307         EbuildUselessCdS, EbuildNestedDie,
308         EbuildPatches, EbuildQuotedA,
309         IUseUndefined, InheritAutotools,
310         EMakeParallelDisabled, DeprecatedBindnowFlags)))
311
312 def run_checks(contents, pkg):
313         checks = _constant_checks
314
315         for lc in checks:
316                 lc.new(pkg)
317         for num, line in enumerate(contents):
318                 for lc in checks:
319                         ignore = lc.ignore_line
320                         if not ignore or not ignore.match(line):
321                                 e = lc.check(num, line)
322                                 if e:
323                                         yield lc.repoman_check_name, e % (num + 1)
324         for lc in checks:
325                 i = lc.end()
326                 if i is not None:
327                         for e in i:
328                                 yield lc.repoman_check_name, e