diff options
author | Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> | 2022-09-22 23:32:16 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-22 17:32:16 -0400 |
commit | 705ac4cd88f3efae44309cd46494780ffaa93ca1 (patch) | |
tree | 41c2877ef252d4bc9efb518e88b624423299ad11 /pylint | |
parent | 1918a4d21ca803d43c65eaa198b905bf2cf5be3f (diff) | |
download | pylint-git-705ac4cd88f3efae44309cd46494780ffaa93ca1.tar.gz |
Add ``consider-using-augmented-assign`` checker (#7514)
Co-authored-by: clavedeluna <danalitovsky+git@gmail.com>
Co-authored-by: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com>
Diffstat (limited to 'pylint')
-rw-r--r-- | pylint/checkers/refactoring/recommendation_checker.py | 20 | ||||
-rw-r--r-- | pylint/checkers/utils.py | 65 |
2 files changed, 85 insertions, 0 deletions
diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index f610763be..8a3b2d204 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -9,6 +9,7 @@ from astroid import nodes from pylint import checkers from pylint.checkers import utils +from pylint.interfaces import INFERENCE class RecommendationChecker(checkers.BaseChecker): @@ -60,6 +61,12 @@ class RecommendationChecker(checkers.BaseChecker): "which could potentially be a f-string. The use of f-strings is preferred. " "Requires Python 3.6 and ``py-version >= 3.6``.", ), + "C0210": ( + "Use '%s' to do an augmented assign directly", + "consider-using-augmented-assign", + "Emitted when an assignment is referring to the object that it is assigning " + "to. This can be changed to be an augmented assign.", + ), } def open(self) -> None: @@ -417,3 +424,16 @@ class RecommendationChecker(checkers.BaseChecker): line=node.lineno, col_offset=node.col_offset, ) + + @utils.only_required_for_messages("consider-using-augmented-assign") + def visit_assign(self, node: nodes.Assign) -> None: + is_aug, op = utils.is_augmented_assign(node) + if is_aug: + self.add_message( + "consider-using-augmented-assign", + args=f"{op}=", + node=node, + line=node.lineno, + col_offset=node.col_offset, + confidence=INFERENCE, + ) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index fe462b258..2a4426083 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1963,3 +1963,68 @@ def is_hashable(node: nodes.NodeNG) -> bool: return False except astroid.InferenceError: return True + + +def get_full_name_of_attribute(node: nodes.Attribute | nodes.AssignAttr) -> str: + """Return the full name of an attribute and the classes it belongs to. + + For example: "Class1.Class2.attr" + """ + parent = node.parent + ret = node.attrname or "" + while isinstance(parent, (nodes.Attribute, nodes.Name)): + if isinstance(parent, nodes.Attribute): + ret = f"{parent.attrname}.{ret}" + else: + ret = f"{parent.name}.{ret}" + parent = parent.parent + return ret + + +def _is_target_name_in_binop_side( + target: nodes.AssignName | nodes.AssignAttr, side: nodes.NodeNG | None +) -> bool: + """Determine whether the target name-like node is referenced in the side node.""" + if isinstance(side, nodes.Name): + if isinstance(target, nodes.AssignName): + return target.name == side.name # type: ignore[no-any-return] + return False + if isinstance(side, nodes.Attribute) and isinstance(target, nodes.AssignAttr): + return get_full_name_of_attribute(target) == get_full_name_of_attribute(side) + return False + + +def is_augmented_assign(node: nodes.Assign) -> tuple[bool, str]: + """Determine if the node is assigning itself (with modifications) to itself. + + For example: x = 1 + x + """ + if not isinstance(node.value, nodes.BinOp): + return False, "" + + binop = node.value + target = node.targets[0] + + if not isinstance(target, (nodes.AssignName, nodes.AssignAttr)): + return False, "" + + # We don't want to catch x = "1" + x or x = "%s" % x + if isinstance(binop.left, nodes.Const) and isinstance( + binop.left.value, (str, bytes) + ): + return False, "" + + # This could probably be improved but for now we disregard all assignments from calls + if isinstance(binop.left, nodes.Call) or isinstance(binop.right, nodes.Call): + return False, "" + + if _is_target_name_in_binop_side(target, binop.left): + return True, binop.op + if _is_target_name_in_binop_side(target, binop.right): + inferred_left = safe_infer(binop.left) + if isinstance(inferred_left, nodes.Const) and isinstance( + inferred_left.value, int + ): + return True, binop.op + return False, "" + return False, "" |