#!/usr/bin/python2 # # 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 . from __future__ import print_function 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.checkers.typecheck import TypeChecker from pylint.checkers.utils import safe_infer from astroid import Class, Instance, Module, InferenceError, Function from pylint.reporters.text import TextReporter except ImportError: print("To use {0}, please install pylint.".format(sys.argv[0]), file=sys.stderr) 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') class IPATypeChecker(TypeChecker): NAMESPACE_ATTRS = ['Command', 'Object', 'Method', 'Backend', 'Updater', 'Advice'] LOGGING_ATTRS = ['log', 'debug', 'info', 'warning', 'error', 'exception', 'critical'] # 'class or module': ['generated', 'properties'] ignore = { # Python standard library & 3rd party classes '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'], 'pytest': ['fixture', 'raises', 'skip', 'yield_fixture', 'mark', 'fail'], 'unittest.case': ['assertEqual', 'assertRaises'], 'nose.tools': ['assert_equal', 'assert_raises'], 'datetime.tzinfo': ['houroffset', 'minoffset', 'utcoffset', 'dst'], 'nss.nss.subject_public_key_info': ['public_key'], # IPA classes 'ipalib.base.NameSpace': ['add', 'mod', 'del', 'show', 'find'], 'ipalib.cli.Collector': ['__options'], 'ipalib.config.Env': ['*'], '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.parameters.DNSNameParam': ['only_absolute', 'only_relative'], '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, 'ipatests.test_integration.base.IntegrationTest': [ 'domain', 'master', 'replicas', 'clients', 'ad_domains'] } 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 isinstance(owner, Module): if node.attrname in self.ignore.get(owner.name, ()): return elif isinstance(owner, Class) or type(owner) is Instance: ignored = self._find_ignored_attrs(owner) for pattern in ignored: if fnmatchcase(node.attrname, pattern): return super(IPATypeChecker, self).visit_getattr(node) def visit_callfunc(self, node): called = safe_infer(node.func) if isinstance(called, Function): if called.name in self.ignore.get(called.root().name, []): return super(IPATypeChecker, self).visit_callfunc(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, confidence=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.set_option('disable', 'I') linter.set_option('enable', 'F') linter.set_option('disable', 'import-error') linter.set_option('enable', 'E') linter.set_option('enable', 'W') linter.set_option('disable', 'abstract-method') linter.set_option('disable', 'anomalous-backslash-in-string') linter.set_option('disable', 'arguments-differ') linter.set_option('disable', 'attribute-defined-outside-init') linter.set_option('disable', 'bad-builtin') linter.set_option('disable', 'bad-indentation') linter.set_option('disable', 'bare-except') linter.set_option('disable', 'broad-except') linter.set_option('disable', 'dangerous-default-value') linter.set_option('disable', 'eval-used') linter.set_option('disable', 'exec-used') linter.set_option('disable', 'fixme') linter.set_option('disable', 'global-statement') linter.set_option('disable', 'global-variable-not-assigned') linter.set_option('disable', 'global-variable-undefined') linter.set_option('disable', 'no-init') linter.set_option('disable', 'pointless-except') linter.set_option('disable', 'pointless-statement') linter.set_option('disable', 'pointless-string-statement') linter.set_option('disable', 'protected-access') linter.set_option('disable', 'redefine-in-handler') linter.set_option('disable', 'redefined-builtin') linter.set_option('disable', 'redefined-outer-name') linter.set_option('disable', 'reimported') linter.set_option('disable', 'relative-import') linter.set_option('disable', 'super-init-not-called') linter.set_option('disable', 'undefined-loop-variable') linter.set_option('disable', 'unnecessary-lambda') linter.set_option('disable', 'unnecessary-pass') linter.set_option('disable', 'unnecessary-semicolon') linter.set_option('disable', 'unused-argument') linter.set_option('disable', 'unused-variable') linter.set_option('disable', 'useless-else-on-loop') linter.set_option('disable', 'unbalanced-tuple-unpacking') linter.set_option('disable', 'unpacking-non-sequence') linter.set_option('enable', 'C') linter.set_option('disable', 'bad-classmethod-argument') linter.set_option('disable', 'bad-continuation') linter.set_option('disable', 'bad-mcs-classmethod-argument') linter.set_option('disable', 'bad-mcs-method-argument') linter.set_option('disable', 'bad-whitespace') linter.set_option('disable', 'blacklisted-name') linter.set_option('disable', 'invalid-name') linter.set_option('disable', 'line-too-long') linter.set_option('disable', 'missing-docstring') linter.set_option('disable', 'multiple-imports') linter.set_option('disable', 'multiple-statements') linter.set_option('disable', 'old-style-class') linter.set_option('disable', 'superfluous-parens') linter.set_option('disable', 'too-many-lines') linter.set_option('disable', 'unidiomatic-typecheck') linter.set_option('disable', 'R') linter.set_reporter(TextReporter()) linter.set_option('msg-template', '{path}:{line}: [{msg_id}({symbol}), {obj}] {msg})') linter.set_option('reports', False) linter.set_option('persistent', False) linter.set_option('disable', 'python3') linter.check(files) if linter.msg_status != 0: print(""" =============================================================================== Errors were found during the static code check. """, file=sys.stderr) if len(linter.missing) > 0: print("There are some missing imports:", file=sys.stderr) for mod in sorted(linter.missing): print(" " + mod, file=sys.stderr) print(""" Please make sure all of the required and optional (python-gssapi, python-rhsm) python packages are installed. """, file=sys.stderr) print("""\ 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. =============================================================================== """, file=sys.stderr) if options.fail: return linter.msg_status else: return 0 if __name__ == "__main__": sys.exit(main())