repoman: change preserve_old_lib msg, bug #480244
[portage.git] / pym / repoman / checks.py
index 4343ab119224703c85262b8fdadfc2a19e40bf00..c60db3d02359f1dfdf18e9c949a5d3eedd7bba59 100644 (file)
@@ -1,24 +1,38 @@
 # repoman: Checks
 # repoman: Checks
-# Copyright 2007 Gentoo Foundation
+# Copyright 2007-2013 Gentoo Foundation
 # Distributed under the terms of the GNU General Public License v2
 # Distributed under the terms of the GNU General Public License v2
-# $Id$
 
 """This module contains functions used in Repoman to ascertain the quality
 and correctness of an ebuild."""
 
 
 """This module contains functions used in Repoman to ascertain the quality
 and correctness of an ebuild."""
 
-import os
+from __future__ import unicode_literals
+
+import codecs
+from itertools import chain
 import re
 import time
 import repoman.errors as errors
 import re
 import time
 import repoman.errors as errors
+import portage
+from portage.eapi import eapi_supports_prefix, eapi_has_implicit_rdepend, \
+       eapi_has_src_prepare_and_src_configure, eapi_has_dosed_dohard, \
+       eapi_exports_AA
+from portage.const import _ENABLE_INHERIT_CHECK
 
 class LineCheck(object):
        """Run a check on a line of an ebuild."""
        """A regular expression to determine whether to ignore the line"""
        ignore_line = False
 
 class LineCheck(object):
        """Run a check on a line of an ebuild."""
        """A regular expression to determine whether to ignore the line"""
        ignore_line = False
+       """True if lines containing nothing more than comments with optional
+       leading whitespace should be ignored"""
+       ignore_comment = True
 
        def new(self, pkg):
                pass
 
 
        def new(self, pkg):
                pass
 
+       def check_eapi(self, eapi):
+               """ returns if the check should be run in the given EAPI (default is True) """
+               return True
+
        def check(self, num, line):
                """Run the check on line and return error if there is one"""
                if self.re.match(line):
        def check(self, num, line):
                """Run the check on line and return error if there is one"""
                if self.re.match(line):
@@ -27,26 +41,55 @@ class LineCheck(object):
        def end(self):
                pass
 
        def end(self):
                pass
 
+class PhaseCheck(LineCheck):
+       """ basic class for function detection """
+
+       func_end_re = re.compile(r'^\}$')
+       phases_re = re.compile('(%s)' % '|'.join((
+               'pkg_pretend', 'pkg_setup', 'src_unpack', 'src_prepare',
+               'src_configure', 'src_compile', 'src_test', 'src_install',
+               'pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm',
+               'pkg_config')))
+       in_phase = ''
+
+       def check(self, num, line):
+               m = self.phases_re.match(line)
+               if m is not None:
+                       self.in_phase = m.group(1)
+               if self.in_phase != '' and \
+                               self.func_end_re.match(line) is not None:
+                       self.in_phase = ''
+
+               return self.phase_check(num, line)
+
+       def phase_check(self, num, line):
+               """ override this function for your checks """
+               pass
+
 class EbuildHeader(LineCheck):
        """Ensure ebuilds have proper headers
                Copyright header errors
                CVS header errors
                License header errors
 class EbuildHeader(LineCheck):
        """Ensure ebuilds have proper headers
                Copyright header errors
                CVS header errors
                License header errors
-       
+
        Args:
                modification_year - Year the ebuild was last modified
        """
 
        repoman_check_name = 'ebuild.badheader'
 
        Args:
                modification_year - Year the ebuild was last modified
        """
 
        repoman_check_name = 'ebuild.badheader'
 
-       gentoo_copyright = r'^# Copyright ((1999|200\d)-)?%s Gentoo Foundation$'
+       gentoo_copyright = r'^# Copyright ((1999|2\d\d\d)-)?%s Gentoo Foundation$'
        # Why a regex here, use a string match
        # gentoo_license = re.compile(r'^# Distributed under the terms of the GNU General Public License v2$')
        # Why a regex here, use a string match
        # gentoo_license = re.compile(r'^# Distributed under the terms of the GNU General Public License v2$')
-       gentoo_license = r'# Distributed under the terms of the GNU General Public License v2'
-       cvs_header = re.compile(r'^#\s*\$Header.*\$$')
+       gentoo_license = '# Distributed under the terms of the GNU General Public License v2'
+       cvs_header = re.compile(r'^# \$Header: .*\$$')
+       ignore_comment = False
 
        def new(self, pkg):
 
        def new(self, pkg):
-               self.modification_year = str(time.gmtime(pkg.mtime)[0])
+               if pkg.mtime is None:
+                       self.modification_year = r'2\d\d\d'
+               else:
+                       self.modification_year = str(time.gmtime(pkg.mtime)[0])
                self.gentoo_copyright_re = re.compile(
                        self.gentoo_copyright % self.modification_year)
 
                self.gentoo_copyright_re = re.compile(
                        self.gentoo_copyright % self.modification_year)
 
@@ -56,7 +99,7 @@ class EbuildHeader(LineCheck):
                elif num == 0:
                        if not self.gentoo_copyright_re.match(line):
                                return errors.COPYRIGHT_ERROR
                elif num == 0:
                        if not self.gentoo_copyright_re.match(line):
                                return errors.COPYRIGHT_ERROR
-               elif num == 1 and line.strip() != self.gentoo_license:
+               elif num == 1 and line.rstrip('\n') != self.gentoo_license:
                        return errors.LICENSE_ERROR
                elif num == 2:
                        if not self.cvs_header.match(line):
                        return errors.LICENSE_ERROR
                elif num == 2:
                        if not self.cvs_header.match(line):
@@ -69,8 +112,9 @@ class EbuildWhitespace(LineCheck):
        repoman_check_name = 'ebuild.minorsyn'
 
        ignore_line = re.compile(r'(^$)|(^(\t)*#)')
        repoman_check_name = 'ebuild.minorsyn'
 
        ignore_line = re.compile(r'(^$)|(^(\t)*#)')
