# 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 this first to avoid builtin namespace pollution from pylint.checkers import utils #pylint: disable=unused-import import sys import os import tokenize from collections import defaultdict from contextlib import contextmanager from operator import attrgetter from warnings import warn from itertools import chain try: import multiprocessing except ImportError: multiprocessing = None import six from logilab.common.configuration import ( UnsupportedAction, OptionsManagerMixIn) from logilab.common.optik_ext import check_csv from logilab.common.interface import implements from logilab.common.textutils import splitstrip, unquote from logilab.common.ureports import Table, Text, Section from logilab.common.__pkginfo__ import version as common_version from astroid import MANAGER, AstroidBuildingException from astroid.__pkginfo__ import version as astroid_version from astroid.modutils import load_module_from_name, get_module_part from pylint.utils import ( MSG_TYPES, OPTION_RGX, PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, MessagesStore, FileState, EmptyReport, expand_modules, tokenize_module, Message) from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker, CONFIDENCE_LEVELS from pylint.checkers import (BaseTokenChecker, table_lines_from_stats, initialize as checkers_initialize) from pylint.reporters import initialize as reporters_initialize, CollectingReporter from pylint import config from pylint.__pkginfo__ import version 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 = {} for stat in 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 return merged # 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 !'), 'F0003': ('ignored builtin module %s', 'ignored-builtin-module', 'Used to indicate that the user asked to analyze a builtin ' 'module which has been skipped.'), '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.'), } def _deprecated_option(shortname, opt_type): def _warn_deprecated(option, optname, *args): # pylint: disable=unused-argument sys.stderr.write('Warning: option %s is deprecated and ignored.\n' % (optname,)) return {'short': shortname, 'help': 'DEPRECATED', 'hide': True, 'type': opt_type, 'action': 'callback', 'callback': _warn_deprecated} if multiprocessing is not None: class ChildLinter(multiprocessing.Process): # pylint: disable=no-member def run(self): tasks_queue, results_queue, config = self._args # pylint: disable=no-member for file_or_module in iter(tasks_queue.get, 'STOP'): result = self._run_linter(config, 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, config, file_or_module): linter = PyLinter() # Register standard checkers. linter.load_default_plugins() # Load command line plugins. # TODO linter.load_plugin_modules(self._plugins) linter.disable('pointless-except') linter.disable('suppressed-message') linter.disable('useless-suppression') # TODO(cpopa): the sub-linters will not know all the options # because they are not available here, as they are patches to # PyLinter options. The following is just a hack to handle # just a part of the options available in the Run class. if 'disable_msg' in config: # Disable everything again. We don't have access # to the original linter though. for msgid in config['disable_msg']: linter.disable(msgid) for key in set(config) - set(dict(linter.options)): del config[key] config['jobs'] = 1 # Child does not parallelize any further. linter.load_configuration(**config) linter.set_reporter(CollectingReporter()) # 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(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, 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__ = (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', {'default': 0, 'type' : 'yn', 'metavar' : '', 'group': 'Reports', 'level': 1, 'help' : 'Add a comment according to your evaluation note. ' 'This is used by the global evaluation report (RP0004).'}), ('confidence', {'type' : 'multiple_choice', 'metavar': '', 'default': '', 'choices': [c.name for c in 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 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': '