|
8 | 8 | import re
|
9 | 9 | import sys
|
10 | 10 | import warnings
|
11 |
| -from collections import defaultdict, namedtuple |
| 11 | +from collections import Counter, defaultdict, namedtuple |
12 | 12 | from contextlib import suppress
|
13 | 13 | from functools import lru_cache, partial
|
14 | 14 | from keyword import iskeyword
|
@@ -362,6 +362,17 @@ class B040CaughtException:
|
362 | 362 | has_note: bool
|
363 | 363 |
|
364 | 364 |
|
| 365 | +class B041UnhandledKeyType: |
| 366 | + """ |
| 367 | + A dictionary key of a type that we do not check for duplicates. |
| 368 | + """ |
| 369 | + |
| 370 | + |
| 371 | +@attr.define(frozen=True) |
| 372 | +class B041VariableKeyType: |
| 373 | + name: str |
| 374 | + |
| 375 | + |
365 | 376 | @attr.s
|
366 | 377 | class BugBearVisitor(ast.NodeVisitor):
|
367 | 378 | filename = attr.ib()
|
@@ -633,6 +644,35 @@ def visit_Set(self, node) -> None:
|
633 | 644 | self.check_for_b033(node)
|
634 | 645 | self.generic_visit(node)
|
635 | 646 |
|
| 647 | + def visit_Dict(self, node) -> None: |
| 648 | + self.check_for_b041(node) |
| 649 | + self.generic_visit(node) |
| 650 | + |
| 651 | + def check_for_b041(self, node) -> None: |
| 652 | + # Complain if there are duplicate key-value pairs in a dictionary literal. |
| 653 | + def convert_to_value(item): |
| 654 | + if isinstance(item, ast.Constant): |
| 655 | + return item.value |
| 656 | + elif isinstance(item, ast.Tuple): |
| 657 | + return tuple(convert_to_value(i) for i in item.elts) |
| 658 | + elif isinstance(item, ast.Name): |
| 659 | + return B041VariableKeyType(item.id) |
| 660 | + else: |
| 661 | + return B041UnhandledKeyType() |
| 662 | + |
| 663 | + keys = [convert_to_value(key) for key in node.keys] |
| 664 | + key_counts = Counter(keys) |
| 665 | + duplicate_keys = [key for key, count in key_counts.items() if count > 1] |
| 666 | + for key in duplicate_keys: |
| 667 | + key_indices = [i for i, i_key in enumerate(keys) if i_key == key] |
| 668 | + seen = set() |
| 669 | + for index in key_indices: |
| 670 | + value = convert_to_value(node.values[index]) |
| 671 | + if value in seen: |
| 672 | + key_node = node.keys[index] |
| 673 | + self.errors.append(B041(key_node.lineno, key_node.col_offset)) |
| 674 | + seen.add(value) |
| 675 | + |
636 | 676 | def check_for_b005(self, node) -> None:
|
637 | 677 | if isinstance(node, ast.Import):
|
638 | 678 | for name in node.names:
|
@@ -2327,6 +2367,8 @@ def visit_Lambda(self, node) -> None:
|
2327 | 2367 | message="B040 Exception with added note not used. Did you forget to raise it?"
|
2328 | 2368 | )
|
2329 | 2369 |
|
| 2370 | +B041 = Error(message=("B041 Repeated key-value pair in dictionary literal.")) |
| 2371 | + |
2330 | 2372 | # Warnings disabled by default.
|
2331 | 2373 | B901 = Error(
|
2332 | 2374 | message=(
|
|
0 commit comments