+       ignore_comment = False
        leading_spaces = re.compile(r'^[\S\t]')
        leading_spaces = re.compile(r'^[\S\t]')
-       trailing_whitespace = re.compile(r'.*([\S]$)')  
+       trailing_whitespace = re.compile(r'.*([\S]$)')
 
        def check(self, num, line):
                if self.leading_spaces.match(line) is None:
 
        def check(self, num, line):
                if self.leading_spaces.match(line) is None:
@@ -78,6 +122,26 @@ class EbuildWhitespace(LineCheck):
                if self.trailing_whitespace.match(line) is None:
                        return errors.TRAILING_WHITESPACE_ERROR
 
                if self.trailing_whitespace.match(line) is None:
                        return errors.TRAILING_WHITESPACE_ERROR
 
+class EbuildBlankLine(LineCheck):
+       repoman_check_name = 'ebuild.minorsyn'
+       ignore_comment = False
+       blank_line = re.compile(r'^$')
+
+       def new(self, pkg):
+               self.line_is_blank = False
+
+       def check(self, num, line):
+               if self.line_is_blank and self.blank_line.match(line):
+                       return 'Useless blank line on line: %d'
+               if self.blank_line.match(line):
+                       self.line_is_blank = True
+               else:
+                       self.line_is_blank = False
+
+       def end(self):
+               if self.line_is_blank:
+                       yield 'Useless blank line on last line'
+
 class EbuildQuote(LineCheck):
        """Ensure ebuilds have valid quoting around things like D,FILESDIR, etc..."""
 
 class EbuildQuote(LineCheck):
        """Ensure ebuilds have valid quoting around things like D,FILESDIR, etc..."""
 
@@ -89,13 +153,20 @@ class EbuildQuote(LineCheck):
        _ignored_commands = ["local", "export"] + _message_commands
        ignore_line = re.compile(r'(^$)|(^\s*#.*)|(^\s*\w+=.*)' + \
                r'|(^\s*(' + "|".join(_ignored_commands) + r')\s+)')
        _ignored_commands = ["local", "export"] + _message_commands
        ignore_line = re.compile(r'(^$)|(^\s*#.*)|(^\s*\w+=.*)' + \
                r'|(^\s*(' + "|".join(_ignored_commands) + r')\s+)')
+       ignore_comment = False
        var_names = ["D", "DISTDIR", "FILESDIR", "S", "T", "ROOT", "WORKDIR"]
 
        var_names = ["D", "DISTDIR", "FILESDIR", "S", "T", "ROOT", "WORKDIR"]
 
+       # EAPI=3/Prefix vars
+       var_names += ["ED", "EPREFIX", "EROOT"]
+
        # variables for games.eclass
        var_names += ["Ddir", "GAMES_PREFIX_OPT", "GAMES_DATADIR",
                "GAMES_DATADIR_BASE", "GAMES_SYSCONFDIR", "GAMES_STATEDIR",
                "GAMES_LOGDIR", "GAMES_BINDIR"]
 
        # variables for games.eclass
        var_names += ["Ddir", "GAMES_PREFIX_OPT", "GAMES_DATADIR",
                "GAMES_DATADIR_BASE", "GAMES_SYSCONFDIR", "GAMES_STATEDIR",
                "GAMES_LOGDIR", "GAMES_BINDIR"]
 
+       # variables for multibuild.eclass
+       var_names += ["BUILD_DIR"]
+
        var_names = "(%s)" % "|".join(var_names)
        var_reference = re.compile(r'\$(\{'+var_names+'\}|' + \
                var_names + '\W)')
        var_names = "(%s)" % "|".join(var_names)
        var_reference = re.compile(r'\$(\{'+var_names+'\}|' + \
                var_names + '\W)')
@@ -103,7 +174,7 @@ class EbuildQuote(LineCheck):
                r'\}?[^"\'\s]*(\s|$)')
        cond_begin =  re.compile(r'(^|\s+)\[\[($|\\$|\s+)')
        cond_end =  re.compile(r'(^|\s+)\]\]($|\\$|\s+)')
                r'\}?[^"\'\s]*(\s|$)')
        cond_begin =  re.compile(r'(^|\s+)\[\[($|\\$|\s+)')
        cond_end =  re.compile(r'(^|\s+)\]\]($|\\$|\s+)')
-       
+
        def check(self, num, line):
                if self.var_reference.search(line) is None:
                        return
        def check(self, num, line):
                if self.var_reference.search(line) is None:
                        return
@@ -155,29 +226,29 @@ class EbuildAssignment(LineCheck):
        """Ensure ebuilds don't assign to readonly variables."""
 
        repoman_check_name = 'variable.readonly'
        """Ensure ebuilds don't assign to readonly variables."""
 
        repoman_check_name = 'variable.readonly'
-
        readonly_assignment = re.compile(r'^\s*(export\s+)?(A|CATEGORY|P|PV|PN|PR|PVR|PF|D|WORKDIR|FILESDIR|FEATURES|USE)=')
        readonly_assignment = re.compile(r'^\s*(export\s+)?(A|CATEGORY|P|PV|PN|PR|PVR|PF|D|WORKDIR|FILESDIR|FEATURES|USE)=')
-       line_continuation = re.compile(r'([^#]*\S)(\s+|\t)\\$')
-       ignore_line = re.compile(r'(^$)|(^(\t)*#)')
-
-       def __init__(self):
-               self.previous_line = None
 
        def check(self, num, line):
                match = self.readonly_assignment.match(line)
                e = None
 
        def check(self, num, line):
                match = self.readonly_assignment.match(line)
                e = None
