#!/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', 'dist', 'install/po/test_i18n.py', 'lite-server.py', 'make-lint', 'make-test', 'tests') class IPATypeChecker(TypeChecker): # 'class': ('generated', 'properties',) ignore = { 'ipalib.base.NameSpace': ('find',), 'ipalib.cli.Collector': ('__options',), 'ipalib.config.Env': ('*'), 'ipalib.plugable.API': ('Command', 'Object', 'Method', 'Property', 'Backend', 'log', 'plugins'), 'ipalib.plugable.Plugin': ('Command', 'Object', 'Method', 'Property', 'Backend', 'env', 'debug', 'info', 'warning', 'error', 'critical', 'exception', 'context', 'log'), 'ipalib.plugins.baseldap.CallbackInterface': ('pre_callback', 'post_callback', 'exc_callback'), 'ipalib.plugins.misc.env': ('env',), 'ipalib.parameters.Param': ('cli_name', 'cli_short_name', 'label', 'doc', 'required', 'multivalue', 'primary_key', 'normalizer', 'default', 'default_from', 'create_default', 'autofill', 'query', 'attribute', 'include', 'exclude', 'flags', 'hint', 'alwaysask'), 'ipalib.parameters.Bool': ('truths', 'falsehoods'), 'ipalib.parameters.Int': ('minvalue', 'maxvalue'), 'ipalib.parameters.Float': ('minvalue', 'maxvalue'), 'ipalib.parameters.Data': ('minlength', 'maxlength', 'length', 'pattern', 'pattern_errmsg'), 'ipalib.parameters.Enum': ('values',), 'ipalib.parameters.List': ('separator', 'skipspace'), 'ipalib.parameters.File': ('stdin_if_missing'), 'urlparse.SplitResult': ('netloc',), } 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 not isinstance(owner, 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())