diff options
Diffstat (limited to 'checkers/logging.py')
| -rw-r--r-- | checkers/logging.py | 122 |
1 files changed, 95 insertions, 27 deletions
diff --git a/checkers/logging.py b/checkers/logging.py index 17851e4e0..f19273b93 100644 --- a/checkers/logging.py +++ b/checkers/logging.py @@ -17,8 +17,32 @@ from logilab import astng from pylint import checkers from pylint import interfaces +from pylint.checkers import utils + + +MSGS = { + 'W6501': ('Specify string format arguments as logging function parameters', + '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/.'), + 'E6500': ('Unsupported logging format character %r (%#02x) at index %d', + 'Used when an unsupported format character is used in a logging\ + statement format string.'), + 'E6501': ('Logging format string ends in middle of conversion specifier', + 'Used when a logging statement format string terminates before\ + the end of a conversion specifier.'), + 'E6505': ('Too many arguments for logging format string', + 'Used when a logging format string is given too few arguments.'), + 'E6506': ('Not enough arguments for logging format string', + 'Used when a logging format string is given too many arguments'), + } -EAGER_STRING_INTERPOLATION = 'W6501' CHECKED_CONVENIENCE_FUNCTIONS = set([ 'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn', @@ -29,21 +53,8 @@ class LoggingChecker(checkers.BaseChecker): """Checks use of the logging module.""" __implements__ = interfaces.IASTNGChecker - name = 'logging' - - msgs = {EAGER_STRING_INTERPOLATION: - ('Specify string format arguments as logging function parameters', - '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/.') - } + msgs = MSGS def visit_module(self, unused_node): """Clears any state left in this checker from last module checked.""" @@ -67,30 +78,87 @@ class LoggingChecker(checkers.BaseChecker): or not isinstance(node.func.expr, astng.Name) or node.func.expr.name != self._logging_name): return - self._CheckConvenienceMethods(node) - self._CheckLogMethod(node) + self._check_convenience_methods(node) + self._check_log_methods(node) - def _CheckConvenienceMethods(self, node): + def _check_convenience_methods(self, node): """Checks calls to logging convenience methods (like logging.warn).""" if node.func.attrname not in CHECKED_CONVENIENCE_FUNCTIONS: return - if not node.args: - # Either no args, or star args, or double-star args. Beyond the - # scope of this checker in any case. + 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 if isinstance(node.args[0], astng.BinOp) and node.args[0].op == '%': - self.add_message(EAGER_STRING_INTERPOLATION, node=node) + self.add_message('W6501', node=node) + elif isinstance(node.args[0], astng.Const): + self._check_format_string(node, 0) - def _CheckLogMethod(self, node): + def _check_log_methods(self, node): """Checks calls to logging.log(level, format, *format_args).""" if node.func.attrname != 'log': return - if len(node.args) < 2: - # Either a malformed call or something with crazy star args or - # double-star args magic. Beyond the scope of this checker. + 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 if isinstance(node.args[1], astng.BinOp) and node.args[1].op == '%': - self.add_message(EAGER_STRING_INTERPOLATION, node=node) + self.add_message('W6501', node=node) + elif isinstance(node.args[1], astng.Const): + self._check_format_string(node, 1) + + 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 = self._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, basestring): + # 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, e: + c = format_string[e.index] + self.add_message('E6500', node=node, args=(c, ord(c), e.index)) + return + except utils.IncompleteFormatString: + self.add_message('E6501', node=node) + return + if num_args > required_num_args: + self.add_message('E6505', node=node) + elif num_args < required_num_args: + self.add_message('E6506', node=node) + + def _count_supplied_tokens(self, 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, astng.Keyword)) + def register(linter): """Required method to auto-register this checker.""" |
