OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2009-2010 Google, Inc. |
| 2 # This program is free software; you can redistribute it and/or modify it under |
| 3 # the terms of the GNU General Public License as published by the Free Software |
| 4 # Foundation; either version 2 of the License, or (at your option) any later |
| 5 # version. |
| 6 # |
| 7 # This program is distributed in the hope that it will be useful, but WITHOUT |
| 8 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 9 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
| 10 # |
| 11 # You should have received a copy of the GNU General Public License along with |
| 12 # this program; if not, write to the Free Software Foundation, Inc., |
| 13 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 14 """checker for use of Python logging |
| 15 """ |
| 16 |
| 17 import astroid |
| 18 from pylint import checkers |
| 19 from pylint import interfaces |
| 20 from pylint.checkers import utils |
| 21 from pylint.checkers.utils import check_messages |
| 22 |
| 23 import six |
| 24 |
| 25 |
| 26 MSGS = { |
| 27 'W1201': ('Specify string format arguments as logging function parameters', |
| 28 'logging-not-lazy', |
| 29 'Used when a logging statement has a call form of ' |
| 30 '"logging.<logging method>(format_string % (format_args...))". ' |
| 31 'Such calls should leave string interpolation to the logging ' |
| 32 'method itself and be written ' |
| 33 '"logging.<logging method>(format_string, format_args...)" ' |
| 34 'so that the program may avoid incurring the cost of the ' |
| 35 'interpolation in those cases in which no message will be ' |
| 36 'logged. For more, see ' |
| 37 'http://www.python.org/dev/peps/pep-0282/.'), |
| 38 'W1202': ('Use % formatting in logging functions but pass the % ' |
| 39 'parameters as arguments', |
| 40 'logging-format-interpolation', |
| 41 'Used when a logging statement has a call form of ' |
| 42 '"logging.<logging method>(format_string.format(format_args...))"' |
| 43 '. Such calls should use % formatting instead, but leave ' |
| 44 'interpolation to the logging function by passing the parameters ' |
| 45 'as arguments.'), |
| 46 'E1200': ('Unsupported logging format character %r (%#02x) at index %d', |
| 47 'logging-unsupported-format', |
| 48 'Used when an unsupported format character is used in a logging\ |
| 49 statement format string.'), |
| 50 'E1201': ('Logging format string ends in middle of conversion specifier', |
| 51 'logging-format-truncated', |
| 52 'Used when a logging statement format string terminates before\ |
| 53 the end of a conversion specifier.'), |
| 54 'E1205': ('Too many arguments for logging format string', |
| 55 'logging-too-many-args', |
| 56 'Used when a logging format string is given too few arguments.'), |
| 57 'E1206': ('Not enough arguments for logging format string', |
| 58 'logging-too-few-args', |
| 59 'Used when a logging format string is given too many arguments'), |
| 60 } |
| 61 |
| 62 |
| 63 CHECKED_CONVENIENCE_FUNCTIONS = set([ |
| 64 'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn', |
| 65 'warning']) |
| 66 |
| 67 def is_method_call(callfunc_node, types=(), methods=()): |
| 68 """Determines if a CallFunc node represents a method call. |
| 69 |
| 70 Args: |
| 71 callfunc_node: The CallFunc AST node to check. |
| 72 types: Optional sequence of caller type names to restrict check. |
| 73 methods: Optional sequence of method names to restrict check. |
| 74 |
| 75 Returns: |
| 76 True, if the node represents a method call for the given type and |
| 77 method names, False otherwise. |
| 78 """ |
| 79 if not isinstance(callfunc_node, astroid.CallFunc): |
| 80 return False |
| 81 func = utils.safe_infer(callfunc_node.func) |
| 82 return (isinstance(func, astroid.BoundMethod) |
| 83 and isinstance(func.bound, astroid.Instance) |
| 84 and (func.bound.name in types if types else True) |
| 85 and (func.name in methods if methods else True)) |
| 86 |
| 87 |
| 88 |
| 89 class LoggingChecker(checkers.BaseChecker): |
| 90 """Checks use of the logging module.""" |
| 91 |
| 92 __implements__ = interfaces.IAstroidChecker |
| 93 name = 'logging' |
| 94 msgs = MSGS |
| 95 |
| 96 options = (('logging-modules', |
| 97 {'default': ('logging',), |
| 98 'type': 'csv', |
| 99 'metavar': '<comma separated list>', |
| 100 'help': 'Logging modules to check that the string format ' |
| 101 'arguments are in logging function parameter format'} |
| 102 ), |
| 103 ) |
| 104 |
| 105 def visit_module(self, node): # pylint: disable=unused-argument |
| 106 """Clears any state left in this checker from last module checked.""" |
| 107 # The code being checked can just as easily "import logging as foo", |
| 108 # so it is necessary to process the imports and store in this field |
| 109 # what name the logging module is actually given. |
| 110 self._logging_names = set() |
| 111 logging_mods = self.config.logging_modules |
| 112 |
| 113 self._logging_modules = set(logging_mods) |
| 114 self._from_imports = {} |
| 115 for logging_mod in logging_mods: |
| 116 parts = logging_mod.rsplit('.', 1) |
| 117 if len(parts) > 1: |
| 118 self._from_imports[parts[0]] = parts[1] |
| 119 |
| 120 def visit_from(self, node): |
| 121 """Checks to see if a module uses a non-Python logging module.""" |
| 122 try: |
| 123 logging_name = self._from_imports[node.modname] |
| 124 for module, as_name in node.names: |
| 125 if module == logging_name: |
| 126 self._logging_names.add(as_name or module) |
| 127 except KeyError: |
| 128 pass |
| 129 |
| 130 def visit_import(self, node): |
| 131 """Checks to see if this module uses Python's built-in logging.""" |
| 132 for module, as_name in node.names: |
| 133 if module in self._logging_modules: |
| 134 self._logging_names.add(as_name or module) |
| 135 |
| 136 @check_messages(*(MSGS.keys())) |
| 137 def visit_callfunc(self, node): |
| 138 """Checks calls to logging methods.""" |
| 139 def is_logging_name(): |
| 140 return (isinstance(node.func, astroid.Getattr) and |
| 141 isinstance(node.func.expr, astroid.Name) and |
| 142 node.func.expr.name in self._logging_names) |
| 143 |
| 144 def is_logger_class(): |
| 145 try: |
| 146 for inferred in node.func.infer(): |
| 147 if isinstance(inferred, astroid.BoundMethod): |
| 148 parent = inferred._proxied.parent |
| 149 if (isinstance(parent, astroid.Class) and |
| 150 (parent.qname() == 'logging.Logger' or |
| 151 any(ancestor.qname() == 'logging.Logger' |
| 152 for ancestor in parent.ancestors()))): |
| 153 return True, inferred._proxied.name |
| 154 except astroid.exceptions.InferenceError: |
| 155 pass |
| 156 return False, None |
| 157 |
| 158 if is_logging_name(): |
| 159 name = node.func.attrname |
| 160 else: |
| 161 result, name = is_logger_class() |
| 162 if not result: |
| 163 return |
| 164 self._check_log_method(node, name) |
| 165 |
| 166 def _check_log_method(self, node, name): |
| 167 """Checks calls to logging.log(level, format, *format_args).""" |
| 168 if name == 'log': |
| 169 if node.starargs or node.kwargs or len(node.args) < 2: |
| 170 # Either a malformed call, star args, or double-star args. Beyon
d |
| 171 # the scope of this checker. |
| 172 return |
| 173 format_pos = 1 |
| 174 elif name in CHECKED_CONVENIENCE_FUNCTIONS: |
| 175 if node.starargs or node.kwargs or not node.args: |
| 176 # Either no args, star args, or double-star args. Beyond the |
| 177 # scope of this checker. |
| 178 return |
| 179 format_pos = 0 |
| 180 else: |
| 181 return |
| 182 |
| 183 if isinstance(node.args[format_pos], astroid.BinOp) and node.args[format
_pos].op == '%': |
| 184 self.add_message('logging-not-lazy', node=node) |
| 185 elif isinstance(node.args[format_pos], astroid.CallFunc): |
| 186 self._check_call_func(node.args[format_pos]) |
| 187 elif isinstance(node.args[format_pos], astroid.Const): |
| 188 self._check_format_string(node, format_pos) |
| 189 |
| 190 def _check_call_func(self, callfunc_node): |
| 191 """Checks that function call is not format_string.format(). |
| 192 |
| 193 Args: |
| 194 callfunc_node: CallFunc AST node to be checked. |
| 195 """ |
| 196 if is_method_call(callfunc_node, ('str', 'unicode'), ('format',)): |
| 197 self.add_message('logging-format-interpolation', node=callfunc_node) |
| 198 |
| 199 def _check_format_string(self, node, format_arg): |
| 200 """Checks that format string tokens match the supplied arguments. |
| 201 |
| 202 Args: |
| 203 node: AST node to be checked. |
| 204 format_arg: Index of the format string in the node arguments. |
| 205 """ |
| 206 num_args = _count_supplied_tokens(node.args[format_arg + 1:]) |
| 207 if not num_args: |
| 208 # If no args were supplied, then all format strings are valid - |
| 209 # don't check any further. |
| 210 return |
| 211 format_string = node.args[format_arg].value |
| 212 if not isinstance(format_string, six.string_types): |
| 213 # If the log format is constant non-string (e.g. logging.debug(5)), |
| 214 # ensure there are no arguments. |
| 215 required_num_args = 0 |
| 216 else: |
| 217 try: |
| 218 keyword_args, required_num_args = \ |
| 219 utils.parse_format_string(format_string) |
| 220 if keyword_args: |
| 221 # Keyword checking on logging strings is complicated by |
| 222 # special keywords - out of scope. |
| 223 return |
| 224 except utils.UnsupportedFormatCharacter as ex: |
| 225 char = format_string[ex.index] |
| 226 self.add_message('logging-unsupported-format', node=node, |
| 227 args=(char, ord(char), ex.index)) |
| 228 return |
| 229 except utils.IncompleteFormatString: |
| 230 self.add_message('logging-format-truncated', node=node) |
| 231 return |
| 232 if num_args > required_num_args: |
| 233 self.add_message('logging-too-many-args', node=node) |
| 234 elif num_args < required_num_args: |
| 235 self.add_message('logging-too-few-args', node=node) |
| 236 |
| 237 |
| 238 def _count_supplied_tokens(args): |
| 239 """Counts the number of tokens in an args list. |
| 240 |
| 241 The Python log functions allow for special keyword arguments: func, |
| 242 exc_info and extra. To handle these cases correctly, we only count |
| 243 arguments that aren't keywords. |
| 244 |
| 245 Args: |
| 246 args: List of AST nodes that are arguments for a log format string. |
| 247 |
| 248 Returns: |
| 249 Number of AST nodes that aren't keywords. |
| 250 """ |
| 251 return sum(1 for arg in args if not isinstance(arg, astroid.Keyword)) |
| 252 |
| 253 |
| 254 def register(linter): |
| 255 """Required method to auto-register this checker.""" |
| 256 linter.register_checker(LoggingChecker(linter)) |
OLD | NEW |