message_definition.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. import sys
  6. import warnings
  7. from typing import TYPE_CHECKING, Any
  8. from astroid import nodes
  9. from pylint.constants import _SCOPE_EXEMPT, MSG_TYPES, WarningScope
  10. from pylint.exceptions import InvalidMessageError
  11. from pylint.utils import normalize_text
  12. if TYPE_CHECKING:
  13. from pylint.checkers import BaseChecker
  14. class MessageDefinition:
  15. # pylint: disable-next=too-many-arguments
  16. def __init__(
  17. self,
  18. checker: BaseChecker,
  19. msgid: str,
  20. msg: str,
  21. description: str,
  22. symbol: str,
  23. scope: str,
  24. minversion: tuple[int, int] | None = None,
  25. maxversion: tuple[int, int] | None = None,
  26. old_names: list[tuple[str, str]] | None = None,
  27. shared: bool = False,
  28. default_enabled: bool = True,
  29. ) -> None:
  30. self.checker_name = checker.name
  31. self.check_msgid(msgid)
  32. self.msgid = msgid
  33. self.symbol = symbol
  34. self.msg = msg
  35. self.description = description
  36. self.scope = scope
  37. self.minversion = minversion
  38. self.maxversion = maxversion
  39. self.shared = shared
  40. self.default_enabled = default_enabled
  41. self.old_names: list[tuple[str, str]] = []
  42. if old_names:
  43. for old_msgid, old_symbol in old_names:
  44. self.check_msgid(old_msgid)
  45. self.old_names.append(
  46. (old_msgid, old_symbol),
  47. )
  48. @staticmethod
  49. def check_msgid(msgid: str) -> None:
  50. if len(msgid) != 5:
  51. raise InvalidMessageError(f"Invalid message id {msgid!r}")
  52. if msgid[0] not in MSG_TYPES:
  53. raise InvalidMessageError(f"Bad message type {msgid[0]} in {msgid!r}")
  54. def __eq__(self, other: Any) -> bool:
  55. return (
  56. isinstance(other, MessageDefinition)
  57. and self.msgid == other.msgid
  58. and self.symbol == other.symbol
  59. )
  60. def __repr__(self) -> str:
  61. return f"MessageDefinition:{self.symbol} ({self.msgid})"
  62. def __str__(self) -> str:
  63. return f"{repr(self)}:\n{self.msg} {self.description}"
  64. def may_be_emitted(
  65. self,
  66. py_version: tuple[int, ...] | sys._version_info | None = None,
  67. ) -> bool:
  68. """Return True if message may be emitted using the configured py_version."""
  69. if py_version is None:
  70. py_version = sys.version_info
  71. warnings.warn(
  72. "'py_version' will be a required parameter of "
  73. "'MessageDefinition.may_be_emitted' in pylint 3.0. The most likely "
  74. "solution is to use 'linter.config.py_version' if you need to keep "
  75. "using this function, or to use 'MessageDefinition.is_message_enabled'"
  76. " instead.",
  77. DeprecationWarning,
  78. )
  79. if self.minversion is not None and self.minversion > py_version:
  80. return False
  81. if self.maxversion is not None and self.maxversion <= py_version:
  82. return False
  83. return True
  84. def format_help(self, checkerref: bool = False) -> str:
  85. """Return the help string for the given message id."""
  86. desc = self.description
  87. if checkerref:
  88. desc += f" This message belongs to the {self.checker_name} checker."
  89. title = self.msg
  90. if self.minversion or self.maxversion:
  91. restr = []
  92. if self.minversion:
  93. restr.append(f"< {'.'.join(str(n) for n in self.minversion)}")
  94. if self.maxversion:
  95. restr.append(f">= {'.'.join(str(n) for n in self.maxversion)}")
  96. restriction = " or ".join(restr)
  97. if checkerref:
  98. desc += f" It can't be emitted when using Python {restriction}."
  99. else:
  100. desc += (
  101. f" This message can't be emitted when using Python {restriction}."
  102. )
  103. msg_help = normalize_text(" ".join(desc.split()), indent=" ")
  104. message_id = f"{self.symbol} ({self.msgid})"
  105. if title != "%s":
  106. title = title.splitlines()[0]
  107. return f":{message_id}: *{title.rstrip(' ')}*\n{msg_help}"
  108. return f":{message_id}:\n{msg_help}"
  109. def check_message_definition(
  110. self, line: int | None, node: nodes.NodeNG | None
  111. ) -> None:
  112. """Check MessageDefinition for possible errors."""
  113. if self.msgid[0] not in _SCOPE_EXEMPT:
  114. # Fatal messages and reports are special, the node/scope distinction
  115. # does not apply to them.
  116. if self.scope == WarningScope.LINE:
  117. if line is None:
  118. raise InvalidMessageError(
  119. f"Message {self.msgid} must provide line, got None"
  120. )
  121. if node is not None:
  122. raise InvalidMessageError(
  123. f"Message {self.msgid} must only provide line, "
  124. f"got line={line}, node={node}"
  125. )
  126. elif self.scope == WarningScope.NODE:
  127. # Node-based warnings may provide an override line.
  128. if node is None:
  129. raise InvalidMessageError(
  130. f"Message {self.msgid} must provide Node, got None"
  131. )