Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / pylint / checkers / logging.py @ 745

History | View | Annotate | Download (10.6 KB)

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 six
18

    
19
import astroid
20

    
21
from pylint import checkers
22
from pylint import interfaces
23
from pylint.checkers import utils
24
from pylint.checkers.utils import check_messages
25

    
26

    
27

    
28
MSGS = {
29
    'W1201': ('Specify string format arguments as logging function parameters',
30
              'logging-not-lazy',
31
              'Used when a logging statement has a call form of '
32
              '"logging.<logging method>(format_string % (format_args...))". '
33
              'Such calls should leave string interpolation to the logging '
34
              'method itself and be written '
35
              '"logging.<logging method>(format_string, format_args...)" '
36
              'so that the program may avoid incurring the cost of the '
37
              'interpolation in those cases in which no message will be '
38
              'logged. For more, see '
39
              'http://www.python.org/dev/peps/pep-0282/.'),
40
    'W1202': ('Use % formatting in logging functions and pass the % '
41
              'parameters as arguments',
42
              'logging-format-interpolation',
43
              'Used when a logging statement has a call form of '
44
              '"logging.<logging method>(format_string.format(format_args...))"'
45
              '. Such calls should use % formatting instead, but leave '
46
              'interpolation to the logging function by passing the parameters '
47
              'as arguments.'),
48
    'E1200': ('Unsupported logging format character %r (%#02x) at index %d',
49
              'logging-unsupported-format',
50
              'Used when an unsupported format character is used in a logging\
51
              statement format string.'),
52
    'E1201': ('Logging format string ends in middle of conversion specifier',
53
              'logging-format-truncated',
54
              'Used when a logging statement format string terminates before\
55
              the end of a conversion specifier.'),
56
    'E1205': ('Too many arguments for logging format string',
57
              'logging-too-many-args',
58
              'Used when a logging format string is given too few arguments.'),
59
    'E1206': ('Not enough arguments for logging format string',
60
              'logging-too-few-args',
61
              'Used when a logging format string is given too many arguments'),
62
    }
63

    
64

    
65
CHECKED_CONVENIENCE_FUNCTIONS = set([
66
    'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn',
67
    'warning'])
68

    
69
def is_method_call(callfunc_node, types=(), methods=()):
70
    """Determines if a CallFunc node represents a method call.
71

72
    Args:
73
      callfunc_node: The CallFunc AST node to check.
74
      types: Optional sequence of caller type names to restrict check.
75
      methods: Optional sequence of method names to restrict check.
76

77
    Returns:
78
      True, if the node represents a method call for the given type and
79
      method names, False otherwise.
80
    """
81
    if not isinstance(callfunc_node, astroid.Call):
82
        return False
83
    func = utils.safe_infer(callfunc_node.func)
84
    return (isinstance(func, astroid.BoundMethod)
85
            and isinstance(func.bound, astroid.Instance)
86
            and (func.bound.name in types if types else True)
87
            and (func.name in methods if methods else True))
88

    
89

    
90

    
91
class LoggingChecker(checkers.BaseChecker):
92
    """Checks use of the logging module."""
93

    
94
    __implements__ = interfaces.IAstroidChecker
95
    name = 'logging'
96
    msgs = MSGS
97

    
98
    options = (('logging-modules',
99
                {'default': ('logging',),
100
                 'type': 'csv',
101
                 'metavar': '<comma separated list>',
102
                 'help': 'Logging modules to check that the string format '
103
                         'arguments are in logging function parameter format'}
104
               ),
105
              )
106

    
107
    def visit_module(self, node): # pylint: disable=unused-argument
108
        """Clears any state left in this checker from last module checked."""
109
        # The code being checked can just as easily "import logging as foo",
110
        # so it is necessary to process the imports and store in this field
111
        # what name the logging module is actually given.
112
        self._logging_names = set()
113
        logging_mods = self.config.logging_modules
114

    
115
        self._logging_modules = set(logging_mods)
116
        self._from_imports = {}
117
        for logging_mod in logging_mods:
118
            parts = logging_mod.rsplit('.', 1)
119
            if len(parts) > 1:
120
                self._from_imports[parts[0]] = parts[1]
121

    
122
    def visit_importfrom(self, node):
123
        """Checks to see if a module uses a non-Python logging module."""
124
        try:
125
            logging_name = self._from_imports[node.modname]
126
            for module, as_name in node.names:
127
                if module == logging_name:
128
                    self._logging_names.add(as_name or module)
129
        except KeyError:
130
            pass
131

    
132
    def visit_import(self, node):
133
        """Checks to see if this module uses Python's built-in logging."""
134
        for module, as_name in node.names:
135
            if module in self._logging_modules:
136
                self._logging_names.add(as_name or module)
137

    
138
    @check_messages(*(MSGS.keys()))
139
    def visit_call(self, node):
140
        """Checks calls to logging methods."""
141
        def is_logging_name():
142
            return (isinstance(node.func, astroid.Attribute) and
143
                    isinstance(node.func.expr, astroid.Name) and
144
                    node.func.expr.name in self._logging_names)
145

    
146
        def is_logger_class():
147
            try:
148
                for inferred in node.func.infer():
149
                    if isinstance(inferred, astroid.BoundMethod):
150
                        parent = inferred._proxied.parent
151
                        if (isinstance(parent, astroid.ClassDef) and
152
                                (parent.qname() == 'logging.Logger' or
153
                                 any(ancestor.qname() == 'logging.Logger'
154
                                     for ancestor in parent.ancestors()))):
155
                            return True, inferred._proxied.name
156
            except astroid.exceptions.InferenceError:
157
                pass
158
            return False, None
159

    
160
        if is_logging_name():
161
            name = node.func.attrname
162
        else:
163
            result, name = is_logger_class()
164
            if not result:
165
                return
166
        self._check_log_method(node, name)
167

    
168
    def _check_log_method(self, node, name):
169
        """Checks calls to logging.log(level, format, *format_args)."""
170
        if name == 'log':
171
            if node.starargs or node.kwargs or len(node.args) < 2:
172
                # Either a malformed call, star args, or double-star args. Beyond
173
                # the scope of this checker.
174
                return
175
            format_pos = 1
176
        elif name in CHECKED_CONVENIENCE_FUNCTIONS:
177
            if node.starargs or node.kwargs or not node.args:
178
                # Either no args, star args, or double-star args. Beyond the
179
                # scope of this checker.
180
                return
181
            format_pos = 0
182
        else:
183
            return
184

    
185
        if isinstance(node.args[format_pos], astroid.BinOp) and node.args[format_pos].op == '%':
186
            self.add_message('logging-not-lazy', node=node)
187
        elif isinstance(node.args[format_pos], astroid.Call):
188
            self._check_call_func(node.args[format_pos])
189
        elif isinstance(node.args[format_pos], astroid.Const):
190
            self._check_format_string(node, format_pos)
191

    
192
    def _check_call_func(self, callfunc_node):
193
        """Checks that function call is not format_string.format().
194

195
        Args:
196
          callfunc_node: CallFunc AST node to be checked.
197
        """
198
        if is_method_call(callfunc_node, ('str', 'unicode'), ('format',)):
199
            self.add_message('logging-format-interpolation', node=callfunc_node)
200

    
201
    def _check_format_string(self, node, format_arg):
202
        """Checks that format string tokens match the supplied arguments.
203

204
        Args:
205
          node: AST node to be checked.
206
          format_arg: Index of the format string in the node arguments.
207
        """
208
        num_args = _count_supplied_tokens(node.args[format_arg + 1:])
209
        if not num_args:
210
            # If no args were supplied, then all format strings are valid -
211
            # don't check any further.
212
            return
213
        format_string = node.args[format_arg].value
214
        if not isinstance(format_string, six.string_types):
215
            # If the log format is constant non-string (e.g. logging.debug(5)),
216
            # ensure there are no arguments.
217
            required_num_args = 0
218
        else:
219
            try:
220
                keyword_args, required_num_args = \
221
                    utils.parse_format_string(format_string)
222
                if keyword_args:
223
                    # Keyword checking on logging strings is complicated by
224
                    # special keywords - out of scope.
225
                    return
226
            except utils.UnsupportedFormatCharacter as ex:
227
                char = format_string[ex.index]
228
                self.add_message('logging-unsupported-format', node=node,
229
                                 args=(char, ord(char), ex.index))
230
                return
231
            except utils.IncompleteFormatString:
232
                self.add_message('logging-format-truncated', node=node)
233
                return
234
        if num_args > required_num_args:
235
            self.add_message('logging-too-many-args', node=node)
236
        elif num_args < required_num_args:
237
            self.add_message('logging-too-few-args', node=node)
238

    
239

    
240
def _count_supplied_tokens(args):
241
    """Counts the number of tokens in an args list.
242

243
    The Python log functions allow for special keyword arguments: func,
244
    exc_info and extra. To handle these cases correctly, we only count
245
    arguments that aren't keywords.
246

247
    Args:
248
      args: List of AST nodes that are arguments for a log format string.
249

250
    Returns:
251
      Number of AST nodes that aren't keywords.
252
    """
253
    return sum(1 for arg in args if not isinstance(arg, astroid.Keyword))
254

    
255

    
256
def register(linter):
257
    """Required method to auto-register this checker."""
258
    linter.register_checker(LoggingChecker(linter))