1
1
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
2
2
# For details: https://github.com/PyCQA/pylint/blob/master/LICENSE
3
+ from typing import cast
3
4
4
5
import astroid
5
6
@@ -26,6 +27,14 @@ class RecommendationChecker(checkers.BaseChecker):
26
27
"method. It is enough to just iterate through the dictionary itself, as "
27
28
'in "for key in dictionary".' ,
28
29
),
30
+ "C0206" : (
31
+ "Consider iterating with .items()" ,
32
+ "consider-using-dict-items" ,
33
+ "Emitted when iterating over the keys of a dictionary and accessing the "
34
+ "value by index lookup."
35
+ "Both the key and value can be accessed by iterating using the .items() "
36
+ "method of the dictionary instead." ,
37
+ ),
29
38
}
30
39
31
40
@staticmethod
@@ -53,8 +62,12 @@ def visit_call(self, node):
53
62
if isinstance (node .parent , (astroid .For , astroid .Comprehension )):
54
63
self .add_message ("consider-iterating-dictionary" , node = node )
55
64
56
- @utils .check_messages ("consider-using-enumerate" )
57
- def visit_for (self , node ):
65
+ @utils .check_messages ("consider-using-enumerate" , "consider-using-dict-items" )
66
+ def visit_for (self , node : astroid .For ) -> None :
67
+ self ._check_consider_using_enumerate (node )
68
+ self ._check_consider_using_dict_items (node )
69
+
70
+ def _check_consider_using_enumerate (self , node : astroid .For ) -> None :
58
71
"""Emit a convention whenever range and len are used for indexing."""
59
72
# Verify that we have a `range([start], len(...), [stop])` call and
60
73
# that the object which is iterated is used as a subscript in the
@@ -119,3 +132,81 @@ def visit_for(self, node):
119
132
continue
120
133
self .add_message ("consider-using-enumerate" , node = node )
121
134
return
135
+
136
+ def _check_consider_using_dict_items (self , node : astroid .For ) -> None :
137
+ """Add message when accessing dict values by index lookup."""
138
+ # Verify that we have a .keys() call and
139
+ # that the object which is iterated is used as a subscript in the
140
+ # body of the for.
141
+
142
+ iterating_object_name = utils .get_iterating_dictionary_name (node )
143
+ if iterating_object_name is None :
144
+ return
145
+
146
+ # Verify that the body of the for loop uses a subscript
147
+ # with the object that was iterated. This uses some heuristics
148
+ # in order to make sure that the same object is used in the
149
+ # for body.
150
+ for child in node .body :
151
+ for subscript in child .nodes_of_class (astroid .Subscript ):
152
+ subscript = cast (astroid .Subscript , subscript )
153
+
154
+ if not isinstance (subscript .value , (astroid .Name , astroid .Attribute )):
155
+ continue
156
+
157
+ value = subscript .slice
158
+ if isinstance (value , astroid .Index ):
159
+ value = value .value
160
+ if (
161
+ not isinstance (value , astroid .Name )
162
+ or value .name != node .target .name
163
+ or iterating_object_name != subscript .value .as_string ()
164
+ ):
165
+ continue
166
+ last_definition_lineno = value .lookup (value .name )[1 ][- 1 ].lineno
167
+ if last_definition_lineno > node .lineno :
168
+ # Ignore this subscript if it has been redefined after
169
+ # the for loop. This checks for the line number using .lookup()
170
+ # to get the line number where the iterating object was last
171
+ # defined and compare that to the for loop's line number
172
+ continue
173
+ if (
174
+ isinstance (subscript .parent , astroid .Assign )
175
+ and subscript in subscript .parent .targets
176
+ or isinstance (subscript .parent , astroid .AugAssign )
177
+ and subscript == subscript .parent .target
178
+ ):
179
+ # Ignore this subscript if it is the target of an assignment
180
+ continue
181
+
182
+ self .add_message ("consider-using-dict-items" , node = node )
183
+ return
184
+
185
+ @utils .check_messages ("consider-using-dict-items" )
186
+ def visit_comprehension (self , node : astroid .Comprehension ) -> None :
187
+ iterating_object_name = utils .get_iterating_dictionary_name (node )
188
+ if iterating_object_name is None :
189
+ return
190
+
191
+ children = list (node .parent .get_children ())
192
+ if node .ifs :
193
+ children .extend (node .ifs )
194
+ for child in children :
195
+ for subscript in child .nodes_of_class (astroid .Subscript ):
196
+ subscript = cast (astroid .Subscript , subscript )
197
+
198
+ if not isinstance (subscript .value , (astroid .Name , astroid .Attribute )):
199
+ continue
200
+
201
+ value = subscript .slice
202
+ if isinstance (value , astroid .Index ):
203
+ value = value .value
204
+ if (
205
+ not isinstance (value , astroid .Name )
206
+ or value .name != node .target .name
207
+ or iterating_object_name != subscript .value .as_string ()
208
+ ):
209
+ continue
210
+
211
+ self .add_message ("consider-using-dict-items" , node = node )
212
+ return
0 commit comments