aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Dolbec <dolsen@gentoo.org>2017-07-15 00:15:22 +0000
committerZac Medico <zmedico@gentoo.org>2018-03-29 20:51:16 -0700
commit4e81d2552ca50bb38e8499611c4cd999056a1ea6 (patch)
tree83ab02a6b5fffb0fbef49532f23da221297a761d
parentrepoman: Update modules/scan/module.py for linechecks config addition (diff)
downloadportage-4e81d2552ca50bb38e8499611c4cd999056a1ea6.tar.gz
portage-4e81d2552ca50bb38e8499611c4cd999056a1ea6.tar.bz2
portage-4e81d2552ca50bb38e8499611c4cd999056a1ea6.zip
repoman: Initial creation of a new linechecks sub module plugin system
This new module system will be for splitting the multicheck module checks into a fully configurable, plugable system.
-rw-r--r--repoman/pym/repoman/modules/linechecks/__init__.py1
-rw-r--r--repoman/pym/repoman/modules/linechecks/base.py101
-rw-r--r--repoman/pym/repoman/modules/linechecks/config.py103
-rw-r--r--repoman/pym/repoman/modules/linechecks/controller.py145
4 files changed, 350 insertions, 0 deletions
diff --git a/repoman/pym/repoman/modules/linechecks/__init__.py b/repoman/pym/repoman/modules/linechecks/__init__.py
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/repoman/pym/repoman/modules/linechecks/__init__.py
@@ -0,0 +1 @@
+
diff --git a/repoman/pym/repoman/modules/linechecks/base.py b/repoman/pym/repoman/modules/linechecks/base.py
new file mode 100644
index 000000000..4e3d6f0b4
--- /dev/null
+++ b/repoman/pym/repoman/modules/linechecks/base.py
@@ -0,0 +1,101 @@
+
+import logging
+import re
+
+
+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 __init__(self, errors):
+ self.errors = errors
+
+ def new(self, pkg):
+ pass
+
+ def check_eapi(self, eapi):
+ """Returns if check should be run in the given EAPI (default: 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):
+ return self.errors[self.error]
+
+ def end(self):
+ pass
+
+
+class InheritEclass(LineCheck):
+ """
+ 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.
+ """
+
+ def __init__(
+ self, eclass, eclass_eapi_functions, errors, 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
+ self.errors = errors
+ inherit_re = eclass
+ self._eclass_eapi_functions = eclass_eapi_functions
+ 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.
+ logging.debug("InheritEclass, eclass: %s, funcs: %s", eclass, funcs)
+ self._func_re = re.compile(
+ r'(^|[|&{(])\s*(\w+=.*)?\b(' + r'|'.join(funcs) + r')\b')
+
+ def new(self, pkg):
+ 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 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 = self._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 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
diff --git a/repoman/pym/repoman/modules/linechecks/config.py b/repoman/pym/repoman/modules/linechecks/config.py
new file mode 100644
index 000000000..bba43a875
--- /dev/null
+++ b/repoman/pym/repoman/modules/linechecks/config.py
@@ -0,0 +1,103 @@
+# -*- coding:utf-8 -*-
+# repoman: Checks
+# Copyright 2007-2017 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+"""This module contains functions used in Repoman to ascertain the quality
+and correctness of an ebuild."""
+
+from __future__ import unicode_literals
+
+import collections
+import logging
+import os
+from copy import deepcopy
+
+from repoman._portage import portage
+from repoman.config import load_config
+
+# Avoid a circular import issue in py2.7
+portage.proxy.lazyimport.lazyimport(globals(),
+ 'portage.util:stack_lists',
+)
+
+
+def merge(dict1, dict2):
+ ''' Return a new dictionary by merging two dictionaries recursively. '''
+
+ result = deepcopy(dict1)
+
+ for key, value in dict2.items():
+ if isinstance(value, collections.Mapping):
+ result[key] = merge(result.get(key, {}), value)
+ else:
+ result[key] = deepcopy(dict2[key])
+
+ return result
+
+
+class LineChecksConfig(object):
+ '''Holds our LineChecks configuration data and operation functions'''
+
+ def __init__(self, repo_settings):
+ '''Class init
+
+ @param repo_settings: RepoSettings instance
+ @param configpaths: ordered list of filepaths to load
+ '''
+ self.repo_settings = repo_settings
+ self.infopaths = [os.path.join(path, 'linechecks.yaml') for path in self.repo_settings.masters_list]
+ logging.debug("LineChecksConfig; configpaths: %s", self.infopaths)
+ self.info_config = None
+ self._config = None
+ self.usex_supported_eapis = None
+ self.in_iuse_supported_eapis = None
+ self.get_libdir_supported_eapis = None
+ self.eclass_eapi_functions = {}
+ self.eclass_export_functions = None
+ self.eclass_info = {}
+ self.eclass_info_experimental_inherit = {}
+ self.errors = {}
+ self.load_checks_info()
+
+ def load_checks_info(self, infopaths=None):
+ '''load the config files in order
+
+ @param infopaths: ordered list of filepaths to load
+ '''
+ if infopaths:
+ self.infopaths = infopaths
+ elif not self.infopaths:
+ logging.error("LineChecksConfig; Error: No linechecks.yaml files defined")
+ configs = load_config(self.infopaths, 'yaml')
+ if configs == {}:
+ logging.error("LineChecksConfig: Failed to load a valid 'linechecks.yaml' file at paths: %s", self.infopaths)
+ return False
+ logging.debug("LineChecksConfig: linechecks.yaml configs: %s", configs)
+ self.info_config = configs
+
+ self.errors = self.info_config['errors']
+ self.usex_supported_eapis = self.info_config.get('usex_supported_eapis', [])
+ self.in_iuse_supported_eapis = self.info_config.get('in_iuse_supported_eapis', [])
+ self.eclass_info_experimental_inherit = self.info_config.get('eclass_info_experimental_inherit', [])
+ self.get_libdir_supported_eapis = self.in_iuse_supported_eapis
+ self.eclass_eapi_functions = {
+ "usex": lambda eapi: eapi not in self.usex_supported_eapis,
+ "in_iuse": lambda eapi: eapi not in self.in_iuse_supported_eapis,
+ "get_libdir": lambda eapi: eapi not in self.get_libdir_supported_eapis,
+ }
+
+ # eclasses that export ${ECLASS}_src_(compile|configure|install)
+ self.eclass_export_functions = self.info_config.get('eclass_export_functions', [])
+
+ self.eclass_info_experimental_inherit = self.info_config.get('eclass_info_experimental_inherit', {})
+ # These are "eclasses are the whole ebuild" type thing.
+ try:
+ self.eclass_info_experimental_inherit['eutils']['exempt_eclasses'] = self.eclass_export_functions
+ except KeyError:
+ pass
+ try:
+ self.eclass_info_experimental_inherit['multilib']['exempt_eclasses'] = self.eclass_export_functions + [
+ 'autotools', 'libtool', 'multilib-minimal']
+ except KeyError:
+ pass
diff --git a/repoman/pym/repoman/modules/linechecks/controller.py b/repoman/pym/repoman/modules/linechecks/controller.py
new file mode 100644
index 000000000..7082a5d02
--- /dev/null
+++ b/repoman/pym/repoman/modules/linechecks/controller.py
@@ -0,0 +1,145 @@
+
+import logging
+import operator
+import os
+import re
+
+from repoman.modules.linechecks.base import InheritEclass
+from repoman.modules.linechecks.config import LineChecksConfig
+from repoman._portage import portage
+
+# Avoid a circular import issue in py2.7
+portage.proxy.lazyimport.lazyimport(globals(),
+ 'portage.module:Modules',
+)
+
+MODULES_PATH = os.path.dirname(__file__)
+# initial development debug info
+logging.debug("LineChecks module path: %s", MODULES_PATH)
+
+
+class LineCheckController(object):
+ '''Initializes and runs the LineCheck checks'''
+
+ def __init__(self, repo_settings, linechecks):
+ '''Class init
+
+ @param repo_settings: RepoSettings instance
+ '''
+ self.repo_settings = repo_settings
+ self.linechecks = linechecks
+ self.config = LineChecksConfig(repo_settings)
+
+ self.controller = Modules(path=MODULES_PATH, namepath="repoman.modules.linechecks")
+ logging.debug("LineCheckController; module_names: %s", self.controller.module_names)
+
+ self._constant_checks = None
+
+ self._here_doc_re = re.compile(r'.*<<[-]?(\w+)\s*(>\s*\S+\s*)?$')
+ self._ignore_comment_re = re.compile(r'^\s*#')
+ self._continuation_re = re.compile(r'(\\)*$')
+
+ def checks_init(self, experimental_inherit=False):
+ '''Initialize the main variables
+
+ @param experimental_inherit boolean
+ '''
+ if not experimental_inherit:
+ # Emulate the old eprefixify.defined and inherit.autotools checks.
+ self._eclass_info = self.config.eclass_info
+ else:
+ self._eclass_info = self.config.eclass_info_experimental_inherit
+
+ self._constant_checks = []
+ logging.debug("LineCheckController; modules: %s", self.linechecks)
+ # Add in the pluggable modules
+ for mod in self.linechecks:
+ mod_class = self.controller.get_class(mod)
+ logging.debug("LineCheckController; module_name: %s, class: %s", mod, mod_class.__name__)
+ self._constant_checks.append(mod_class(self.config.errors))
+ # Add in the InheritEclass checks
+ logging.debug("LineCheckController; eclass_info.items(): %s", list(self.config.eclass_info))
+ for k, kwargs in self.config.eclass_info.items():
+ logging.debug("LineCheckController; k: %s, kwargs: %s", k, kwargs)
+ self._constant_checks.append(
+ InheritEclass(
+ k,
+ self.config.eclass_eapi_functions,
+ self.config.errors,
+ **kwargs
+ )
+ )
+
+
+ def run_checks(self, contents, pkg):
+ '''Run the configured linechecks
+
+ @param contents: the ebjuild contents to check
+ @param pkg: the package being checked
+ '''
+ if self._constant_checks is None:
+ self.checks_init()
+ checks = self._constant_checks
+ here_doc_delim = None
+ multiline = None
+
+ for lc in checks:
+ lc.new(pkg)
+
+ multinum = 0
+ for num, line in enumerate(contents):
+
+ # Check if we're inside a here-document.
+ if here_doc_delim is not None:
+ if here_doc_delim.match(line):
+ here_doc_delim = None
+ if here_doc_delim is None:
+ here_doc = self._here_doc_re.match(line)
+ if here_doc is not None:
+ 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
+ # A line ending with an even number of backslashes does not count,
+ # because the last backslash is escaped. Therefore, search for an
+ # odd number of backslashes.
+ line_escaped = operator.sub(*self._continuation_re.search(line).span()) % 2 == 1
+ if multiline:
+ # Chop off the \ and \n bytes from the previous line.
+ multiline = multiline[:-2] + line
+ if not line_escaped:
+ line = multiline
+ num = multinum
+ multiline = None
+ else:
+ continue
+ else:
+ if line_escaped:
+ multinum = num
+ multiline = line
+ continue
+
+ if not line.endswith("#nowarn\n"):
+ # Finally we have a full line to parse.
+ is_comment = self._ignore_comment_re.match(line) is not None
+ for lc in checks:
+ 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()
+ if i is not None:
+ for e in i:
+ yield lc.repoman_check_name, e