-               if match and (not self.previous_line or not self.line_continuation.match(self.previous_line)):
+               if match is not None:
                        e = errors.READONLY_ASSIGNMENT_ERROR
                        e = errors.READONLY_ASSIGNMENT_ERROR
-               self.previous_line = line
                return e
 
                return e
 
+class Eapi3EbuildAssignment(EbuildAssignment):
+       """Ensure ebuilds don't assign to readonly EAPI 3-introduced variables."""
+
+       readonly_assignment = re.compile(r'\s*(export\s+)?(ED|EPREFIX|EROOT)=')
+
+       def check_eapi(self, eapi):
+               return eapi_supports_prefix(eapi)
 
 class EbuildNestedDie(LineCheck):
 
 class EbuildNestedDie(LineCheck):
-       """Check ebuild for nested die statements (die statements in subshells"""
-       
+       """Check ebuild for nested die statements (die statements in subshells)"""
+
        repoman_check_name = 'ebuild.nesteddie'
        nesteddie_re = re.compile(r'^[^#]*\s\(\s[^)]*\bdie\b')
        repoman_check_name = 'ebuild.nesteddie'
        nesteddie_re = re.compile(r'^[^#]*\s\(\s[^)]*\bdie\b')
-       
+
        def check(self, num, line):
                if self.nesteddie_re.match(line):
                        return errors.NESTED_DIE_ERROR
        def check(self, num, line):
                if self.nesteddie_re.match(line):
                        return errors.NESTED_DIE_ERROR
@@ -187,7 +258,7 @@ class EbuildUselessDodoc(LineCheck):
        """Check ebuild for useless files in dodoc arguments."""
        repoman_check_name = 'ebuild.minorsyn'
        uselessdodoc_re = re.compile(
        """Check ebuild for useless files in dodoc arguments."""
        repoman_check_name = 'ebuild.minorsyn'
        uselessdodoc_re = re.compile(
-               r'^\s*dodoc(\s+|\s+.*\s+)(ABOUT-NLS|COPYING|LICENSE)($|\s)')
+               r'^\s*dodoc(\s+|\s+.*\s+)(ABOUT-NLS|COPYING|LICENCE|LICENSE)($|\s)')
 
        def check(self, num, line):
                match = self.uselessdodoc_re.match(line)
 
        def check(self, num, line):
                match = self.uselessdodoc_re.match(line)
@@ -198,7 +269,7 @@ class EbuildUselessDodoc(LineCheck):
 class EbuildUselessCdS(LineCheck):
        """Check for redundant cd ${S} statements"""
        repoman_check_name = 'ebuild.minorsyn'
 class EbuildUselessCdS(LineCheck):
        """Check for redundant cd ${S} statements"""
        repoman_check_name = 'ebuild.minorsyn'
-       method_re = re.compile(r'^\s*src_(compile|install|test)\s*\(\)')
+       method_re = re.compile(r'^\s*src_(prepare|configure|compile|install|test)\s*\(\)')
        cds_re = re.compile(r'^\s*cd\s+("\$(\{S\}|S)"|\$(\{S\}|S))\s')
 
        def __init__(self):
        cds_re = re.compile(r'^\s*cd\s+("\$(\{S\}|S)"|\$(\{S\}|S))\s')
 
        def __init__(self):
@@ -213,54 +284,35 @@ class EbuildUselessCdS(LineCheck):
                        self.check_next_line = True
 
 class EapiDefinition(LineCheck):
                        self.check_next_line = True
 
 class EapiDefinition(LineCheck):
-       """ Check that EAPI is defined before inherits"""
+       """
+       Check that EAPI assignment conforms to PMS section 7.3.1
+       (first non-comment, non-blank line).
+       """
        repoman_check_name = 'EAPI.definition'
        repoman_check_name = 'EAPI.definition'
-
-       eapi_re = re.compile(r'^EAPI=')
-       inherit_re = re.compile(r'^\s*inherit\s')
-
-       def new(self, pkg):
-               self.inherit_line = None
-
-       def check(self, num, line):
-               if self.eapi_re.match(line) is not None:
-                       if self.inherit_line is not None:
-                               return errors.EAPI_DEFINED_AFTER_INHERIT
-               elif self.inherit_re.match(line) is not None:
-                       self.inherit_line = line
-
-class SrcUnpackPatches(LineCheck):
-       repoman_check_name = 'ebuild.minorsyn'
-
-       ignore_line = re.compile(r'(^\s*#)')
-       src_unpack_re = re.compile(r'^src_unpack\(\)')
-       func_end_re = re.compile(r'^\}$')
-       src_prepare_tools_re = re.compile(r'\s(e?patch|sed)\s')
+       ignore_comment = True
+       _eapi_re = portage._pms_eapi_re
 
        def new(self, pkg):
 
        def new(self, pkg):
-               if pkg.metadata['EAPI'] not in ('0', '1'):
-                       self.eapi = pkg.metadata['EAPI']
-               else:
-                       self.eapi = None
-               self.in_src_unpack = None
+               self._cached_eapi = pkg.eapi
+               self._parsed_eapi = None
+               self._eapi_line_num = None
 
        def check(self, num, line):
 
        def check(self, num, line):
+               if self._eapi_line_num is None and line.strip():
+                       self._eapi_line_num = num + 1
+                       m = self._eapi_re.match(line)
+                       if m is not None:
+                               self._parsed_eapi = m.group(2)
 
 
-               if self.eapi is not None:
-
-                       if self.in_src_unpack is None and \
-                               self.src_unpack_re.match(line) is not None:
-                                       self.in_src_unpack = True
-
-                       if self.in_src_unpack is True and \
-                               self.func_end_re.match(line) is not None:
-                               self.in_src_unpack = False
-
-                       if self.in_src_unpack:
-                               m = self.src_prepare_tools_re.search(line)
-                               if m is not None:
-                                       return ("'%s'" % m.group(1)) + \
-                                               " call should be moved to src_prepare from line: %d"
+       def end(self):
+               if self._parsed_eapi is None:
+                       if self._cached_eapi != "0":
+                               yield "valid EAPI assignment must occur on or before line: %s" % \
+                                       self._eapi_line_num
+               elif self._parsed_eapi != self._cached_eapi:
+                       yield ("bash returned EAPI '%s' which does not match "
+                               "assignment on line: %s") % \
+                               (self._cached_eapi, self._eapi_line_num)
 
 class EbuildPatches(LineCheck):
        """Ensure ebuilds use bash arrays for PATCHES to ensure white space safety"""
 
 class EbuildPatches(LineCheck):
        """Ensure ebuilds use bash arrays for PATCHES to ensure white space safety"""
