Remove unused os import.
[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 re
10 import time
11 import repoman.errors as errors
12
13 class LineCheck(object):
14         """Run a check on a line of an ebuild."""
15         """A regular expression to determine whether to ignore the line"""
16         ignore_line = False
17
18         def new(self, pkg):
19                 pass
20
21         def check_eapi(self, eapi):
22                 """ returns if the check should be run in the given EAPI (default is True) """
23                 return True
24
25         def check(self, num, line):
26                 """Run the check on line and return error if there is one"""
27                 if self.re.match(line):
28                         return self.error
29
30         def end(self):
31                 pass
32
33 class EbuildHeader(LineCheck):
34         """Ensure ebuilds have proper headers
35                 Copyright header errors
36                 CVS header errors
37                 License header errors
38         
39         Args:
40                 modification_year - Year the ebuild was last modified
41         """
42
43         repoman_check_name = 'ebuild.badheader'
44
45         gentoo_copyright = r'^# Copyright ((1999|200\d)-)?%s Gentoo Foundation$'
46         # Why a regex here, use a string match
47         # gentoo_license = re.compile(r'^# Distributed under the terms of the GNU General Public License v2$')
48         gentoo_license = r'# Distributed under the terms of the GNU General Public License v2'
49         cvs_header = re.compile(r'^#\s*\$Header.*\$$')
50
51         def new(self, pkg):
52                 self.modification_year = str(time.gmtime(pkg.mtime)[0])
53                 self.gentoo_copyright_re = re.compile(
54                         self.gentoo_copyright % self.modification_year)
55
56         def check(self, num, line):
57                 if num > 2:
58                         return
59                 elif num == 0:
60                         if not self.gentoo_copyright_re.match(line):
61                                 return errors.COPYRIGHT_ERROR
62                 elif num == 1 and line.strip() != self.gentoo_license:
63                         return errors.LICENSE_ERROR
64                 elif num == 2:
65                         if not self.cvs_header.match(line):
66                                 return errors.CVS_HEADER_ERROR
67
68
69 class EbuildWhitespace(LineCheck):
70         """Ensure ebuilds have proper whitespacing"""
71
72         repoman_check_name = 'ebuild.minorsyn'
73
74         ignore_line = re.compile(r'(^$)|(^(\t)*#)')
75         leading_spaces = re.compile(r'^[\S\t]')
76         trailing_whitespace = re.compile(r'.*([\S]$)')  
77
78         def check(self, num, line):
79                 if self.leading_spaces.match(line) is None:
80                         return errors.LEADING_SPACES_ERROR
81                 if self.trailing_whitespace.match(line) is None:
82                         return errors.TRAILING_WHITESPACE_ERROR
83
84 class EbuildBlankLine(LineCheck):
85         repoman_check_name = 'ebuild.minorsyn'
86         blank_line = re.compile(r'^$')
87
88         def new(self, pkg):
89                 self.line_is_blank = False
90
91         def check(self, num, line):
92                 if self.line_is_blank and self.blank_line.match(line):
93                         return 'Useless blank line on line: %d'
94                 if self.blank_line.match(line):
95                         self.line_is_blank = True
96                 else:
97                         self.line_is_blank = False
98
99         def end(self):
100                 if self.line_is_blank:
101                         yield 'Useless blank line on last line'
102
103 class EbuildQuote(LineCheck):
104         """Ensure ebuilds have valid quoting around things like D,FILESDIR, etc..."""
105
106         repoman_check_name = 'ebuild.minorsyn'
107         _message_commands = ["die", "echo", "eerror",
108                 "einfo", "elog", "eqawarn", "ewarn"]
109         _message_re = re.compile(r'\s(' + "|".join(_message_commands) + \
110                 r')\s+"[^"]*"\s*$')
111         _ignored_commands = ["local", "export"] + _message_commands
112         ignore_line = re.compile(r'(^$)|(^\s*#.*)|(^\s*\w+=.*)' + \
113                 r'|(^\s*(' + "|".join(_ignored_commands) + r')\s+)')
114         var_names = ["D", "DISTDIR", "FILESDIR", "S", "T", "ROOT", "WORKDIR"]
115
116         # variables for games.eclass
117         var_names += ["Ddir", "GAMES_PREFIX_OPT", "GAMES_DATADIR",
118                 "GAMES_DATADIR_BASE", "GAMES_SYSCONFDIR", "GAMES_STATEDIR",
119                 "GAMES_LOGDIR", "GAMES_BINDIR"]
120
121         var_names = "(%s)" % "|".join(var_names)
122         var_reference = re.compile(r'\$(\{'+var_names+'\}|' + \
123                 var_names + '\W)')
124         missing_quotes = re.compile(r'(\s|^)[^"\'\s]*\$\{?' + var_names + \
125                 r'\}?[^"\'\s]*(\s|$)')
126         cond_begin =  re.compile(r'(^|\s+)\[\[($|\\$|\s+)')
127         cond_end =  re.compile(r'(^|\s+)\]\]($|\\$|\s+)')
128         
129         def check(self, num, line):
130                 if self.var_reference.search(line) is None:
131                         return
132                 # There can be multiple matches / violations on a single line. We
133                 # have to make sure none of the matches are violators. Once we've
134                 # found one violator, any remaining matches on the same line can
135                 # be ignored.
136                 pos = 0
137                 while pos <= len(line) - 1:
138                         missing_quotes = self.missing_quotes.search(line, pos)
139                         if not missing_quotes:
140                                 break
141                         # If the last character of the previous match is a whitespace
142                         # character, that character may be needed for the next
143                         # missing_quotes match, so search overlaps by 1 character.
144                         group = missing_quotes.group()
145                         pos = missing_quotes.end() - 1
146
147                         # Filter out some false positives that can
148                         # get through the missing_quotes regex.
149                         if self.var_reference.search(group) is None:
150                                 continue
151
152                         # Filter matches that appear to be an
153                         # argument to a message command.
154                         # For example: false || ewarn "foo $WORKDIR/bar baz"
155                         message_match = self._message_re.search(line)
156                         if message_match is not None and \
157                                 message_match.start() < pos and \
158                                 message_match.end() > pos:
159                                 break
160
161                         # This is an attempt to avoid false positives without getting
162                         # too complex, while possibly allowing some (hopefully
163                         # unlikely) violations to slip through. We just assume
164                         # everything is correct if the there is a ' [[ ' or a ' ]] '
165                         # anywhere in the whole line (possibly continued over one
166                         # line).
167                         if self.cond_begin.search(line) is not None:
168                                 continue
169                         if self.cond_end.search(line) is not None:
170                                 continue
171
172                         # Any remaining matches on the same line can be ignored.
173                         return errors.MISSING_QUOTES_ERROR
174
175
176 class EbuildAssignment(LineCheck):
177         """Ensure ebuilds don't assign to readonly variables."""
178
179         repoman_check_name = 'variable.readonly'
180
181         readonly_assignment = re.compile(r'^\s*(export\s+)?(A|CATEGORY|P|PV|PN|PR|PVR|PF|D|WORKDIR|FILESDIR|FEATURES|USE)=')
182         line_continuation = re.compile(r'([^#]*\S)(\s+|\t)\\$')
183         ignore_line = re.compile(r'(^$)|(^(\t)*#)')
184
185         def __init__(self):
186                 self.previous_line = None
187
188         def check(self, num, line):
189                 match = self.readonly_assignment.match(line)
190                 e = None
191                 if match and (not self.previous_line or not self.line_continuation.match(self.previous_line)):
192                         e = errors.READONLY_ASSIGNMENT_ERROR
193                 self.previous_line = line
194                 return e
195
196
197 class EbuildNestedDie(LineCheck):
198         """Check ebuild for nested die statements (die statements in subshells"""
199         
200         repoman_check_name = 'ebuild.nesteddie'
201         nesteddie_re = re.compile(r'^[^#]*\s\(\s[^)]*\bdie\b')
202         
203         def check(self, num, line):
204                 if self.nesteddie_re.match(line):
205                         return errors.NESTED_DIE_ERROR
206
207
208 class EbuildUselessDodoc(LineCheck):
209         """Check ebuild for useless files in dodoc arguments."""
210         repoman_check_name = 'ebuild.minorsyn'
211         uselessdodoc_re = re.compile(
212                 r'^\s*dodoc(\s+|\s+.*\s+)(ABOUT-NLS|COPYING|LICENSE)($|\s)')
213
214         def check(self, num, line):
215                 match = self.uselessdodoc_re.match(line)
216                 if match:
217                         return "Useless dodoc '%s'" % (match.group(2), ) + " on line: %d"
218
219
220 class EbuildUselessCdS(LineCheck):
221         """Check for redundant cd ${S} statements"""
222         repoman_check_name = 'ebuild.minorsyn'
223         method_re = re.compile(r'^\s*src_(prepare|configure|compile|install|test)\s*\(\)')
224         cds_re = re.compile(r'^\s*cd\s+("\$(\{S\}|S)"|\$(\{S\}|S))\s')
225
226         def __init__(self):
227                 self.check_next_line = False
228
229         def check(self, num, line):
230                 if self.check_next_line:
231                         self.check_next_line = False
232                         if self.cds_re.match(line):
233                                 return errors.REDUNDANT_CD_S_ERROR
234                 elif self.method_re.match(line):
235                         self.check_next_line = True
236
237 class EapiDefinition(LineCheck):
238         """ Check that EAPI is defined before inherits"""
239         repoman_check_name = 'EAPI.definition'
240
241         eapi_re = re.compile(r'^EAPI=')
242         inherit_re = re.compile(r'^\s*inherit\s')
243
244         def new(self, pkg):
245                 self.inherit_line = None
246
247         def check(self, num, line):
248                 if self.eapi_re.match(line) is not None:
249                         if self.inherit_line is not None:
250                                 return errors.EAPI_DEFINED_AFTER_INHERIT
251                 elif self.inherit_re.match(line) is not None:
252                         self.inherit_line = line
253
254 class EbuildPatches(LineCheck):
255         """Ensure ebuilds use bash arrays for PATCHES to ensure white space safety"""
256         repoman_check_name = 'ebuild.patches'
257         re = re.compile(r'^\s*PATCHES=[^\(]')
258         error = errors.PATCHES_ERROR
259
260 class EbuildQuotedA(LineCheck):
261         """Ensure ebuilds have no quoting around ${A}"""
262
263         repoman_check_name = 'ebuild.minorsyn'
264         a_quoted = re.compile(r'.*\"\$(\{A\}|A)\"')
265
266         def check(self, num, line):
267                 match = self.a_quoted.match(line)
268                 if match:
269                         return "Quoted \"${A}\" on line: %d"
270
271 class ImplicitRuntimeDeps(LineCheck):
272         """
273         Detect the case where DEPEND is set and RDEPEND is unset in the ebuild,
274         since this triggers implicit RDEPEND=$DEPEND assignment.
275         """
276
277         _assignment_re = re.compile(r'^\s*(R?DEPEND)=')
278
279         def new(self, pkg):
280                 # RDEPEND=DEPEND is no longer available in EAPI=3
281                 if pkg.metadata['EAPI'] in ('0', '1', '2'):
282                         self.repoman_check_name = 'RDEPEND.implicit'
283                 else:
284                         self.repoman_check_name = 'EAPI.incompatible'
285                 self._rdepend = False
286                 self._depend = False
287
288         def check(self, num, line):
289                 if not self._rdepend:
290                         m = self._assignment_re.match(line)
291                         if m is None:
292                                 pass
293                         elif m.group(1) == "RDEPEND":
294                                 self._rdepend = True
295                         elif m.group(1) == "DEPEND":
296                                 self._depend = True
297
298         def end(self):
299                 if self._depend and not self._rdepend:
300                         yield 'RDEPEND is not explicitly assigned'
301
302 class InheritAutotools(LineCheck):
303         """
304         Make sure appropriate functions are called in
305         ebuilds that inherit autotools.eclass.
306         """
307
308         repoman_check_name = 'inherit.autotools'
309         ignore_line = re.compile(r'(^|\s*)#')
310         _inherit_autotools_re = re.compile(r'^\s*inherit\s(.*\s)?autotools(\s|$)')
311         _autotools_funcs = (
312                 "eaclocal", "eautoconf", "eautoheader",
313                 "eautomake", "eautoreconf", "_elibtoolize")
314         _autotools_func_re = re.compile(r'\b(' + \
315                 "|".join(_autotools_funcs) + r')\b')
316         # Exempt eclasses:
317         # git - An EGIT_BOOTSTRAP variable may be used to call one of
318         #       the autotools functions.
319         # subversion - An ESVN_BOOTSTRAP variable may be used to call one of
320         #       the autotools functions.
321         _exempt_eclasses = frozenset(["git", "subversion"])
322
323         def new(self, pkg):
324                 self._inherit_autotools = None
325                 self._autotools_func_call = None
326                 self._disabled = self._exempt_eclasses.intersection(pkg.inherited)
327
328         def check(self, num, line):
329                 if self._disabled:
330                         return
331                 if self._inherit_autotools is None:
332                         self._inherit_autotools = self._inherit_autotools_re.match(line)
333                 if self._inherit_autotools is not None and \
334                         self._autotools_func_call is None:
335                         self._autotools_func_call = self._autotools_func_re.search(line)
336
337         def end(self):
338                 if self._inherit_autotools and self._autotools_func_call is None:
339                         yield 'no eauto* function called'
340
341 class IUseUndefined(LineCheck):
342         """
343         Make sure the ebuild defines IUSE (style guideline
344         says to define IUSE even when empty).
345         """
346
347         repoman_check_name = 'IUSE.undefined'
348         _iuse_def_re = re.compile(r'^IUSE=.*')
349
350         def new(self, pkg):
351                 self._iuse_def = None
352
353         def check(self, num, line):
354                 if self._iuse_def is None:
355                         self._iuse_def = self._iuse_def_re.match(line)
356
357         def end(self):
358                 if self._iuse_def is None:
359                         yield 'IUSE is not defined'
360
361 class EMakeParallelDisabled(LineCheck):
362         """Check for emake -j1 calls which disable parallelization."""
363         repoman_check_name = 'upstream.workaround'
364         re = re.compile(r'^\s*emake\s+.*-j\s*1\b')
365         error = errors.EMAKE_PARALLEL_DISABLED
366
367 class EMakeParallelDisabledViaMAKEOPTS(LineCheck):
368         """Check for MAKEOPTS=-j1 that disables parallelization."""
369         repoman_check_name = 'upstream.workaround'
370         re = re.compile(r'^\s*MAKEOPTS=(\'|")?.*-j\s*1\b')
371         error = errors.EMAKE_PARALLEL_DISABLED_VIA_MAKEOPTS
372
373 class DeprecatedBindnowFlags(LineCheck):
374         """Check for calls to the deprecated bindnow-flags function."""
375         repoman_check_name = 'ebuild.minorsyn'
376         re = re.compile(r'.*\$\(bindnow-flags\)')
377         error = errors.DEPRECATED_BINDNOW_FLAGS
378
379 class WantAutoDefaultValue(LineCheck):
380         """Check setting WANT_AUTO* to latest (default value)."""
381         repoman_check_name = 'ebuild.minorsyn'
382         _re = re.compile(r'^WANT_AUTO(CONF|MAKE)=(\'|")?latest')
383
384         def check(self, num, line):
385                 m = self._re.match(line)
386                 if m is not None:
387                         return 'WANT_AUTO' + m.group(1) + \
388                                 ' redundantly set to default value "latest" on line: %d'
389
390 class PhaseCheck(LineCheck):
391         """ basic class for function detection """
392
393         ignore_line = re.compile(r'(^\s*#)')
394         func_end_re = re.compile(r'^\}$')
395         in_phase = ''
396
397         def __init__(self):
398                 self.phases = ('pkg_setup', 'pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm', 'pkg_pretend',
399                         'src_unpack', 'src_prepare', 'src_compile', 'src_test', 'src_install')
400                 phase_re = '('
401                 for phase in self.phases:
402                         phase_re += phase + '|'
403                 phase_re = phase_re[:-1] + ')'
404                 self.phases_re = re.compile(phase_re)
405
406         def check(self, num, line):
407                 m = self.phases_re.match(line)
408                 if m is not None:
409                         self.in_phase = m.group(1)
410                 if self.in_phase != '' and \
411                                 self.func_end_re.match(line) is not None:
412                         self.in_phase = ''
413
414                 return self.phase_check(num, line)
415
416         def phase_check(self, num, line):
417                 """ override this function for your checks """
418                 pass
419
420 class SrcCompileEconf(PhaseCheck):
421         repoman_check_name = 'ebuild.minorsyn'
422         configure_re = re.compile(r'\s(econf|./configure)')
423
424         def check_eapi(self, eapi):
425                 return eapi not in ('0', '1')
426
427         def phase_check(self, num, line):
428                 if self.in_phase == 'src_compile':
429                         m = self.configure_re.match(line)
430                         if m is not None:
431                                 return ("'%s'" % m.group(1)) + \
432                                         " call should be moved to src_configure from line: %d"
433
434 class SrcUnpackPatches(PhaseCheck):
435         repoman_check_name = 'ebuild.minorsyn'
436         src_prepare_tools_re = re.compile(r'\s(e?patch|sed)\s')
437
438         def new(self, pkg):
439                 if pkg.metadata['EAPI'] not in ('0', '1'):
440                         self.eapi = pkg.metadata['EAPI']
441                 else:
442                         self.eapi = None
443                 self.in_src_unpack = None
444
445         def check_eapi(self, eapi):
446                 return eapi not in ('0', '1')
447
448         def phase_check(self, num, line):
449                 if self.in_phase == 'src_unpack':
450                         m = self.src_prepare_tools_re.search(line)
451                         if m is not None:
452                                 return ("'%s'" % m.group(1)) + \
453                                         " call should be moved to src_prepare from line: %d"
454
455 # EAPI-3 checks
456 class Eapi3IncompatibleFuncs(LineCheck):
457         repoman_check_name = 'EAPI.incompatible'
458         ignore_line = re.compile(r'(^\s*#)')
459         banned_commands_re = re.compile(r'^\s*(dosed|dohard)')
460
461         def new(self, pkg):
462                 self.eapi = pkg.metadata['EAPI']
463
464         def check_eapi(self, eapi):
465                 return self.eapi not in ('0', '1', '2')
466
467         def check(self, num, line):
468                 m = self.banned_commands_re.match(line)
469                 if m is not None:
470                         return ("'%s'" % m.group(1)) + \
471                                 " has been banned in EAPI=3 on line: %d"
472
473 class Eapi3GoneVars(LineCheck):
474         repoman_check_name = 'EAPI.incompatible'
475         ignore_line = re.compile(r'(^\s*#)')
476         undefined_vars_re = re.compile(r'.*\$(\{(AA|KV)\}|(AA|KV))')
477
478         def new(self, pkg):
479                 self.eapi = pkg.metadata['EAPI']
480
481         def check_eapi(self, eapi):
482                 return self.eapi not in ('0', '1', '2')
483
484         def check(self, num, line):
485                 m = self.undefined_vars_re.match(line)
486                 if m is not None:
487                         return ("variable '$%s'" % m.group(1)) + \
488                                 " is gone in EAPI=3 on line: %d"
489
490
491 _constant_checks = tuple((c() for c in (
492         EbuildHeader, EbuildWhitespace, EbuildBlankLine, EbuildQuote,
493         EbuildAssignment, EbuildUselessDodoc,
494         EbuildUselessCdS, EbuildNestedDie,
495         EbuildPatches, EbuildQuotedA, EapiDefinition,
496         IUseUndefined, ImplicitRuntimeDeps, InheritAutotools,
497         EMakeParallelDisabled, EMakeParallelDisabledViaMAKEOPTS,
498         DeprecatedBindnowFlags, SrcUnpackPatches, WantAutoDefaultValue,
499         SrcCompileEconf, Eapi3IncompatibleFuncs, Eapi3GoneVars)))
500
501 _here_doc_re = re.compile(r'.*\s<<[-]?(\w+)$')
502
503 def run_checks(contents, pkg):
504         checks = _constant_checks
505         here_doc_delim = None
506
507         for lc in checks:
508                 lc.new(pkg)
509         for num, line in enumerate(contents):
510
511                 # Check if we're inside a here-document.
512                 if here_doc_delim is not None:
513                         if here_doc_delim.match(line):
514                                 here_doc_delim = None
515                 if here_doc_delim is None:
516                         here_doc = _here_doc_re.match(line)
517                         if here_doc is not None:
518                                 here_doc_delim = re.compile(r'^\s*%s$' % here_doc.group(1))
519
520                 if here_doc_delim is None:
521                         # We're not in a here-document.
522                         for lc in checks:
523                                 if lc.check_eapi(pkg.metadata['EAPI']):
524                                         ignore = lc.ignore_line
525                                         if not ignore or not ignore.match(line):
526                                                 e = lc.check(num, line)
527                                                 if e:
528                                                         yield lc.repoman_check_name, e % (num + 1)
529
530         for lc in checks:
531                 i = lc.end()
532                 if i is not None:
533                         for e in i:
534                                 yield lc.repoman_check_name, e