17
17
from logilab import astng
18
18
from pylint import checkers
19
19
from pylint import interfaces
20
+ from pylint .checkers import utils
21
+
22
+
23
+ MSGS = {
24
+ 'W6501' : ('Specify string format arguments as logging function parameters' ,
25
+ 'Used when a logging statement has a call form of '
26
+ '"logging.<logging method>(format_string % (format_args...))". '
27
+ 'Such calls should leave string interpolation to the logging '
28
+ 'method itself and be written '
29
+ '"logging.<logging method>(format_string, format_args...)" '
30
+ 'so that the program may avoid incurring the cost of the '
31
+ 'interpolation in those cases in which no message will be '
32
+ 'logged. For more, see '
33
+ 'http://www.python.org/dev/peps/pep-0282/.' ),
34
+ 'E6500' : ('Unsupported logging format character %r (%#02x) at index %d' ,
35
+ 'Used when an unsupported format character is used in a logging\
36
+ statement format string.' ),
37
+ 'E6501' : ('Logging format string ends in middle of conversion specifier' ,
38
+ 'Used when a logging statement format string terminates before\
39
+ the end of a conversion specifier.' ),
40
+ 'E6505' : ('Too many arguments for logging format string' ,
41
+ 'Used when a logging format string is given too few arguments.' ),
42
+ 'E6506' : ('Not enough arguments for logging format string' ,
43
+ 'Used when a logging format string is given too many arguments' ),
44
+ }
20
45
21
- EAGER_STRING_INTERPOLATION = 'W6501'
22
46
23
47
CHECKED_CONVENIENCE_FUNCTIONS = set ([
24
48
'critical' , 'debug' , 'error' , 'exception' , 'fatal' , 'info' , 'warn' ,
@@ -29,21 +53,8 @@ class LoggingChecker(checkers.BaseChecker):
29
53
"""Checks use of the logging module."""
30
54
31
55
__implements__ = interfaces .IASTNGChecker
32
-
33
56
name = 'logging'
34
-
35
- msgs = {EAGER_STRING_INTERPOLATION :
36
- ('Specify string format arguments as logging function parameters' ,
37
- 'Used when a logging statement has a call form of '
38
- '"logging.<logging method>(format_string % (format_args...))". '
39
- 'Such calls should leave string interpolation to the logging '
40
- 'method itself and be written '
41
- '"logging.<logging method>(format_string, format_args...)" '
42
- 'so that the program may avoid incurring the cost of the '
43
- 'interpolation in those cases in which no message will be '
44
- 'logged. For more, see '
45
- 'http://www.python.org/dev/peps/pep-0282/.' )
46
- }
57
+ msgs = MSGS
47
58
48
59
def visit_module (self , unused_node ):
49
60
"""Clears any state left in this checker from last module checked."""
@@ -67,30 +78,87 @@ def visit_callfunc(self, node):
67
78
or not isinstance (node .func .expr , astng .Name )
68
79
or node .func .expr .name != self ._logging_name ):
69
80
return
70
- self ._CheckConvenienceMethods (node )
71
- self ._CheckLogMethod (node )
81
+ self ._check_convenience_methods (node )
82
+ self ._check_log_methods (node )
72
83
73
- def _CheckConvenienceMethods (self , node ):
84
+ def _check_convenience_methods (self , node ):
74
85
"""Checks calls to logging convenience methods (like logging.warn)."""
75
86
if node .func .attrname not in CHECKED_CONVENIENCE_FUNCTIONS :
76
87
return
77
- if not node .args :
78
- # Either no args, or star args, or double-star args. Beyond the
79
- # scope of this checker in any case .
88
+ if node . starargs or node . kwargs or not node .args :
89
+ # Either no args, star args, or double-star args. Beyond the
90
+ # scope of this checker.
80
91
return
81
92
if isinstance (node .args [0 ], astng .BinOp ) and node .args [0 ].op == '%' :
82
- self .add_message (EAGER_STRING_INTERPOLATION , node = node )
93
+ self .add_message ('W6501' , node = node )
94
+ elif isinstance (node .args [0 ], astng .Const ):
95
+ self ._check_format_string (node , 0 )
83
96
84
- def _CheckLogMethod (self , node ):
97
+ def _check_log_methods (self , node ):
85
98
"""Checks calls to logging.log(level, format, *format_args)."""
86
99
if node .func .attrname != 'log' :
87
100
return
88
- if len (node .args ) < 2 :
89
- # Either a malformed call or something with crazy star args or
90
- # double-star args magic. Beyond the scope of this checker.
101
+ if node . starargs or node . kwargs or len (node .args ) < 2 :
102
+ # Either a malformed call, star args, or double- star args. Beyond
103
+ # the scope of this checker.
91
104
return
92
105
if isinstance (node .args [1 ], astng .BinOp ) and node .args [1 ].op == '%' :
93
- self .add_message (EAGER_STRING_INTERPOLATION , node = node )
106
+ self .add_message ('W6501' , node = node )
107
+ elif isinstance (node .args [1 ], astng .Const ):
108
+ self ._check_format_string (node , 1 )
109
+
110
+ def _check_format_string (self , node , format_arg ):
111
+ """Checks that format string tokens match the supplied arguments.
112
+
113
+ Args:
114
+ node: AST node to be checked.
115
+ format_arg: Index of the format string in the node arguments.
116
+ """
117
+ num_args = self ._count_supplied_tokens (node .args [format_arg + 1 :])
118
+ if not num_args :
119
+ # If no args were supplied, then all format strings are valid -
120
+ # don't check any further.
121
+ return
122
+ format_string = node .args [format_arg ].value
123
+ if not isinstance (format_string , basestring ):
124
+ # If the log format is constant non-string (e.g. logging.debug(5)),
125
+ # ensure there are no arguments.
126
+ required_num_args = 0
127
+ else :
128
+ try :
129
+ keyword_args , required_num_args = \
130
+ utils .parse_format_string (format_string )
131
+ if keyword_args :
132
+ # Keyword checking on logging strings is complicated by
133
+ # special keywords - out of scope.
134
+ return
135
+ except utils .UnsupportedFormatCharacter , e :
136
+ c = format_string [e .index ]
137
+ self .add_message ('E6500' , node = node , args = (c , ord (c ), e .index ))
138
+ return
139
+ except utils .IncompleteFormatString :
140
+ self .add_message ('E6501' , node = node )
141
+ return
142
+ if num_args > required_num_args :
143
+ self .add_message ('E6505' , node = node )
144
+ elif num_args < required_num_args :
145
+ self .add_message ('E6506' , node = node )
146
+
147
+ def _count_supplied_tokens (self , args ):
148
+ """Counts the number of tokens in an args list.
149
+
150
+ The Python log functions allow for special keyword arguments: func,
151
+ exc_info and extra. To handle these cases correctly, we only count
152
+ arguments that aren't keywords.
153
+
154
+ Args:
155
+ args: List of AST nodes that are arguments for a log format string.
156
+
157
+ Returns:
158
+ Number of AST nodes that aren't keywords.
159
+ """
160
+ return sum (1 for arg in args if not isinstance (arg , astng .Keyword ))
161
+
94
162
95
163
def register (linter ):
96
164
"""Required method to auto-register this checker."""
0 commit comments