OLD | NEW |
1 # Copyright (c) 2009-2010 Google, Inc. | 1 # Copyright (c) 2009-2010 Google, Inc. |
2 # This program is free software; you can redistribute it and/or modify it under | 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 | 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 | 4 # Foundation; either version 2 of the License, or (at your option) any later |
5 # version. | 5 # version. |
6 # | 6 # |
7 # This program is distributed in the hope that it will be useful, but WITHOUT | 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 | 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. | 9 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
10 # | 10 # |
11 # You should have received a copy of the GNU General Public License along with | 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., | 12 # this program; if not, write to the Free Software Foundation, Inc., |
13 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | 13 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
14 """checker for use of Python logging | 14 """checker for use of Python logging |
15 """ | 15 """ |
16 | 16 |
17 import astroid | 17 import astroid |
18 from pylint import checkers | 18 from pylint import checkers |
19 from pylint import interfaces | 19 from pylint import interfaces |
20 from pylint.checkers import utils | 20 from pylint.checkers import utils |
21 from pylint.checkers.utils import check_messages | 21 from pylint.checkers.utils import check_messages |
22 | 22 |
| 23 import six |
| 24 |
| 25 |
23 MSGS = { | 26 MSGS = { |
24 'W1201': ('Specify string format arguments as logging function parameters', | 27 'W1201': ('Specify string format arguments as logging function parameters', |
25 'logging-not-lazy', | 28 'logging-not-lazy', |
26 'Used when a logging statement has a call form of ' | 29 'Used when a logging statement has a call form of ' |
27 '"logging.<logging method>(format_string % (format_args...))". ' | 30 '"logging.<logging method>(format_string % (format_args...))". ' |
28 'Such calls should leave string interpolation to the logging ' | 31 'Such calls should leave string interpolation to the logging ' |
29 'method itself and be written ' | 32 'method itself and be written ' |
30 '"logging.<logging method>(format_string, format_args...)" ' | 33 '"logging.<logging method>(format_string, format_args...)" ' |
31 'so that the program may avoid incurring the cost of the ' | 34 'so that the program may avoid incurring the cost of the ' |
32 'interpolation in those cases in which no message will be ' | 35 'interpolation in those cases in which no message will be ' |
33 'logged. For more, see ' | 36 'logged. For more, see ' |
34 'http://www.python.org/dev/peps/pep-0282/.'), | 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.'), |
35 'E1200': ('Unsupported logging format character %r (%#02x) at index %d', | 46 'E1200': ('Unsupported logging format character %r (%#02x) at index %d', |
36 'logging-unsupported-format', | 47 'logging-unsupported-format', |
37 'Used when an unsupported format character is used in a logging\ | 48 'Used when an unsupported format character is used in a logging\ |
38 statement format string.'), | 49 statement format string.'), |
39 'E1201': ('Logging format string ends in middle of conversion specifier', | 50 'E1201': ('Logging format string ends in middle of conversion specifier', |
40 'logging-format-truncated', | 51 'logging-format-truncated', |
41 'Used when a logging statement format string terminates before\ | 52 'Used when a logging statement format string terminates before\ |
42 the end of a conversion specifier.'), | 53 the end of a conversion specifier.'), |
43 'E1205': ('Too many arguments for logging format string', | 54 'E1205': ('Too many arguments for logging format string', |
44 'logging-too-many-args', | 55 'logging-too-many-args', |
45 'Used when a logging format string is given too few arguments.'), | 56 'Used when a logging format string is given too few arguments.'), |
46 'E1206': ('Not enough arguments for logging format string', | 57 'E1206': ('Not enough arguments for logging format string', |
47 'logging-too-few-args', | 58 'logging-too-few-args', |
48 'Used when a logging format string is given too many arguments'), | 59 'Used when a logging format string is given too many arguments'), |
49 } | 60 } |
50 | 61 |
51 | 62 |
52 CHECKED_CONVENIENCE_FUNCTIONS = set([ | 63 CHECKED_CONVENIENCE_FUNCTIONS = set([ |
53 'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn', | 64 'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn', |
54 'warning']) | 65 'warning']) |
55 | 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 |
56 | 88 |
57 class LoggingChecker(checkers.BaseChecker): | 89 class LoggingChecker(checkers.BaseChecker): |
58 """Checks use of the logging module.""" | 90 """Checks use of the logging module.""" |
59 | 91 |
60 __implements__ = interfaces.IAstroidChecker | 92 __implements__ = interfaces.IAstroidChecker |
61 name = 'logging' | 93 name = 'logging' |
62 msgs = MSGS | 94 msgs = MSGS |
63 | 95 |
64 options = (('logging-modules', | 96 options = (('logging-modules', |
65 {'default' : ('logging',), | 97 {'default': ('logging',), |
66 'type' : 'csv', | 98 'type': 'csv', |
67 'metavar' : '<comma separated list>', | 99 'metavar': '<comma separated list>', |
68 'help' : 'Logging modules to check that the string format ' | 100 'help': 'Logging modules to check that the string format ' |
69 'arguments are in logging function parameter format'} | 101 'arguments are in logging function parameter format'} |
70 ), | 102 ), |
71 ) | 103 ) |
72 | 104 |
73 def visit_module(self, unused_node): | 105 def visit_module(self, node): # pylint: disable=unused-argument |
74 """Clears any state left in this checker from last module checked.""" | 106 """Clears any state left in this checker from last module checked.""" |
75 # The code being checked can just as easily "import logging as foo", | 107 # The code being checked can just as easily "import logging as foo", |
76 # so it is necessary to process the imports and store in this field | 108 # so it is necessary to process the imports and store in this field |
77 # what name the logging module is actually given. | 109 # what name the logging module is actually given. |
78 self._logging_names = set() | 110 self._logging_names = set() |
79 logging_mods = self.config.logging_modules | 111 logging_mods = self.config.logging_modules |
80 | 112 |
81 self._logging_modules = set(logging_mods) | 113 self._logging_modules = set(logging_mods) |
82 self._from_imports = {} | 114 self._from_imports = {} |
83 for logging_mod in logging_mods: | 115 for logging_mod in logging_mods: |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
143 if node.starargs or node.kwargs or not node.args: | 175 if node.starargs or node.kwargs or not node.args: |
144 # Either no args, star args, or double-star args. Beyond the | 176 # Either no args, star args, or double-star args. Beyond the |
145 # scope of this checker. | 177 # scope of this checker. |
146 return | 178 return |
147 format_pos = 0 | 179 format_pos = 0 |
148 else: | 180 else: |
149 return | 181 return |
150 | 182 |
151 if isinstance(node.args[format_pos], astroid.BinOp) and node.args[format
_pos].op == '%': | 183 if isinstance(node.args[format_pos], astroid.BinOp) and node.args[format
_pos].op == '%': |
152 self.add_message('logging-not-lazy', node=node) | 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]) |
153 elif isinstance(node.args[format_pos], astroid.Const): | 187 elif isinstance(node.args[format_pos], astroid.Const): |
154 self._check_format_string(node, format_pos) | 188 self._check_format_string(node, format_pos) |
155 | 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 |
156 def _check_format_string(self, node, format_arg): | 199 def _check_format_string(self, node, format_arg): |
157 """Checks that format string tokens match the supplied arguments. | 200 """Checks that format string tokens match the supplied arguments. |
158 | 201 |
159 Args: | 202 Args: |
160 node: AST node to be checked. | 203 node: AST node to be checked. |
161 format_arg: Index of the format string in the node arguments. | 204 format_arg: Index of the format string in the node arguments. |
162 """ | 205 """ |
163 num_args = _count_supplied_tokens(node.args[format_arg + 1:]) | 206 num_args = _count_supplied_tokens(node.args[format_arg + 1:]) |
164 if not num_args: | 207 if not num_args: |
165 # If no args were supplied, then all format strings are valid - | 208 # If no args were supplied, then all format strings are valid - |
166 # don't check any further. | 209 # don't check any further. |
167 return | 210 return |
168 format_string = node.args[format_arg].value | 211 format_string = node.args[format_arg].value |
169 if not isinstance(format_string, basestring): | 212 if not isinstance(format_string, six.string_types): |
170 # If the log format is constant non-string (e.g. logging.debug(5)), | 213 # If the log format is constant non-string (e.g. logging.debug(5)), |
171 # ensure there are no arguments. | 214 # ensure there are no arguments. |
172 required_num_args = 0 | 215 required_num_args = 0 |
173 else: | 216 else: |
174 try: | 217 try: |
175 keyword_args, required_num_args = \ | 218 keyword_args, required_num_args = \ |
176 utils.parse_format_string(format_string) | 219 utils.parse_format_string(format_string) |
177 if keyword_args: | 220 if keyword_args: |
178 # Keyword checking on logging strings is complicated by | 221 # Keyword checking on logging strings is complicated by |
179 # special keywords - out of scope. | 222 # special keywords - out of scope. |
180 return | 223 return |
181 except utils.UnsupportedFormatCharacter, ex: | 224 except utils.UnsupportedFormatCharacter as ex: |
182 char = format_string[ex.index] | 225 char = format_string[ex.index] |
183 self.add_message('logging-unsupported-format', node=node, | 226 self.add_message('logging-unsupported-format', node=node, |
184 args=(char, ord(char), ex.index)) | 227 args=(char, ord(char), ex.index)) |
185 return | 228 return |
186 except utils.IncompleteFormatString: | 229 except utils.IncompleteFormatString: |
187 self.add_message('logging-format-truncated', node=node) | 230 self.add_message('logging-format-truncated', node=node) |
188 return | 231 return |
189 if num_args > required_num_args: | 232 if num_args > required_num_args: |
190 self.add_message('logging-too-many-args', node=node) | 233 self.add_message('logging-too-many-args', node=node) |
191 elif num_args < required_num_args: | 234 elif num_args < required_num_args: |
(...skipping 12 matching lines...) Expand all Loading... |
204 | 247 |
205 Returns: | 248 Returns: |
206 Number of AST nodes that aren't keywords. | 249 Number of AST nodes that aren't keywords. |
207 """ | 250 """ |
208 return sum(1 for arg in args if not isinstance(arg, astroid.Keyword)) | 251 return sum(1 for arg in args if not isinstance(arg, astroid.Keyword)) |
209 | 252 |
210 | 253 |
211 def register(linter): | 254 def register(linter): |
212 """Required method to auto-register this checker.""" | 255 """Required method to auto-register this checker.""" |
213 linter.register_checker(LoggingChecker(linter)) | 256 linter.register_checker(LoggingChecker(linter)) |
OLD | NEW |