@@ -279,19 +331,36 @@ class EbuildQuotedA(LineCheck):
                if match:
                        return "Quoted \"${A}\" on line: %d"
 
                if match:
                        return "Quoted \"${A}\" on line: %d"
 
+class NoOffsetWithHelpers(LineCheck):
+       """ Check that the image location, the alternate root offset, and the
+       offset prefix (D, ROOT, ED, EROOT and EPREFIX) are not used with
+       helpers """
+
+       repoman_check_name = 'variable.usedwithhelpers'
+       # Ignore matches in quoted strings like this:
+       # elog "installed into ${ROOT}usr/share/php5/apc/."
+       re = re.compile(r'^[^#"\']*\b(docinto|docompress|dodir|dohard|exeinto|fowners|fperms|insinto|into)\s+"?\$\{?(D|ROOT|ED|EROOT|EPREFIX)\b.*')
+       error = errors.NO_OFFSET_WITH_HELPERS
+
 class ImplicitRuntimeDeps(LineCheck):
        """
        Detect the case where DEPEND is set and RDEPEND is unset in the ebuild,
 class ImplicitRuntimeDeps(LineCheck):
        """
        Detect the case where DEPEND is set and RDEPEND is unset in the ebuild,
-       since this triggers implicit RDEPEND=$DEPEND assignment.
+       since this triggers implicit RDEPEND=$DEPEND assignment (prior to EAPI 4).
        """
 
        repoman_check_name = 'RDEPEND.implicit'
        """
 
        repoman_check_name = 'RDEPEND.implicit'
-       _assignment_re = re.compile(r'^\s*(R?DEPEND)=')
+       _assignment_re = re.compile(r'^\s*(R?DEPEND)\+?=')
 
        def new(self, pkg):
                self._rdepend = False
                self._depend = False
 
 
        def new(self, pkg):
                self._rdepend = False
                self._depend = False
 
+       def check_eapi(self, eapi):
+               # Beginning with EAPI 4, there is no
+               # implicit RDEPEND=$DEPEND assignment
+               # to be concerned with.
+               return eapi_has_implicit_rdepend(eapi)
+
        def check(self, num, line):
                if not self._rdepend:
                        m = self._assignment_re.match(line)
        def check(self, num, line):
                if not self._rdepend:
                        m = self._assignment_re.match(line)
@@ -306,77 +375,324 @@ class ImplicitRuntimeDeps(LineCheck):
                if self._depend and not self._rdepend:
                        yield 'RDEPEND is not explicitly assigned'
 
                if self._depend and not self._rdepend:
                        yield 'RDEPEND is not explicitly assigned'
 
-class InheritAutotools(LineCheck):
-       """
-       Make sure appropriate functions are called in
-       ebuilds that inherit autotools.eclass.
-       """
-
-       repoman_check_name = 'inherit.autotools'
-       ignore_line = re.compile(r'(^|\s*)#')
-       _inherit_autotools_re = re.compile(r'^\s*inherit\s(.*\s)?autotools(\s|$)')
-       _autotools_funcs = (
-               "eaclocal", "eautoconf", "eautoheader",
-               "eautomake", "eautoreconf", "_elibtoolize")
-       _autotools_func_re = re.compile(r'\b(' + \
-               "|".join(_autotools_funcs) + r')\b')
-       # Exempt eclasses:
-       # git - An EGIT_BOOTSTRAP variable may be used to call one of
-       #       the autotools functions.
-       # subversion - An ESVN_BOOTSTRAP variable may be used to call one of
-       #       the autotools functions.
-       _exempt_eclasses = frozenset(["git", "subversion"])
+class InheritDeprecated(LineCheck):
+       """Check if ebuild directly or indirectly inherits a deprecated eclass."""
+
+       repoman_check_name = 'inherit.deprecated'
+
+       # deprecated eclass : new eclass (False if no new eclass)
+       deprecated_classes = {
+               "bash-completion": "bash-completion-r1",
+               "boost-utils": False,
+               "distutils": "distutils-r1",
+               "gems": "ruby-fakegem",
+               "git": "git-2",
+               "mono": "mono-env",
+               "mozconfig-2": "mozconfig-3",
+               "mozcoreconf": "mozcoreconf-2",
+               "php-ext-pecl-r1": "php-ext-pecl-r2",
+               "php-ext-source-r1": "php-ext-source-r2",
+               "php-pear": "php-pear-r1",
+               "python": "python-r1 / python-single-r1 / python-any-r1",
+               "python-distutils-ng": "python-r1 + distutils-r1",
+               "qt3": False,
+               "qt4": "qt4-r2",
+               "ruby": "ruby-ng",
+               "ruby-gnome2": "ruby-ng-gnome2",
+               "x-modular": "xorg-2",
+               }
+
+       _inherit_re = re.compile(r'^\s*inherit\s(.*)$')
 
        def new(self, pkg):
 
        def new(self, pkg):
-               self._inherit_autotools = None
-               self._autotools_func_call = None
-               self._disabled = self._exempt_eclasses.intersection(pkg.inherited)
+               self._errors = []
+               self._indirect_deprecated = set(eclass for eclass in \
+                       self.deprecated_classes if eclass in pkg.inherited)
 
        def check(self, num, line):
 
        def check(self, num, line):
