commit: b6e8cfe29d860d018bb386cdfbbcde6cb2edaadc Author: Brian Dolbec <dolsen <AT> gentoo <DOT> org> AuthorDate: Sat Jul 15 00:15:22 2017 +0000 Commit: Brian Dolbec <dolsen <AT> gentoo <DOT> org> CommitDate: Sat Jul 15 02:25:44 2017 +0000 URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=b6e8cfe2
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. repoman/pym/repoman/modules/linechecks/__init__.py | 1 + repoman/pym/repoman/modules/linechecks/base.py | 101 +++++++++++++++ repoman/pym/repoman/modules/linechecks/config.py | 94 ++++++++++++++ .../pym/repoman/modules/linechecks/controller.py | 140 +++++++++++++++++++++ 4 files changed, 336 insertions(+) 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..0044afe79 --- /dev/null +++ b/repoman/pym/repoman/modules/linechecks/config.py @@ -0,0 +1,94 @@ +# -*- 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 +import yaml +from copy import deepcopy + +from portage.util import stack_lists +from repoman.config import load_config + + +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 configpaths: 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['usex_supported_eapis'] + self.in_iuse_supported_eapis = self.info_config['in_iuse_supported_eapis'] + self.eclass_info_experimental_inherit = self.info_config['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['eclass_export_functions'] + + self.eclass_info_experimental_inherit = self.info_config['eclass_info_experimental_inherit'] + # These are "eclasses are the whole ebuild" type thing. + self.eclass_info_experimental_inherit['eutils']['exempt_eclasses'] = self.eclass_export_functions + self.eclass_info_experimental_inherit['multilib']['exempt_eclasses'] = self.eclass_export_functions + [ + 'autotools', 'libtool', 'multilib-minimal'] diff --git a/repoman/pym/repoman/modules/linechecks/controller.py b/repoman/pym/repoman/modules/linechecks/controller.py new file mode 100644 index 000000000..5c7f5c924 --- /dev/null +++ b/repoman/pym/repoman/modules/linechecks/controller.py @@ -0,0 +1,140 @@ + +import logging +import operator +import os +import re + +from portage.module import Modules +from repoman.modules.linechecks.base import InheritEclass +from repoman.modules.linechecks.config import LineChecksConfig + +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