blob: f9097abcae50e6602006046fe18431092e163a24 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
# Copyright (c) 2008 Fabrice Douchant <Fabrice.Douchant@logilab.fr>
# Copyright (c) 2009 Vincent
# Copyright (c) 2009 Mads Kiilerich <mads@kiilerich.com>
# Copyright (c) 2011-2014 Google, Inc.
# Copyright (c) 2012 David Pursehouse <david.pursehouse@sonymobile.com>
# Copyright (c) 2012 Kevin Jing Qiu <kevin.jing.qiu@gmail.com>
# Copyright (c) 2012 FELD Boris <lothiraldan@gmail.com>
# Copyright (c) 2012 JT Olds <jtolds@xnet5.com>
# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com>
# Copyright (c) 2014-2015 Michal Nowikowski <godfryd@gmail.com>
# Copyright (c) 2014 Brett Cannon <brett@python.org>
# Copyright (c) 2014 Alexandru Coman <fcoman@bitdefender.com>
# Copyright (c) 2014 Daniel Harding <dharding@living180.net>
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
# Copyright (c) 2014 Dan Goldsmith <djgoldsmith@googlemail.com>
# Copyright (c) 2015-2016 Florian Bruhin <me@the-compiler.org>
# Copyright (c) 2015 Aru Sahni <arusahni@gmail.com>
# Copyright (c) 2015 Steven Myint <hg@stevenmyint.com>
# Copyright (c) 2015 Simu Toni <simutoni@gmail.com>
# Copyright (c) 2015 Mihai Balint <balint.mihai@gmail.com>
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
# Copyright (c) 2016-2017 Łukasz Rogalski <rogalski.91@gmail.com>
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
# Copyright (c) 2016 Alan Evangelista <alanoe@linux.vnet.ibm.com>
# Copyright (c) 2017-2018 Ville Skyttä <ville.skytta@iki.fi>
# Copyright (c) 2017-2018 hippo91 <guillaume.peillex@gmail.com>
# Copyright (c) 2017 Daniel Miller <millerdev@gmail.com>
# Copyright (c) 2017 Roman Ivanov <me@roivanov.com>
# Copyright (c) 2017 Ned Batchelder <ned@nedbatchelder.com>
# Copyright (c) 2018 Randall Leeds <randall@bleeds.info>
# Copyright (c) 2018 Mike Frysinger <vapier@gmail.com>
# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
# Copyright (c) 2018 Ville Skyttä <ville.skytta@upcloud.com>
# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
# Copyright (c) 2018 Anthony Sottile <asottile@umich.edu>
# Copyright (c) 2018 Jason Owen <jason.a.owen@gmail.com>
# Copyright (c) 2018 Gary Tyler McLeod <mail@garytyler.com>
# Copyright (c) 2018 Yuval Langer <yuvallanger@mail.tau.ac.il>
# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
# Copyright (c) 2018 kapsh <kapsh@kap.sh>
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
# pylint: disable=broad-except
""" %prog [options] modules_or_packages
Check that module(s) satisfy a coding standard (and more !).
%prog --help
Display this help message and exit.
%prog --help-msg <msg-id>[,<msg-id>]
Display help messages about given message identifiers and exit.
"""
from __future__ import print_function
import argparse
import collections
import contextlib
import operator
import os
import sys
import tokenize
import warnings
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 exceptions
from pylint import utils
from pylint import config
from pylint.__pkginfo__ import version
from pylint.reporters.ureports import nodes as report_nodes
FULL_VERSION = "%%(prog)s %s\nastroid %s\nPython %s" % (
version,
astroid_version,
sys.version,
)
MANAGER = astroid.MANAGER
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()
return None
def _merge_stats(stats):
merged = {}
by_msg = collections.Counter()
nested_keys = set()
for stat in stats:
message_stats = stat.pop("by_msg", {})
by_msg.update(message_stats)
for key, item in stat.items():
if key not in merged:
merged[key] = item
else:
if isinstance(item, dict):
nested_keys.add(key)
elif isinstance(item, set):
merged[key].update(item)
else:
merged[key] = merged[key] + item
for key in nested_keys:
merged[key] = _merge_stats(stat.get(key, {}) for stat in stats)
if by_msg:
merged["by_msg"] = by_msg
return merged
# some reporting functions ####################################################
def report_total_messages_stats(sect, stats, previous_stats, _, __):
"""make total errors / warnings report"""
lines = ["type", "number", "previous", "difference"]
lines += checkers.table_lines_from_stats(
stats, previous_stats, ("convention", "refactor", "warning", "error")
)
sect.append(report_nodes.Table(children=lines, cols=4, rheaders=1))
def report_messages_stats(sect, stats, _, __, ___):
"""make messages type report"""
if not stats["by_msg"]:
# don't print this report when we didn't detected any errors
raise exceptions.EmptyReportError()
in_order = sorted(
[
(value, msg_id)
for msg_id, value in stats["by_msg"].items()
if not msg_id.startswith("I")
]
)
in_order.reverse()
lines = ("message id", "occurrences")
for value, msg_id in in_order:
lines += (msg_id, str(value))
sect.append(report_nodes.Table(children=lines, cols=2, rheaders=1))
def report_messages_by_module_stats(sect, stats, _, __, ____):
"""make errors / warnings by modules report"""
if len(stats["by_module"]) == 1:
# don't print this report when we are analysing a single module
raise exceptions.EmptyReportError()
by_mod = collections.defaultdict(dict)
for m_type in ("fatal", "error", "warning", "refactor", "convention"):
total = stats[m_type]
for module in stats["by_module"]:
mod_total = stats["by_module"][module][m_type]
if total == 0:
percent = 0
else:
percent = float((mod_total) * 100) / total
by_mod[module][m_type] = percent
sorted_result = []
for module, mod_info in by_mod.items():
sorted_result.append(
(
mod_info["error"],
mod_info["warning"],
mod_info["refactor"],
mod_info["convention"],
module,
)
)
sorted_result.sort()
sorted_result.reverse()
lines = ["module", "error", "warning", "refactor", "convention"]
for line in sorted_result:
# Don't report clean modules.
if all(entry == 0 for entry in line[:-1]):
continue
lines.append(line[-1])
for val in line[:-1]:
lines.append("%.2f" % val)
if len(lines) == 5:
raise exceptions.EmptyReportError()
sect.append(report_nodes.Table(children=lines, cols=5, rheaders=1))
# 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 occurred 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.",
),
"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.",
),
}
# pylint: disable=too-many-instance-attributes
class PyLinter(utils.MessagesHandlerMixIn, 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 plugin developers: you may have to call
`astroid.builder.MANAGER.astroid_cache.clear()` across runs if you want
to ensure the latest code version is actually checked.
"""
__implements__ = (interfaces.ITokenChecker,)
name = "master"
priority = 0
level = 0
msgs = MSGS
options = (
(
"ignore",
{
"type": "csv",
"metavar": "<file>,...",
"dest": "black_list",
"default": ("CVS",),
"help": "Add files or directories to the blacklist. "
"They should be base names, not paths.",
},
),
(
"ignore-patterns",
{
"type": "regexp_csv",
"metavar": "<pattern>,...",
"dest": "black_list_re",
"default": (),
"help": "Add files or directories matching the regex patterns to the"
" blacklist. The regex matches against base names, not paths.",
},
),
(
"persistent",
{
"default": True,
"type": "yn",
"metavar": "<y_or_n>",
"level": 1,
"help": "Pickle collected data for later comparisons.",
},
),
(
"load-plugins",
{
"type": "csv",
"metavar": "<modules>",
"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": "<format>",
"short": "f",
"group": "Reports",
"help": "Set the output format. Available formats are text,"
" parseable, colorized, json and msvs (visual studio)."
" You can also give a reporter class, e.g. mypackage.mymodule."
"MyReporterClass.",
},
),
(
"reports",
{
"default": False,
"type": "yn",
"metavar": "<y_or_n>",
"short": "r",
"group": "Reports",
"help": "Tells whether to display a full report or only the "
"messages",
},
),
(
"evaluation",
{
"type": "string",
"metavar": "<python_expression>",
"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).",
},
),
(
"score",
{
"default": True,
"type": "yn",
"metavar": "<y_or_n>",
"short": "s",
"group": "Reports",
"help": "Activate the evaluation score.",
},
),
(
"confidence",
{
"type": "multiple_choice",
"metavar": "<levels>",
"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": "_msg_on",
"metavar": "<msg ids>",
"dest": "msg_toggles",
"action": "append",
"short": "e",
"default": [],
"group": "Messages control",
"help": "Enable 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). "
'See also the "--disable" option for examples. ',
},
),
(
"disable",
{
"type": "_msg_off",
"metavar": "<msg ids>",
"dest": "msg_toggles",
"action": "append",
"short": "d",
"default": [],
"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": "<template>",
"default": "",
"group": "Reports",
"help": (
"Template used to display messages. "
"This is a python new-style format string "
"used to format the message information. "
"See doc for all details"
),
},
),
(
"jobs",
{
"type": "int",
"metavar": "<n-processes>",
"short": "j",
"default": 1,
"help": """Use multiple processes to speed up Pylint.""",
},
),
(
"unsafe-load-any-extension",
{
"type": "yn",
"metavar": "<yn>",
"default": False,
"hide": True,
"help": (
"Allow loading of arbitrary C extensions. Extensions"
" are imported into the active Python interpreter and"
" may run arbitrary code."
),
},
),
(
"limit-inference-results",
{
"type": "int",
"metavar": "<number-of-results>",
"default": 100,
"help": (
"Control the amount of potential inferred values when inferring "
"a single object. This can help the performance when dealing with "
"large functions or complex, nested conditions. "
),
},
),
(
"extension-pkg-whitelist",
{
"type": "csv",
"metavar": "<pkg>,...",
"default": [],
"help": (
"A comma-separated list of package or module names"
" from where C extensions may be loaded. Extensions are"
" loading into the active Python interpreter and may run"
" arbitrary code"
),
},
),
(
"suggestion-mode",
{
"type": "yn",
"metavar": "<yn>",
"default": True,
"help": (
"When enabled, pylint would attempt to guess common "
"misconfiguration and emit user-friendly hints instead "
"of false-positive error messages"
),
},
),
(
"exit-zero",
{
"type": "yn",
"default": False,
"help": (
"Always return a 0 (non-error) status code, even if "
"lint errors are found. This is primarily useful in "
"continuous integration scripts."
),
},
),
)
option_groups = (
("Messages control", "Options controlling analysis messages"),
("Reports", "Options related to output formatting and reporting"),
)
reports = ()
def __init__(self, config=None):
# some stuff has to be done before ancestors initialization...
#
# messages store / checkers / reporter / astroid manager
self.config = config
self._checkers = collections.defaultdict(list)
self._pragma_lineno = {}
self._ignore_file = False
# visit variables
self.current_name = None
self.current_file = None
super().__init__(config)
# provided reports
self._dynamic_plugins = set()
def load_plugin_configuration(self):
"""Call the configuration hook for plugins
This walks through the list of plugins, grabs the "load_configuration"
hook, if exposed, and calls it to allow plugins to configure specific
settings.
"""
for modname in self._dynamic_plugins:
module = modutils.load_module_from_name(modname)
if hasattr(module, "load_configuration"):
module.load_configuration(self)
# checkers manipulation methods ############################################
def disable(self, msgid, scope="module", line=None, ignore_unknown=False):
"""Do not output messages that have the given ID.
:param msgid: The ID of the message to disable.
This can also be "all" to disable all messages,
the ID of a category to disable all messages of that category,
or the name of a checker to disable all messages from the checker.
:type msgid: str
:param scope: Must be "module". Provided for backwards compatibility.
:type scope: str.
:param line: The line number that the message is disabled on.
:type line: int
:param ignore_unknown: If this is False,
an :exc:`UnknownMessageError` is raised when a message with the
given ID cannot be found.
If this is True, the exception is not raised.
:param ignore_unknown: bool
:raises ValueError: If `scope` is anything other than "module".
"""
self._set_msg_status(
msgid, enable=False, scope=scope, line=line, ignore_unknown=ignore_unknown
)
def enable(self, msgid, scope="module", line=None, ignore_unknown=False):
"""Enable outputting messages that have the given ID.
:param msgid: The ID of the message to enable.
This can also be "all" to enable all messages,
the ID of a category to enable all messages of that category,
or the name of a checker to enable all messages from the checker.
:type msgid: str
:param scope: Must be "module". Provided for backwards compatibility.
:type scope: str.
:param line: The line number that the message is enabled on.
:type line: int
:param ignore_unknown: If this is False,
an :exc:`UnknownMessageError` is raised when a message with the
given ID cannot be found.
If this is True, the exception is not raised.
:param ignore_unknown: bool
"""
self._set_msg_status(
msgid, enable=True, scope=scope, line=line, ignore_unknown=ignore_unknown
)
# block level option handling #############################################
#
# see func_block_disable_msg.py test case for expected behaviour
def process_tokens(self, tokens):
"""process tokens from the current module to search for module/block
level options
"""
options_methods = {"enable": self.enable, "disable": self.disable}
control_pragmas = {"disable", "enable"}
for (tok_type, content, start, _, _) in tokens:
if tok_type != tokenize.COMMENT:
continue
match = utils.OPTION_RGX.search(content)
if match is None:
continue
first_group = match.group(1)
if (
first_group.strip() == "disable-all"
or first_group.strip() == "skip-file"
):
if first_group.strip() == "disable-all":
self.add_message(
"deprecated-pragma",
line=start[0],
args=("disable-all", "skip-file"),
)
self.add_message("file-ignored", line=start[0])
self._ignore_file = True
return
try:
opt, value = first_group.split("=", 1)
except ValueError:
self.add_message(
"bad-inline-option", args=first_group.strip(), line=start[0]
)
continue
opt = opt.strip()
if opt in options_methods:
meth = options_methods[opt]
for msgid in utils._splitstrip(value):
# Add the line where a control pragma was encountered.
if opt in control_pragmas:
self._pragma_lineno[msgid] = start[0]
try:
if (opt, msgid) == ("disable", "all"):
self.add_message(
"deprecated-pragma",
line=start[0],
args=("disable=all", "skip-file"),
)
self.add_message("file-ignored", line=start[0])
self._ignore_file = True
return
meth(msgid, "module", start[0])
except exceptions.UnknownMessageError:
self.add_message("bad-option-value", args=msgid, line=start[0])
else:
self.add_message("unrecognized-inline-option", args=opt, line=start[0])
# code checking methods ###################################################
def check(self, module_desc, walker, rawcheckers, tokencheckers):
modname = module_desc.name
filepath = module_desc.path
self.set_current_module(modname, filepath)
# get the module representation
ast_node = self.get_ast(filepath, modname)
if ast_node is None:
return
# XXX to be correct we need to keep module_msgs_state for every
# analyzed module (the problem stands with localized messages which
# are only detected in the .close step)
self.file_state = utils.FileState(module_desc.basename)
self._ignore_file = False
# fix the current file (if the source file was not available or
# if it's actually a c extension)
self.current_file = ast_node.file # pylint: disable=maybe-no-member
self.check_astroid_module(ast_node, walker, rawcheckers, tokencheckers)
# warn about spurious inline messages handling
spurious_messages = self.file_state.iter_spurious_suppression_messages(
self.msgs_store
)
for msgid, line, args in spurious_messages:
self.add_message(msgid, line, None, args)
def set_current_module(self, modname, filepath=None):
"""set the name of the currently analyzed module and
init statistics for it
"""
if not modname and filepath is None:
return
self.reporter.on_set_current_module(modname, filepath)
self.current_name = modname
self.current_file = filepath or modname
self.stats["by_module"][modname] = {}
self.stats["by_module"][modname]["statement"] = 0
for msg_cat in utils.MSG_TYPES.values():
self.stats["by_module"][modname][msg_cat] = 0
def get_ast(self, filepath, modname):
"""return an ast(roid) representation for a module"""
try:
return MANAGER.ast_from_file(filepath, modname, source=True)
except astroid.AstroidSyntaxError as ex:
# pylint: disable=no-member
self.add_message(
"syntax-error", line=getattr(ex.error, "lineno", 0), args=str(ex.error)
)
except astroid.AstroidBuildingException as ex:
self.add_message("parse-error", args=ex)
except Exception as ex:
import traceback
traceback.print_exc()
self.add_message("astroid-error", args=(ex.__class__, ex))
def check_astroid_module(self, ast_node, walker, rawcheckers, tokencheckers):
"""Check a module from its astroid representation."""
try:
tokens = utils.tokenize_module(ast_node)
except tokenize.TokenError as ex:
self.add_message("syntax-error", line=ex.args[1][0], args=ex.args[0])
return None
if not ast_node.pure_python:
self.add_message("raw-checker-failed", args=ast_node.name)
else:
# assert astroid.file.endswith('.py')
# invoke ITokenChecker interface on self to fetch module/block
# level options
self.process_tokens(tokens)
if self._ignore_file:
return False
# walk ast to collect line numbers
self.file_state.collect_block_lines(self.msgs_store, ast_node)
# run raw and tokens checkers
for checker in rawcheckers:
checker.process_module(ast_node)
for checker in tokencheckers:
checker.process_tokens(tokens)
# generate events to astroid checkers
walker.walk(ast_node)
return True
def open(self):
self._init_msg_states()
def add_stats(self, **kwargs):
"""add some stats entries to the statistic dictionary
raise an AssertionError if there is a key conflict
"""
for key, value in kwargs.items():
if key[-1] == "_":
key = key[:-1]
assert key not in self.stats
self.stats[key] = value
return self.stats
# utilities ###################################################################
@contextlib.contextmanager
def fix_import_path(args):
"""Prepare sys.path for running the linter checks.
Within this context, each of the given arguments is importable.
Paths are added to sys.path in corresponding order to the arguments.
We avoid adding duplicate directories to sys.path.
`sys.path` is reset to its original value upon exiting this context.
"""
orig = list(sys.path)
changes = []
for arg in args:
path = _get_python_path(arg)
if path in changes:
continue
else:
changes.append(path)
sys.path[:] = changes + ["."] + sys.path
try:
yield
finally:
sys.path[:] = orig
def guess_lint_path(args):
"""Attempt to determine the file being linted from a list of arguments.
:param args: The list of command line arguments to guess from.
:type args: list(str)
:returns: The path to file being linted if it can be guessed.
None otherwise.
:rtype: str or None
"""
value = None
# We only care if it's a path. If it's a module,
# we can't get a config from it
if args and os.path.exists(args[-1]):
value = args[-1]
return value
class ReportRegistry:
def __init__(self):
self.reports = {}
self._reports_state = {}
super().__init__()
self.register_report(
"RP0001", "Messages by category", report_total_messages_stats
)
self.register_report(
"RP0002", "% errors / warnings by module", report_messages_by_module_stats
)
self.register_report("RP0003", "Messages", report_messages_stats)
def register_report(self, reportid, r_title, r_cb, checker_classes=()):
"""Register a report
:param reportid: The unique identifier for the report.
:type reportid: str
:param r_title: The report's title.
:type r_title: str
:param r_cb: The method to call to make the report.
:type r_cb: callable
:param checker_classes: The checkers that create states
needed by this report.
:type checker_classes: class
"""
reportid = reportid.upper()
self.reports[reportid] = (r_title, r_cb, checker_classes)
def enable_report(self, reportid):
"""Enable the report of the given id.
:param reportid: The unique identifier of the report to enable.
:type reportid: str
"""
reportid = reportid.upper()
self._reports_state[reportid] = True
def disable_report(self, reportid):
"""Disable the report of the given id.
:param reportid: The unique identifier of the report to disable.
:type reportid: str
"""
reportid = reportid.upper()
self._reports_state[reportid] = False
def disable_reporters(self):
"""Disable all reporters."""
for report_id in self.reports:
self.disable_report(report_id)
def report_is_enabled(self, reportid):
"""Check if the report with the given id is enabled.
:param reportid: The unique identifier of the report to check.
:type reportid: str
:returns: True if the report is enabled, False otherwise.
:rtype: bool
"""
return self._reports_state.get(reportid, True)
class PluginRegistry(utils.MessagesHandlerMixIn, ReportRegistry):
"""A class to register checkers to."""
def __init__(self, config, register_options=(lambda options: None)):
super().__init__(config)
self.register_options = register_options
self._checkers = collections.defaultdict(list)
self._reporters = {}
self._python3_porting_mode = False
self._error_mode = False
self.register_options(PyLinter.options)
self.msgs_store.register_messages_from_checker(PyLinter)
def for_all_checkers(self):
"""Loop through all registered checkers.
:returns: Each registered checker.
:rtype: iterable(BaseChecker)
"""
for checkers in self._checkers.values():
yield from checkers
def register_checker(self, checker):
"""Register a checker.
:param checker: The checker to register.
:type checker: BaseChecker
:raises InvalidCheckerError: If the priority of the checker is
invalid.
"""
# Allow instances to be passed for backwards compatibility
if isinstance(checker, checkers.BaseChecker):
checker = checker.__class__
existing_checker_types = set(
existing_checker
for name_checkers in self._checkers.values()
for existing_checker in name_checkers
)
if checker in existing_checker_types:
msg_fmt = (
"Not registering checker {}. A checker of type {} has "
"already been registered."
)
msg = msg_fmt.format(checker.name, checker.__name__)
warnings.warn(msg)
return
if checker.priority > 0:
msg = "{}.priority must be <= 0".format(checker.__class__)
raise exceptions.InvalidCheckerError(msg)
self._checkers[checker.name].append(checker)
self.register_options(checker.options)
if hasattr(checker, "msgs"):
self.msgs_store.register_messages_from_checker(checker)
# Register the checker, but disable all of its messages.
# TODO(cpopa): we should have a better API for this.
if not getattr(checker, "enabled", True):
self.disable(checker.name)
def register_reporter(self, reporter_class):
if reporter_class.name in self._reporters:
# TODO: Raise if classes are the same
duplicate = self._reporters[reporter_class.name]
msg = "A reporter called {} has already been registered ({})."
msg = msg.format(reporter.name, duplicate.__class__)
warnings.warn(msg)
self._reporters[reporter_class.name] = reporter_class
def disable(self, msgid, scope="package", line=None, ignore_unknown=False):
"""Do not output messages that have the given ID.
:param msgid: The ID of the message to disable.
This can also be "all" to disable all messages,
the ID of a category to disable all messages of that category,
or the name of a checker to disable all messages from the checker.
:type msgid: str
:param scope: Must be "package". Provided for backwards compatibility.
:type scope: str.
:param line: The line number that the message is disabled on.
This is not used and is provided for backwards compatibility only.
:type line: int
:param ignore_unknown: If this is False,
an :exc:`UnknownMessageError` is raised when a message with the
given ID cannot be found.
If this is True, the exception is not raised.
:param ignore_unknown: bool
:raises ValueError: If `scope` is anything other than "package".
"""
self._set_msg_status(
msgid, enable=False, scope=scope, line=line, ignore_unknown=ignore_unknown
)
def enable(self, msgid, scope="package", line=None, ignore_unknown=False):
"""Enable outputting messages that have the given ID.
:param msgid: The ID of the message to enable.
This can also be "all" to enable all messages,
the ID of a category to enable all messages of that category,
or the name of a checker to enable all messages from the checker.
:type msgid: str
:param scope: Must be "package". Provided for backwards compatibility.
:type scope: str.
:param line: The line number that the message is enabled on.
This is not used and is provided for backwards compatibility only.
:type line: int
:param ignore_unknown: If this is False,
an :exc:`UnknownMessageError` is raised when a message with the
given ID cannot be found.
If this is True, the exception is not raised.
:param ignore_unknown: bool
:raises ValueError: If `scope` is anything other than "package".
"""
self._set_msg_status(
msgid, enable=True, scope=scope, line=line, ignore_unknown=ignore_unknown
)
def error_mode(self):
"""Enable only errors; no reports, no persistent"""
self._error_mode = True
self.disable_noerror_messages()
self.disable("miscellaneous")
if self._python3_porting_mode:
self.disable("all")
for msg_id in self._checker_messages("python3"):
if msg_id.startswith("E"):
self.enable(msg_id)
else:
self.disable("python3")
def python3_porting_mode(self):
"""Disable all other checkers and enable Python 3 warnings."""
self.disable("all")
self.enable("python3")
if self._error_mode:
# The error mode was activated, using the -E flag.
# So we'll need to enable only the errors from the
# Python 3 porting checker.
for msg_id in self._checker_messages("python3"):
if msg_id.startswith("E"):
self.enable(msg_id)
else:
self.disable(msg_id)
self._python3_porting_mode = True
class Runner(object):
"""A class to manager how the linter runs."""
option_definitions = ()
"""The runner specific configuration options.
:type: set(OptionDefinition)
"""
class CLIRunner(Runner):
option_definitions = (
("module_or_package", {"positional": True, "nargs": argparse.REMAINDER}),
(
"rcfile",
{
"type": "string",
"metavar": "<file>",
"help": "Specify a configuration file.",
},
),
(
"init-hook",
{
"type": "string",
"metavar": "<code>",
"level": 1,
"help": "Python code to execute, usually for sys.path "
"manipulation such as pygtk.require().",
},
),
(
"help-msg",
{
"type": "string",
"metavar": "<msg-id>",
"group": "Commands",
"default": None,
"help": "Display a help message for the given message id and "
"exit. The value may be a comma separated list of message ids.",
},
),
(
"list-msgs",
{
"group": "Commands",
"level": 1,
"action": "store_true",
"default": False,
"help": "Generate pylint's messages.",
},
),
(
"list-conf-levels",
{
"group": "Commands",
"level": 1,
"action": "store_true",
"default": False,
"help": "Generate pylint's confidence levels.",
},
),
(
"full-documentation",
{
"metavar": "<msg-id>",
"default": None,
"group": "Commands",
"level": 1,
"help": "Generate pylint's full documentation.",
},
),
(
"generate-rcfile",
{
"group": "Commands",
"action": "store_true",
"default": False,
"help": "Generate a sample configuration file according to "
"the current configuration. You can put other options "
"before this one to get them in the generated "
"configuration.",
},
),
(
"generate-man",
{
"group": "Commands",
"action": "store_true",
"default": False,
"help": "Generate pylint's man page.",
"hide": True,
},
),
(
"errors-only",
{
"short": "E",
"action": "store_true",
"default": False,
"help": "In error mode, checkers without error messages are "
"disabled and for others, only the ERROR messages are "
"displayed, and no reports are done by default"
"",
},
),
(
"py3k",
{
"action": "store_true",
"default": False,
"help": "In Python 3 porting mode, all checkers will be "
"disabled and only messages emitted by the porting "
"checker will be displayed",
},
),
(
"version",
{
"group": "Commands",
"action": "version",
"version": FULL_VERSION,
"help": "Print the version of pylint and important " "dependencies",
},
),
)
option_groups = (
(
"Commands",
"Options which are actually commands. Options in this \
group are mutually exclusive.",
),
)
description = (
"pylint [options] module_or_package\n"
"\n"
" Check that a module satisfies a coding standard (and more !).\n"
"\n"
" pylint --help\n"
"\n"
" Display this help message and exit.\n"
"\n"
" pylint --help-msg <msg-id>[,<msg-id>]\n"
"\n"
" Display help messages about given message identifiers and exit.\n"
)
def __init__(self):
super().__init__()
self._global_config = config.Configuration()
self._plugin_registry = PluginRegistry(self._global_config)
self._loaded_plugins = set()
self._reporter = None
self._config_parsers = []
def run(self, args):
# Phase 1: Preprocessing
option_definitions = self.option_definitions + PyLinter.options
parser = config.CLIParser(self.description)
parser.add_option_definitions(option_definitions)
parser.add_help_section("Environment variables", config.ENV_HELP, level=1)
# pylint: disable=bad-continuation
parser.add_help_section(
"Output",
"Using the default text output, the message format is : \n"
" \n"
" MESSAGE_TYPE: LINE_NUM:[OBJECT:] MESSAGE \n"
" \n"
"There are 5 kind of message types : \n"
" * (C) convention, for programming standard violation \n"
" * (R) refactor, for bad code smell \n"
" * (W) warning, for python specific problems \n"
" * (E) error, for probable bugs in the code \n"
" * (F) fatal, if an error occurred which prevented pylint from doing further\n"
"processing.\n",
level=1,
)
parser.add_help_section(
"Output status code",
"Pylint should leave with following status code: \n"
" * 0 if everything went fine \n"
" * 1 if a fatal message was issued \n"
" * 2 if an error message was issued \n"
" * 4 if a warning message was issued \n"
" * 8 if a refactor message was issued \n"
" * 16 if a convention message was issued \n"
" * 32 on usage error \n"
" \n"
"status 1 to 16 will be bit-ORed so you can know which different categories has\n"
"been issued by analysing pylint output status code\n",
level=1,
)
self._global_config.add_options(option_definitions)
parsed = parser.preprocess(args, "init_hook", "rcfile", "load_plugins")
# Call init-hook
if parsed.init_hook:
exec(parsed.init_hook)
# Load rcfile, else system rcfile
file_parser = config.IniFileParser()
file_parser.add_option_definitions(PyLinter.options)
rcfile = parsed.rcfile or config.PYLINTRC
if rcfile:
file_parsed = file_parser.preprocess(rcfile, "init_hook", "load_plugins")
if file_parsed.init_hook:
exec(file_parsed.init_hook)
if file_parsed.load_plugins:
old_value = getattr(parsed, "load_plugins", [])
parsed.load_plugins = old_value + file_parsed.load_plugins
def register_options(options):
self._global_config.add_options(options)
parser.add_option_definitions(options)
file_parser.add_option_definitions(options)
self._plugin_registry.register_options = register_options
self._file_parser = file_parser
self._cli_parser = parser
checkers.initialize(self._plugin_registry)
# Load plugins from CLI
plugins = parsed.load_plugins or []
for plugin in plugins:
self.load_plugin(plugin)
# TODO: This is for per directory config support (#618)
# Phase 2: Discover more plugins found in config files
# Walk and discover config files, watching for blacklists as we go
# Load plugins from config files
# Phase 3: Full load
# Fully load config files
if rcfile:
file_parser.parse(rcfile, self._global_config)
# Fully load CLI into global config
self._cli_config = config.Configuration()
parser.parse(args, self._cli_config)
self._global_config += self._cli_config
if self._global_config.generate_rcfile:
file_parser.write()
sys.exit(0)
# TODO: if global_config.generate_man
if self._global_config.errors_only:
self._plugin_registry.error_mode()
if file_parser.has_option("MESSAGES CONTROL", "disable"):
value = file_parser.get("MESSAGES CONTROL", "disable")
self.config.set_option("disable", value)
self._global_config.reports = False
self._global_config.persistent = False
self._global_config.score = False
if self._global_config.py3k:
self._plugin_registry.python3_porting_mode()
if file_parser.has_option("MESSAGES CONTROL", "disable"):
value = file_parser.get("MESSAGES CONTROL", "disable")
self._global_config.set_option("disable", value)
if self._global_config.full_documentation:
self._plugin_registry.print_full_documentation()
sys.exit(0)
if self._global_config.list_conf_levels:
for level in interfaces.CONFIDENCE_LEVELS:
print("%-18s: %s" % level)
sys.exit(0)
if self._global_config.list_msgs:
self._plugin_registry.msgs_store.list_messages()
sys.exit(0)
if self._global_config.help_msg:
msg = utils._splitstrip(self._global_config.help_msg)
self._plugin_registry.msgs_store.help_message(msg)
sys.exit(0)
if not self._global_config.module_or_package:
sys.exit(0)
self.load_default_plugins()
self._plugin_registry.disable("I")
self._plugin_registry.enable("c-extension-no-member")
for checker in self._plugin_registry.for_all_checkers():
checker.config = self._global_config
if not self._global_config.reports:
self._plugin_registry.disable_reporters()
with fix_import_path(self._global_config.module_or_package):
assert self._global_config.jobs == 1
base_name, all_states, status_code = self.check(
self._global_config.module_or_package
)
self.generate_reports(base_name, all_states)
if self._global_config.exit_zero:
sys.exit(0)
else:
sys.exit(status_code)
def load_plugin(self, module_name):
if module_name in self._loaded_plugins:
msg = "Already loaded plugin {0}. Ignoring".format(module_name)
warnings.warn(msg)
else:
module = astroid.modutils.load_module_from_name(module_name)
module.register(self._plugin_registry)
def load_plugins(self, module_names):
"""Load a plugin.
Args:
module_names (list(str)): The name of plugin modules to load.
"""
for module_name in module_names:
self.load_plugin(module_name)
def load_default_plugins(self):
"""Load all of the default plugins."""
reporters.initialize(self._plugin_registry)
# Make sure to load the default reporter, because
# the option has been set before the plugins had been loaded.
if not self._reporter:
self.load_reporter()
def load_reporter(self):
name = self._global_config.output_format.lower()
if name in self._plugin_registry._reporters:
self._reporter = self._plugin_registry._reporters[name](
config=self._global_config
)
self._plugin_registry.reporter = self._reporter
else:
try:
reporter_class = self._load_reporter_class()
except (ImportError, AttributeError):
raise exceptions.InvalidReporterError(name)
else:
self._reporter = reporter_class(config=self._global_config)
self._plugin_registry.reporter = self._reporter
def _load_reporter_class(self):
qname = self._global_config.output_format
module = modutils.load_module_from_name(modutils.get_module_part(qname))
class_name = qname.split(".")[-1]
reporter_class = getattr(module, class_name)
return reporter_class
def generate_reports(self, base_name, all_states):
"""close the whole package /module, it's time to make reports !
if persistent run, pickle results for later comparison
"""
# Display whatever messages are left on the reporter.
self._reporter.display_messages(report_nodes.Section())
if base_name is not None:
# load previous results if any
previous_stats = config.load_results(base_name)
# XXX code below needs refactoring to be more reporter agnostic
self._reporter.on_close(self._plugin_registry.stats, previous_stats)
if self._global_config.reports:
sect = self.make_reports(
self._plugin_registry.stats, previous_stats, all_states
)
else:
sect = report_nodes.Section()
if self._global_config.reports:
self._reporter.display_reports(sect)
self._report_evaluation(base_name)
# save results if persistent run
if self._global_config.persistent:
config.save_results(self._plugin_registry.stats, base_name)
else:
self._reporter.on_close(self._plugin_registry.stats, {})
def report_order(self):
"""A list of reports, sorted in the order in which they must be called.
:returns: The list of reports.
:rtype: list(str)
"""
reports = sorted(self._plugin_registry.reports)
for final_report in ("RP0001", "RP0002", "RP0003"):
try:
reports.remove(final_report)
except ValueError:
pass
else:
reports.append(final_report)
return reports
def make_reports(self, stats, old_stats, all_states):
"""Render the registered reports.
:param stats: The statistics dictionary for this run.
:type stats: dict
:param old_stats: The statistics dictionary for the previous run.
:type old_stats: dict
:returns: The complete report.
:rtype: pylint.reporters.ureports.nodes.Section
"""
sect = report_nodes.Section(
"Report", "%s statements analysed." % (stats["statement"])
)
for report_id in self.report_order():
r_title, r_cb, checker_classes = self._plugin_registry.reports[report_id]
states = [
all_states.get(checker_class) for checker_class in checker_classes
]
if not self._plugin_registry.report_is_enabled(report_id):
continue
report_sect = report_nodes.Section(r_title)
try:
r_cb(report_sect, stats, old_stats, self._global_config, states)
except exceptions.EmptyReportError:
continue
report_sect.report_id = report_id
sect.append(report_sect)
return sect
def _report_evaluation(self, base_name):
"""make the global evaluation report"""
# check with at least check 1 statements (usually 0 when there is a
# syntax error preventing pylint from further processing)
previous_stats = config.load_results(base_name)
if self._plugin_registry.stats["statement"] == 0:
return
# get a global note for the code
evaluation = self._global_config.evaluation
try:
note = eval(
evaluation, {}, self._plugin_registry.stats
) # pylint: disable=eval-used
except Exception as ex:
msg = "An exception occurred while rating: %s" % ex
else:
self._plugin_registry.stats["global_note"] = note
msg = "Your code has been rated at %.2f/10" % note
pnote = previous_stats.get("global_note")
if pnote is not None:
msg += " (previous run: %.2f/10, %+.2f)" % (pnote, note - pnote)
if self._global_config.score:
sect = report_nodes.EvaluationSection(msg)
self._reporter.display_reports(sect)
def get_checkers(self):
"""return all available checkers as a list"""
return [c for c in self._plugin_registry.for_all_checkers()]
def prepare_checkers(self, linter):
"""return checkers needed for activated messages and reports"""
# get needed checkers
neededcheckers = set()
for checker_cls in self.get_checkers()[1:]:
if any(linter.is_message_enabled(msg) for msg in checker_cls.msgs):
neededcheckers.add(checker_cls)
for report_id, (_, _, checker_classes) in self._plugin_registry.reports.items():
if self._plugin_registry.report_is_enabled(report_id):
neededcheckers.update(checker_classes)
# Sort checkers by priority
neededcheckers = sorted(
neededcheckers, key=operator.attrgetter("priority"), reverse=True
)
return neededcheckers
# pylint: disable=unused-argument
@staticmethod
def should_analyze_file(modname, path, is_argument=False):
"""Returns whether or not a module should be checked.
This implementation returns True for all python source files,
indicating that all files should be linted.
Subclasses may override this method to indicate that modules satisfying
certain conditions should not be linted.
:param str modname: The name of the module to be checked.
:param str path: The full path to the source code of the module.
:param bool is_argument: Whetter the file is an argument to pylint or not.
Files which respect this property are always
checked, since the user requested it explicitly.
:returns: True if the module should be checked.
:rtype: bool
"""
if is_argument:
return True
return path.endswith(".py")
# pylint: enable=unused-argument
def close_registration(self):
"""Stop registering plugins and prepare everything for checking."""
MANAGER.always_load_extensions = self._global_config.unsafe_load_any_extension
MANAGER.max_inferable_values = self._global_config.limit_inference_results
MANAGER.extension_package_whitelist.update(
self._global_config.extension_pkg_whitelist
)
def check(self, files_or_modules):
"""main checking entry: check a list of files or modules from their
name.
"""
# initialize msgs_state now that all messages have been registered into the store
self.close_registration()
if not isinstance(files_or_modules, (list, tuple)):
files_or_modules = (files_or_modules,)
# notify global begin
all_states = collections.defaultdict(list)
all_stats = [self._plugin_registry.stats]
# build ast and check modules or packages
expanded_files = utils.expand_files(
files_or_modules,
self._plugin_registry,
self._global_config.black_list,
self._global_config.black_list_re,
)
config_store = config.ConfigurationStore()
for module_desc in expanded_files:
directory = os.path.dirname(module_desc.path)
local_config = config_store[directory]
if not local_config:
local_file = config.find_nearby_pylintrc(directory)
if not local_file:
local_config = self._global_config
else:
local_config = self._global_config.copy()
self._file_parser.parse(local_file, local_config)
local_config += self._cli_config
config_store[directory] = local_config
for module_desc in expanded_files:
modname = module_desc.name
filepath = module_desc.path
if not module_desc.isarg and not self.should_analyze_file(
modname, filepath
):
continue
directory = os.path.dirname(module_desc.path)
local_config = config_store.get_config_for(directory)
linter = PyLinter(local_config)
linter.msgs_store = self._plugin_registry.msgs_store
for msg_ids, enable in local_config.msg_toggles:
for msg_id in msg_ids:
if enable:
linter.enable(msg_id, scope="directory")
else:
linter.disable(msg_id, scope="directory")
linter.open()
walker = utils.PyLintASTWalker(self._plugin_registry)
allcheckers = []
tokencheckers = [linter]
rawcheckers = []
for checker_cls in self.prepare_checkers(linter):
checker = checker_cls(linter)
checker.linter = linter
all_states[checker_cls].append(checker.open())
allcheckers.append(checker)
if interfaces.implements(checker, interfaces.ITokenChecker):
tokencheckers.append(checker)
if interfaces.implements(checker, interfaces.IRawChecker):
rawcheckers.append(checker)
if interfaces.implements(checker, interfaces.IAstroidChecker):
walker.add_checker(checker)
linter.reporter = self._reporter
linter.check(module_desc, walker, rawcheckers, tokencheckers)
self._plugin_registry.stats["statement"] += walker.nbstatements
all_stats.append(linter.stats)
for checker in reversed(allcheckers):
checker.close()
linter = PyLinter(self._global_config)
linter.msgs_store = self._plugin_registry.msgs_store
linter.reporter = self._reporter
for msg_ids, enable in self._global_config.msg_toggles:
for msg_id in msg_ids:
if enable:
linter.enable(msg_id, scope="directory")
else:
linter.disable(msg_id, scope="directory")
linter.open()
# TODO: What about checkers that have been enabled in a local config?
for checker_cls in reversed(self.get_checkers()):
checker = checker_cls(linter)
checker.linter = linter
checker.global_close(all_states[checker_cls])
all_stats.append(linter.stats)
self._plugin_registry.stats = _merge_stats(all_stats)
return module_desc.basename, all_states, linter.msg_status
if __name__ == "__main__":
CLIRunner().run(sys.argv[1:])