summaryrefslogtreecommitdiff
path: root/pylint
diff options
context:
space:
mode:
authorDaniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>2022-09-22 23:32:16 +0200
committerGitHub <noreply@github.com>2022-09-22 17:32:16 -0400
commit705ac4cd88f3efae44309cd46494780ffaa93ca1 (patch)
tree41c2877ef252d4bc9efb518e88b624423299ad11 /pylint
parent1918a4d21ca803d43c65eaa198b905bf2cf5be3f (diff)
downloadpylint-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.py20
-rw-r--r--pylint/checkers/utils.py65
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, ""