eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyNodeVisitor.py

changeset 8189
17df5c8df8c1
child 8191
9125da0c227e
equal deleted inserted replaced
8188:bfa6c0969acf 8189:17df5c8df8c1
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a node visitor checking for code that could be simplified.
8 """
9
10 import ast
11 import collections
12
13 try:
14 from ast import unparse
15 except ImportError:
16 # Python < 3.9
17 from .ast_unparse import unparse
18
19 ######################################################################
20 ## The following code is derived from the flake8-simplify package.
21 ##
22 ## Original License:
23 ##
24 ## MIT License
25 ##
26 ## Copyright (c) 2020 Martin Thoma
27 ##
28 ## Permission is hereby granted, free of charge, to any person obtaining a copy
29 ## of this software and associated documentation files (the "Software"), to
30 ## deal in the Software without restriction, including without limitation the
31 ## rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
32 ## sell copies of the Software, and to permit persons to whom the Software is
33 ## furnished to do so, subject to the following conditions:
34 ##
35 ## The above copyright notice and this permission notice shall be included in
36 ## all copies or substantial portions of the Software.
37 ##
38 ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39 ## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40 ## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41 ## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42 ## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
43 ## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
44 ## IN THE SOFTWARE.
45 ######################################################################
46
47 BOOL_CONST_TYPES = (ast.Constant, ast.NameConstant)
48 AST_CONST_TYPES = (ast.Constant, ast.NameConstant, ast.Str, ast.Num)
49 STR_TYPES = (ast.Constant, ast.Str)
50
51
52 class SimplifyNodeVisitor(ast.NodeVisitor):
53 """
54 Class to traverse the AST node tree and check for code that can be
55 simplified.
56 """
57 def __init__(self, errorCallback):
58 """
59 Constructor
60
61 @param errorCallback callback function to register an error
62 @type func
63 """
64 super(SimplifyNodeVisitor, self).__init__()
65
66 self.__error = errorCallback
67
68 def visit_Expr(self, node):
69 """
70 Public method to process an Expr node.
71
72 @param node reference to the Expr node
73 @type ast.Expr
74 """
75 self.__check112(node)
76
77 self.generic_visit(node)
78
79 def visit_BoolOp(self, node):
80 """
81 Public method to process a BoolOp node.
82
83 @param node reference to the BoolOp node
84 @type ast.BoolOp
85 """
86 self.__check101(node)
87 self.__check109(node)
88
89 self.generic_visit(node)
90
91 def visit_If(self, node):
92 """
93 Public method to process an If node.
94
95 @param node reference to the If node
96 @type ast.If
97 """
98 self.__check102(node)
99 self.__check103(node)
100 self.__check106(node)
101 self.__check108(node)
102
103 self.generic_visit(node)
104
105 def visit_For(self, node):
106 """
107 Public method to process a For node.
108
109 @param node reference to the For node
110 @type ast.For
111 """
112 self.__check104(node)
113 self.__check110_111(node)
114
115 self.generic_visit(node)
116
117 def visit_Try(self, node):
118 """
119 Public method to process a Try node.
120
121 @param node reference to the Try node
122 @type ast.Try
123 """
124 self.__check105(node)
125 self.__check107(node)
126
127 self.generic_visit(node)
128
129 #############################################################
130 ## Helper methods for the various checkers below
131 #############################################################
132
133 def __getDuplicatedIsinstanceCall(self, node):
134 """
135 Private method to get a list of isinstance arguments which could
136 be combined.
137
138 @param node reference to the AST node to be inspected
139 @type ast.BoolOp
140 @return list of variable names of duplicated isinstance calls
141 @rtype list of str
142 """
143 counter = collections.defaultdict(int)
144
145 for call in node.values:
146 # Ensure this is a call of the built-in isinstance() function.
147 if not isinstance(call, ast.Call) or len(call.args) != 2:
148 continue
149 functionName = unparse(call.func)
150 if functionName != "isinstance":
151 continue
152
153 arg0Name = unparse(call.args[0])
154 counter[arg0Name] += 1
155
156 return [name for name, count in counter.items() if count > 1]
157
158 #############################################################
159 ## Methods to check for possible code simplifications below
160 #############################################################
161
162 def __check101(self, node):
163 """
164 Private method to check for duplicate isinstance() calls.
165
166 @param node reference to the AST node to be checked
167 @type ast.BoolOp
168 """
169 if isinstance(node.op, ast.Or):
170 for variable in self.__getDuplicatedIsinstanceCall(node):
171 self.__error(node.lineno - 1, node.col_offset, "Y101",
172 variable)
173
174 def __check102(self, node):
175 """
176 Private method to check for nested if statements without else blocks.
177
178 @param node reference to the AST node to be checked
179 @type ast.If
180 """
181 # ## Pattern 1
182 # if a: <---
183 # if b: <---
184 # c
185 isPattern1 = (
186 node.orelse == [] and
187 len(node.body) == 1 and
188 isinstance(node.body[0], ast.If) and
189 node.body[0].orelse == []
190 )
191 # ## Pattern 2
192 # if a: < irrelvant for here
193 # pass
194 # elif b: <--- this is treated like a nested block
195 # if c: <---
196 # d
197 if isPattern1:
198 self.__error(node.lineno - 1, node.col_offset, "Y102")
199
200 def __check103(self, node):
201 """
202 Private method to check for calls that wrap a condition to return
203 a bool.
204
205 @param node reference to the AST node to be checked
206 @type ast.If
207 """
208 # if cond:
209 # return True
210 # else:
211 # return False
212 if not (
213 len(node.body) != 1 or
214 not isinstance(node.body[0], ast.Return) or
215 not isinstance(node.body[0].value, BOOL_CONST_TYPES) or
216 not (
217 node.body[0].value.value is True or
218 node.body[0].value.value is False
219 ) or
220 len(node.orelse) != 1 or
221 not isinstance(node.orelse[0], ast.Return) or
222 not isinstance(node.orelse[0].value, BOOL_CONST_TYPES) or
223 not (
224 node.orelse[0].value.value is True or
225 node.orelse[0].value.value is False
226 )
227 ):
228 condition = unparse(node.test)
229 self.__error(node.lineno - 1, node.col_offset, "Y103", condition)
230
231 def __check104(self, node):
232 """
233 Private method to check for "iterate and yield" patterns.
234
235 @param node reference to the AST node to be checked
236 @type ast.For
237 """
238 # for item in iterable:
239 # yield item
240 if not (
241 len(node.body) != 1 or
242 not isinstance(node.body[0], ast.Expr) or
243 not isinstance(node.body[0].value, ast.Yield) or
244 not isinstance(node.target, ast.Name) or
245 not isinstance(node.body[0].value.value, ast.Name) or
246 node.target.id != node.body[0].value.value.id or
247 node.orelse != []
248 ):
249 iterable = unparse(node.iter)
250 self.__error(node.lineno - 1, node.col_offset, "Y104", iterable)
251
252 def __check105(self, node):
253 """
254 Private method to check for "try-except-pass" patterns.
255
256 @param node reference to the AST node to be checked
257 @type ast.Try
258 """
259 # try:
260 # foo()
261 # except ValueError:
262 # pass
263 if not (
264 len(node.body) != 1 or
265 len(node.handlers) != 1 or
266 not isinstance(node.handlers[0], ast.ExceptHandler) or
267 len(node.handlers[0].body) != 1 or
268 not isinstance(node.handlers[0].body[0], ast.Pass) or
269 node.orelse != []
270 ):
271 if node.handlers[0].type is None:
272 exception = "Exception"
273 else:
274 exception = unparse(node.handlers[0].type)
275 self.__error(node.lineno - 1, node.col_offset, "Y105", exception)
276
277 def __check106(self, node):
278 """
279 Private method to check for calls where an exception is raised in else.
280
281 @param node reference to the AST node to be checked
282 @type ast.If
283 """
284 # if cond:
285 # return True
286 # else:
287 # raise Exception
288 just_one = (
289 len(node.body) == 1 and
290 len(node.orelse) >= 1 and
291 isinstance(node.orelse[-1], ast.Raise) and
292 not isinstance(node.body[-1], ast.Raise)
293 )
294 many = (
295 len(node.body) > 2 * len(node.orelse) and
296 len(node.orelse) >= 1 and
297 isinstance(node.orelse[-1], ast.Raise) and
298 not isinstance(node.body[-1], ast.Raise)
299 )
300 if just_one or many:
301 self.__error(node.lineno - 1, node.col_offset, "Y106")
302
303 def __check107(self, node):
304 """
305 Private method to check for calls where try/except and finally have
306 'return'.
307
308 @param node reference to the AST node to be checked
309 @type ast.Try
310 """
311 # def foo():
312 # try:
313 # 1 / 0
314 # return "1"
315 # except:
316 # return "2"
317 # finally:
318 # return "3"
319 tryHasReturn = False
320 for stmt in node.body:
321 if isinstance(stmt, ast.Return):
322 tryHasReturn = True
323 break
324
325 exceptHasReturn = False
326 for stmt2 in node.handlers:
327 if isinstance(stmt2, ast.Return):
328 exceptHasReturn = True
329 break
330
331 finallyHasReturn = False
332 finallyReturn = None
333 for stmt in node.finalbody:
334 if isinstance(stmt, ast.Return):
335 finallyHasReturn = True
336 finallyReturn = stmt
337 break
338
339 if (tryHasReturn or exceptHasReturn) and finallyHasReturn:
340 if finallyReturn is not None:
341 self.__error(finallyReturn.lineno - 1,
342 finallyReturn.col_offset, "Y107")
343
344 def __check108(self, node):
345 """
346 Private method to check for if-elses which could be a ternary
347 operator assignment.
348
349 @param node reference to the AST node to be checked
350 @type ast.If
351 """
352 # if a:
353 # b = c
354 # else:
355 # b = d
356 if (
357 len(node.body) == 1 and
358 len(node.orelse) == 1 and
359 isinstance(node.body[0], ast.Assign) and
360 isinstance(node.orelse[0], ast.Assign) and
361 len(node.body[0].targets) == 1 and
362 len(node.orelse[0].targets) == 1 and
363 isinstance(node.body[0].targets[0], ast.Name) and
364 isinstance(node.orelse[0].targets[0], ast.Name) and
365 node.body[0].targets[0].id == node.orelse[0].targets[0].id
366 ):
367 assign = unparse(node.body[0].targets[0])
368 body = unparse(node.body[0].value)
369 cond = unparse(node.test)
370 orelse = unparse(node.orelse[0].value)
371 self.__error(node.lineno - 1, node.col_offset, "Y108",
372 assign, body, cond, orelse)
373
374 def __check109(self, node):
375 """
376 Private method to check for multiple equalities with the same value
377 are combined via "or".
378
379 @param node reference to the AST node to be checked
380 @type ast.BoolOp
381 """
382 # if a == b or a == c:
383 # d
384 if isinstance(node.op, ast.Or):
385 equalities = [
386 value
387 for value in node.values
388 if isinstance(value, ast.Compare) and
389 len(value.ops) == 1 and
390 isinstance(value.ops[0], ast.Eq)
391 ]
392 ids = [] # (name, compared_to)
393 for eq in equalities:
394 if isinstance(eq.left, ast.Name):
395 ids.append((eq.left, eq.comparators[0]))
396 if (
397 len(eq.comparators) == 1 and
398 isinstance(eq.comparators[0], ast.Name)
399 ):
400 ids.append((eq.comparators[0], eq.left))
401
402 id2count = {}
403 for identifier, comparedTo in ids:
404 if identifier.id not in id2count:
405 id2count[identifier.id] = []
406 id2count[identifier.id].append(comparedTo)
407 for value, values in id2count.items():
408 if len(values) == 1:
409 continue
410
411 self.__error(node.lineno - 1, node.col_offset, "Y109",
412 value, unparse(ast.List(elts=values)),
413 unparse(node))
414
415 def __check110_111(self, node):
416 """
417 Private method to check if any / all could be used.
418
419 @param node reference to the AST node to be checked
420 @type ast.For
421 """
422 # for x in iterable:
423 # if check(x):
424 # return True
425 # return False
426 #
427 # for x in iterable:
428 # if check(x):
429 # return False
430 # return True
431 if (
432 len(node.body) == 1
433 and isinstance(node.body[0], ast.If)
434 and len(node.body[0].body) == 1
435 and isinstance(node.body[0].body[0], ast.Return)
436 and isinstance(node.body[0].body[0].value, BOOL_CONST_TYPES)
437 and hasattr(node.body[0].body[0].value, "value")
438 ):
439 check = unparse(node.body[0].test)
440 target = unparse(node.target)
441 iterable = unparse(node.iter)
442 if node.body[0].body[0].value.value is True:
443 self.__error(node.lineno - 1, node.col_offset, "Y110",
444 check, target, iterable)
445 elif node.body[0].body[0].value.value is False:
446 check = "not " + check
447 if check.startswith("not not"):
448 check = check[len("not not "):]
449 self.__error(node.lineno - 1, node.col_offset, "Y111",
450 check, target, iterable)
451
452 def __check112(self, node):
453 """
454 Public method to check for non-capitalized calls to environment
455 variables.
456
457 @param node reference to the AST node to be checked
458 @type ast.Expr
459 """
460 # os.environ["foo"]
461 # os.environ.get("bar")
462 isIndexCall = (
463 isinstance(node.value, ast.Subscript)
464 and isinstance(node.value.value, ast.Attribute)
465 and isinstance(node.value.value.value, ast.Name)
466 and node.value.value.value.id == "os"
467 and node.value.value.attr == "environ"
468 and (
469 (
470 isinstance(node.value.slice, ast.Index)
471 and isinstance(node.value.slice.value, STR_TYPES)
472 )
473 or isinstance(node.value.slice, ast.Constant)
474 )
475 )
476 if isIndexCall:
477 subscript = node.value
478 slice_ = subscript.slice
479 if isinstance(slice_, ast.Index):
480 # Python < 3.9
481 stringPart = slice_.value # type: ignore
482 if isinstance(stringPart, ast.Str):
483 envName = stringPart.s # Python 3.6 / 3.7 fallback
484 else:
485 envName = stringPart.value
486 elif isinstance(slice_, ast.Constant):
487 # Python 3.9
488 envName = slice_.value
489
490 # Check if this has a change
491 hasChange = envName != envName.upper()
492
493 isGetCall = (
494 isinstance(node.value, ast.Call)
495 and isinstance(node.value.func, ast.Attribute)
496 and isinstance(node.value.func.value, ast.Attribute)
497 and isinstance(node.value.func.value.value, ast.Name)
498 and node.value.func.value.value.id == "os"
499 and node.value.func.value.attr == "environ"
500 and node.value.func.attr == "get"
501 and len(node.value.args) in [1, 2]
502 and isinstance(node.value.args[0], STR_TYPES)
503 )
504 if isGetCall:
505 call = node.value
506 stringPart = call.args[0]
507 if isinstance(stringPart, ast.Str):
508 envName = stringPart.s # Python 3.6 / 3.7 fallback
509 else:
510 envName = stringPart.value
511 # Check if this has a change
512 hasChange = envName != envName.upper()
513 if not (isIndexCall or isGetCall) or not hasChange:
514 return
515 if isIndexCall:
516 original = unparse(node)
517 expected = f"os.environ['{envName.upper()}']"
518 elif isGetCall:
519 original = unparse(node)
520 if len(node.value.args) == 1:
521 expected = f"os.environ.get('{envName.upper()}')"
522 else:
523 defaultValue = unparse(node.value.args[1])
524 expected = (
525 f"os.environ.get('{envName.upper()}', '{defaultValue}')"
526 )
527 else:
528 return
529
530 self.__error(node.lineno - 1, node.col_offset, "Y112", expected,
531 original)
532
533 #
534 # eflag: noqa = M891

eric ide

mercurial