logging.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. # Copyright (c) 2009-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
  2. # Copyright (c) 2009, 2012, 2014 Google, Inc.
  3. # Copyright (c) 2012 Mike Bryant <leachim@leachim.info>
  4. # Copyright (c) 2014 Brett Cannon <brett@python.org>
  5. # Copyright (c) 2014 Arun Persaud <arun@nubati.net>
  6. # Copyright (c) 2015-2020 Claudiu Popa <pcmanticore@gmail.com>
  7. # Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
  8. # Copyright (c) 2016, 2019-2020 Ashley Whetter <ashley@awhetter.co.uk>
  9. # Copyright (c) 2016 Chris Murray <chris@chrismurray.scot>
  10. # Copyright (c) 2017 guillaume2 <guillaume.peillex@gmail.col>
  11. # Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
  12. # Copyright (c) 2018 Alan Chan <achan961117@gmail.com>
  13. # Copyright (c) 2018 Yury Gribov <tetra2005@gmail.com>
  14. # Copyright (c) 2018 Mike Frysinger <vapier@gmail.com>
  15. # Copyright (c) 2018 Mariatta Wijaya <mariatta@python.org>
  16. # Copyright (c) 2019-2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
  17. # Copyright (c) 2019 Djailla <bastien.vallet@gmail.com>
  18. # Copyright (c) 2019 Svet <svet@hyperscience.com>
  19. # Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
  20. # Copyright (c) 2021 Nick Drozd <nicholasdrozd@gmail.com>
  21. # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
  22. # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  23. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  24. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  25. """checker for use of Python logging
  26. """
  27. import string
  28. from typing import Set
  29. import astroid
  30. from astroid import nodes
  31. from pylint import checkers, interfaces
  32. from pylint.checkers import utils
  33. from pylint.checkers.utils import check_messages, infer_all
  34. MSGS = { # pylint: disable=consider-using-namedtuple-or-dataclass
  35. "W1201": (
  36. "Use %s formatting in logging functions",
  37. "logging-not-lazy",
  38. "Used when a logging statement has a call form of "
  39. '"logging.<logging method>(format_string % (format_args...))". '
  40. "Use another type of string formatting instead. "
  41. "You can use % formatting but leave interpolation to "
  42. "the logging function by passing the parameters as arguments. "
  43. "If logging-fstring-interpolation is disabled then "
  44. "you can use fstring formatting. "
  45. "If logging-format-interpolation is disabled then "
  46. "you can use str.format.",
  47. ),
  48. "W1202": (
  49. "Use %s formatting in logging functions",
  50. "logging-format-interpolation",
  51. "Used when a logging statement has a call form of "
  52. '"logging.<logging method>(format_string.format(format_args...))". '
  53. "Use another type of string formatting instead. "
  54. "You can use % formatting but leave interpolation to "
  55. "the logging function by passing the parameters as arguments. "
  56. "If logging-fstring-interpolation is disabled then "
  57. "you can use fstring formatting. "
  58. "If logging-not-lazy is disabled then "
  59. "you can use % formatting as normal.",
  60. ),
  61. "W1203": (
  62. "Use %s formatting in logging functions",
  63. "logging-fstring-interpolation",
  64. "Used when a logging statement has a call form of "
  65. '"logging.<logging method>(f"...")".'
  66. "Use another type of string formatting instead. "
  67. "You can use % formatting but leave interpolation to "
  68. "the logging function by passing the parameters as arguments. "
  69. "If logging-format-interpolation is disabled then "
  70. "you can use str.format. "
  71. "If logging-not-lazy is disabled then "
  72. "you can use % formatting as normal.",
  73. ),
  74. "E1200": (
  75. "Unsupported logging format character %r (%#02x) at index %d",
  76. "logging-unsupported-format",
  77. "Used when an unsupported format character is used in a logging "
  78. "statement format string.",
  79. ),
  80. "E1201": (
  81. "Logging format string ends in middle of conversion specifier",
  82. "logging-format-truncated",
  83. "Used when a logging statement format string terminates before "
  84. "the end of a conversion specifier.",
  85. ),
  86. "E1205": (
  87. "Too many arguments for logging format string",
  88. "logging-too-many-args",
  89. "Used when a logging format string is given too many arguments.",
  90. ),
  91. "E1206": (
  92. "Not enough arguments for logging format string",
  93. "logging-too-few-args",
  94. "Used when a logging format string is given too few arguments.",
  95. ),
  96. }
  97. CHECKED_CONVENIENCE_FUNCTIONS = {
  98. "critical",
  99. "debug",
  100. "error",
  101. "exception",
  102. "fatal",
  103. "info",
  104. "warn",
  105. "warning",
  106. }
  107. def is_method_call(func, types=(), methods=()):
  108. """Determines if a BoundMethod node represents a method call.
  109. Args:
  110. func (astroid.BoundMethod): The BoundMethod AST node to check.
  111. types (Optional[String]): Optional sequence of caller type names to restrict check.
  112. methods (Optional[String]): Optional sequence of method names to restrict check.
  113. Returns:
  114. bool: true if the node represents a method call for the given type and
  115. method names, False otherwise.
  116. """
  117. return (
  118. isinstance(func, astroid.BoundMethod)
  119. and isinstance(func.bound, astroid.Instance)
  120. and (func.bound.name in types if types else True)
  121. and (func.name in methods if methods else True)
  122. )
  123. class LoggingChecker(checkers.BaseChecker):
  124. """Checks use of the logging module."""
  125. __implements__ = interfaces.IAstroidChecker
  126. name = "logging"
  127. msgs = MSGS
  128. options = (
  129. (
  130. "logging-modules",
  131. {
  132. "default": ("logging",),
  133. "type": "csv",
  134. "metavar": "<comma separated list>",
  135. "help": "Logging modules to check that the string format "
  136. "arguments are in logging function parameter format.",
  137. },
  138. ),
  139. (
  140. "logging-format-style",
  141. {
  142. "default": "old",
  143. "type": "choice",
  144. "metavar": "<old (%) or new ({)>",
  145. "choices": ["old", "new"],
  146. "help": "The type of string formatting that logging methods do. "
  147. "`old` means using % formatting, `new` is for `{}` formatting.",
  148. },
  149. ),
  150. )
  151. def visit_module(self, _: nodes.Module) -> None:
  152. """Clears any state left in this checker from last module checked."""
  153. # The code being checked can just as easily "import logging as foo",
  154. # so it is necessary to process the imports and store in this field
  155. # what name the logging module is actually given.
  156. self._logging_names: Set[str] = set()
  157. logging_mods = self.config.logging_modules
  158. self._format_style = self.config.logging_format_style
  159. self._logging_modules = set(logging_mods)
  160. self._from_imports = {}
  161. for logging_mod in logging_mods:
  162. parts = logging_mod.rsplit(".", 1)
  163. if len(parts) > 1:
  164. self._from_imports[parts[0]] = parts[1]
  165. def visit_importfrom(self, node: nodes.ImportFrom) -> None:
  166. """Checks to see if a module uses a non-Python logging module."""
  167. try:
  168. logging_name = self._from_imports[node.modname]
  169. for module, as_name in node.names:
  170. if module == logging_name:
  171. self._logging_names.add(as_name or module)
  172. except KeyError:
  173. pass
  174. def visit_import(self, node: nodes.Import) -> None:
  175. """Checks to see if this module uses Python's built-in logging."""
  176. for module, as_name in node.names:
  177. if module in self._logging_modules:
  178. self._logging_names.add(as_name or module)
  179. @check_messages(*MSGS)
  180. def visit_call(self, node: nodes.Call) -> None:
  181. """Checks calls to logging methods."""
  182. def is_logging_name():
  183. return (
  184. isinstance(node.func, nodes.Attribute)
  185. and isinstance(node.func.expr, nodes.Name)
  186. and node.func.expr.name in self._logging_names
  187. )
  188. def is_logger_class():
  189. for inferred in infer_all(node.func):
  190. if isinstance(inferred, astroid.BoundMethod):
  191. parent = inferred._proxied.parent
  192. if isinstance(parent, nodes.ClassDef) and (
  193. parent.qname() == "logging.Logger"
  194. or any(
  195. ancestor.qname() == "logging.Logger"
  196. for ancestor in parent.ancestors()
  197. )
  198. ):
  199. return True, inferred._proxied.name
  200. return False, None
  201. if is_logging_name():
  202. name = node.func.attrname
  203. else:
  204. result, name = is_logger_class()
  205. if not result:
  206. return
  207. self._check_log_method(node, name)
  208. def _check_log_method(self, node, name):
  209. """Checks calls to logging.log(level, format, *format_args)."""
  210. if name == "log":
  211. if node.starargs or node.kwargs or len(node.args) < 2:
  212. # Either a malformed call, star args, or double-star args. Beyond
  213. # the scope of this checker.
  214. return
  215. format_pos = 1
  216. elif name in CHECKED_CONVENIENCE_FUNCTIONS:
  217. if node.starargs or node.kwargs or not node.args:
  218. # Either no args, star args, or double-star args. Beyond the
  219. # scope of this checker.
  220. return
  221. format_pos = 0
  222. else:
  223. return
  224. if isinstance(node.args[format_pos], nodes.BinOp):
  225. binop = node.args[format_pos]
  226. emit = binop.op == "%"
  227. if binop.op == "+":
  228. total_number_of_strings = sum(
  229. 1
  230. for operand in (binop.left, binop.right)
  231. if self._is_operand_literal_str(utils.safe_infer(operand))
  232. )
  233. emit = total_number_of_strings > 0
  234. if emit:
  235. self.add_message(
  236. "logging-not-lazy",
  237. node=node,
  238. args=(self._helper_string(node),),
  239. )
  240. elif isinstance(node.args[format_pos], nodes.Call):
  241. self._check_call_func(node.args[format_pos])
  242. elif isinstance(node.args[format_pos], nodes.Const):
  243. self._check_format_string(node, format_pos)
  244. elif isinstance(node.args[format_pos], nodes.JoinedStr):
  245. self.add_message(
  246. "logging-fstring-interpolation",
  247. node=node,
  248. args=(self._helper_string(node),),
  249. )
  250. def _helper_string(self, node):
  251. """Create a string that lists the valid types of formatting for this node."""
  252. valid_types = ["lazy %"]
  253. if not self.linter.is_message_enabled(
  254. "logging-fstring-formatting", node.fromlineno
  255. ):
  256. valid_types.append("fstring")
  257. if not self.linter.is_message_enabled(
  258. "logging-format-interpolation", node.fromlineno
  259. ):
  260. valid_types.append(".format()")
  261. if not self.linter.is_message_enabled("logging-not-lazy", node.fromlineno):
  262. valid_types.append("%")
  263. return " or ".join(valid_types)
  264. @staticmethod
  265. def _is_operand_literal_str(operand):
  266. """
  267. Return True if the operand in argument is a literal string
  268. """
  269. return isinstance(operand, nodes.Const) and operand.name == "str"
  270. def _check_call_func(self, node: nodes.Call):
  271. """Checks that function call is not format_string.format()."""
  272. func = utils.safe_infer(node.func)
  273. types = ("str", "unicode")
  274. methods = ("format",)
  275. if (
  276. isinstance(func, astroid.BoundMethod)
  277. and is_method_call(func, types, methods)
  278. and not is_complex_format_str(func.bound)
  279. ):
  280. self.add_message(
  281. "logging-format-interpolation",
  282. node=node,
  283. args=(self._helper_string(node),),
  284. )
  285. def _check_format_string(self, node, format_arg):
  286. """Checks that format string tokens match the supplied arguments.
  287. Args:
  288. node (nodes.NodeNG): AST node to be checked.
  289. format_arg (int): Index of the format string in the node arguments.
  290. """
  291. num_args = _count_supplied_tokens(node.args[format_arg + 1 :])
  292. if not num_args:
  293. # If no args were supplied the string is not interpolated and can contain
  294. # formatting characters - it's used verbatim. Don't check any further.
  295. return
  296. format_string = node.args[format_arg].value
  297. required_num_args = 0
  298. if isinstance(format_string, bytes):
  299. format_string = format_string.decode()
  300. if isinstance(format_string, str):
  301. try:
  302. if self._format_style == "old":
  303. keyword_args, required_num_args, _, _ = utils.parse_format_string(
  304. format_string
  305. )
  306. if keyword_args:
  307. # Keyword checking on logging strings is complicated by
  308. # special keywords - out of scope.
  309. return
  310. elif self._format_style == "new":
  311. (
  312. keyword_arguments,
  313. implicit_pos_args,
  314. explicit_pos_args,
  315. ) = utils.parse_format_method_string(format_string)
  316. keyword_args_cnt = len(
  317. {k for k, l in keyword_arguments if not isinstance(k, int)}
  318. )
  319. required_num_args = (
  320. keyword_args_cnt + implicit_pos_args + explicit_pos_args
  321. )
  322. except utils.UnsupportedFormatCharacter as ex:
  323. char = format_string[ex.index]
  324. self.add_message(
  325. "logging-unsupported-format",
  326. node=node,
  327. args=(char, ord(char), ex.index),
  328. )
  329. return
  330. except utils.IncompleteFormatString:
  331. self.add_message("logging-format-truncated", node=node)
  332. return
  333. if num_args > required_num_args:
  334. self.add_message("logging-too-many-args", node=node)
  335. elif num_args < required_num_args:
  336. self.add_message("logging-too-few-args", node=node)
  337. def is_complex_format_str(node: nodes.NodeNG) -> bool:
  338. """Return whether the node represents a string with complex formatting specs."""
  339. inferred = utils.safe_infer(node)
  340. if inferred is None or not (
  341. isinstance(inferred, nodes.Const) and isinstance(inferred.value, str)
  342. ):
  343. return True
  344. try:
  345. parsed = list(string.Formatter().parse(inferred.value))
  346. except ValueError:
  347. # This format string is invalid
  348. return False
  349. return any(format_spec for (_, _, format_spec, _) in parsed)
  350. def _count_supplied_tokens(args):
  351. """Counts the number of tokens in an args list.
  352. The Python log functions allow for special keyword arguments: func,
  353. exc_info and extra. To handle these cases correctly, we only count
  354. arguments that aren't keywords.
  355. Args:
  356. args (list): AST nodes that are arguments for a log format string.
  357. Returns:
  358. int: Number of AST nodes that aren't keywords.
  359. """
  360. return sum(1 for arg in args if not isinstance(arg, nodes.Keyword))
  361. def register(linter):
  362. """Required method to auto-register this checker."""
  363. linter.register_checker(LoggingChecker(linter))