|
4 | 4 |
|
5 | 5 | from __future__ import annotations
|
6 | 6 |
|
| 7 | +from copy import copy |
7 | 8 | from typing import TYPE_CHECKING, TypeGuard, cast
|
8 | 9 |
|
9 | 10 | from astroid import nodes
|
10 | 11 |
|
11 | 12 | from pylint.checkers import BaseChecker, utils
|
12 | 13 | from pylint.checkers.utils import only_required_for_messages, safe_infer
|
13 |
| -from pylint.interfaces import INFERENCE |
| 14 | +from pylint.interfaces import HIGH, INFERENCE |
14 | 15 |
|
15 | 16 | if TYPE_CHECKING:
|
16 | 17 | from pylint.lint import PyLinter
|
@@ -74,6 +75,14 @@ class CodeStyleChecker(BaseChecker):
|
74 | 75 | "default_enabled": False,
|
75 | 76 | },
|
76 | 77 | ),
|
| 78 | + "R6106": ( |
| 79 | + "Rewrite conditional expression to '%s'", |
| 80 | + "improve-conditionals", |
| 81 | + "Rewrite negated if expressions to improve readability.", |
| 82 | + { |
| 83 | + # "default_enabled": False, |
| 84 | + }, |
| 85 | + ), |
77 | 86 | }
|
78 | 87 | options = (
|
79 | 88 | (
|
@@ -320,6 +329,76 @@ def visit_assign(self, node: nodes.Assign) -> None:
|
320 | 329 | confidence=INFERENCE,
|
321 | 330 | )
|
322 | 331 |
|
| 332 | + @staticmethod |
| 333 | + def _can_be_inverted(node: nodes.NodeNG) -> bool: |
| 334 | + match node: |
| 335 | + case nodes.UnaryOp(op="not"): |
| 336 | + return True |
| 337 | + case nodes.Compare( |
| 338 | + ops=[("!=" | "not in", _)] |
| 339 | + | [("<" | "<=" | ">" | ">=", nodes.Const(value=int()))] |
| 340 | + ): |
| 341 | + return True |
| 342 | + return False |
| 343 | + |
| 344 | + @staticmethod |
| 345 | + def _invert_node(node: nodes.NodeNG) -> nodes.NodeNG: |
| 346 | + match node: |
| 347 | + case nodes.UnaryOp(op="not"): |
| 348 | + new_node = copy(node.operand) |
| 349 | + new_node.parent = node |
| 350 | + return new_node |
| 351 | + case nodes.Compare(left=left, ops=[(op, n)]): |
| 352 | + new_node = copy(node) |
| 353 | + match op: |
| 354 | + case "!=": |
| 355 | + new_op = "==" |
| 356 | + case "not in": |
| 357 | + new_op = "in" |
| 358 | + case "<": |
| 359 | + new_op = ">=" |
| 360 | + case "<=": |
| 361 | + new_op = ">" |
| 362 | + case ">": |
| 363 | + new_op = "<=" |
| 364 | + case ">=": |
| 365 | + new_op = "<" |
| 366 | + case _: # pragma: no cover |
| 367 | + raise AssertionError |
| 368 | + new_node.postinit(left=left, ops=[(new_op, n)]) |
| 369 | + return new_node |
| 370 | + case _: # pragma: no cover |
| 371 | + raise AssertionError |
| 372 | + |
| 373 | + @only_required_for_messages("improve-conditionals") |
| 374 | + def visit_boolop(self, node: nodes.BoolOp) -> None: |
| 375 | + if node.op == "or" and all(self._can_be_inverted(val) for val in node.values): |
| 376 | + new_boolop = copy(node) |
| 377 | + new_boolop.op = "and" |
| 378 | + new_boolop.postinit([self._invert_node(val) for val in node.values]) |
| 379 | + |
| 380 | + if isinstance(node.parent, nodes.UnaryOp) and node.parent.op == "not": |
| 381 | + target_node = node.parent |
| 382 | + new_node = new_boolop |
| 383 | + else: |
| 384 | + target_node = node |
| 385 | + new_node = nodes.UnaryOp( |
| 386 | + op="not", |
| 387 | + lineno=0, |
| 388 | + col_offset=0, |
| 389 | + end_lineno=None, |
| 390 | + end_col_offset=None, |
| 391 | + parent=node.parent, |
| 392 | + ) |
| 393 | + new_node.postinit(operand=new_boolop) |
| 394 | + |
| 395 | + self.add_message( |
| 396 | + "improve-conditionals", |
| 397 | + node=target_node, |
| 398 | + args=(new_node.as_string(),), |
| 399 | + confidence=HIGH, |
| 400 | + ) |
| 401 | + |
323 | 402 |
|
324 | 403 | def register(linter: PyLinter) -> None:
|
325 | 404 | linter.register_checker(CodeStyleChecker(linter))
|
0 commit comments