InheritEclass: base and cmake-utils exemptions
[portage.git] / pym / repoman / checks.py
1 # repoman: Checks
2 # Copyright 2007-2012 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 """This module contains functions used in Repoman to ascertain the quality
6 and correctness of an ebuild."""
7
8 import codecs
9 from itertools import chain
10 import re
11 import time
12 import repoman.errors as errors
13 import portage
14 from portage.eapi import eapi_supports_prefix, eapi_has_implicit_rdepend, \
15         eapi_has_src_prepare_and_src_configure, eapi_has_dosed_dohard, \
16         eapi_exports_AA
17
18 class LineCheck(object):
19         """Run a check on a line of an ebuild."""
20         """A regular expression to determine whether to ignore the line"""
21         ignore_line = False
22         """True if lines containing nothing more than comments with optional
23         leading whitespace should be ignored"""
24         ignore_comment = True
25
26         def new(self, pkg):
27                 pass
28
29         def check_eapi(self, eapi):
30                 """ returns if the check should be run in the given EAPI (default is True) """
31                 return True
32
33         def check(self, num, line):
34                 """Run the check on line and return error if there is one"""
35                 if self.re.match(line):
36                         return self.error
37
38         def end(self):
39                 pass
40
41 class PhaseCheck(LineCheck):
42         """ basic class for function detection """
43
44         func_end_re = re.compile(r'^\}$')
45         phases_re = re.compile('(%s)' % '|'.join((
46                 'pkg_pretend', 'pkg_setup', 'src_unpack', 'src_prepare',
47                 'src_configure', 'src_compile', 'src_test', 'src_install',
48                 'pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm',
49                 'pkg_config')))
50         in_phase = ''
51
52         def check(self, num, line):
53                 m = self.phases_re.match(line)
54                 if m is not None:
55                         self.in_phase = m.group(1)
56                 if self.in_phase != '' and \
57                                 self.func_end_re.match(line) is not None:
58                         self.in_phase = ''
59
60                 return self.phase_check(num, line)
61
62         def phase_check(self, num, line):
63                 """ override this function for your checks """
64                 pass
65
66 class EbuildHeader(LineCheck):
67         """Ensure ebuilds have proper headers
68                 Copyright header errors
69                 CVS header errors
70                 License header errors
71         
72         Args:
73                 modification_year - Year the ebuild was last modified
74         """
75
76         repoman_check_name = 'ebuild.badheader'
77
78         gentoo_copyright = r'^# Copyright ((1999|2\d\d\d)-)?%s Gentoo Foundation$'
79         # Why a regex here, use a string match
80         # gentoo_license = re.compile(r'^# Distributed under the terms of the GNU General Public License v2$')
81         gentoo_license = '# Distributed under the terms of the GNU General Public License v2'
82         cvs_header = re.compile(r'^# \$Header: .*\$$')
83         ignore_comment = False
84
85         def new(self, pkg):
86                 if pkg.mtime is None:
87                         self.modification_year = r'2\d\d\d'
88                 else:
89                         self.modification_year = str(time.gmtime(pkg.mtime)[0])
90                 self.gentoo_copyright_re = re.compile(
91                         self.gentoo_copyright % self.modification_year)
92
93         def check(self, num, line):
94                 if num > 2:
95                         return
96                 elif num == 0:
97                         if not self.gentoo_copyright_re.match(line):
98                                 return errors.COPYRIGHT_ERROR
99                 elif num == 1 and line.rstrip('\n') != self.gentoo_license:
100                         return errors.LICENSE_ERROR
101                 elif num == 2:
102                         if not self.cvs_header.match(line):
103                                 return errors.CVS_HEADER_ERROR
104
105
106 class EbuildWhitespace(LineCheck):
107         """Ensure ebuilds have proper whitespacing"""
108
109         repoman_check_name = 'ebuild.minorsyn'
110
111         ignore_line = re.compile(r'(^$)|(^(\t)*#)')
112         ignore_comment = False
113         leading_spaces = re.compile(r'^[\S\t]')
114         trailing_whitespace = re.compile(r'.*([\S]$)')  
115
116         def check(self, num, line):
117                 if self.leading_spaces.match(line) is None:
118                         return errors.LEADING_SPACES_ERROR
119                 if self.trailing_whitespace.match(line) is None:
120                         return errors.TRAILING_WHITESPACE_ERROR
121
122 class EbuildBlankLine(LineCheck):
123         repoman_check_name = 'ebuild.minorsyn'
124         ignore_comment = False
125         blank_line = re.compile(r'^$')
126
127         def new(self, pkg):
128                 self.line_is_blank = False
129
130         def check(self, num, line):
131                 if self.line_is_blank and self.blank_line.match(line):
132                         return 'Useless blank line on line: %d'
133                 if self.blank_line.match(line):
134                         self.line_is_blank = True
135                 else:
136                         self.line_is_blank = False
137
138         def end(self):
139                 if self.line_is_blank:
140                         yield 'Useless blank line on last line'
141
142 class EbuildQuote(LineCheck):
143         """Ensure ebuilds have valid quoting around things like D,FILESDIR, etc..."""
144
145         repoman_check_name = 'ebuild.minorsyn'
146         _message_commands = ["die", "echo", "eerror",
147                 "einfo", "elog", "eqawarn", "ewarn"]
148         _message_re = re.compile(r'\s(' + "|".join(_message_commands) + \
149                 r')\s+"[^"]*"\s*$')
150         _ignored_commands = ["local", "export"] + _message_commands
151         ignore_line = re.compile(r'(^$)|(^\s*#.*)|(^\s*\w+=.*)' + \
152                 r'|(^\s*(' + "|".join(_ignored_commands) + r')\s+)')
153         ignore_comment = False
154         var_names = ["D", "DISTDIR", "FILESDIR", "S", "T", "ROOT", "WORKDIR"]
155
156         # EAPI=3/Prefix vars
157         var_names += ["ED", "EPREFIX", "EROOT"]
158
159         # variables for games.eclass
160         var_names += ["Ddir", "GAMES_PREFIX_OPT", "GAMES_DATADIR",
161                 "GAMES_DATADIR_BASE", "GAMES_SYSCONFDIR", "GAMES_STATEDIR",
162                 "GAMES_LOGDIR", "GAMES_BINDIR"]
163
164         var_names = "(%s)" % "|".join(var_names)
165         var_reference = re.compile(r'\$(\{'+var_names+'\}|' + \
166                 var_names + '\W)')
167         missing_quotes = re.compile(r'(\s|^)[^"\'\s]*\$\{?' + var_names + \
168                 r'\}?[^"\'\s]*(\s|$)')
169         cond_begin =  re.compile(r'(^|\s+)\[\[($|\\$|\s+)')
170         cond_end =  re.compile(r'(^|\s+)\]\]($|\\$|\s+)')
171         
172         def check(self, num, line):
173                 if self.var_reference.search(line) is None:
174                         return
175                 # There can be multiple matches / violations on a single line. We
176                 # have to make sure none of the matches are violators. Once we've
177                 # found one violator, any remaining matches on the same line can
178                 # be ignored.
179                 pos = 0
180                 while pos <= len(line) - 1:
181                         missing_quotes = self.missing_quotes.search(line, pos)
182                         if not missing_quotes:
183                                 break
184                         # If the last character of the previous match is a whitespace
185                         # character, that character may be needed for the next
186                         # missing_quotes match, so search overlaps by 1 character.
187                         group = missing_quotes.group()
188                         pos = missing_quotes.end() - 1
189
190                         # Filter out some false positives that can
191                         # get through the missing_quotes regex.
192                         if self.var_reference.search(group) is None:
193                                 continue
194
195                         # Filter matches that appear to be an
196                         # argument to a message command.
197                         # For example: false || ewarn "foo $WORKDIR/bar baz"
198                         message_match = self._message_re.search(line)
199                         if message_match is not None and \
200                                 message_match.start() < pos and \
201                                 message_match.end() > pos:
202                                 break
203
204                         # This is an attempt to avoid false positives without getting
205                         # too complex, while possibly allowing some (hopefully
206                         # unlikely) violations to slip through. We just assume
207                         # everything is correct if the there is a ' [[ ' or a ' ]] '
208                         # anywhere in the whole line (possibly continued over one
209                         # line).
210                         if self.cond_begin.search(line) is not None:
211                                 continue
212                         if self.cond_end.search(line) is not None:
213                                 continue
214
215                         # Any remaining matches on the same line can be ignored.
216                         return errors.MISSING_QUOTES_ERROR
217
218
219 class EbuildAssignment(LineCheck):
220         """Ensure ebuilds don't assign to readonly variables."""
221
222         repoman_check_name = 'variable.readonly'
223
224         readonly_assignment = re.compile(r'^\s*(export\s+)?(A|CATEGORY|P|PV|PN|PR|PVR|PF|D|WORKDIR|FILESDIR|FEATURES|USE)=')
225         line_continuation = re.compile(r'([^#]*\S)(\s+|\t)\\$')
226         ignore_line = re.compile(r'(^$)|(^(\t)*#)')
227         ignore_comment = False
228
229         def __init__(self):
230                 self.previous_line = None
231
232         def check(self, num, line):
233                 match = self.readonly_assignment.match(line)
234                 e = None
235                 if match and (not self.previous_line or not self.line_continuation.match(self.previous_line)):
236                         e = errors.READONLY_ASSIGNMENT_ERROR
237                 self.previous_line = line
238                 return e
239
240 class Eapi3EbuildAssignment(EbuildAssignment):
241         """Ensure ebuilds don't assign to readonly EAPI 3-introduced variables."""
242
243         readonly_assignment = re.compile(r'\s*(export\s+)?(ED|EPREFIX|EROOT)=')
244
245         def check_eapi(self, eapi):
246                 return eapi_supports_prefix(eapi)
247
248 class EbuildNestedDie(LineCheck):
249         """Check ebuild for nested die statements (die statements in subshells"""
250         
251         repoman_check_name = 'ebuild.nesteddie'
252         nesteddie_re = re.compile(r'^[^#]*\s\(\s[^)]*\bdie\b')
253         
254         def check(self, num, line):
255                 if self.nesteddie_re.match(line):
256                         return errors.NESTED_DIE_ERROR
257
258
259 class EbuildUselessDodoc(LineCheck):
260         """Check ebuild for useless files in dodoc arguments."""
261         repoman_check_name = 'ebuild.minorsyn'
262         uselessdodoc_re = re.compile(
263                 r'^\s*dodoc(\s+|\s+.*\s+)(ABOUT-NLS|COPYING|LICENCE|LICENSE)($|\s)')
264
265         def check(self, num, line):
266                 match = self.uselessdodoc_re.match(line)
267                 if match:
268                         return "Useless dodoc '%s'" % (match.group(2), ) + " on line: %d"
269
270
271 class EbuildUselessCdS(LineCheck):
272         """Check for redundant cd ${S} statements"""
273         repoman_check_name = 'ebuild.minorsyn'
274         method_re = re.compile(r'^\s*src_(prepare|configure|compile|install|test)\s*\(\)')
275         cds_re = re.compile(r'^\s*cd\s+("\$(\{S\}|S)"|\$(\{S\}|S))\s')
276
277         def __init__(self):
278                 self.check_next_line = False
279
280         def check(self, num, line):
281                 if self.check_next_line:
282                         self.check_next_line = False
283                         if self.cds_re.match(line):
284                                 return errors.REDUNDANT_CD_S_ERROR
285                 elif self.method_re.match(line):
286                         self.check_next_line = True
287
288 class EapiDefinition(LineCheck):
289         """
290         Check that EAPI assignment conforms to PMS section 7.3.1
291         (first non-comment, non-blank line).
292         """
293         repoman_check_name = 'EAPI.definition'
294         ignore_comment = True
295         _eapi_re = portage._pms_eapi_re
296
297         def new(self, pkg):
298                 self._cached_eapi = pkg.metadata['EAPI']
299                 self._parsed_eapi = None
300                 self._eapi_line_num = None
301
302         def check(self, num, line):
303                 if self._eapi_line_num is None and line.strip():
304                         self._eapi_line_num = num + 1
305                         m = self._eapi_re.match(line)
306                         if m is not None:
307                                 self._parsed_eapi = m.group(2)
308
309         def end(self):
310                 if self._parsed_eapi is None:
311                         if self._cached_eapi != "0":
312                                 yield "valid EAPI assignment must occur on or before line: %s" % \
313                                         self._eapi_line_num
314                 elif self._parsed_eapi != self._cached_eapi:
315                         yield ("bash returned EAPI '%s' which does not match "
316                                 "assignment on line: %s") % \
317                                 (self._cached_eapi, self._eapi_line_num)
318
319 class EbuildPatches(LineCheck):
320         """Ensure ebuilds use bash arrays for PATCHES to ensure white space safety"""
321         repoman_check_name = 'ebuild.patches'
322         re = re.compile(r'^\s*PATCHES=[^\(]')
323         error = errors.PATCHES_ERROR
324
325 class EbuildQuotedA(LineCheck):
326         """Ensure ebuilds have no quoting around ${A}"""
327
328         repoman_check_name = 'ebuild.minorsyn'
329         a_quoted = re.compile(r'.*\"\$(\{A\}|A)\"')
330
331         def check(self, num, line):
332                 match = self.a_quoted.match(line)
333                 if match:
334                         return "Quoted \"${A}\" on line: %d"
335
336 class NoOffsetWithHelpers(LineCheck):
337         """ Check that the image location, the alternate root offset, and the
338         offset prefix (D, ROOT, ED, EROOT and EPREFIX) are not used with
339         helpers """
340
341         repoman_check_name = 'variable.usedwithhelpers'
342         # Ignore matches in quoted strings like this:
343         # elog "installed into ${ROOT}usr/share/php5/apc/."
344         re = re.compile(r'^[^#"\']*\b(docinto|docompress|dodir|dohard|exeinto|fowners|fperms|insinto|into)\s+"?\$\{?(D|ROOT|ED|EROOT|EPREFIX)\b.*')
345         error = errors.NO_OFFSET_WITH_HELPERS
346
347 class ImplicitRuntimeDeps(LineCheck):
348         """
349         Detect the case where DEPEND is set and RDEPEND is unset in the ebuild,
350         since this triggers implicit RDEPEND=$DEPEND assignment (prior to EAPI 4).
351         """
352
353         repoman_check_name = 'RDEPEND.implicit'
354         _assignment_re = re.compile(r'^\s*(R?DEPEND)\+?=')
355
356         def new(self, pkg):
357                 self._rdepend = False
358                 self._depend = False
359
360         def check_eapi(self, eapi):
361                 # Beginning with EAPI 4, there is no
362                 # implicit RDEPEND=$DEPEND assignment
363                 # to be concerned with.
364                 return eapi_has_implicit_rdepend(eapi)
365
366         def check(self, num, line):
367                 if not self._rdepend:
368                         m = self._assignment_re.match(line)
369                         if m is None:
370                                 pass
371                         elif m.group(1) == "RDEPEND":
372                                 self._rdepend = True
373                         elif m.group(1) == "DEPEND":
374                                 self._depend = True
375
376         def end(self):
377                 if self._depend and not self._rdepend:
378                         yield 'RDEPEND is not explicitly assigned'
379
380 class InheritDeprecated(LineCheck):
381         """Check if ebuild directly or indirectly inherits a deprecated eclass."""
382
383         repoman_check_name = 'inherit.deprecated'
384
385         # deprecated eclass : new eclass (False if no new eclass)
386         deprecated_classes = {
387                 "bash-completion": "bash-completion-r1",
388                 "gems": "ruby-fakegem",
389                 "git": "git-2",
390                 "mozconfig-2": "mozconfig-3",
391                 "mozcoreconf": "mozcoreconf-2",
392                 "php-ext-pecl-r1": "php-ext-pecl-r2",
393                 "php-ext-source-r1": "php-ext-source-r2",
394                 "php-pear": "php-pear-r1",
395                 "qt3": False,
396                 "qt4": "qt4-r2",
397                 "ruby": "ruby-ng",
398                 "ruby-gnome2": "ruby-ng-gnome2",
399                 "x-modular": "xorg-2",
400                 }
401
402         _inherit_re = re.compile(r'^\s*inherit\s(.*)$')
403
404         def new(self, pkg):
405                 self._errors = []
406                 self._indirect_deprecated = set(eclass for eclass in \
407                         self.deprecated_classes if eclass in pkg.inherited)
408
409         def check(self, num, line):
410
411                 direct_inherits = None
412                 m = self._inherit_re.match(line)
413                 if m is not None:
414                         direct_inherits = m.group(1)
415                         if direct_inherits:
416                                 direct_inherits = direct_inherits.split()
417
418                 if not direct_inherits:
419                         return
420
421                 for eclass in direct_inherits:
422                         replacement = self.deprecated_classes.get(eclass)
423                         if replacement is None:
424                                 pass
425                         elif replacement is False:
426                                 self._indirect_deprecated.discard(eclass)
427                                 self._errors.append("please migrate from " + \
428                                         "'%s' (no replacement) on line: %d" % (eclass, num + 1))
429                         else:
430                                 self._indirect_deprecated.discard(eclass)
431                                 self._errors.append("please migrate from " + \
432                                         "'%s' to '%s' on line: %d" % \
433                                         (eclass, replacement, num + 1))
434
435         def end(self):
436                 for error in self._errors:
437                         yield error
438                 del self._errors
439
440                 for eclass in self._indirect_deprecated:
441                         replacement = self.deprecated_classes[eclass]
442                         if replacement is False:
443                                 yield "please migrate from indirect " + \
444                                         "inherit of '%s' (no replacement)" % (eclass,)
445                         else:
446                                 yield "please migrate from indirect " + \
447                                         "inherit of '%s' to '%s'" % \
448                                         (eclass, replacement)
449                 del self._indirect_deprecated
450
451 class InheritEclass(LineCheck):
452         """
453         Base class for checking for missing inherits, as well as excess inherits.
454
455         Args:
456                 eclass: Set to the name of your eclass.
457                 funcs: A tuple of functions that this eclass provides.
458                 comprehensive: Is the list of functions complete?
459                 exempt_eclasses: If these eclasses are inherited, disable the missing
460                                   inherit check.
461         """
462
463         def __init__(self, eclass, funcs=None, comprehensive=False,
464                 exempt_eclasses=None, **kwargs):
465                 self._eclass = eclass
466                 self._comprehensive = comprehensive
467                 self._exempt_eclasses = exempt_eclasses
468                 inherit_re = eclass
469                 subclasses = _eclass_subclass_info.get(eclass)
470                 if subclasses is not None:
471                         inherit_re = '(%s)' % '|'.join([eclass] + list(subclasses))
472                 self._inherit_re = re.compile(r'^(\s*|.*[|&]\s*)\binherit\s(.*\s)?%s(\s|$)' % inherit_re)
473                 self._func_re = re.compile(r'\b(' + '|'.join(funcs) + r')\b')
474
475         def new(self, pkg):
476                 self.repoman_check_name = 'inherit.missing'
477                 # We can't use pkg.inherited because that tells us all the eclass that
478                 # have been inherited and not just the ones we inherit directly.
479                 self._inherit = False
480                 self._func_call = False
481                 if self._exempt_eclasses is not None:
482                         self._disabled = any(x in pkg.inherited for x in self._exempt_eclasses)
483                 else:
484                         self._disabled = False
485
486         def check(self, num, line):
487                 if not self._inherit:
488                         self._inherit = self._inherit_re.match(line)
489                 if not self._inherit:
490                         if self._disabled:
491                                 return
492                         s = self._func_re.search(line)
493                         if s:
494                                 self._func_call = True
495                                 return '%s.eclass is not inherited, but "%s" found at line: %s' % \
496                                         (self._eclass, s.group(0), '%d')
497                 elif not self._func_call:
498                         self._func_call = self._func_re.search(line)
499
500         def end(self):
501                 if not self._disabled and self._comprehensive and self._inherit and not self._func_call:
502                         self.repoman_check_name = 'inherit.unused'
503                         yield 'no function called from %s.eclass; please drop' % self._eclass
504
505 _eclass_info = {
506         'autotools': {
507                 'funcs': (
508                         'eaclocal', 'eautoconf', 'eautoheader',
509                         'eautomake', 'eautoreconf', '_elibtoolize',
510                         'eautopoint'
511                 ),
512                 'comprehensive': True,
513
514                 # Exempt eclasses:
515                 # git - An EGIT_BOOTSTRAP variable may be used to call one of
516                 #       the autotools functions.
517                 # subversion - An ESVN_BOOTSTRAP variable may be used to call one of
518                 #       the autotools functions.
519                 'exempt_eclasses': ('git', 'git-2', 'subversion', 'autotools-utils')
520         },
521
522         'eutils': {
523                 'funcs': (
524                         'estack_push', 'estack_pop', 'eshopts_push', 'eshopts_pop',
525                         'eumask_push', 'eumask_pop', 'epatch', 'epatch_user',
526                         'emktemp', 'edos2unix', 'in_iuse', 'use_if_iuse', 'usex',
527                         'makeopts_jobs'
528                 ),
529                 'comprehensive': False,
530
531                 # These are "eclasses are the whole ebuild" type thing.
532                 'exempt_eclasses': ('base', 'cmake-utils', 'toolchain', 'toolchain-binutils', 'vim'),
533
534                 #'inherited_api': ('multilib', 'user',),
535         },
536
537         'flag-o-matic': {
538                 'funcs': (
539                         'filter-(ld)?flags', 'strip-flags', 'strip-unsupported-flags',
540                         'append-((ld|c(pp|xx)?))?flags', 'append-libs',
541                 ),
542                 'comprehensive': False
543         },
544
545         'libtool': {
546                 'funcs': (
547                         'elibtoolize',
548                 ),
549                 'comprehensive': True
550         },
551
552         'multilib': {
553                 'funcs': (
554                         'get_libdir',
555                 ),
556
557                 # These are "eclasses are the whole ebuild" type thing.
558                 'exempt_eclasses': ('cmake-utils',),
559
560                 'comprehensive': False
561         },
562
563         'prefix': {
564                 'funcs': (
565                         'eprefixify',
566                 ),
567                 'comprehensive': True
568         },
569
570         'toolchain-funcs': {
571                 'funcs': (
572                         'gen_usr_ldscript',
573                 ),
574                 'comprehensive': False
575         },
576
577         'user': {
578                 'funcs': (
579                         'enewuser', 'enewgroup',
580                         'egetent', 'egethome', 'egetshell'
581                 ),
582                 'comprehensive': True
583         }
584 }
585
586 _eclass_subclass_info = {}
587
588 for k, v in _eclass_info.items():
589         inherited_api = v.get('inherited_api')
590         if inherited_api is not None:
591                 for parent in inherited_api:
592                         _eclass_subclass_info.setdefault(parent, set()).add(k)
593
594 class IUseUndefined(LineCheck):
595         """
596         Make sure the ebuild defines IUSE (style guideline
597         says to define IUSE even when empty).
598         """
599
600         repoman_check_name = 'IUSE.undefined'
601         _iuse_def_re = re.compile(r'^IUSE=.*')
602
603         def new(self, pkg):
604                 self._iuse_def = None
605
606         def check(self, num, line):
607                 if self._iuse_def is None:
608                         self._iuse_def = self._iuse_def_re.match(line)
609
610         def end(self):
611                 if self._iuse_def is None:
612                         yield 'IUSE is not defined'
613
614 class EMakeParallelDisabled(PhaseCheck):
615         """Check for emake -j1 calls which disable parallelization."""
616         repoman_check_name = 'upstream.workaround'
617         re = re.compile(r'^\s*emake\s+.*-j\s*1\b')
618         error = errors.EMAKE_PARALLEL_DISABLED
619
620         def phase_check(self, num, line):
621                 if self.in_phase == 'src_compile' or self.in_phase == 'src_install':
622                         if self.re.match(line):
623                                 return self.error
624
625 class EMakeParallelDisabledViaMAKEOPTS(LineCheck):
626         """Check for MAKEOPTS=-j1 that disables parallelization."""
627         repoman_check_name = 'upstream.workaround'
628         re = re.compile(r'^\s*MAKEOPTS=(\'|")?.*-j\s*1\b')
629         error = errors.EMAKE_PARALLEL_DISABLED_VIA_MAKEOPTS
630
631 class NoAsNeeded(LineCheck):
632         """Check for calls to the no-as-needed function."""
633         repoman_check_name = 'upstream.workaround'
634         re = re.compile(r'.*\$\(no-as-needed\)')
635         error = errors.NO_AS_NEEDED
636
637 class PreserveOldLib(LineCheck):
638         """Check for calls to the preserve_old_lib function."""
639         repoman_check_name = 'upstream.workaround'
640         re = re.compile(r'.*preserve_old_lib')
641         error = errors.PRESERVE_OLD_LIB
642
643 class SandboxAddpredict(LineCheck):
644         """Check for calls to the addpredict function."""
645         repoman_check_name = 'upstream.workaround'
646         re = re.compile(r'(^|\s)addpredict\b')
647         error = errors.SANDBOX_ADDPREDICT
648
649 class DeprecatedBindnowFlags(LineCheck):
650         """Check for calls to the deprecated bindnow-flags function."""
651         repoman_check_name = 'ebuild.minorsyn'
652         re = re.compile(r'.*\$\(bindnow-flags\)')
653         error = errors.DEPRECATED_BINDNOW_FLAGS
654
655 class WantAutoDefaultValue(LineCheck):
656         """Check setting WANT_AUTO* to latest (default value)."""
657         repoman_check_name = 'ebuild.minorsyn'
658         _re = re.compile(r'^WANT_AUTO(CONF|MAKE)=(\'|")?latest')
659
660         def check(self, num, line):
661                 m = self._re.match(line)
662                 if m is not None:
663                         return 'WANT_AUTO' + m.group(1) + \
664                                 ' redundantly set to default value "latest" on line: %d'
665
666 class SrcCompileEconf(PhaseCheck):
667         repoman_check_name = 'ebuild.minorsyn'
668         configure_re = re.compile(r'\s(econf|./configure)')
669
670         def check_eapi(self, eapi):
671                 return eapi_has_src_prepare_and_src_configure(eapi)
672
673         def phase_check(self, num, line):
674                 if self.in_phase == 'src_compile':
675                         m = self.configure_re.match(line)
676                         if m is not None:
677                                 return ("'%s'" % m.group(1)) + \
678                                         " call should be moved to src_configure from line: %d"
679
680 class SrcUnpackPatches(PhaseCheck):
681         repoman_check_name = 'ebuild.minorsyn'
682         src_prepare_tools_re = re.compile(r'\s(e?patch|sed)\s')
683
684         def check_eapi(self, eapi):
685                 return eapi_has_src_prepare_and_src_configure(eapi)
686
687         def phase_check(self, num, line):
688                 if self.in_phase == 'src_unpack':
689                         m = self.src_prepare_tools_re.search(line)
690                         if m is not None:
691                                 return ("'%s'" % m.group(1)) + \
692                                         " call should be moved to src_prepare from line: %d"
693
694 class BuiltWithUse(LineCheck):
695         repoman_check_name = 'ebuild.minorsyn'
696         re = re.compile(r'(^|.*\b)built_with_use\b')
697         error = errors.BUILT_WITH_USE
698
699 class DeprecatedUseq(LineCheck):
700         """Checks for use of the deprecated useq function"""
701         repoman_check_name = 'ebuild.minorsyn'
702         re = re.compile(r'(^|.*\b)useq\b')
703         error = errors.USEQ_ERROR
704
705 class DeprecatedHasq(LineCheck):
706         """Checks for use of the deprecated hasq function"""
707         repoman_check_name = 'ebuild.minorsyn'
708         re = re.compile(r'(^|.*\b)hasq\b')
709         error = errors.HASQ_ERROR
710
711 # EAPI-3 checks
712 class Eapi3DeprecatedFuncs(LineCheck):
713         repoman_check_name = 'EAPI.deprecated'
714         deprecated_commands_re = re.compile(r'^\s*(check_license)\b')
715
716         def check_eapi(self, eapi):
717                 return eapi not in ('0', '1', '2')
718
719         def check(self, num, line):
720                 m = self.deprecated_commands_re.match(line)
721                 if m is not None:
722                         return ("'%s'" % m.group(1)) + \
723                                 " has been deprecated in EAPI=3 on line: %d"
724
725 # EAPI-4 checks
726 class Eapi4IncompatibleFuncs(LineCheck):
727         repoman_check_name = 'EAPI.incompatible'
728         banned_commands_re = re.compile(r'^\s*(dosed|dohard)')
729
730         def check_eapi(self, eapi):
731                 return not eapi_has_dosed_dohard(eapi)
732
733         def check(self, num, line):
734                 m = self.banned_commands_re.match(line)
735                 if m is not None:
736                         return ("'%s'" % m.group(1)) + \
737                                 " has been banned in EAPI=4 on line: %d"
738
739 class Eapi4GoneVars(LineCheck):
740         repoman_check_name = 'EAPI.incompatible'
741         undefined_vars_re = re.compile(r'.*\$(\{(AA|KV|EMERGE_FROM)\}|(AA|KV|EMERGE_FROM))')
742
743         def check_eapi(self, eapi):
744                 # AA, KV, and EMERGE_FROM should not be referenced in EAPI 4 or later.
745                 return not eapi_exports_AA(eapi)
746
747         def check(self, num, line):
748                 m = self.undefined_vars_re.match(line)
749                 if m is not None:
750                         return ("variable '$%s'" % m.group(1)) + \
751                                 " is gone in EAPI=4 on line: %d"
752
753 class PortageInternal(LineCheck):
754         repoman_check_name = 'portage.internal'
755         ignore_comment = True
756         # Match when the command is preceded only by leading whitespace or a shell
757         # operator such as (, {, |, ||, or &&. This prevents false postives in
758         # things like elog messages, as reported in bug #413285.
759         re = re.compile(r'^(\s*|.*[|&{(]+\s*)\b(ecompress|ecompressdir|env-update|prepall|prepalldocs|preplib)\b')
760
761         def check(self, num, line):
762                 """Run the check on line and return error if there is one"""
763                 m = self.re.match(line)
764                 if m is not None:
765                         return ("'%s'" % m.group(2)) + " called on line: %d"
766
767 _constant_checks = tuple(chain((c() for c in (
768         EbuildHeader, EbuildWhitespace, EbuildBlankLine, EbuildQuote,
769         EbuildAssignment, Eapi3EbuildAssignment, EbuildUselessDodoc,
770         EbuildUselessCdS, EbuildNestedDie,
771         EbuildPatches, EbuildQuotedA, EapiDefinition,
772         ImplicitRuntimeDeps, IUseUndefined,
773         EMakeParallelDisabled, EMakeParallelDisabledViaMAKEOPTS, NoAsNeeded,
774         DeprecatedBindnowFlags, SrcUnpackPatches, WantAutoDefaultValue,
775         SrcCompileEconf, Eapi3DeprecatedFuncs, NoOffsetWithHelpers,
776         Eapi4IncompatibleFuncs, Eapi4GoneVars, BuiltWithUse,
777         PreserveOldLib, SandboxAddpredict, PortageInternal,
778         DeprecatedUseq, DeprecatedHasq)),
779         (InheritEclass(k, **kwargs) for k, kwargs in _eclass_info.items())))
780
781 _here_doc_re = re.compile(r'.*\s<<[-]?(\w+)$')
782 _ignore_comment_re = re.compile(r'^\s*#')
783
784 def run_checks(contents, pkg):
785         unicode_escape_codec = codecs.lookup('unicode_escape')
786         unicode_escape = lambda x: unicode_escape_codec.decode(x)[0]
787         checks = _constant_checks
788         here_doc_delim = None
789         multiline = None
790
791         for lc in checks:
792                 lc.new(pkg)
793         for num, line in enumerate(contents):
794
795                 # Check if we're inside a here-document.
796                 if here_doc_delim is not None:
797                         if here_doc_delim.match(line):
798                                 here_doc_delim = None
799                 if here_doc_delim is None:
800                         here_doc = _here_doc_re.match(line)
801                         if here_doc is not None:
802                                 here_doc_delim = re.compile(r'^\s*%s$' % here_doc.group(1))
803                 if here_doc_delim is not None:
804                         continue
805
806                 # Unroll multiline escaped strings so that we can check things:
807                 #               inherit foo bar \
808                 #                       moo \
809                 #                       cow
810                 # This will merge these lines like so:
811                 #               inherit foo bar         moo     cow
812                 try:
813                         # A normal line will end in the two bytes: <\> <\n>.  So decoding
814                         # that will result in python thinking the <\n> is being escaped
815                         # and eat the single <\> which makes it hard for us to detect.
816                         # Instead, strip the newline (which we know all lines have), and
817                         # append a <0>.  Then when python escapes it, if the line ended
818                         # in a <\>, we'll end up with a <\0> marker to key off of.  This
819                         # shouldn't be a problem with any valid ebuild ...
820                         line_escaped = unicode_escape(line.rstrip('\n') + '0')
821                 except SystemExit:
822                         raise
823                 except:
824                         # Who knows what kind of crazy crap an ebuild will have
825                         # in it -- don't allow it to kill us.
826                         line_escaped = line
827                 if multiline:
828                         # Chop off the \ and \n bytes from the previous line.
829                         multiline = multiline[:-2] + line
830                         if not line_escaped.endswith('\0'):
831                                 line = multiline
832                                 num = multinum
833                                 multiline = None
834                         else:
835                                 continue
836                 else:
837                         if line_escaped.endswith('\0'):
838                                 multinum = num
839                                 multiline = line
840                                 continue
841
842                 # Finally we have a full line to parse.
843                 is_comment = _ignore_comment_re.match(line) is not None
844                 for lc in checks:
845                         if is_comment and lc.ignore_comment:
846                                 continue
847                         if lc.check_eapi(pkg.metadata['EAPI']):
848                                 ignore = lc.ignore_line
849                                 if not ignore or not ignore.match(line):
850                                         e = lc.check(num, line)
851                                         if e:
852                                                 yield lc.repoman_check_name, e % (num + 1)
853
854         for lc in checks:
855                 i = lc.end()
856                 if i is not None:
857                         for e in i:
858                                 yield lc.repoman_check_name, e