deprecated.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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. """Checker mixin for deprecated functionality."""
  5. from __future__ import annotations
  6. from collections.abc import Container, Iterable
  7. from itertools import chain
  8. import astroid
  9. from astroid import nodes
  10. from astroid.bases import Instance
  11. from pylint.checkers import utils
  12. from pylint.checkers.base_checker import BaseChecker
  13. from pylint.checkers.utils import get_import_name, infer_all, safe_infer
  14. from pylint.interfaces import INFERENCE
  15. from pylint.typing import MessageDefinitionTuple
  16. ACCEPTABLE_NODES = (
  17. astroid.BoundMethod,
  18. astroid.UnboundMethod,
  19. nodes.FunctionDef,
  20. nodes.ClassDef,
  21. nodes.Attribute,
  22. )
  23. class DeprecatedMixin(BaseChecker):
  24. """A mixin implementing logic for checking deprecated symbols.
  25. A class implementing mixin must define "deprecated-method" Message.
  26. """
  27. DEPRECATED_ATTRIBUTE_MESSAGE: dict[str, MessageDefinitionTuple] = {
  28. "W4906": (
  29. "Using deprecated attribute %r",
  30. "deprecated-attribute",
  31. "The attribute is marked as deprecated and will be removed in the future.",
  32. {"shared": True},
  33. ),
  34. }
  35. DEPRECATED_MODULE_MESSAGE: dict[str, MessageDefinitionTuple] = {
  36. "W4901": (
  37. "Deprecated module %r",
  38. "deprecated-module",
  39. "A module marked as deprecated is imported.",
  40. {"old_names": [("W0402", "old-deprecated-module")], "shared": True},
  41. ),
  42. }
  43. DEPRECATED_METHOD_MESSAGE: dict[str, MessageDefinitionTuple] = {
  44. "W4902": (
  45. "Using deprecated method %s()",
  46. "deprecated-method",
  47. "The method is marked as deprecated and will be removed in the future.",
  48. {"old_names": [("W1505", "old-deprecated-method")], "shared": True},
  49. ),
  50. }
  51. DEPRECATED_ARGUMENT_MESSAGE: dict[str, MessageDefinitionTuple] = {
  52. "W4903": (
  53. "Using deprecated argument %s of method %s()",
  54. "deprecated-argument",
  55. "The argument is marked as deprecated and will be removed in the future.",
  56. {"old_names": [("W1511", "old-deprecated-argument")], "shared": True},
  57. ),
  58. }
  59. DEPRECATED_CLASS_MESSAGE: dict[str, MessageDefinitionTuple] = {
  60. "W4904": (
  61. "Using deprecated class %s of module %s",
  62. "deprecated-class",
  63. "The class is marked as deprecated and will be removed in the future.",
  64. {"old_names": [("W1512", "old-deprecated-class")], "shared": True},
  65. ),
  66. }
  67. DEPRECATED_DECORATOR_MESSAGE: dict[str, MessageDefinitionTuple] = {
  68. "W4905": (
  69. "Using deprecated decorator %s()",
  70. "deprecated-decorator",
  71. "The decorator is marked as deprecated and will be removed in the future.",
  72. {"old_names": [("W1513", "old-deprecated-decorator")], "shared": True},
  73. ),
  74. }
  75. @utils.only_required_for_messages("deprecated-attribute")
  76. def visit_attribute(self, node: nodes.Attribute) -> None:
  77. """Called when an `Attribute` node is visited."""
  78. self.check_deprecated_attribute(node)
  79. @utils.only_required_for_messages(
  80. "deprecated-method",
  81. "deprecated-argument",
  82. "deprecated-class",
  83. "deprecated-module",
  84. )
  85. def visit_call(self, node: nodes.Call) -> None:
  86. """Called when a :class:`nodes.Call` node is visited."""
  87. self.check_deprecated_class_in_call(node)
  88. for inferred in infer_all(node.func):
  89. # Calling entry point for deprecation check logic.
  90. self.check_deprecated_method(node, inferred)
  91. if (
  92. isinstance(inferred, nodes.FunctionDef)
  93. and inferred.qname() == "builtins.__import__"
  94. and len(node.args) == 1
  95. and (mod_path_node := utils.safe_infer(node.args[0]))
  96. and isinstance(mod_path_node, nodes.Const)
  97. ):
  98. self.check_deprecated_module(node, mod_path_node.value)
  99. @utils.only_required_for_messages(
  100. "deprecated-module",
  101. "deprecated-class",
  102. )
  103. def visit_import(self, node: nodes.Import) -> None:
  104. """Triggered when an import statement is seen."""
  105. for name in (name for name, _ in node.names):
  106. self.check_deprecated_module(node, name)
  107. if "." in name:
  108. # Checking deprecation for import module with class
  109. mod_name, class_name = name.split(".", 1)
  110. self.check_deprecated_class(node, mod_name, (class_name,))
  111. def deprecated_decorators(self) -> Iterable[str]:
  112. """Callback returning the deprecated decorators.
  113. Returns:
  114. collections.abc.Container of deprecated decorator names.
  115. """
  116. return ()
  117. @utils.only_required_for_messages("deprecated-decorator")
  118. def visit_decorators(self, node: nodes.Decorators) -> None:
  119. """Triggered when a decorator statement is seen."""
  120. children = list(node.get_children())
  121. if not children:
  122. return
  123. if isinstance(children[0], nodes.Call):
  124. inferred = safe_infer(children[0].func)
  125. else:
  126. inferred = safe_infer(children[0])
  127. if not isinstance(inferred, (nodes.ClassDef, nodes.FunctionDef)):
  128. return
  129. qname = inferred.qname()
  130. if qname in self.deprecated_decorators():
  131. self.add_message("deprecated-decorator", node=node, args=qname)
  132. @utils.only_required_for_messages("deprecated-module", "deprecated-class")
  133. def visit_importfrom(self, node: nodes.ImportFrom) -> None:
  134. """Triggered when a from statement is seen."""
  135. basename = get_import_name(node, node.modname)
  136. assert basename is not None, "Module name should not be None"
  137. self.check_deprecated_module(node, basename)
  138. class_names = (name for name, _ in node.names)
  139. self.check_deprecated_class(node, basename, class_names)
  140. def deprecated_methods(self) -> Container[str]:
  141. """Callback returning the deprecated methods/functions.
  142. Returns:
  143. collections.abc.Container of deprecated function/method names.
  144. """
  145. return ()
  146. def deprecated_arguments(self, method: str) -> Iterable[tuple[int | None, str]]:
  147. """Callback returning the deprecated arguments of method/function.
  148. Args:
  149. method (str): name of function/method checked for deprecated arguments
  150. Returns:
  151. collections.abc.Iterable in form:
  152. ((POSITION1, PARAM1), (POSITION2: PARAM2) ...)
  153. where
  154. * POSITIONX - position of deprecated argument PARAMX in function definition.
  155. If argument is keyword-only, POSITIONX should be None.
  156. * PARAMX - name of the deprecated argument.
  157. E.g. suppose function:
  158. .. code-block:: python
  159. def bar(arg1, arg2, arg3, arg4, arg5='spam')
  160. with deprecated arguments `arg2` and `arg4`. `deprecated_arguments` should return:
  161. .. code-block:: python
  162. ((1, 'arg2'), (3, 'arg4'))
  163. """
  164. # pylint: disable=unused-argument
  165. return ()
  166. def deprecated_modules(self) -> Iterable[str]:
  167. """Callback returning the deprecated modules.
  168. Returns:
  169. collections.abc.Container of deprecated module names.
  170. """
  171. return ()
  172. def deprecated_classes(self, module: str) -> Iterable[str]:
  173. """Callback returning the deprecated classes of module.
  174. Args:
  175. module (str): name of module checked for deprecated classes
  176. Returns:
  177. collections.abc.Container of deprecated class names.
  178. """
  179. # pylint: disable=unused-argument
  180. return ()
  181. def deprecated_attributes(self) -> Iterable[str]:
  182. """Callback returning the deprecated attributes."""
  183. return ()
  184. def check_deprecated_attribute(self, node: nodes.Attribute) -> None:
  185. """Checks if the attribute is deprecated."""
  186. inferred_expr = safe_infer(node.expr)
  187. if not isinstance(inferred_expr, (nodes.ClassDef, Instance, nodes.Module)):
  188. return
  189. attribute_qname = ".".join((inferred_expr.qname(), node.attrname))
  190. for deprecated_name in self.deprecated_attributes():
  191. if attribute_qname == deprecated_name:
  192. self.add_message(
  193. "deprecated-attribute",
  194. node=node,
  195. args=(attribute_qname,),
  196. confidence=INFERENCE,
  197. )
  198. def check_deprecated_module(self, node: nodes.Import, mod_path: str | None) -> None:
  199. """Checks if the module is deprecated."""
  200. for mod_name in self.deprecated_modules():
  201. if mod_path == mod_name or (
  202. mod_path and mod_path.startswith(mod_name + ".")
  203. ):
  204. self.add_message("deprecated-module", node=node, args=mod_path)
  205. def check_deprecated_method(self, node: nodes.Call, inferred: nodes.NodeNG) -> None:
  206. """Executes the checker for the given node.
  207. This method should be called from the checker implementing this mixin.
  208. """
  209. # Reject nodes which aren't of interest to us.
  210. if not isinstance(inferred, ACCEPTABLE_NODES):
  211. return
  212. match node.func:
  213. case nodes.Attribute(attrname=func_name) | nodes.Name(name=func_name):
  214. pass
  215. case _:
  216. # Not interested in other nodes.
  217. return
  218. qnames = {inferred.qname(), func_name}
  219. if any(name in self.deprecated_methods() for name in qnames):
  220. self.add_message("deprecated-method", node=node, args=(func_name,))
  221. return
  222. num_of_args = len(node.args)
  223. kwargs = {kw.arg for kw in node.keywords} if node.keywords else {}
  224. deprecated_arguments = (self.deprecated_arguments(qn) for qn in qnames)
  225. for position, arg_name in chain(*deprecated_arguments):
  226. if arg_name in kwargs:
  227. # function was called with deprecated argument as keyword argument
  228. self.add_message(
  229. "deprecated-argument", node=node, args=(arg_name, func_name)
  230. )
  231. elif position is not None and position < num_of_args:
  232. # function was called with deprecated argument as positional argument
  233. self.add_message(
  234. "deprecated-argument", node=node, args=(arg_name, func_name)
  235. )
  236. def check_deprecated_class(
  237. self, node: nodes.NodeNG, mod_name: str, class_names: Iterable[str]
  238. ) -> None:
  239. """Checks if the class is deprecated."""
  240. for class_name in class_names:
  241. if class_name in self.deprecated_classes(mod_name):
  242. self.add_message(
  243. "deprecated-class", node=node, args=(class_name, mod_name)
  244. )
  245. def check_deprecated_class_in_call(self, node: nodes.Call) -> None:
  246. """Checks if call the deprecated class."""
  247. match node.func:
  248. case nodes.Attribute(expr=nodes.Name(name=mod_name), attrname=class_name):
  249. self.check_deprecated_class(node, mod_name, (class_name,))