# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # 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 2 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ %prog [options] module_or_package Check that a module satisfies a coding standard (and more !). %prog --help Display this help message and exit. %prog --help-msg [,] Display help messages about given message identifiers and exit. """ from __future__ import print_function import collections import contextlib import operator import os try: import multiprocessing except ImportError: multiprocessing = None import sys import tokenize import warnings import six import astroid from astroid.__pkginfo__ import version as astroid_version from astroid import modutils from pylint import checkers from pylint import interfaces from pylint import reporters from pylint import utils from pylint import config from pylint.__pkginfo__ import version from pylint.reporters.ureports import nodes as report_nodes MANAGER = astroid.MANAGER INCLUDE_IDS_HELP = ("Deprecated. It was used to include message\'s " "id in output. Use --msg-template instead.") SYMBOLS_HELP = ("Deprecated. It was used to include symbolic ids of " "messages in output. Use --msg-template instead.") def _get_new_args(message): location = ( message.abspath, message.path, message.module, message.obj, message.line, message.column, ) return ( message.msg_id, message.symbol, location, message.msg, message.confidence, ) def _get_python_path(filepath): dirname = os.path.realpath(os.path.expanduser(filepath)) if not os.path.isdir(dirname): dirname = os.path.dirname(dirname) while True: if not os.path.exists(os.path.join(dirname, "__init__.py")): return dirname old_dirname = dirname dirname = os.path.dirname(dirname) if old_dirname == dirname: return os.getcwd() def _merge_stats(stats): merged = {} by_msg = collections.Counter() for stat in stats: message_stats = stat.pop('by_msg', {}) by_msg.update(message_stats) for key, item in six.iteritems(stat): if key not in merged: merged[key] = item else: if isinstance(item, dict): merged[key].update(item) else: merged[key] = merged[key] + item merged['by_msg'] = by_msg return merged @contextlib.contextmanager def _patch_sysmodules(): # Context manager that permits running pylint, on Windows, with -m switch # and with --jobs, as in 'python -2 -m pylint .. --jobs'. # For more details why this is needed, # see Python issue http://bugs.python.org/issue10845. mock_main = __name__ != '__main__' # -m switch if mock_main: sys.modules['__main__'] = sys.modules[__name__] try: yield finally: if mock_main: sys.modules.pop('__main__') # Python Linter class ######################################################### MSGS = { 'F0001': ('%s', 'fatal', 'Used when an error occurred preventing the analysis of a \ module (unable to find it for instance).'), 'F0002': ('%s: %s', 'astroid-error', 'Used when an unexpected error occurred while building the ' 'Astroid representation. This is usually accompanied by a ' 'traceback. Please report such errors !'), 'F0010': ('error while code parsing: %s', 'parse-error', 'Used when an exception occured while building the Astroid ' 'representation which could be handled by astroid.'), 'I0001': ('Unable to run raw checkers on built-in module %s', 'raw-checker-failed', 'Used to inform that a built-in module has not been checked ' 'using the raw checkers.'), 'I0010': ('Unable to consider inline option %r', 'bad-inline-option', 'Used when an inline option is either badly formatted or can\'t ' 'be used inside modules.'), 'I0011': ('Locally disabling %s (%s)', 'locally-disabled', 'Used when an inline option disables a message or a messages ' 'category.'), 'I0012': ('Locally enabling %s (%s)', 'locally-enabled', 'Used when an inline option enables a message or a messages ' 'category.'), 'I0013': ('Ignoring entire file', 'file-ignored', 'Used to inform that the file will not be checked'), 'I0020': ('Suppressed %s (from line %d)', 'suppressed-message', 'A message was triggered on a line, but suppressed explicitly ' 'by a disable= comment in the file. This message is not ' 'generated for messages that are ignored due to configuration ' 'settings.'), 'I0021': ('Useless suppression of %s', 'useless-suppression', 'Reported when a message is explicitly disabled for a line or ' 'a block of code, but never triggered.'), 'I0022': ('Pragma "%s" is deprecated, use "%s" instead', 'deprecated-pragma', 'Some inline pylint options have been renamed or reworked, ' 'only the most recent form should be used. ' 'NOTE:skip-all is only available with pylint >= 0.26', {'old_names': [('I0014', 'deprecated-disable-all')]}), 'E0001': ('%s', 'syntax-error', 'Used when a syntax error is raised for a module.'), 'E0011': ('Unrecognized file option %r', 'unrecognized-inline-option', 'Used when an unknown inline option is encountered.'), 'E0012': ('Bad option value %r', 'bad-option-value', 'Used when a bad value for an inline option is encountered.'), } if multiprocessing is not None: class ChildLinter(multiprocessing.Process): def run(self): # pylint: disable=no-member, unbalanced-tuple-unpacking tasks_queue, results_queue, self._config = self._args self._config["jobs"] = 1 # Child does not parallelize any further. self._python3_porting_mode = self._config.pop( 'python3_porting_mode', None) self._plugins = self._config.pop('plugins', None) # Run linter for received files/modules. for file_or_module in iter(tasks_queue.get, 'STOP'): result = self._run_linter(file_or_module[0]) try: results_queue.put(result) except Exception as ex: print("internal error with sending report for module %s" % file_or_module, file=sys.stderr) print(ex, file=sys.stderr) results_queue.put({}) def _run_linter(self, file_or_module): linter = PyLinter() # Register standard checkers. linter.load_default_plugins() # Load command line plugins. if self._plugins: linter.load_plugin_modules(self._plugins) linter.load_configuration(**self._config) linter.set_reporter(reporters.CollectingReporter()) # Enable the Python 3 checker mode. This option is # passed down from the parent linter up to here, since # the Python 3 porting flag belongs to the Run class, # instead of the Linter class. if self._python3_porting_mode: linter.python3_porting_mode() # Run the checks. linter.check(file_or_module) msgs = [_get_new_args(m) for m in linter.reporter.messages] return (file_or_module, linter.file_state.base_name, linter.current_name, msgs, linter.stats, linter.msg_status) class PyLinter(config.OptionsManagerMixIn, utils.MessagesHandlerMixIn, utils.ReportsHandlerMixIn, checkers.BaseTokenChecker): """lint Python modules using external checkers. This is the main checker controlling the other ones and the reports generation. It is itself both a raw checker and an astroid checker in order to: * handle message activation / deactivation at the module level * handle some basic but necessary stats'data (number of classes, methods...) IDE plugins developpers: you may have to call `astroid.builder.MANAGER.astroid_cache.clear()` accross run if you want to ensure the latest code version is actually checked. """ __implements__ = (interfaces.ITokenChecker, ) name = 'master' priority = 0 level = 0 msgs = MSGS @staticmethod def make_options(): return (('ignore', {'type' : 'csv', 'metavar' : '[,...]', 'dest' : 'black_list', 'default' : ('CVS',), 'help' : 'Add files or directories to the blacklist. ' 'They should be base names, not paths.'}), ('persistent', {'default': True, 'type' : 'yn', 'metavar' : '', 'level': 1, 'help' : 'Pickle collected data for later comparisons.'}), ('load-plugins', {'type' : 'csv', 'metavar' : '', 'default' : (), 'level': 1, 'help' : 'List of plugins (as comma separated values of ' 'python modules names) to load, usually to register ' 'additional checkers.'}), ('output-format', {'default': 'text', 'type': 'string', 'metavar' : '', 'short': 'f', 'group': 'Reports', 'help' : 'Set the output format. Available formats are text,' ' parseable, colorized, msvs (visual studio) and html. You ' 'can also give a reporter class, eg mypackage.mymodule.' 'MyReporterClass.'}), ('files-output', {'default': 0, 'type' : 'yn', 'metavar' : '', 'group': 'Reports', 'level': 1, 'help' : 'Put messages in a separate file for each module / ' 'package specified on the command line instead of printing ' 'them on stdout. Reports (if any) will be written in a file ' 'name "pylint_global.[txt|html]".'}), ('reports', {'default': 1, 'type' : 'yn', 'metavar' : '', 'short': 'r', 'group': 'Reports', 'help' : 'Tells whether to display a full report or only the ' 'messages'}), ('evaluation', {'type' : 'string', 'metavar' : '', 'group': 'Reports', 'level': 1, 'default': '10.0 - ((float(5 * error + warning + refactor + ' 'convention) / statement) * 10)', 'help' : 'Python expression which should return a note less ' 'than 10 (10 is the highest note). You have access ' 'to the variables errors warning, statement which ' 'respectively contain the number of errors / ' 'warnings messages and the total number of ' 'statements analyzed. This is used by the global ' 'evaluation report (RP0004).'}), ('comment', utils.deprecated_option(opt_type='yn')), ('confidence', {'type' : 'multiple_choice', 'metavar': '', 'default': '', 'choices': [c.name for c in interfaces.CONFIDENCE_LEVELS], 'group': 'Messages control', 'help' : 'Only show warnings with the listed confidence levels.' ' Leave empty to show all. Valid levels: %s' % ( ', '.join(c.name for c in interfaces.CONFIDENCE_LEVELS),)}), ('enable', {'type' : 'csv', 'metavar': '', 'short': 'e', 'group': 'Messages control', 'help' : 'Enable the message, report, category or checker with the ' 'given id(s). You can either give multiple identifier ' 'separated by comma (,) or put this option multiple time. ' 'See also the "--disable" option for examples. '}), ('disable', {'type' : 'csv', 'metavar': '', 'short': 'd', 'group': 'Messages control', 'help' : 'Disable the message, report, category or checker ' 'with the given id(s). You can either give multiple identifiers' ' separated by comma (,) or put this option multiple times ' '(only on the command line, not in the configuration file ' 'where it should appear only once).' 'You can also use "--disable=all" to disable everything first ' 'and then reenable specific checks. For example, if you want ' 'to run only the similarities checker, you can use ' '"--disable=all --enable=similarities". ' 'If you want to run only the classes checker, but have no ' 'Warning level messages displayed, use' '"--disable=all --enable=classes --disable=W"'}), ('msg-template', {'type' : 'string', 'metavar': '