function_checker.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. """Function checker for Python code."""
  5. from __future__ import annotations
  6. from itertools import chain
  7. from astroid import nodes
  8. from pylint.checkers import utils
  9. from pylint.checkers.base.basic_checker import _BasicChecker
  10. class FunctionChecker(_BasicChecker):
  11. """Check if a function definition handles possible side effects."""
  12. msgs = {
  13. "W0135": (
  14. "The context used in function %r will not be exited.",
  15. "contextmanager-generator-missing-cleanup",
  16. "Used when a contextmanager is used inside a generator function"
  17. " and the cleanup is not handled.",
  18. )
  19. }
  20. @utils.only_required_for_messages("contextmanager-generator-missing-cleanup")
  21. def visit_functiondef(self, node: nodes.FunctionDef) -> None:
  22. self._check_contextmanager_generator_missing_cleanup(node)
  23. @utils.only_required_for_messages("contextmanager-generator-missing-cleanup")
  24. def visit_asyncfunctiondef(self, node: nodes.AsyncFunctionDef) -> None:
  25. self._check_contextmanager_generator_missing_cleanup(node)
  26. def _check_contextmanager_generator_missing_cleanup(
  27. self, node: nodes.FunctionDef
  28. ) -> None:
  29. """Check a FunctionDef to find if it is a generator
  30. that uses a contextmanager internally.
  31. If it is, check if the contextmanager is properly cleaned up. Otherwise, add message.
  32. :param node: FunctionDef node to check
  33. :type node: nodes.FunctionDef
  34. """
  35. # if function does not use a Yield statement, it can't be a generator
  36. with_nodes = list(node.nodes_of_class(nodes.With))
  37. if not with_nodes:
  38. return
  39. # check for Yield inside the With statement
  40. yield_nodes = list(
  41. chain.from_iterable(
  42. with_node.nodes_of_class(nodes.Yield) for with_node in with_nodes
  43. )
  44. )
  45. if not yield_nodes:
  46. return
  47. # infer the call that yields a value, and check if it is a contextmanager
  48. for with_node in with_nodes:
  49. for call, held in with_node.items:
  50. if held is None:
  51. # if we discard the value, then we can skip checking it
  52. continue
  53. # safe infer is a generator
  54. inferred_node = getattr(utils.safe_infer(call), "parent", None)
  55. if not isinstance(inferred_node, nodes.FunctionDef):
  56. continue
  57. if self._node_fails_contextmanager_cleanup(inferred_node, yield_nodes):
  58. self.add_message(
  59. "contextmanager-generator-missing-cleanup",
  60. node=with_node,
  61. args=(node.name,),
  62. )
  63. @staticmethod
  64. def _node_fails_contextmanager_cleanup(
  65. node: nodes.FunctionDef, yield_nodes: list[nodes.Yield]
  66. ) -> bool:
  67. """Check if a node fails contextmanager cleanup.
  68. Current checks for a contextmanager:
  69. - only if the context manager yields a non-constant value
  70. - only if the context manager lacks a finally, or does not catch GeneratorExit
  71. - only if some statement follows the yield, some manually cleanup happens
  72. :param node: Node to check
  73. :type node: nodes.FunctionDef
  74. :return: True if fails, False otherwise
  75. :param yield_nodes: List of Yield nodes in the function body
  76. :type yield_nodes: list[nodes.Yield]
  77. :rtype: bool
  78. """
  79. def check_handles_generator_exceptions(try_node: nodes.Try) -> bool:
  80. # needs to handle either GeneratorExit, Exception, or bare except
  81. for handler in try_node.handlers:
  82. if handler.type is None:
  83. # handles all exceptions (bare except)
  84. return True
  85. inferred = utils.safe_infer(handler.type)
  86. if inferred and inferred.qname() in {
  87. "builtins.GeneratorExit",
  88. "builtins.Exception",
  89. }:
  90. return True
  91. return False
  92. # if context manager yields a non-constant value, then continue checking
  93. if any(
  94. yield_node.value is None or isinstance(yield_node.value, nodes.Const)
  95. for yield_node in yield_nodes
  96. ):
  97. return False
  98. # Check if yield expression is last statement
  99. yield_nodes = list(node.nodes_of_class(nodes.Yield))
  100. if len(yield_nodes) == 1:
  101. n = yield_nodes[0].parent
  102. while n is not node:
  103. if n.next_sibling() is not None:
  104. break
  105. n = n.parent
  106. else:
  107. # No next statement found
  108. return False
  109. # if function body has multiple Try, filter down to the ones that have a yield node
  110. try_with_yield_nodes = [
  111. try_node
  112. for try_node in node.nodes_of_class(nodes.Try)
  113. if any(try_node.nodes_of_class(nodes.Yield))
  114. ]
  115. if not try_with_yield_nodes:
  116. # no try blocks at all, so checks after this line do not apply
  117. return True
  118. # if the contextmanager has a finally block, then it is fine
  119. if all(try_node.finalbody for try_node in try_with_yield_nodes):
  120. return False
  121. # if the contextmanager catches GeneratorExit, then it is fine
  122. if all(
  123. check_handles_generator_exceptions(try_node)
  124. for try_node in try_with_yield_nodes
  125. ):
  126. return False
  127. return True