Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add B041: Duplicate key-value pairs in dictionary literals #496

Merged
merged 3 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ second usage. Save the result to a list if the result is needed multiple times.

**B040**: Caught exception with call to ``add_note`` not used. Did you forget to ``raise`` it?

**B041**: Repeated key-value pair in dictionary literal.

Opinionated warnings
~~~~~~~~~~~~~~~~~~~~

Expand Down
44 changes: 43 additions & 1 deletion bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import re
import sys
import warnings
from collections import defaultdict, namedtuple
from collections import Counter, defaultdict, namedtuple
from contextlib import suppress
from functools import lru_cache, partial
from keyword import iskeyword
Expand Down Expand Up @@ -362,6 +362,17 @@ class B040CaughtException:
has_note: bool


class B041UnhandledKeyType:
"""
A dictionary key of a type that we do not check for duplicates.
"""


@attr.define(frozen=True)
class B041VariableKeyType:
name: str


@attr.s
class BugBearVisitor(ast.NodeVisitor):
filename = attr.ib()
Expand Down Expand Up @@ -633,6 +644,35 @@ def visit_Set(self, node) -> None:
self.check_for_b033(node)
self.generic_visit(node)

def visit_Dict(self, node) -> None:
self.check_for_b041(node)
self.generic_visit(node)

def check_for_b041(self, node) -> None:
# Complain if there are duplicate key-value pairs in a dictionary literal.
def convert_to_value(item):
if isinstance(item, ast.Constant):
return item.value
elif isinstance(item, ast.Tuple):
return tuple(convert_to_value(i) for i in item.elts)
elif isinstance(item, ast.Name):
return B041VariableKeyType(item.id)
else:
return B041UnhandledKeyType()

keys = [convert_to_value(key) for key in node.keys]
key_counts = Counter(keys)
duplicate_keys = [key for key, count in key_counts.items() if count > 1]
for key in duplicate_keys:
key_indices = [i for i, i_key in enumerate(keys) if i_key == key]
seen = set()
for index in key_indices:
value = convert_to_value(node.values[index])
if value in seen:
key_node = node.keys[index]
self.errors.append(B041(key_node.lineno, key_node.col_offset))
seen.add(value)

def check_for_b005(self, node) -> None:
if isinstance(node, ast.Import):
for name in node.names:
Expand Down Expand Up @@ -2327,6 +2367,8 @@ def visit_Lambda(self, node) -> None:
message="B040 Exception with added note not used. Did you forget to raise it?"
)

B041 = Error(message=("B041 Repeated key-value pair in dictionary literal."))

# Warnings disabled by default.
B901 = Error(
message=(
Expand Down
24 changes: 24 additions & 0 deletions tests/b041.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
a = 1
test = {'yes': 1, 'yes': 1}
test = {'yes': 1, 'yes': 1, 'no': 2, 'no': 2}
test = {'yes': 1, 'yes': 1, 'yes': 1}
test = {1: 1, 1.0: 1}
test = {True: 1, True: 1}
test = {None: 1, None: 1}
test = {a: a, a: a}

# no error if either keys or values are different
test = {'yes': 1, 'yes': 2}
test = {1: 1, 2: 1}
test = {(0, 1): 1, (0, 2): 1}
test = {(0, 1): 1, (0, 1): 2}
b = 1
test = {a: a, b: a}
test = {a: a, a: b}
class TestClass:
pass
f = TestClass()
f.a = 1
test = {f.a: 1, f.a: 1}


18 changes: 18 additions & 0 deletions tests/test_bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
B037,
B039,
B040,
B041,
B901,
B902,
B903,
Expand Down Expand Up @@ -666,6 +667,23 @@ def test_b040(self) -> None:
)
self.assertEqual(errors, expected)

def test_b041(self) -> None:
filename = Path(__file__).absolute().parent / "b041.py"
bbc = BugBearChecker(filename=str(filename))
errors = list(bbc.run())
expected = self.errors(
B041(2, 18),
B041(3, 18),
B041(3, 37),
B041(4, 18),
B041(4, 28),
B041(5, 14),
B041(6, 17),
B041(7, 17),
B041(8, 14),
)
self.assertEqual(errors, expected)

def test_b908(self):
filename = Path(__file__).absolute().parent / "b908.py"
bbc = BugBearChecker(filename=str(filename))
Expand Down