Skip to content

Commit a93d3ce

Browse files
author
Madda
committed
Check for duplicate dictionary keys
1 parent d721eaf commit a93d3ce

File tree

3 files changed

+160
-1
lines changed

3 files changed

+160
-1
lines changed

pyflakes/checker.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,31 @@ class Definition(Binding):
127127
"""
128128

129129

130+
class UnhandledKeyType(object):
131+
"""
132+
A dictionary key of a type that we cannot or do not check for duplicates.
133+
"""
134+
135+
136+
class VariableKey(object):
137+
"""
138+
A dictionary key which is a variable.
139+
140+
@ivar item: The variable AST object.
141+
"""
142+
def __init__(self, item):
143+
self.name = item.id
144+
145+
def __eq__(self, compare):
146+
return (
147+
compare.__class__ == self.__class__
148+
and compare.name == self.name
149+
)
150+
151+
def __hash__(self):
152+
return hash(self.name)
153+
154+
130155
class Importation(Definition):
131156
"""
132157
A binding created by an import statement.
@@ -849,7 +874,7 @@ def ignore(self, node):
849874
PASS = ignore
850875

851876
# "expr" type nodes
852-
BOOLOP = BINOP = UNARYOP = IFEXP = DICT = SET = \
877+
BOOLOP = BINOP = UNARYOP = IFEXP = SET = \
853878
COMPARE = CALL = REPR = ATTRIBUTE = SUBSCRIPT = \
854879
STARRED = NAMECONSTANT = handleChildren
855880

@@ -870,6 +895,33 @@ def ignore(self, node):
870895
# additional node types
871896
COMPREHENSION = KEYWORD = FORMATTEDVALUE = handleChildren
872897

898+
def DICT(self, node):
899+
keys = node.keys
900+
keys = [self.convert_to_value(key) for key in keys]
901+
for key in set(keys):
902+
if keys.count(key) > 1:
903+
if isinstance(key, VariableKey):
904+
self.report(messages.DuplicateVariableDictionaryKey,
905+
node, key.name)
906+
else:
907+
self.report(messages.DuplicateDictionaryKey, node, key)
908+
self.handleChildren(node)
909+
910+
def convert_to_value(self, item):
911+
if isinstance(item, ast.Str):
912+
return item.s
913+
elif isinstance(item, ast.Tuple):
914+
return tuple(self.convert_to_value(i) for i in item.elts)
915+
elif isinstance(item, ast.Num):
916+
return item.n
917+
elif isinstance(item, ast.Name):
918+
return VariableKey(item=item)
919+
elif (not PY33) and isinstance(item, ast.NameConstant):
920+
# None, True, False are nameconstants in python3, but names in 2
921+
return item.value
922+
else:
923+
return UnhandledKeyType()
924+
873925
def ASSERT(self, node):
874926
if isinstance(node.test, ast.Tuple) and node.test.elts != []:
875927
self.report(messages.AssertTuple, node)

pyflakes/messages.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,22 @@ def __init__(self, filename, loc, name):
116116
self.message_args = (name,)
117117

118118

119+
class DuplicateDictionaryKey(Message):
120+
message = 'dictionary key %s duplicated'
121+
122+
def __init__(self, filename, loc, key):
123+
Message.__init__(self, filename, loc)
124+
self.message_args = (key,)
125+
126+
127+
class DuplicateVariableDictionaryKey(Message):
128+
message = 'dictionary key variable %s duplicated'
129+
130+
def __init__(self, filename, loc, key):
131+
Message.__init__(self, filename, loc)
132+
self.message_args = (key,)
133+
134+
119135
class LateFutureImport(Message):
120136
message = 'from __future__ imports must occur at the beginning of the file'
121137

pyflakes/test/test_other.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,97 @@ class Test(TestCase):
1313
def test_duplicateArgs(self):
1414
self.flakes('def fu(bar, bar): pass', m.DuplicateArgument)
1515

16+
def test_duplicate_keys(self):
17+
self.flakes("{'yes': 1, 'yes': 1}", m.DuplicateDictionaryKey)
18+
19+
def test_multiple_duplicate_keys(self):
20+
self.flakes(
21+
"{'yes': 1, 'yes': 1, 'no': 2, 'no': 3}",
22+
m.DuplicateDictionaryKey, m.DuplicateDictionaryKey)
23+
24+
def test_duplicate_keys_in_function(self):
25+
self.flakes('''
26+
def f(thing):
27+
pass
28+
f({'yes': 1, 'yes': 4})
29+
''', m.DuplicateDictionaryKey)
30+
31+
def test_duplicate_keys_in_lambda(self):
32+
self.flakes("lambda x: {(0,1): 1, (0,1): 1}",
33+
m.DuplicateDictionaryKey)
34+
35+
def test_duplicate_keys_tuples(self):
36+
self.flakes("{(0,1): 1, (0,1): 1}", m.DuplicateDictionaryKey)
37+
38+
def test_duplicate_keys_ints(self):
39+
self.flakes("{1: 1, 1: 1}", m.DuplicateDictionaryKey)
40+
41+
def test_duplicate_keys_bools(self):
42+
if version_info < (3, 4):
43+
expected = m.DuplicateVariableDictionaryKey
44+
else:
45+
expected = m.DuplicateDictionaryKey
46+
self.flakes("{True: 1, True: 1}", expected)
47+
48+
def test_duplicate_keys_none(self):
49+
if version_info < (3, 4):
50+
expected = m.DuplicateVariableDictionaryKey
51+
else:
52+
expected = m.DuplicateDictionaryKey
53+
self.flakes("{None: 1, None: 1}", expected)
54+
55+
def test_duplicate_variable_keys(self):
56+
self.flakes('''
57+
a = 1
58+
{a: 2, a: 3}
59+
''', m.DuplicateVariableDictionaryKey)
60+
61+
def test_duplicate_key_float_and_int(self):
62+
self.flakes('''
63+
{1: 1, 1.0: 1}
64+
''', m.DuplicateDictionaryKey)
65+
66+
def test_no_duplicate_key_errors(self):
67+
self.flakes('''
68+
{'yes': 1, 'no': 1}
69+
''')
70+
71+
def test_no_duplicate_key_errors_func_call(self):
72+
self.flakes('''
73+
def test(thing):
74+
pass
75+
test({True: 1, None: 2, False: 1})
76+
''')
77+
78+
def test_no_duplicate_key_errors_bool_or_none(self):
79+
self.flakes("{True: 1, None: 2, False: 1}")
80+
81+
def test_no_duplicate_key_errors_ints(self):
82+
self.flakes('''
83+
{1: 1, 2: 1}
84+
''')
85+
86+
def test_no_duplicate_key_errors_vars(self):
87+
self.flakes('''
88+
test = 'yes'
89+
rest = 'yes'
90+
{test: 1, rest: 2}
91+
''')
92+
93+
def test_no_duplicate_key_errors_tuples(self):
94+
self.flakes('''
95+
{(0,1): 1, (0,2): 1}
96+
''')
97+
98+
def test_no_duplicate_key_errors_instance_attributes(self):
99+
self.flakes('''
100+
class Test():
101+
pass
102+
f = Test()
103+
f.a = 1
104+
{f.a: 1, f.a: 1}
105+
''')
106+
16107
def test_localReferencedBeforeAssignment(self):
17108
self.flakes('''
18109
a = 1

0 commit comments

Comments
 (0)