-               if self._disabled:
+
+               direct_inherits = None
+               m = self._inherit_re.match(line)
+               if m is not None:
+                       direct_inherits = m.group(1)
+                       if direct_inherits:
+                               direct_inherits = direct_inherits.split()
+
+               if not direct_inherits:
                        return
                        return
-               if self._inherit_autotools is None:
-                       self._inherit_autotools = self._inherit_autotools_re.match(line)
-               if self._inherit_autotools is not None and \
-                       self._autotools_func_call is None:
-                       self._autotools_func_call = self._autotools_func_re.search(line)
 
 
-       def end(self):
-               if self._inherit_autotools and self._autotools_func_call is None:
-                       yield 'no eauto* function called'
+               for eclass in direct_inherits:
+                       replacement = self.deprecated_classes.get(eclass)
+                       if replacement is None:
+                               pass
+                       elif replacement is False:
+                               self._indirect_deprecated.discard(eclass)
+                               self._errors.append("please migrate from " + \
+                                       "'%s' (no replacement) on line: %d" % (eclass, num + 1))
+                       else:
+                               self._indirect_deprecated.discard(eclass)
+                               self._errors.append("please migrate from " + \
+                                       "'%s' to '%s' on line: %d" % \
+                                       (eclass, replacement, num + 1))
 
 
-class IUseUndefined(LineCheck):
+       def end(self):
+               for error in self._errors:
+                       yield error
+               del self._errors
+
+               for eclass in self._indirect_deprecated:
+                       replacement = self.deprecated_classes[eclass]
+                       if replacement is False:
+                               yield "please migrate from indirect " + \
+                                       "inherit of '%s' (no replacement)" % (eclass,)
+                       else:
+                               yield "please migrate from indirect " + \
+                                       "inherit of '%s' to '%s'" % \
+                                       (eclass, replacement)
+               del self._indirect_deprecated
+
+class InheritEclass(LineCheck):
        """
        """
-       Make sure the ebuild defines IUSE (style guideline
-       says to define IUSE even when empty).
+       Base class for checking for missing inherits, as well as excess inherits.
+
+       Args:
+               eclass: Set to the name of your eclass.
+               funcs: A tuple of functions that this eclass provides.
+               comprehensive: Is the list of functions complete?
+               exempt_eclasses: If these eclasses are inherited, disable the missing
+                                 inherit check.
        """
 
        """
 
-       repoman_check_name = 'IUSE.undefined'
-       _iuse_def_re = re.compile(r'^IUSE=.*')
+       def __init__(self, eclass, funcs=None, comprehensive=False,
+               exempt_eclasses=None, ignore_missing=False, **kwargs):
+               self._eclass = eclass
+               self._comprehensive = comprehensive
+               self._exempt_eclasses = exempt_eclasses
+               self._ignore_missing = ignore_missing
+               inherit_re = eclass
+               self._inherit_re = re.compile(r'^(\s*|.*[|&]\s*)\binherit\s(.*\s)?%s(\s|$)' % inherit_re)
+               # Match when the function is preceded only by leading whitespace, a
+               # shell operator such as (, {, |, ||, or &&, or optional variable
+               # setting(s). This prevents false positives in things like elog
+               # messages, as reported in bug #413285.
+               self._func_re = re.compile(r'(^|[|&{(])\s*(\w+=.*)?\b(' + '|'.join(funcs) + r')\b')
 
        def new(self, pkg):
 
        def new(self, pkg):
-               self._iuse_def = None
+               self.repoman_check_name = 'inherit.missing'
+               # We can't use pkg.inherited because that tells us all the eclasses that
+               # have been inherited and not just the ones we inherit directly.
+               self._inherit = False
+               self._func_call = False
+               if self._exempt_eclasses is not None:
+                       inherited = pkg.inherited
+                       self._disabled = any(x in inherited for x in self._exempt_eclasses)
+               else:
+                       self._disabled = False
+               self._eapi = pkg.eapi
 
        def check(self, num, line):
 
        def check(self, num, line):
-               if self._iuse_def is None:
-                       self._iuse_def = self._iuse_def_re.match(line)
+               if not self._inherit:
+                       self._inherit = self._inherit_re.match(line)
+               if not self._inherit:
+                       if self._disabled or self._ignore_missing:
+                               return
+                       s = self._func_re.search(line)
+                       if s is not None:
+                               func_name = s.group(3)
+                               eapi_func = _eclass_eapi_functions.get(func_name)
+                               if eapi_func is None or not eapi_func(self._eapi):
+                                       self._func_call = True
+                                       return ('%s.eclass is not inherited, '
+                                               'but "%s" found at line: %s') % \
+                                               (self._eclass, func_name, '%d')
+               elif not self._func_call:
+                       self._func_call = self._func_re.search(line)
 
        def end(self):
 
        def end(self):
