not_checker.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  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. import astroid
  5. from astroid import nodes
  6. from pylint import checkers
  7. from pylint.checkers import utils
  8. class NotChecker(checkers.BaseChecker):
  9. """Checks for too many not in comparison expressions.
  10. - "not not" should trigger a warning
  11. - "not" followed by a comparison should trigger a warning
  12. """
  13. msgs = {
  14. "C0117": (
  15. 'Consider changing "%s" to "%s"',
  16. "unnecessary-negation",
  17. "Used when a boolean expression contains an unneeded negation, "
  18. "e.g. when two negation operators cancel each other out.",
  19. {"old_names": [("C0113", "unneeded-not")]},
  20. )
  21. }
  22. name = "refactoring"
  23. reverse_op = {
  24. "<": ">=",
  25. "<=": ">",
  26. ">": "<=",
  27. ">=": "<",
  28. "==": "!=",
  29. "!=": "==",
  30. "in": "not in",
  31. "is": "is not",
  32. }
  33. # sets are not ordered, so for example "not set(LEFT_VALS) <= set(RIGHT_VALS)" is
  34. # not equivalent to "set(LEFT_VALS) > set(RIGHT_VALS)"
  35. skipped_nodes = (nodes.Set,)
  36. # 'builtins' py3, '__builtin__' py2
  37. skipped_classnames = [f"builtins.{qname}" for qname in ("set", "frozenset")]
  38. @utils.only_required_for_messages("unnecessary-negation")
  39. def visit_unaryop(self, node: nodes.UnaryOp) -> None:
  40. if node.op != "not":
  41. return
  42. operand = node.operand
  43. if isinstance(operand, nodes.UnaryOp) and operand.op == "not":
  44. self.add_message(
  45. "unnecessary-negation",
  46. node=node,
  47. args=(node.as_string(), operand.operand.as_string()),
  48. )
  49. elif isinstance(operand, nodes.Compare):
  50. left = operand.left
  51. # ignore multiple comparisons
  52. if len(operand.ops) > 1:
  53. return
  54. operator, right = operand.ops[0]
  55. if operator not in self.reverse_op:
  56. return
  57. # Ignore __ne__ as function of __eq__
  58. frame = node.frame()
  59. if frame.name == "__ne__" and operator == "==":
  60. return
  61. for _type in (utils.node_type(left), utils.node_type(right)):
  62. if not _type:
  63. return
  64. if isinstance(_type, self.skipped_nodes):
  65. return
  66. if (
  67. isinstance(_type, astroid.Instance)
  68. and _type.qname() in self.skipped_classnames
  69. ):
  70. return
  71. suggestion = (
  72. f"{left.as_string()} {self.reverse_op[operator]} {right.as_string()}"
  73. )
  74. self.add_message(
  75. "unnecessary-negation", node=node, args=(node.as_string(), suggestion)
  76. )