@@ -26,6 +26,14 @@ class RecommendationChecker(checkers.BaseChecker):
26
26
"method. It is enough to just iterate through the dictionary itself, as "
27
27
'in "for key in dictionary".' ,
28
28
),
29
+ "C0206" : (
30
+ "Consider iterating with .items()" ,
31
+ "consider-using-dict-items" ,
32
+ "Emitted when the keys of a dictionary are iterated and the items of the "
33
+ "dictionary is accessed by indexing with the key in each iteration. "
34
+ "Both the key and value can be accessed by iterating using the .items() "
35
+ "method of the dictionary instead." ,
36
+ ),
29
37
}
30
38
31
39
@staticmethod
@@ -53,8 +61,12 @@ def visit_call(self, node):
53
61
if isinstance (node .parent , (astroid .For , astroid .Comprehension )):
54
62
self .add_message ("consider-iterating-dictionary" , node = node )
55
63
56
- @utils .check_messages ("consider-using-enumerate" )
64
+ @utils .check_messages ("consider-using-enumerate" , "consider-using-dict-items" )
57
65
def visit_for (self , node ):
66
+ self ._check_consider_using_enumerate (node )
67
+ self ._check_consider_using_dict_items (node )
68
+
69
+ def _check_consider_using_enumerate (self , node ):
58
70
"""Emit a convention whenever range and len are used for indexing."""
59
71
# Verify that we have a `range([start], len(...), [stop])` call and
60
72
# that the object which is iterated is used as a subscript in the
@@ -119,3 +131,60 @@ def visit_for(self, node):
119
131
continue
120
132
self .add_message ("consider-using-enumerate" , node = node )
121
133
return
134
+
135
+ def _check_consider_using_dict_items (self , node ):
136
+ """Emit a convention whenever range and len are used for indexing."""
137
+ # Verify that we have a .keys() call and
138
+ # that the object which is iterated is used as a subscript in the
139
+ # body of the for.
140
+
141
+ if not isinstance (node .iter , astroid .Call ):
142
+ # Is it a dictionary?
143
+ if not isinstance (node .iter , astroid .Name ):
144
+ return
145
+ inferred = utils .safe_infer (node .iter )
146
+ if not isinstance (inferred , astroid .Dict ) and not isinstance (
147
+ inferred , astroid .DictComp
148
+ ):
149
+ return
150
+ iterating_object_name = node .iter .as_string ()
151
+
152
+ else :
153
+ # Is it a proper keys call?
154
+ if isinstance (node .iter .func , astroid .Name ):
155
+ return
156
+ if node .iter .func .attrname != "keys" :
157
+ return
158
+ inferred = utils .safe_infer (node .iter .func )
159
+ if not isinstance (inferred , astroid .BoundMethod ) or not isinstance (
160
+ inferred .bound , astroid .Dict
161
+ ):
162
+ return
163
+ iterating_object_name = node .iter .as_string ().split ("." )[0 ]
164
+
165
+ # Verify that the body of the for loop uses a subscript
166
+ # with the object that was iterated. This uses some heuristics
167
+ # in order to make sure that the same object is used in the
168
+ # for body.
169
+ for child in node .body :
170
+ for subscript in child .nodes_of_class (astroid .Subscript ):
171
+ if not isinstance (subscript .value , astroid .Name ):
172
+ continue
173
+
174
+ value = subscript .slice
175
+ if isinstance (value , astroid .Index ):
176
+ value = value .value
177
+ if not isinstance (value , astroid .Name ):
178
+ continue
179
+ if value .name != node .target .name :
180
+ continue
181
+ if iterating_object_name != subscript .value .name :
182
+ continue
183
+ if subscript .value .scope () != node .scope ():
184
+ # Ignore this subscript if it's not in the same
185
+ # scope. This means that in the body of the for
186
+ # loop, another scope was created, where the same
187
+ # name for the iterating object was used.
188
+ continue
189
+ self .add_message ("consider-using-dict-items" , node = node )
190
+ return
0 commit comments