-               if self._iuse_def is None:
-                       yield 'IUSE is not defined'
-
-class EMakeParallelDisabled(LineCheck):
+               if not self._disabled and self._comprehensive and self._inherit and not self._func_call:
+                       self.repoman_check_name = 'inherit.unused'
+                       yield 'no function called from %s.eclass; please drop' % self._eclass
+
+_eclass_eapi_functions = {
+       "usex" : lambda eapi: eapi not in ("0", "1", "2", "3", "4", "4-python", "4-slot-abi")
+}
+
+# eclasses that export ${ECLASS}_src_(compile|configure|install)
+_eclass_export_functions = (
+       'ant-tasks', 'apache-2', 'apache-module', 'aspell-dict',
+       'autotools-utils', 'base', 'bsdmk', 'cannadic',
+       'clutter', 'cmake-utils', 'db', 'distutils', 'elisp',
+       'embassy', 'emboss', 'emul-linux-x86', 'enlightenment',
+       'font-ebdftopcf', 'font', 'fox', 'freebsd', 'freedict',
+       'games', 'games-ggz', 'games-mods', 'gdesklets',
+       'gems', 'gkrellm-plugin', 'gnatbuild', 'gnat', 'gnome2',
+       'gnome-python-common', 'gnustep-base', 'go-mono', 'gpe',
+       'gst-plugins-bad', 'gst-plugins-base', 'gst-plugins-good',
+       'gst-plugins-ugly', 'gtk-sharp-module', 'haskell-cabal',
+       'horde', 'java-ant-2', 'java-pkg-2', 'java-pkg-simple',
+       'java-virtuals-2', 'kde4-base', 'kde4-meta', 'kernel-2',
+       'latex-package', 'linux-mod', 'mozlinguas', 'myspell',
+       'myspell-r2', 'mysql', 'mysql-v2', 'mythtv-plugins',
+       'oasis', 'obs-service', 'office-ext', 'perl-app',
+       'perl-module', 'php-ext-base-r1', 'php-ext-pecl-r2',
+       'php-ext-source-r2', 'php-lib-r1', 'php-pear-lib-r1',
+       'php-pear-r1', 'python-distutils-ng', 'python',
+       'qt4-build', 'qt4-r2', 'rox-0install', 'rox', 'ruby',
+       'ruby-ng', 'scsh', 'selinux-policy-2', 'sgml-catalog',
+       'stardict', 'sword-module', 'tetex-3', 'tetex',
+       'texlive-module', 'toolchain-binutils', 'toolchain',
+       'twisted', 'vdr-plugin-2', 'vdr-plugin', 'vim',
+       'vim-plugin', 'vim-spell', 'virtuoso', 'vmware',
+       'vmware-mod', 'waf-utils', 'webapp', 'xemacs-elisp',
+       'xemacs-packages', 'xfconf', 'x-modular', 'xorg-2',
+       'zproduct'
+)
+
+_eclass_info = {
+       'autotools': {
+               'funcs': (
+                       'eaclocal', 'eautoconf', 'eautoheader',
+                       'eautomake', 'eautoreconf', '_elibtoolize',
+                       'eautopoint'
+               ),
+               'comprehensive': True,
+
+               # Exempt eclasses:
+               # git - An EGIT_BOOTSTRAP variable may be used to call one of
+               #       the autotools functions.
+               # subversion - An ESVN_BOOTSTRAP variable may be used to call one of
+               #       the autotools functions.
+               'exempt_eclasses': ('git', 'git-2', 'subversion', 'autotools-utils')
+       },
+
+       'eutils': {
+               'funcs': (
+                       'estack_push', 'estack_pop', 'eshopts_push', 'eshopts_pop',
+                       'eumask_push', 'eumask_pop', 'epatch', 'epatch_user',
+                       'emktemp', 'edos2unix', 'in_iuse', 'use_if_iuse', 'usex'
+               ),
+               'comprehensive': False,
+
+               # These are "eclasses are the whole ebuild" type thing.
+               'exempt_eclasses': _eclass_export_functions,
+       },
+
+       'flag-o-matic': {
+               'funcs': (
+                       'filter-(ld)?flags', 'strip-flags', 'strip-unsupported-flags',
+                       'append-((ld|c(pp|xx)?))?flags', 'append-libs',
+               ),
+               'comprehensive': False
+       },
+
+       'libtool': {
+               'funcs': (
+                       'elibtoolize',
+               ),
+               'comprehensive': True,
+               'exempt_eclasses': ('autotools',)
+       },
+
+       'multilib': {
+               'funcs': (
+                       'get_libdir',
+               ),
+
+               # These are "eclasses are the whole ebuild" type thing.
+               'exempt_eclasses': _eclass_export_functions + ('autotools', 'libtool',
+                       'multilib-minimal'),
+
+               'comprehensive': False
+       },
+
+       'multiprocessing': {
+               'funcs': (
+                       'makeopts_jobs',
+               ),
+               'comprehensive': False
+       },
+
+       'prefix': {
+               'funcs': (
+                       'eprefixify',
+               ),
+               'comprehensive': True
+       },
+
+       'toolchain-funcs': {
+               'funcs': (
+                       'gen_usr_ldscript',
+               ),
+               'comprehensive': False
+       },
+
+       'user': {
+               'funcs': (
+                       'enewuser', 'enewgroup',
+                       'egetent', 'egethome', 'egetshell', 'esethome'
+               ),
+               'comprehensive': True
+       }
+}
+
+if not _ENABLE_INHERIT_CHECK:
+       # Since the InheritEclass check is experimental, in the stable branch
+       # we emulate the old eprefixify.defined and inherit.autotools checks.
+       _eclass_info = {
+               'autotools': {
+                       'funcs': (
+                               'eaclocal', 'eautoconf', 'eautoheader',
+                               'eautomake', 'eautoreconf', '_elibtoolize',
+                               'eautopoint'
+                       ),
+                       'comprehensive': True,
+                       'ignore_missing': True,
+                       'exempt_eclasses': ('git', 'git-2', 'subversion', 'autotools-utils')
+               },
+
+               'prefix': {
+                       'funcs': (
+                               'eprefixify',
+                       ),
+                       'comprehensive': False
+               }
+       }
+
+class EMakeParallelDisabled(PhaseCheck):
        """Check for emake -j1 calls which disable parallelization."""
        repoman_check_name = 'upstream.workaround'
        re = re.compile(r'^\s*emake\s+.*-j\s*1\b')
        error = errors.EMAKE_PARALLEL_DISABLED
 
        """Check for emake -j1 calls which disable parallelization."""
        repoman_check_name = 'upstream.workaround'
        re = re.compile(r'^\s*emake\s+.*-j\s*1\b')
        error = errors.EMAKE_PARALLEL_DISABLED
 
