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
-# Copyright 2007 Gentoo Foundation
+# Copyright 2007-2013 Gentoo Foundation
 # 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."""
 
-import os
+from __future__ import unicode_literals
+
+import codecs
+from itertools import chain
 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
+       """True if lines containing nothing more than comments with optional
+       leading whitespace should be ignored"""
+       ignore_comment = True
 
        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):
@@ -27,26 +41,55 @@ class LineCheck(object):
        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
-       
+
        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$')
-       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):
-               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)
 
@@ -56,7 +99,7 @@ class EbuildHeader(LineCheck):
                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):
@@ -69,8 +112,9 @@ class EbuildWhitespace(LineCheck):
        repoman_check_name = 'ebuild.minorsyn'
 
        ignore_line = re.compile(r'(^$)|(^(\t)*#)')
+       ignore_comment = False
        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:
@@ -78,6 +122,26 @@ class EbuildWhitespace(LineCheck):
                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..."""
 
@@ -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+)')
+       ignore_comment = False
        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 multibuild.eclass
+       var_names += ["BUILD_DIR"]
+
        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+)')
-       
+
        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'
-
        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
-               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
-               self.previous_line = line
                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):
-       """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')
-       
+
        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(
-               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)
@@ -198,7 +269,7 @@ class EbuildUselessDodoc(LineCheck):
 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):
@@ -213,54 +284,35 @@ class EbuildUselessCdS(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'
-
-       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):
-               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):
+               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"""
@@ -279,19 +331,36 @@ class EbuildQuotedA(LineCheck):
                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,
-       since this triggers implicit RDEPEND=$DEPEND assignment.
+       since this triggers implicit RDEPEND=$DEPEND assignment (prior to EAPI 4).
        """
 
        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 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)
@@ -306,77 +375,324 @@ class ImplicitRuntimeDeps(LineCheck):
                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):
-               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):
-               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
-               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):
-               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):
-               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):
-               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
 
+       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 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'
@@ -394,20 +710,134 @@ class WantAutoDefaultValue(LineCheck):
                        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+)$')
+_ignore_comment_re = re.compile(r'^\s*#')
 
 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
+       multiline = None
 
        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:
-                               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:
-                               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()