dataclass_checker.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  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. """Dataclass checkers for Python code."""
  5. from __future__ import annotations
  6. from typing import TYPE_CHECKING
  7. from astroid import nodes
  8. from astroid.brain.brain_dataclasses import DATACLASS_MODULES
  9. from pylint.checkers import BaseChecker, utils
  10. from pylint.interfaces import INFERENCE
  11. if TYPE_CHECKING:
  12. from pylint.lint import PyLinter
  13. def _is_dataclasses_module(node: nodes.Module) -> bool:
  14. """Utility function to check if node is from dataclasses_module."""
  15. return node.name in DATACLASS_MODULES
  16. def _check_name_or_attrname_eq_to(
  17. node: nodes.Name | nodes.Attribute, check_with: str
  18. ) -> bool:
  19. """Utility function to check either a Name/Attribute node's name/attrname with a
  20. given string.
  21. """
  22. if isinstance(node, nodes.Name):
  23. return str(node.name) == check_with
  24. return str(node.attrname) == check_with
  25. class DataclassChecker(BaseChecker):
  26. """Checker that detects invalid or problematic usage in dataclasses.
  27. Checks for
  28. * invalid-field-call
  29. """
  30. name = "dataclass"
  31. msgs = {
  32. "E3701": (
  33. "Invalid usage of field(), %s",
  34. "invalid-field-call",
  35. "The dataclasses.field() specifier should only be used as the value of "
  36. "an assignment within a dataclass, or within the make_dataclass() function.",
  37. ),
  38. }
  39. @utils.only_required_for_messages("invalid-field-call")
  40. def visit_call(self, node: nodes.Call) -> None:
  41. self._check_invalid_field_call(node)
  42. def _check_invalid_field_call(self, node: nodes.Call) -> None:
  43. """Checks for correct usage of the dataclasses.field() specifier in
  44. dataclasses or within the make_dataclass() function.
  45. Emits message
  46. when field() is detected to be used outside a class decorated with
  47. @dataclass decorator and outside make_dataclass() function, or when it
  48. is used improperly within a dataclass.
  49. """
  50. if not isinstance(node.func, (nodes.Name, nodes.Attribute)):
  51. return
  52. if not _check_name_or_attrname_eq_to(node.func, "field"):
  53. return
  54. inferred_func = utils.safe_infer(node.func)
  55. if not (
  56. isinstance(inferred_func, nodes.FunctionDef)
  57. and _is_dataclasses_module(inferred_func.root())
  58. ):
  59. return
  60. scope_node = node.parent
  61. while scope_node and not isinstance(scope_node, (nodes.ClassDef, nodes.Call)):
  62. scope_node = scope_node.parent
  63. if isinstance(scope_node, nodes.Call):
  64. self._check_invalid_field_call_within_call(node, scope_node)
  65. return
  66. if not (scope_node and scope_node.is_dataclass):
  67. self.add_message(
  68. "invalid-field-call",
  69. node=node,
  70. args=(
  71. "it should be used within a dataclass or the make_dataclass() function.",
  72. ),
  73. confidence=INFERENCE,
  74. )
  75. return
  76. if not (isinstance(node.parent, nodes.AnnAssign) and node == node.parent.value):
  77. self.add_message(
  78. "invalid-field-call",
  79. node=node,
  80. args=("it should be the value of an assignment within a dataclass.",),
  81. confidence=INFERENCE,
  82. )
  83. def _check_invalid_field_call_within_call(
  84. self, node: nodes.Call, scope_node: nodes.Call
  85. ) -> None:
  86. """Checks for special case where calling field is valid as an argument of the
  87. make_dataclass() function.
  88. """
  89. inferred_func = utils.safe_infer(scope_node.func)
  90. if (
  91. isinstance(scope_node.func, (nodes.Name, nodes.AssignName))
  92. and scope_node.func.name == "make_dataclass"
  93. and isinstance(inferred_func, nodes.FunctionDef)
  94. and _is_dataclasses_module(inferred_func.root())
  95. ):
  96. return
  97. self.add_message(
  98. "invalid-field-call",
  99. node=node,
  100. args=(
  101. "it should be used within a dataclass or the make_dataclass() function.",
  102. ),
  103. confidence=INFERENCE,
  104. )
  105. def register(linter: PyLinter) -> None:
  106. linter.register_checker(DataclassChecker(linter))