+       def phase_check(self, num, line):
+               if self.in_phase == 'src_compile' or self.in_phase == 'src_install':
+                       if self.re.match(line):
+                               return self.error
+
 class EMakeParallelDisabledViaMAKEOPTS(LineCheck):
        """Check for MAKEOPTS=-j1 that disables parallelization."""
        repoman_check_name = 'upstream.workaround'
        re = re.compile(r'^\s*MAKEOPTS=(\'|")?.*-j\s*1\b')
        error = errors.EMAKE_PARALLEL_DISABLED_VIA_MAKEOPTS
 
 class EMakeParallelDisabledViaMAKEOPTS(LineCheck):
        """Check for MAKEOPTS=-j1 that disables parallelization."""
        repoman_check_name = 'upstream.workaround'
        re = re.compile(r'^\s*MAKEOPTS=(\'|")?.*-j\s*1\b')
        error = errors.EMAKE_PARALLEL_DISABLED_VIA_MAKEOPTS
 
+class NoAsNeeded(LineCheck):
+       """Check for calls to the no-as-needed function."""
+       repoman_check_name = 'upstream.workaround'
+       re = re.compile(r'.*\$\(no-as-needed\)')
+       error = errors.NO_AS_NEEDED
+
+class PreserveOldLib(LineCheck):
+       """Check for calls to the deprecated preserve_old_lib function."""
+       repoman_check_name = 'ebuild.minorsyn'
+       re = re.compile(r'.*preserve_old_lib')
+       error = errors.PRESERVE_OLD_LIB
+
+class SandboxAddpredict(LineCheck):
+       """Check for calls to the addpredict function."""
+       repoman_check_name = 'upstream.workaround'
+       re = re.compile(r'(^|\s)addpredict\b')
+       error = errors.SANDBOX_ADDPREDICT
+
 class DeprecatedBindnowFlags(LineCheck):
        """Check for calls to the deprecated bindnow-flags function."""
        repoman_check_name = 'ebuild.minorsyn'
 class DeprecatedBindnowFlags(LineCheck):
        """Check for calls to the deprecated bindnow-flags function."""
        repoman_check_name = 'ebuild.minorsyn'
@@ -394,20 +710,134 @@ class WantAutoDefaultValue(LineCheck):
                        return 'WANT_AUTO' + m.group(1) + \
                                ' redundantly set to default value "latest" on line: %d'
 
                        return 'WANT_AUTO' + m.group(1) + \
                                ' redundantly set to default value "latest" on line: %d'
 
-_constant_checks = tuple((c() for c in (
-       EbuildHeader, EbuildWhitespace, EbuildQuote,
-       EbuildAssignment, EbuildUselessDodoc,
-       EbuildUselessCdS, EbuildNestedDie,
-       EbuildPatches, EbuildQuotedA, EapiDefinition,
-       IUseUndefined, ImplicitRuntimeDeps, InheritAutotools,
-       EMakeParallelDisabled, EMakeParallelDisabledViaMAKEOPTS,
-       DeprecatedBindnowFlags, SrcUnpackPatches, WantAutoDefaultValue)))
+class SrcCompileEconf(PhaseCheck):
+       repoman_check_name = 'ebuild.minorsyn'
+       configure_re = re.compile(r'\s(econf|./configure)')
+
+       def check_eapi(self, eapi):
+               return eapi_has_src_prepare_and_src_configure(eapi)
+
+       def phase_check(self, num, line):
+               if self.in_phase == 'src_compile':
+                       m = self.configure_re.match(line)
+                       if m is not None:
+                               return ("'%s'" % m.group(1)) + \
+                                       " call should be moved to src_configure from line: %d"
+
+class SrcUnpackPatches(PhaseCheck):
+       repoman_check_name = 'ebuild.minorsyn'
+       src_prepare_tools_re = re.compile(r'\s(e?patch|sed)\s')
+
+       def check_eapi(self, eapi):
+               return eapi_has_src_prepare_and_src_configure(eapi)
+
+       def phase_check(self, num, line):
+               if self.in_phase == 'src_unpack':
+                       m = self.src_prepare_tools_re.search(line)
+                       if m is not None:
+                               return ("'%s'" % m.group(1)) + \
+                                       " call should be moved to src_prepare from line: %d"
+
+class BuiltWithUse(LineCheck):
+       repoman_check_name = 'ebuild.minorsyn'
+       re = re.compile(r'(^|.*\b)built_with_use\b')
+       error = errors.BUILT_WITH_USE
+
+class DeprecatedUseq(LineCheck):
+       """Checks for use of the deprecated useq function"""
+       repoman_check_name = 'ebuild.minorsyn'
+       re = re.compile(r'(^|.*\b)useq\b')
+       error = errors.USEQ_ERROR
+
+class DeprecatedHasq(LineCheck):
+       """Checks for use of the deprecated hasq function"""
+       repoman_check_name = 'ebuild.minorsyn'
+       re = re.compile(r'(^|.*\b)hasq\b')
+       error = errors.HASQ_ERROR
+
+# EAPI-3 checks
+class Eapi3DeprecatedFuncs(LineCheck):
+       repoman_check_name = 'EAPI.deprecated'
+       deprecated_commands_re = re.compile(r'^\s*(check_license)\b')
+
+       def check_eapi(self, eapi):
+               return eapi not in ('0', '1', '2')
+
+       def check(self, num, line):
+               m = self.deprecated_commands_re.match(line)
+               if m is not None:
+                       return ("'%s'" % m.group(1)) + \
+                               " has been deprecated in EAPI=3 on line: %d"
+
+# EAPI-4 checks
+class Eapi4IncompatibleFuncs(LineCheck):
+       repoman_check_name = 'EAPI.incompatible'
+       banned_commands_re = re.compile(r'^\s*(dosed|dohard)')
+
+       def check_eapi(self, eapi):
+               return not eapi_has_dosed_dohard(eapi)
+
+       def check(self, num, line):
+               m = self.banned_commands_re.match(line)
+               if m is not None:
+                       return ("'%s'" % m.group(1)) + \
+                               " has been banned in EAPI=4 on line: %d"
+
+class Eapi4GoneVars(LineCheck):
+       repoman_check_name = 'EAPI.incompatible'
+       undefined_vars_re = re.compile(r'.*\$(\{(AA|KV|EMERGE_FROM)\}|(AA|KV|EMERGE_FROM))')
+
+       def check_eapi(self, eapi):
+               # AA, KV, and EMERGE_FROM should not be referenced in EAPI 4 or later.
+               return not eapi_exports_AA(eapi)
+
+       def check(self, num, line):
+               m = self.undefined_vars_re.match(line)
+               if m is not None:
+                       return ("variable '$%s'" % m.group(1)) + \
+                               " is gone in EAPI=4 on line: %d"
+
+class PortageInternal(LineCheck):
+       repoman_check_name = 'portage.internal'
+       ignore_comment = True
+       # Match when the command is preceded only by leading whitespace or a shell
+       # operator such as (, {, |, ||, or &&. This prevents false positives in
+       # things like elog messages, as reported in bug #413285.
+       re = re.compile(r'^(\s*|.*[|&{(]+\s*)\b(ecompress|ecompressdir|env-update|prepall|prepalldocs|preplib)\b')
+
+       def check(self, num, line):
+               """Run the check on line and return error if there is one"""
+               m = self.re.match(line)
+               if m is not None:
+                       return ("'%s'" % m.group(2)) + " called on line: %d"
+
+class PortageInternalVariableAssignment(LineCheck):
+       repoman_check_name = 'portage.internal'
+       internal_assignment = re.compile(r'\s*(export\s+)?(EXTRA_ECONF|EXTRA_EMAKE)\+?=')
+
+       def check(self, num, line):
+               match = self.internal_assignment.match(line)
+               e = None
+               if match is not None:
+                       e = 'Assignment to variable %s' % match.group(2)
+                       e += ' on line: %d'
+               return e
+
+_base_check_classes = (InheritEclass, LineCheck, PhaseCheck)
+_constant_checks = tuple(chain((v() for k, v in globals().items()
+       if isinstance(v, type) and issubclass(v, LineCheck) and v not in _base_check_classes),
+       (InheritEclass(k, **portage._native_kwargs(kwargs))
+       for k, kwargs in _eclass_info.items())))
 
 _here_doc_re = re.compile(r'.*\s<<[-]?(\w+)$')
 
 _here_doc_re = re.compile(r'.*\s<<[-]?(\w+)$')
