Skip to content

Commit

Permalink
Add B041: Duplicate key-value pairs in dictionary literals (#496)
Browse files Browse the repository at this point in the history
* b041 duplicate key in dictionary literal

* only error if both keys and values are the same

* format
  • Loading branch information
yangdanny97 authored Oct 31, 2024
1 parent ea13615 commit 95f8791
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 1 deletion.
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 @@ -668,6 +669,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

0 comments on commit 95f8791

Please sign in to comment.