summaryrefslogtreecommitdiff
path: root/checkers/logging.py
blob: 6986ca4eb9a0b14911f5e94131b620cb596f7bbc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# 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.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307, 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

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/.'),
    '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'])


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

    __implements__ = interfaces.IAstroidChecker
    name = 'logging'
    msgs = MSGS

    def visit_module(self, unused_node):
        """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_name = None

    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 == 'logging':
                if as_name:
                    self._logging_name = as_name
                else:
                    self._logging_name = 'logging'

    @check_messages(*(MSGS.keys()))
    def visit_callfunc(self, node):
        """Checks calls to (simple forms of) logging methods."""
        if (not isinstance(node.func, astroid.Getattr)
            or not isinstance(node.func.expr, astroid.Name)):
            return
        try:
            logger_class = [inferred for inferred in node.func.expr.infer() if (
                isinstance(inferred, astroid.Instance)
                and any(ancestor for ancestor in inferred._proxied.ancestors() if (
                            ancestor.name == 'Logger'
                            and ancestor.parent.name == 'logging')))]
        except astroid.exceptions.InferenceError:
            return
        if (node.func.expr.name != self._logging_name and not logger_class):
            return
        self._check_convenience_methods(node)
        self._check_log_methods(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 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], astroid.BinOp) and node.args[0].op == '%':
            self.add_message('W1201', node=node)
        elif isinstance(node.args[0], astroid.Const):
            self._check_format_string(node, 0)

    def _check_log_methods(self, node):
        """Checks calls to logging.log(level, format, *format_args)."""
        if node.func.attrname != 'log':
            return
        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], astroid.BinOp) and node.args[1].op == '%':
            self.add_message('W1201', node=node)
        elif isinstance(node.args[1], astroid.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('E1200', node=node, args=(c, ord(c), e.index))
                return
            except utils.IncompleteFormatString:
                self.add_message('E1201', node=node)
                return
        if num_args > required_num_args:
            self.add_message('E1205', node=node)
        elif num_args < required_num_args:
            self.add_message('E1206', 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, astroid.Keyword))


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