diff options
Diffstat (limited to 'pylint/checkers/logging.py')
-rw-r--r-- | pylint/checkers/logging.py | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/pylint/checkers/logging.py b/pylint/checkers/logging.py new file mode 100644 index 0000000..897c1c7 --- /dev/null +++ b/pylint/checkers/logging.py @@ -0,0 +1,256 @@ +# Copyright (c) 2009-2010 Google, Inc. +# 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. +"""checker for use of Python logging +""" + +import astroid +from pylint import checkers +from pylint import interfaces +from pylint.checkers import utils +from pylint.checkers.utils import check_messages + +import six + + +MSGS = { + 'W1201': ('Specify string format arguments as logging function parameters', + 'logging-not-lazy', + 'Used when a logging statement has a call form of ' + '"logging.<logging method>(format_string % (format_args...))". ' + 'Such calls should leave string interpolation to the logging ' + 'method itself and be written ' + '"logging.<logging method>(format_string, format_args...)" ' + 'so that the program may avoid incurring the cost of the ' + 'interpolation in those cases in which no message will be ' + 'logged. For more, see ' + 'http://www.python.org/dev/peps/pep-0282/.'), + 'W1202': ('Use % formatting in logging functions but pass the % ' + 'parameters as arguments', + 'logging-format-interpolation', + 'Used when a logging statement has a call form of ' + '"logging.<logging method>(format_string.format(format_args...))"' + '. Such calls should use % formatting instead, but leave ' + 'interpolation to the logging function by passing the parameters ' + 'as arguments.'), + 'E1200': ('Unsupported logging format character %r (%#02x) at index %d', + 'logging-unsupported-format', + 'Used when an unsupported format character is used in a logging\ + statement format string.'), + 'E1201': ('Logging format string ends in middle of conversion specifier', + 'logging-format-truncated', + 'Used when a logging statement format string terminates before\ + the end of a conversion specifier.'), + 'E1205': ('Too many arguments for logging format string', + 'logging-too-many-args', + 'Used when a logging format string is given too few arguments.'), + 'E1206': ('Not enough arguments for logging format string', + 'logging-too-few-args', + 'Used when a logging format string is given too many arguments'), + } + + +CHECKED_CONVENIENCE_FUNCTIONS = set([ + 'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn', + 'warning']) + +def is_method_call(callfunc_node, types=(), methods=()): + """Determines if a CallFunc node represents a method call. + + Args: + callfunc_node: The CallFunc AST node to check. + types: Optional sequence of caller type names to restrict check. + methods: Optional sequence of method names to restrict check. + + Returns: + True, if the node represents a method call for the given type and + method names, False otherwise. + """ + if not isinstance(callfunc_node, astroid.CallFunc): + return False + func = utils.safe_infer(callfunc_node.func) + return (isinstance(func, astroid.BoundMethod) + and isinstance(func.bound, astroid.Instance) + and (func.bound.name in types if types else True) + and (func.name in methods if methods else True)) + + + +class LoggingChecker(checkers.BaseChecker): + """Checks use of the logging module.""" + + __implements__ = interfaces.IAstroidChecker + name = 'logging' + msgs = MSGS + + options = (('logging-modules', + {'default': ('logging',), + 'type': 'csv', + 'metavar': '<comma separated list>', + 'help': 'Logging modules to check that the string format ' + 'arguments are in logging function parameter format'} + ), + ) + + def visit_module(self, node): # pylint: disable=unused-argument + """Clears any state left in this checker from last module checked.""" + # The code being checked can just as easily "import logging as foo", + # so it is necessary to process the imports and store in this field + # what name the logging module is actually given. + self._logging_names = set() + logging_mods = self.config.logging_modules + + self._logging_modules = set(logging_mods) + self._from_imports = {} + for logging_mod in logging_mods: + parts = logging_mod.rsplit('.', 1) + if len(parts) > 1: + self._from_imports[parts[0]] = parts[1] + + def visit_from(self, node): + """Checks to see if a module uses a non-Python logging module.""" + try: + logging_name = self._from_imports[node.modname] + for module, as_name in node.names: + if module == logging_name: + self._logging_names.add(as_name or module) + except KeyError: + pass + + def visit_import(self, node): + """Checks to see if this module uses Python's built-in logging.""" + for module, as_name in node.names: + if module in self._logging_modules: + self._logging_names.add(as_name or module) + + @check_messages(*(MSGS.keys())) + def visit_callfunc(self, node): + """Checks calls to logging methods.""" + def is_logging_name(): + return (isinstance(node.func, astroid.Getattr) and + isinstance(node.func.expr, astroid.Name) and + node.func.expr.name in self._logging_names) + + def is_logger_class(): + try: + for inferred in node.func.infer(): + if isinstance(inferred, astroid.BoundMethod): + parent = inferred._proxied.parent + if (isinstance(parent, astroid.Class) and + (parent.qname() == 'logging.Logger' or + any(ancestor.qname() == 'logging.Logger' + for ancestor in parent.ancestors()))): + return True, inferred._proxied.name + except astroid.exceptions.InferenceError: + pass + return False, None + + if is_logging_name(): + name = node.func.attrname + else: + result, name = is_logger_class() + if not result: + return + self._check_log_method(node, name) + + def _check_log_method(self, node, name): + """Checks calls to logging.log(level, format, *format_args).""" + if name == 'log': + if node.starargs or node.kwargs or len(node.args) < 2: + # Either a malformed call, star args, or double-star args. Beyond + # the scope of this checker. + return + format_pos = 1 + elif name in CHECKED_CONVENIENCE_FUNCTIONS: + if node.starargs or node.kwargs or not node.args: + # Either no args, star args, or double-star args. Beyond the + # scope of this checker. + return + format_pos = 0 + else: + return + + if isinstance(node.args[format_pos], astroid.BinOp) and node.args[format_pos].op == '%': + self.add_message('logging-not-lazy', node=node) + elif isinstance(node.args[format_pos], astroid.CallFunc): + self._check_call_func(node.args[format_pos]) + elif isinstance(node.args[format_pos], astroid.Const): + self._check_format_string(node, format_pos) + + def _check_call_func(self, callfunc_node): + """Checks that function call is not format_string.format(). + + Args: + callfunc_node: CallFunc AST node to be checked. + """ + if is_method_call(callfunc_node, ('str', 'unicode'), ('format',)): + self.add_message('logging-format-interpolation', node=callfunc_node) + + def _check_format_string(self, node, format_arg): + """Checks that format string tokens match the supplied arguments. + + Args: + node: AST node to be checked. + format_arg: Index of the format string in the node arguments. + """ + num_args = _count_supplied_tokens(node.args[format_arg + 1:]) + if not num_args: + # If no args were supplied, then all format strings are valid - + # don't check any further. + return + format_string = node.args[format_arg].value + if not isinstance(format_string, six.string_types): + # If the log format is constant non-string (e.g. logging.debug(5)), + # ensure there are no arguments. + required_num_args = 0 + else: + try: + keyword_args, required_num_args = \ + utils.parse_format_string(format_string) + if keyword_args: + # Keyword checking on logging strings is complicated by + # special keywords - out of scope. + return + except utils.UnsupportedFormatCharacter as ex: + char = format_string[ex.index] + self.add_message('logging-unsupported-format', node=node, + args=(char, ord(char), ex.index)) + return + except utils.IncompleteFormatString: + self.add_message('logging-format-truncated', node=node) + return + if num_args > required_num_args: + self.add_message('logging-too-many-args', node=node) + elif num_args < required_num_args: + self.add_message('logging-too-few-args', node=node) + + +def _count_supplied_tokens(args): + """Counts the number of tokens in an args list. + + The Python log functions allow for special keyword arguments: func, + exc_info and extra. To handle these cases correctly, we only count + arguments that aren't keywords. + + Args: + args: List of AST nodes that are arguments for a log format string. + + Returns: + Number of AST nodes that aren't keywords. + """ + return sum(1 for arg in args if not isinstance(arg, astroid.Keyword)) + + +def register(linter): + """Required method to auto-register this checker.""" + linter.register_checker(LoggingChecker(linter)) |