comparison_checker.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  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. """Comparison checker from the basic checker."""
  5. import astroid
  6. from astroid import nodes
  7. from pylint.checkers import utils
  8. from pylint.checkers.base.basic_checker import _BasicChecker
  9. from pylint.interfaces import HIGH
  10. LITERAL_NODE_TYPES = (nodes.Const, nodes.Dict, nodes.List, nodes.Set)
  11. COMPARISON_OPERATORS = frozenset(("==", "!=", "<", ">", "<=", ">="))
  12. TYPECHECK_COMPARISON_OPERATORS = frozenset(("is", "is not", "==", "!="))
  13. TYPE_QNAME = "builtins.type"
  14. def _is_one_arg_pos_call(call: nodes.NodeNG) -> bool:
  15. """Is this a call with exactly 1 positional argument ?"""
  16. return isinstance(call, nodes.Call) and len(call.args) == 1 and not call.keywords
  17. class ComparisonChecker(_BasicChecker):
  18. """Checks for comparisons.
  19. - singleton comparison: 'expr == True', 'expr == False' and 'expr == None'
  20. - yoda condition: 'const "comp" right' where comp can be '==', '!=', '<',
  21. '<=', '>' or '>=', and right can be a variable, an attribute, a method or
  22. a function
  23. """
  24. msgs = {
  25. "C0121": (
  26. "Comparison %s should be %s",
  27. "singleton-comparison",
  28. "Used when an expression is compared to singleton "
  29. "values like True, False or None.",
  30. ),
  31. "C0123": (
  32. "Use isinstance() rather than type() for a typecheck.",
  33. "unidiomatic-typecheck",
  34. "The idiomatic way to perform an explicit typecheck in "
  35. "Python is to use isinstance(x, Y) rather than "
  36. "type(x) == Y, type(x) is Y. Though there are unusual "
  37. "situations where these give different results.",
  38. {"old_names": [("W0154", "old-unidiomatic-typecheck")]},
  39. ),
  40. "R0123": (
  41. "In '%s', use '%s' when comparing constant literals not '%s' ('%s')",
  42. "literal-comparison",
  43. "Used when comparing an object to a literal, which is usually "
  44. "what you do not want to do, since you can compare to a different "
  45. "literal than what was expected altogether.",
  46. ),
  47. "R0124": (
  48. "Redundant comparison - %s",
  49. "comparison-with-itself",
  50. "Used when something is compared against itself.",
  51. ),
  52. "R0133": (
  53. "Comparison between constants: '%s %s %s' has a constant value",
  54. "comparison-of-constants",
  55. "When two literals are compared with each other the result is a constant. "
  56. "Using the constant directly is both easier to read and more performant. "
  57. "Initializing 'True' and 'False' this way is not required since Python 2.3.",
  58. ),
  59. "W0143": (
  60. "Comparing against a callable, did you omit the parenthesis?",
  61. "comparison-with-callable",
  62. "This message is emitted when pylint detects that a comparison with a "
  63. "callable was made, which might suggest that some parenthesis were omitted, "
  64. "resulting in potential unwanted behaviour.",
  65. ),
  66. "W0177": (
  67. "Comparison %s should be %s",
  68. "nan-comparison",
  69. "Used when an expression is compared to NaN "
  70. "values like numpy.NaN and float('nan').",
  71. ),
  72. }
  73. def _check_singleton_comparison(
  74. self,
  75. left_value: nodes.NodeNG,
  76. right_value: nodes.NodeNG,
  77. root_node: nodes.Compare,
  78. checking_for_absence: bool = False,
  79. ) -> None:
  80. """Check if == or != is being used to compare a singleton value."""
  81. if utils.is_singleton_const(left_value):
  82. singleton, other_value = left_value.value, right_value
  83. elif utils.is_singleton_const(right_value):
  84. singleton, other_value = right_value.value, left_value
  85. else:
  86. return
  87. singleton_comparison_example = {False: "'{} is {}'", True: "'{} is not {}'"}
  88. # True/False singletons have a special-cased message in case the user is
  89. # mistakenly using == or != to check for truthiness
  90. if singleton in {True, False}:
  91. suggestion_template = (
  92. "{} if checking for the singleton value {}, or {} if testing for {}"
  93. )
  94. truthiness_example = {False: "not {}", True: "{}"}
  95. truthiness_phrase = {True: "truthiness", False: "falsiness"}
  96. # Looks for comparisons like x == True or x != False
  97. checking_truthiness = singleton is not checking_for_absence
  98. suggestion = suggestion_template.format(
  99. singleton_comparison_example[checking_for_absence].format(
  100. left_value.as_string(), right_value.as_string()
  101. ),
  102. singleton,
  103. (
  104. "'bool({})'"
  105. if not utils.is_test_condition(root_node) and checking_truthiness
  106. else "'{}'"
  107. ).format(
  108. truthiness_example[checking_truthiness].format(
  109. other_value.as_string()
  110. )
  111. ),
  112. truthiness_phrase[checking_truthiness],
  113. )
  114. else:
  115. suggestion = singleton_comparison_example[checking_for_absence].format(
  116. left_value.as_string(), right_value.as_string()
  117. )
  118. self.add_message(
  119. "singleton-comparison",
  120. node=root_node,
  121. args=(f"'{root_node.as_string()}'", suggestion),
  122. )
  123. def _check_nan_comparison(
  124. self,
  125. left_value: nodes.NodeNG,
  126. right_value: nodes.NodeNG,
  127. root_node: nodes.Compare,
  128. checking_for_absence: bool = False,
  129. ) -> None:
  130. def _is_float_nan(node: nodes.NodeNG) -> bool:
  131. try:
  132. if isinstance(node, nodes.Call) and len(node.args) == 1:
  133. if (
  134. node.args[0].value.lower() == "nan"
  135. and node.inferred()[0].pytype() == "builtins.float"
  136. ):
  137. return True
  138. return False
  139. except AttributeError:
  140. return False
  141. def _is_numpy_nan(node: nodes.NodeNG) -> bool:
  142. if isinstance(node, nodes.Attribute) and node.attrname == "NaN":
  143. if isinstance(node.expr, nodes.Name):
  144. return node.expr.name in {"numpy", "nmp", "np"}
  145. return False
  146. def _is_nan(node: nodes.NodeNG) -> bool:
  147. return _is_float_nan(node) or _is_numpy_nan(node)
  148. nan_left = _is_nan(left_value)
  149. if not nan_left and not _is_nan(right_value):
  150. return
  151. absence_text = ""
  152. if checking_for_absence:
  153. absence_text = "not "
  154. if nan_left:
  155. suggestion = f"'{absence_text}math.isnan({right_value.as_string()})'"
  156. else:
  157. suggestion = f"'{absence_text}math.isnan({left_value.as_string()})'"
  158. self.add_message(
  159. "nan-comparison",
  160. node=root_node,
  161. args=(f"'{root_node.as_string()}'", suggestion),
  162. )
  163. def _check_literal_comparison(
  164. self, literal: nodes.NodeNG, node: nodes.Compare
  165. ) -> None:
  166. """Check if we compare to a literal, which is usually what we do not want to do."""
  167. is_other_literal = isinstance(literal, (nodes.List, nodes.Dict, nodes.Set))
  168. is_const = False
  169. if isinstance(literal, nodes.Const):
  170. if isinstance(literal.value, bool) or literal.value is None:
  171. # Not interested in these values.
  172. return
  173. is_const = isinstance(literal.value, (bytes, str, int, float))
  174. if is_const or is_other_literal:
  175. incorrect_node_str = node.as_string()
  176. if "is not" in incorrect_node_str:
  177. equal_or_not_equal = "!="
  178. is_or_is_not = "is not"
  179. else:
  180. equal_or_not_equal = "=="
  181. is_or_is_not = "is"
  182. fixed_node_str = incorrect_node_str.replace(
  183. is_or_is_not, equal_or_not_equal
  184. )
  185. self.add_message(
  186. "literal-comparison",
  187. args=(
  188. incorrect_node_str,
  189. equal_or_not_equal,
  190. is_or_is_not,
  191. fixed_node_str,
  192. ),
  193. node=node,
  194. confidence=HIGH,
  195. )
  196. def _check_logical_tautology(self, node: nodes.Compare) -> None:
  197. """Check if identifier is compared against itself.
  198. :param node: Compare node
  199. :Example:
  200. val = 786
  201. if val == val: # [comparison-with-itself]
  202. pass
  203. """
  204. left_operand = node.left
  205. right_operand = node.ops[0][1]
  206. operator = node.ops[0][0]
  207. if isinstance(left_operand, nodes.Const) and isinstance(
  208. right_operand, nodes.Const
  209. ):
  210. left_operand = left_operand.value
  211. right_operand = right_operand.value
  212. elif isinstance(left_operand, nodes.Name) and isinstance(
  213. right_operand, nodes.Name
  214. ):
  215. left_operand = left_operand.name
  216. right_operand = right_operand.name
  217. if left_operand == right_operand:
  218. suggestion = f"{left_operand} {operator} {right_operand}"
  219. self.add_message("comparison-with-itself", node=node, args=(suggestion,))
  220. def _check_constants_comparison(self, node: nodes.Compare) -> None:
  221. """When two constants are being compared it is always a logical tautology."""
  222. left_operand = node.left
  223. if not isinstance(left_operand, nodes.Const):
  224. return
  225. right_operand = node.ops[0][1]
  226. if not isinstance(right_operand, nodes.Const):
  227. return
  228. operator = node.ops[0][0]
  229. self.add_message(
  230. "comparison-of-constants",
  231. node=node,
  232. args=(left_operand.value, operator, right_operand.value),
  233. confidence=HIGH,
  234. )
  235. def _check_callable_comparison(self, node: nodes.Compare) -> None:
  236. operator = node.ops[0][0]
  237. if operator not in COMPARISON_OPERATORS:
  238. return
  239. bare_callables = (nodes.FunctionDef, astroid.BoundMethod)
  240. left_operand, right_operand = node.left, node.ops[0][1]
  241. # this message should be emitted only when there is comparison of bare callable
  242. # with non bare callable.
  243. number_of_bare_callables = 0
  244. for operand in left_operand, right_operand:
  245. inferred = utils.safe_infer(operand)
  246. # Ignore callables that raise, as well as typing constants
  247. # implemented as functions (that raise via their decorator)
  248. if (
  249. isinstance(inferred, bare_callables)
  250. and "typing._SpecialForm" not in inferred.decoratornames()
  251. and not any(isinstance(x, nodes.Raise) for x in inferred.body)
  252. ):
  253. number_of_bare_callables += 1
  254. if number_of_bare_callables == 1:
  255. self.add_message("comparison-with-callable", node=node)
  256. @utils.only_required_for_messages(
  257. "singleton-comparison",
  258. "unidiomatic-typecheck",
  259. "literal-comparison",
  260. "comparison-with-itself",
  261. "comparison-of-constants",
  262. "comparison-with-callable",
  263. "nan-comparison",
  264. )
  265. def visit_compare(self, node: nodes.Compare) -> None:
  266. self._check_callable_comparison(node)
  267. self._check_logical_tautology(node)
  268. self._check_unidiomatic_typecheck(node)
  269. self._check_constants_comparison(node)
  270. # NOTE: this checker only works with binary comparisons like 'x == 42'
  271. # but not 'x == y == 42'
  272. if len(node.ops) != 1:
  273. return
  274. left = node.left
  275. operator, right = node.ops[0]
  276. if operator in {"==", "!="}:
  277. self._check_singleton_comparison(
  278. left, right, node, checking_for_absence=operator == "!="
  279. )
  280. if operator in {"==", "!=", "is", "is not"}:
  281. self._check_nan_comparison(
  282. left, right, node, checking_for_absence=operator in {"!=", "is not"}
  283. )
  284. if operator in {"is", "is not"}:
  285. self._check_literal_comparison(right, node)
  286. def _check_unidiomatic_typecheck(self, node: nodes.Compare) -> None:
  287. operator, right = node.ops[0]
  288. if operator in TYPECHECK_COMPARISON_OPERATORS:
  289. left = node.left
  290. if _is_one_arg_pos_call(left):
  291. self._check_type_x_is_y(node, left, operator, right)
  292. def _check_type_x_is_y(
  293. self,
  294. node: nodes.Compare,
  295. left: nodes.NodeNG,
  296. operator: str,
  297. right: nodes.NodeNG,
  298. ) -> None:
  299. """Check for expressions like type(x) == Y."""
  300. left_func = utils.safe_infer(left.func)
  301. if not (
  302. isinstance(left_func, nodes.ClassDef) and left_func.qname() == TYPE_QNAME
  303. ):
  304. return
  305. if operator in {"is", "is not"} and _is_one_arg_pos_call(right):
  306. right_func = utils.safe_infer(right.func)
  307. if (
  308. isinstance(right_func, nodes.ClassDef)
  309. and right_func.qname() == TYPE_QNAME
  310. ):
  311. # type(x) == type(a)
  312. right_arg = utils.safe_infer(right.args[0])
  313. if not isinstance(right_arg, LITERAL_NODE_TYPES):
  314. # not e.g. type(x) == type([])
  315. return
  316. self.add_message("unidiomatic-typecheck", node=node)