|
5 | 5 | from __future__ import annotations |
6 | 6 |
|
7 | 7 | import difflib |
| 8 | +from copy import copy |
8 | 9 | from typing import TYPE_CHECKING, TypeGuard, cast |
9 | 10 |
|
10 | 11 | from astroid import nodes |
11 | 12 |
|
12 | 13 | from pylint.checkers import BaseChecker, utils |
13 | 14 | from pylint.checkers.utils import only_required_for_messages, safe_infer |
14 | | -from pylint.interfaces import INFERENCE |
| 15 | +from pylint.interfaces import HIGH, INFERENCE |
15 | 16 |
|
16 | 17 | if TYPE_CHECKING: |
17 | 18 | from pylint.lint import PyLinter |
@@ -82,6 +83,14 @@ class CodeStyleChecker(BaseChecker): |
82 | 83 | "to 4 times faster than a float call (after the initial import of math). " |
83 | 84 | "This check also catches typos in float calls as a side effect.", |
84 | 85 | ), |
| 86 | + "R6107": ( |
| 87 | + "Rewrite conditional expression to '%s'", |
| 88 | + "improve-conditionals", |
| 89 | + "Rewrite negated if expressions to improve readability.", |
| 90 | + { |
| 91 | + # "default_enabled": False, |
| 92 | + }, |
| 93 | + ), |
85 | 94 | } |
86 | 95 | options = ( |
87 | 96 | ( |
@@ -356,6 +365,76 @@ def visit_assign(self, node: nodes.Assign) -> None: |
356 | 365 | confidence=INFERENCE, |
357 | 366 | ) |
358 | 367 |
|
| 368 | + @staticmethod |
| 369 | + def _can_be_inverted(node: nodes.NodeNG) -> bool: |
| 370 | + match node: |
| 371 | + case nodes.UnaryOp(op="not"): |
| 372 | + return True |
| 373 | + case nodes.Compare( |
| 374 | + ops=[("!=" | "not in", _)] |
| 375 | + | [("<" | "<=" | ">" | ">=", nodes.Const(value=int()))] |
| 376 | + ): |
| 377 | + return True |
| 378 | + return False |
| 379 | + |
| 380 | + @staticmethod |
| 381 | + def _invert_node(node: nodes.NodeNG) -> nodes.NodeNG: |
| 382 | + match node: |
| 383 | + case nodes.UnaryOp(op="not"): |
| 384 | + new_node = copy(node.operand) |
| 385 | + new_node.parent = node |
| 386 | + return new_node |
| 387 | + case nodes.Compare(left=left, ops=[(op, n)]): |
| 388 | + new_node = copy(node) |
| 389 | + match op: |
| 390 | + case "!=": |
| 391 | + new_op = "==" |
| 392 | + case "not in": |
| 393 | + new_op = "in" |
| 394 | + case "<": |
| 395 | + new_op = ">=" |
| 396 | + case "<=": |
| 397 | + new_op = ">" |
| 398 | + case ">": |
| 399 | + new_op = "<=" |
| 400 | + case ">=": |
| 401 | + new_op = "<" |
| 402 | + case _: # pragma: no cover |
| 403 | + raise AssertionError |
| 404 | + new_node.postinit(left=left, ops=[(new_op, n)]) |
| 405 | + return new_node |
| 406 | + case _: # pragma: no cover |
| 407 | + raise AssertionError |
| 408 | + |
| 409 | + @only_required_for_messages("improve-conditionals") |
| 410 | + def visit_boolop(self, node: nodes.BoolOp) -> None: |
| 411 | + if node.op == "or" and all(self._can_be_inverted(val) for val in node.values): |
| 412 | + new_boolop = copy(node) |
| 413 | + new_boolop.op = "and" |
| 414 | + new_boolop.postinit([self._invert_node(val) for val in node.values]) |
| 415 | + |
| 416 | + if isinstance(node.parent, nodes.UnaryOp) and node.parent.op == "not": |
| 417 | + target_node = node.parent |
| 418 | + new_node = new_boolop |
| 419 | + else: |
| 420 | + target_node = node |
| 421 | + new_node = nodes.UnaryOp( |
| 422 | + op="not", |
| 423 | + lineno=0, |
| 424 | + col_offset=0, |
| 425 | + end_lineno=None, |
| 426 | + end_col_offset=None, |
| 427 | + parent=node.parent, |
| 428 | + ) |
| 429 | + new_node.postinit(operand=new_boolop) |
| 430 | + |
| 431 | + self.add_message( |
| 432 | + "improve-conditionals", |
| 433 | + node=target_node, |
| 434 | + args=(new_node.as_string(),), |
| 435 | + confidence=HIGH, |
| 436 | + ) |
| 437 | + |
359 | 438 |
|
360 | 439 | def register(linter: PyLinter) -> None: |
361 | 440 | linter.register_checker(CodeStyleChecker(linter)) |
0 commit comments