Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

logging.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2009-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
  3. # Copyright (c) 2009, 2012, 2014 Google, Inc.
  4. # Copyright (c) 2012 Mike Bryant <leachim@leachim.info>
  5. # Copyright (c) 2014 Brett Cannon <brett@python.org>
  6. # Copyright (c) 2014 Arun Persaud <arun@nubati.net>
  7. # Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
  8. # Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
  9. # Copyright (c) 2016 Chris Murray <chris@chrismurray.scot>
  10. # Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
  11. # Copyright (c) 2017 guillaume2 <guillaume.peillex@gmail.col>
  12. # Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
  13. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  14. # For details: https://github.com/PyCQA/pylint/blob/master/COPYING
  15. """checker for use of Python logging
  16. """
  17. import string
  18. import six
  19. import astroid
  20. from pylint import checkers
  21. from pylint import interfaces
  22. from pylint.checkers import utils
  23. from pylint.checkers.utils import check_messages
  24. MSGS = {
  25. 'W1201': ('Specify string format arguments as logging function parameters',
  26. 'logging-not-lazy',
  27. 'Used when a logging statement has a call form of '
  28. '"logging.<logging method>(format_string % (format_args...))". '
  29. 'Such calls should leave string interpolation to the logging '
  30. 'method itself and be written '
  31. '"logging.<logging method>(format_string, format_args...)" '
  32. 'so that the program may avoid incurring the cost of the '
  33. 'interpolation in those cases in which no message will be '
  34. 'logged. For more, see '
  35. 'http://www.python.org/dev/peps/pep-0282/.'),
  36. 'W1202': ('Use % formatting in logging functions and pass the % '
  37. 'parameters as arguments',
  38. 'logging-format-interpolation',
  39. 'Used when a logging statement has a call form of '
  40. '"logging.<logging method>(format_string.format(format_args...))"'
  41. '. Such calls should use % formatting instead, but leave '
  42. 'interpolation to the logging function by passing the parameters '
  43. 'as arguments.'),
  44. 'E1200': ('Unsupported logging format character %r (%#02x) at index %d',
  45. 'logging-unsupported-format',
  46. 'Used when an unsupported format character is used in a logging\
  47. statement format string.'),
  48. 'E1201': ('Logging format string ends in middle of conversion specifier',
  49. 'logging-format-truncated',
  50. 'Used when a logging statement format string terminates before\
  51. the end of a conversion specifier.'),
  52. 'E1205': ('Too many arguments for logging format string',
  53. 'logging-too-many-args',
  54. 'Used when a logging format string is given too many arguments.'),
  55. 'E1206': ('Not enough arguments for logging format string',
  56. 'logging-too-few-args',
  57. 'Used when a logging format string is given too few arguments.'),
  58. }
  59. CHECKED_CONVENIENCE_FUNCTIONS = {
  60. 'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn', 'warning'
  61. }
  62. def is_method_call(func, types=(), methods=()):
  63. """Determines if a BoundMethod node represents a method call.
  64. Args:
  65. func (astroid.BoundMethod): The BoundMethod AST node to check.
  66. types (Optional[String]): Optional sequence of caller type names to restrict check.
  67. methods (Optional[String]): Optional sequence of method names to restrict check.
  68. Returns:
  69. bool: true if the node represents a method call for the given type and
  70. method names, False otherwise.
  71. """
  72. return (isinstance(func, astroid.BoundMethod)
  73. and isinstance(func.bound, astroid.Instance)
  74. and (func.bound.name in types if types else True)
  75. and (func.name in methods if methods else True))
  76. class LoggingChecker(checkers.BaseChecker):
  77. """Checks use of the logging module."""
  78. __implements__ = interfaces.IAstroidChecker
  79. name = 'logging'
  80. msgs = MSGS
  81. options = (('logging-modules',
  82. {'default': ('logging',),
  83. 'type': 'csv',
  84. 'metavar': '<comma separated list>',
  85. 'help': 'Logging modules to check that the string format '
  86. 'arguments are in logging function parameter format'}
  87. ),
  88. )
  89. def visit_module(self, node): # pylint: disable=unused-argument
  90. """Clears any state left in this checker from last module checked."""
  91. # The code being checked can just as easily "import logging as foo",
  92. # so it is necessary to process the imports and store in this field
  93. # what name the logging module is actually given.
  94. self._logging_names = set()
  95. logging_mods = self.config.logging_modules
  96. self._logging_modules = set(logging_mods)
  97. self._from_imports = {}
  98. for logging_mod in logging_mods:
  99. parts = logging_mod.rsplit('.', 1)
  100. if len(parts) > 1:
  101. self._from_imports[parts[0]] = parts[1]
  102. def visit_importfrom(self, node):
  103. """Checks to see if a module uses a non-Python logging module."""
  104. try:
  105. logging_name = self._from_imports[node.modname]
  106. for module, as_name in node.names:
  107. if module == logging_name:
  108. self._logging_names.add(as_name or module)
  109. except KeyError:
  110. pass
  111. def visit_import(self, node):
  112. """Checks to see if this module uses Python's built-in logging."""
  113. for module, as_name in node.names:
  114. if module in self._logging_modules:
  115. self._logging_names.add(as_name or module)
  116. @check_messages(*(MSGS.keys()))
  117. def visit_call(self, node):
  118. """Checks calls to logging methods."""
  119. def is_logging_name():
  120. return (isinstance(node.func, astroid.Attribute) and
  121. isinstance(node.func.expr, astroid.Name) and
  122. node.func.expr.name in self._logging_names)
  123. def is_logger_class():
  124. try:
  125. for inferred in node.func.infer():
  126. if isinstance(inferred, astroid.BoundMethod):
  127. parent = inferred._proxied.parent
  128. if (isinstance(parent, astroid.ClassDef) and
  129. (parent.qname() == 'logging.Logger' or
  130. any(ancestor.qname() == 'logging.Logger'
  131. for ancestor in parent.ancestors()))):
  132. return True, inferred._proxied.name
  133. except astroid.exceptions.InferenceError:
  134. pass
  135. return False, None
  136. if is_logging_name():
  137. name = node.func.attrname
  138. else:
  139. result, name = is_logger_class()
  140. if not result:
  141. return
  142. self._check_log_method(node, name)
  143. def _check_log_method(self, node, name):
  144. """Checks calls to logging.log(level, format, *format_args)."""
  145. if name == 'log':
  146. if node.starargs or node.kwargs or len(node.args) < 2:
  147. # Either a malformed call, star args, or double-star args. Beyond
  148. # the scope of this checker.
  149. return
  150. format_pos = 1
  151. elif name in CHECKED_CONVENIENCE_FUNCTIONS:
  152. if node.starargs or node.kwargs or not node.args:
  153. # Either no args, star args, or double-star args. Beyond the
  154. # scope of this checker.
  155. return
  156. format_pos = 0
  157. else:
  158. return
  159. if isinstance(node.args[format_pos], astroid.BinOp):
  160. binop = node.args[format_pos]
  161. if (binop.op == '%' or binop.op == '+' and
  162. len([_operand for _operand in (binop.left, binop.right)
  163. if self._is_operand_literal_str(_operand)]) == 1):
  164. self.add_message('logging-not-lazy', node=node)
  165. elif isinstance(node.args[format_pos], astroid.Call):
  166. self._check_call_func(node.args[format_pos])
  167. elif isinstance(node.args[format_pos], astroid.Const):
  168. self._check_format_string(node, format_pos)
  169. @staticmethod
  170. def _is_operand_literal_str(operand):
  171. """
  172. Return True if the operand in argument is a literal string
  173. """
  174. return isinstance(operand, astroid.Const) and operand.name == 'str'
  175. def _check_call_func(self, node):
  176. """Checks that function call is not format_string.format().
  177. Args:
  178. node (astroid.node_classes.Call):
  179. Call AST node to be checked.
  180. """
  181. func = utils.safe_infer(node.func)
  182. types = ('str', 'unicode')
  183. methods = ('format',)
  184. if is_method_call(func, types, methods) and not is_complex_format_str(func.bound):
  185. self.add_message('logging-format-interpolation', node=node)
  186. def _check_format_string(self, node, format_arg):
  187. """Checks that format string tokens match the supplied arguments.
  188. Args:
  189. node (astroid.node_classes.NodeNG): AST node to be checked.
  190. format_arg (int): Index of the format string in the node arguments.
  191. """
  192. num_args = _count_supplied_tokens(node.args[format_arg + 1:])
  193. if not num_args:
  194. # If no args were supplied, then all format strings are valid -
  195. # don't check any further.
  196. return
  197. format_string = node.args[format_arg].value
  198. if not isinstance(format_string, six.string_types):
  199. # If the log format is constant non-string (e.g. logging.debug(5)),
  200. # ensure there are no arguments.
  201. required_num_args = 0
  202. else:
  203. try:
  204. keyword_args, required_num_args = \
  205. utils.parse_format_string(format_string)
  206. if keyword_args:
  207. # Keyword checking on logging strings is complicated by
  208. # special keywords - out of scope.
  209. return
  210. except utils.UnsupportedFormatCharacter as ex:
  211. char = format_string[ex.index]
  212. self.add_message('logging-unsupported-format', node=node,
  213. args=(char, ord(char), ex.index))
  214. return
  215. except utils.IncompleteFormatString:
  216. self.add_message('logging-format-truncated', node=node)
  217. return
  218. if num_args > required_num_args:
  219. self.add_message('logging-too-many-args', node=node)
  220. elif num_args < required_num_args:
  221. self.add_message('logging-too-few-args', node=node)
  222. def is_complex_format_str(node):
  223. """Checks if node represents a string with complex formatting specs.
  224. Args:
  225. node (astroid.node_classes.NodeNG): AST node to check
  226. Returns:
  227. bool: True if inferred string uses complex formatting, False otherwise
  228. """
  229. inferred = utils.safe_infer(node)
  230. if inferred is None or not isinstance(inferred.value, six.string_types):
  231. return True
  232. try:
  233. parsed = list(string.Formatter().parse(inferred.value))
  234. except ValueError:
  235. # This format string is invalid
  236. return False
  237. for _, _, format_spec, _ in parsed:
  238. if format_spec:
  239. return True
  240. return False
  241. def _count_supplied_tokens(args):
  242. """Counts the number of tokens in an args list.
  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. Args:
  247. args (list): AST nodes that are arguments for a log format string.
  248. Returns:
  249. int: Number of AST nodes that aren't keywords.
  250. """
  251. return sum(1 for arg in args if not isinstance(arg, astroid.Keyword))
  252. def register(linter):
  253. """Required method to auto-register this checker."""
  254. linter.register_checker(LoggingChecker(linter))