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

Reply via email to