#!/usr/bin/python # # Authors: # Jakub Hrozek # Jan Cholasta # # Copyright (C) 2011 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys from optparse import OptionParser from fnmatch import fnmatch, fnmatchcase try: from pylint import checkers from pylint.lint import PyLinter from pylint.reporters.text import ParseableTextReporter from pylint.checkers.typecheck import TypeChecker from logilab.astng import Class, Instance, InferenceError except ImportError: print >> sys.stderr, "To use {0}, please install pylint.".format(sys.argv[0]) sys.exit(32) # File names to ignore when searching for python source files IGNORE_FILES = ('.*', '*~', '*.in', '*.pyc', '*.pyo') IGNORE_PATHS = ('build', 'rpmbuild', 'dist', 'install/po/test_i18n.py', 'lite-server.py', 'make-lint', 'make-test', 'ipatests') class IPATypeChecker(TypeChecker): NAMESPACE_ATTRS = ['Command', 'Object', 'Method', 'Property', 'Backend', 'Updater', 'Advice'] LOGGING_ATTRS = ['log', 'debug', 'info', 'warning', 'error', 'exception', 'critical'] # 'class': ['generated', 'properties'] ignore = { # Python standard library & 3rd party classes 'krbV.Principal': ['name'], 'socket._socketobject': ['sendall'], # should be 'subprocess.Popen' '.Popen': ['stdin', 'stdout', 'stderr', 'pid', 'returncode', 'poll', 'wait', 'communicate'], 'urlparse.ResultMixin': ['scheme', 'netloc', 'path', 'query', 'fragment', 'username', 'password', 'hostname', 'port'], 'urlparse.ParseResult': ['params'], # IPA classes 'ipalib.base.NameSpace': ['add', 'mod', 'del', 'show', 'find'], 'ipalib.cli.Collector': ['__options'], 'ipalib.config.Env': ['*'], 'ipalib.krb_utils.KRB5_CCache': LOGGING_ATTRS, 'ipalib.parameters.Param': ['cli_name', 'cli_short_name', 'label', 'default', 'doc', 'required', 'multivalue', 'primary_key', 'normalizer', 'default_from', 'autofill', 'query', 'attribute', 'include', 'exclude', 'flags', 'hint', 'alwaysask', 'sortorder', 'csv', 'option_group'], 'ipalib.parameters.Bool': ['truths', 'falsehoods'], 'ipalib.parameters.Data': ['minlength', 'maxlength', 'length', 'pattern', 'pattern_errmsg'], 'ipalib.parameters.Str': ['noextrawhitespace'], 'ipalib.parameters.Password': ['confirm'], 'ipalib.parameters.File': ['stdin_if_missing'], 'ipalib.plugins.dns.DNSRecord': ['validatedns', 'normalizedns'], 'ipalib.parameters.Enum': ['values'], 'ipalib.parameters.Number': ['minvalue', 'maxvalue'], 'ipalib.parameters.Decimal': ['precision', 'exponential', 'numberclass'], 'ipalib.plugable.API': NAMESPACE_ATTRS + LOGGING_ATTRS, 'ipalib.plugable.Plugin': ['api', 'env'] + NAMESPACE_ATTRS + LOGGING_ATTRS, 'ipalib.session.AuthManager': LOGGING_ATTRS, 'ipalib.session.SessionAuthManager': LOGGING_ATTRS, 'ipalib.session.SessionManager': LOGGING_ATTRS, 'ipaserver.install.ldapupdate.LDAPUpdate': LOGGING_ATTRS, 'ipaserver.rpcserver.KerberosSession': ['api'] + LOGGING_ATTRS, } def _related_classes(self, klass): yield klass for base in klass.ancestors(): yield base def _class_full_name(self, klass): return klass.root().name + '.' + klass.name def _find_ignored_attrs(self, owner): attrs = [] for klass in self._related_classes(owner): name = self._class_full_name(klass) if name in self.ignore: attrs += self.ignore[name] return attrs def visit_getattr(self, node): try: inferred = list(node.expr.infer()) except InferenceError: inferred = [] for owner in inferred: if not isinstance(owner, Class) and type(owner) is not Instance: continue ignored = self._find_ignored_attrs(owner) for pattern in ignored: if fnmatchcase(node.attrname, pattern): return super(IPATypeChecker, self).visit_getattr(node) class IPALinter(PyLinter): ignore = (TypeChecker,) def __init__(self): super(IPALinter, self).__init__() self.missing = set() def register_checker(self, checker): if type(checker) in self.ignore: return super(IPALinter, self).register_checker(checker) def add_message(self, msg_id, line=None, node=None, args=None): if line is None and node is not None: line = node.fromlineno # Record missing packages if msg_id == 'F0401' and self.is_message_enabled(msg_id, line): self.missing.add(args) super(IPALinter, self).add_message(msg_id, line, node, args) def find_files(path, basepath): entries = os.listdir(path) # If this directory is a python package, look no further if '__init__.py' in entries: return [path] result = [] for filename in entries: filepath = os.path.join(path, filename) for pattern in IGNORE_FILES: if fnmatch(filename, pattern): filename = None break if filename is None: continue for pattern in IGNORE_PATHS: patpath = os.path.join(basepath, pattern).replace(os.sep, '/') if filepath == patpath: filename = None break if filename is None: continue if os.path.islink(filepath): continue # Recurse into subdirectories if os.path.isdir(filepath): result += find_files(filepath, basepath) continue # Add all *.py files if filename.endswith('.py'): result.append(filepath) continue # Add any other files beginning with a shebang and having # the word "python" on the first line file = open(filepath, 'r') line = file.readline(128) file.close() if line[:2] == '#!' and line.find('python') >= 0: result.append(filepath) return result def main(): optparser = OptionParser() optparser.add_option('--no-fail', help='report success even if errors were found', dest='fail', default=True, action='store_false') optparser.add_option('--enable-noerror', help='enable warnings and other non-error messages', dest='errors_only', default=True, action='store_false') options, args = optparser.parse_args() cwd = os.getcwd() if len(args) == 0: files = find_files(cwd, cwd) else: files = args for filename in files: dirname = os.path.dirname(filename) if dirname not in sys.path: sys.path.insert(0, dirname) linter = IPALinter() checkers.initialize(linter) linter.register_checker(IPATypeChecker(linter)) if options.errors_only: linter.disable_noerror_messages() linter.enable('F') linter.set_reporter(ParseableTextReporter()) linter.set_option('include-ids', True) linter.set_option('reports', False) linter.set_option('persistent', False) linter.check(files) if linter.msg_status != 0: print >> sys.stderr, """ =============================================================================== Errors were found during the static code check. """ if len(linter.missing) > 0: print >> sys.stderr, "There are some missing imports:" for mod in sorted(linter.missing): print >> sys.stderr, " " + mod print >> sys.stderr, """ Please make sure all of the required and optional (python-krbV, python-rhsm) python packages are installed. """ print >> sys.stderr, """\ If you are certain that any of the reported errors are false positives, please mark them in the source code according to the pylint documentation. =============================================================================== """ if options.fail: return linter.msg_status else: return 0 if __name__ == "__main__": sys.exit(main())