emptystring.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  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. """Looks for comparisons to empty string."""
  5. from __future__ import annotations
  6. import itertools
  7. from typing import TYPE_CHECKING
  8. from astroid import nodes
  9. from pylint import checkers
  10. from pylint.checkers import utils
  11. from pylint.interfaces import HIGH
  12. if TYPE_CHECKING:
  13. from pylint.lint import PyLinter
  14. class CompareToEmptyStringChecker(checkers.BaseChecker):
  15. name = "compare-to-empty-string"
  16. msgs = {
  17. "C1901": (
  18. '"%s" can be simplified to "%s" as an empty string is falsey',
  19. "compare-to-empty-string",
  20. "Used when Pylint detects comparison to an empty string constant.",
  21. )
  22. }
  23. options = ()
  24. @utils.only_required_for_messages("compare-to-empty-string")
  25. def visit_compare(self, node: nodes.Compare) -> None:
  26. """Checks for comparisons to empty string.
  27. Most of the time you should use the fact that empty strings are false.
  28. An exception to this rule is when an empty string value is allowed in the program
  29. and has a different meaning than None!
  30. """
  31. _operators = {"!=", "==", "is not", "is"}
  32. # note: astroid.Compare has the left most operand in node.left while the rest
  33. # are a list of tuples in node.ops the format of the tuple is
  34. # ('compare operator sign', node) here we squash everything into `ops`
  35. # to make it easier for processing later
  36. ops: list[tuple[str, nodes.NodeNG | None]] = [("", node.left)]
  37. ops.extend(node.ops)
  38. iter_ops = iter(ops)
  39. ops = list(itertools.chain(*iter_ops)) # type: ignore[arg-type]
  40. for ops_idx in range(len(ops) - 2):
  41. op_1: nodes.NodeNG | None = ops[ops_idx]
  42. op_2: str = ops[ops_idx + 1] # type: ignore[assignment]
  43. op_3: nodes.NodeNG | None = ops[ops_idx + 2]
  44. error_detected = False
  45. if op_1 is None or op_3 is None or op_2 not in _operators:
  46. continue
  47. node_name = ""
  48. # x ?? ""
  49. if utils.is_empty_str_literal(op_1):
  50. error_detected = True
  51. node_name = op_3.as_string()
  52. # '' ?? X
  53. elif utils.is_empty_str_literal(op_3):
  54. error_detected = True
  55. node_name = op_1.as_string()
  56. if error_detected:
  57. suggestion = f"not {node_name}" if op_2 in {"==", "is"} else node_name
  58. self.add_message(
  59. "compare-to-empty-string",
  60. args=(node.as_string(), suggestion),
  61. node=node,
  62. confidence=HIGH,
  63. )
  64. def register(linter: PyLinter) -> None:
  65. linter.register_checker(CompareToEmptyStringChecker(linter))