+_ignore_comment_re = re.compile(r'^\s*#')
 
 def run_checks(contents, pkg):
 
 def run_checks(contents, pkg):
+       unicode_escape_codec = codecs.lookup('unicode_escape')
+       unicode_escape = lambda x: unicode_escape_codec.decode(x)[0]
        checks = _constant_checks
        here_doc_delim = None
        checks = _constant_checks
        here_doc_delim = None
+       multiline = None
 
        for lc in checks:
                lc.new(pkg)
 
        for lc in checks:
                lc.new(pkg)
@@ -420,16 +850,58 @@ def run_checks(contents, pkg):
                if here_doc_delim is None:
                        here_doc = _here_doc_re.match(line)
                        if here_doc is not None:
                if here_doc_delim is None:
                        here_doc = _here_doc_re.match(line)
                        if here_doc is not None:
-                               here_doc_delim = re.compile('^%s$' % here_doc.group(1))
+                               here_doc_delim = re.compile(r'^\s*%s$' % here_doc.group(1))
+               if here_doc_delim is not None:
+                       continue
+
+               # Unroll multiline escaped strings so that we can check things:
+               #               inherit foo bar \
+               #                       moo \
+               #                       cow
+               # This will merge these lines like so:
+               #               inherit foo bar         moo     cow
+               try:
+                       # A normal line will end in the two bytes: <\> <\n>.  So decoding
+                       # that will result in python thinking the <\n> is being escaped
+                       # and eat the single <\> which makes it hard for us to detect.
+                       # Instead, strip the newline (which we know all lines have), and
+                       # append a <0>.  Then when python escapes it, if the line ended
+                       # in a <\>, we'll end up with a <\0> marker to key off of.  This
+                       # shouldn't be a problem with any valid ebuild ...
+                       line_escaped = unicode_escape(line.rstrip('\n') + '0')
+               except SystemExit:
+                       raise
+               except:
+                       # Who knows what kind of crazy crap an ebuild will have
+                       # in it -- don't allow it to kill us.
+                       line_escaped = line
+               if multiline:
+                       # Chop off the \ and \n bytes from the previous line.
+                       multiline = multiline[:-2] + line
+                       if not line_escaped.endswith('\0'):
+                               line = multiline
+                               num = multinum
+                               multiline = None
+                       else:
+                               continue
+               else:
+                       if line_escaped.endswith('\0'):
+                               multinum = num
+                               multiline = line
+                               continue
 
 
-               if here_doc_delim is None:
-                       # We're not in a here-document.
+               if not line.endswith("#nowarn\n"):
+                       # Finally we have a full line to parse.
+                       is_comment = _ignore_comment_re.match(line) is not None
                        for lc in checks:
                        for lc in checks:
-                               ignore = lc.ignore_line
-                               if not ignore or not ignore.match(line):
-                                       e = lc.check(num, line)
-                                       if e:
-                                               yield lc.repoman_check_name, e % (num + 1)
+                               if is_comment and lc.ignore_comment:
+                                       continue
+                               if lc.check_eapi(pkg.eapi):
+                                       ignore = lc.ignore_line
+                                       if not ignore or not ignore.match(line):
+                                               e = lc.check(num, line)
+                                               if e:
+                                                       yield lc.repoman_check_name, e % (num + 1)
 
        for lc in checks:
                i = lc.end()
 
        for lc in checks:
                i = lc.end()