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

changeset 8191
9125da0c227e
parent 8189
17df5c8df8c1
child 8192
e1157bd8b4c2
equal deleted inserted replaced
8189:17df5c8df8c1 8191:9125da0c227e
7 Module implementing a node visitor checking for code that could be simplified. 7 Module implementing a node visitor checking for code that could be simplified.
8 """ 8 """
9 9
10 import ast 10 import ast
11 import collections 11 import collections
12 import itertools
12 13
13 try: 14 try:
14 from ast import unparse 15 from ast import unparse
15 except ImportError: 16 except ImportError:
16 # Python < 3.9 17 # Python < 3.9
97 """ 98 """
98 self.__check102(node) 99 self.__check102(node)
99 self.__check103(node) 100 self.__check103(node)
100 self.__check106(node) 101 self.__check106(node)
101 self.__check108(node) 102 self.__check108(node)
103 self.__check114(node)
104 self.__check116(node)
102 105
103 self.generic_visit(node) 106 self.generic_visit(node)
104 107
105 def visit_For(self, node): 108 def visit_For(self, node):
106 """ 109 """
109 @param node reference to the For node 112 @param node reference to the For node
110 @type ast.For 113 @type ast.For
111 """ 114 """
112 self.__check104(node) 115 self.__check104(node)
113 self.__check110_111(node) 116 self.__check110_111(node)
117 self.__check113(node)
114 118
115 self.generic_visit(node) 119 self.generic_visit(node)
116 120
117 def visit_Try(self, node): 121 def visit_Try(self, node):
118 """ 122 """
121 @param node reference to the Try node 125 @param node reference to the Try node
122 @type ast.Try 126 @type ast.Try
123 """ 127 """
124 self.__check105(node) 128 self.__check105(node)
125 self.__check107(node) 129 self.__check107(node)
130
131 self.generic_visit(node)
132
133 def visit_Call(self, node):
134 """
135 Public method to process a Call node.
136
137 @param node reference to the Call node
138 @type ast.Call
139 """
140 self.__check115(node)
126 141
127 self.generic_visit(node) 142 self.generic_visit(node)
128 143
129 ############################################################# 144 #############################################################
130 ## Helper methods for the various checkers below 145 ## Helper methods for the various checkers below
152 167
153 arg0Name = unparse(call.args[0]) 168 arg0Name = unparse(call.args[0])
154 counter[arg0Name] += 1 169 counter[arg0Name] += 1
155 170
156 return [name for name, count in counter.items() if count > 1] 171 return [name for name, count in counter.items() if count > 1]
172
173 def __isConstantIncrease(self, expression):
174 """
175 Private method check the given expression for being a constant
176 increase.
177
178 @param expression reference to the expression node
179 @type ast.AugAssign
180 @return flag indicating a constant increase
181 @rtype bool
182 """
183 return (
184 isinstance(expression.op, ast.Add) and
185 isinstance(expression.value, (ast.Constant, ast.Num))
186 )
187
188 def __getIfBodyPairs(self, node):
189 """
190 Private method to extract a list of pairs of test and body for an
191 If node.
192
193 @param node reference to the If node to be processed
194 @type ast.If
195 @return list of pairs of test and body
196 @rtype list of tuples of (ast.expr, [ast.stmt])
197 """
198 pairs = [(node.test, node.body)]
199 orelse = node.orelse
200 while (
201 isinstance(orelse, list) and
202 len(orelse) == 1 and
203 isinstance(orelse[0], ast.If)
204 ):
205 pairs.append((orelse[0].test, orelse[0].body))
206 orelse = orelse[0].orelse
207 return pairs
208
209 def __isSameBody(self, body1, body2):
210 """
211 Private method check, if the given bodies are equivalent.
212
213 @param body1 list of statements of the first body
214 @type list of ast.stmt
215 @param body2 list of statements of the second body
216 @type list of ast.stmt
217 @return flag indicating identical bodies
218 @rtype bool
219 """
220 if len(body1) != len(body2):
221 return False
222 for a, b in zip(body1, body2):
223 try:
224 statementEqual = self.__isStatementEqual(a, b)
225 except RecursionError: # maximum recursion depth
226 statementEqual = False
227 if not statementEqual:
228 return False
229 return True
230
231 def __isStatementEqual(self, a: ast.stmt, b: ast.stmt) -> bool:
232 """
233 Private method to check, if two statements are equal.
234
235 @param a reference to the first statement
236 @type ast.stmt
237 @param b reference to the second statement
238 @type ast.stmt
239 @return flag indicating if the two statements are equal
240 @rtype bool
241 """
242 if type(a) is not type(b):
243 return False
244
245 if isinstance(a, ast.AST):
246 for k, v in vars(a).items():
247 if k in ("lineno", "col_offset", "ctx", "end_lineno",
248 "parent"):
249 continue
250 if not self.__isStatementEqual(v, getattr(b, k)):
251 return False
252 return True
253 elif isinstance(a, list):
254 return all(itertools.starmap(self.__isStatementEqual, zip(a, b)))
255 else:
256 return a == b
157 257
158 ############################################################# 258 #############################################################
159 ## Methods to check for possible code simplifications below 259 ## Methods to check for possible code simplifications below
160 ############################################################# 260 #############################################################
161 261
427 # for x in iterable: 527 # for x in iterable:
428 # if check(x): 528 # if check(x):
429 # return False 529 # return False
430 # return True 530 # return True
431 if ( 531 if (
432 len(node.body) == 1 532 len(node.body) == 1 and
433 and isinstance(node.body[0], ast.If) 533 isinstance(node.body[0], ast.If) and
434 and len(node.body[0].body) == 1 534 len(node.body[0].body) == 1 and
435 and isinstance(node.body[0].body[0], ast.Return) 535 isinstance(node.body[0].body[0], ast.Return) and
436 and isinstance(node.body[0].body[0].value, BOOL_CONST_TYPES) 536 isinstance(node.body[0].body[0].value, BOOL_CONST_TYPES) and
437 and hasattr(node.body[0].body[0].value, "value") 537 hasattr(node.body[0].body[0].value, "value")
438 ): 538 ):
439 check = unparse(node.body[0].test) 539 check = unparse(node.body[0].test)
440 target = unparse(node.target) 540 target = unparse(node.target)
441 iterable = unparse(node.iter) 541 iterable = unparse(node.iter)
442 if node.body[0].body[0].value.value is True: 542 if node.body[0].body[0].value.value is True:
449 self.__error(node.lineno - 1, node.col_offset, "Y111", 549 self.__error(node.lineno - 1, node.col_offset, "Y111",
450 check, target, iterable) 550 check, target, iterable)
451 551
452 def __check112(self, node): 552 def __check112(self, node):
453 """ 553 """
454 Public method to check for non-capitalized calls to environment 554 Private method to check for non-capitalized calls to environment
455 variables. 555 variables.
456 556
457 @param node reference to the AST node to be checked 557 @param node reference to the AST node to be checked
458 @type ast.Expr 558 @type ast.Expr
459 """ 559 """
460 # os.environ["foo"] 560 # os.environ["foo"]
461 # os.environ.get("bar") 561 # os.environ.get("bar")
462 isIndexCall = ( 562 isIndexCall = (
463 isinstance(node.value, ast.Subscript) 563 isinstance(node.value, ast.Subscript) and
464 and isinstance(node.value.value, ast.Attribute) 564 isinstance(node.value.value, ast.Attribute) and
465 and isinstance(node.value.value.value, ast.Name) 565 isinstance(node.value.value.value, ast.Name) and
466 and node.value.value.value.id == "os" 566 node.value.value.value.id == "os" and
467 and node.value.value.attr == "environ" 567 node.value.value.attr == "environ" and
468 and ( 568 (
469 ( 569 (
470 isinstance(node.value.slice, ast.Index) 570 isinstance(node.value.slice, ast.Index) and
471 and isinstance(node.value.slice.value, STR_TYPES) 571 isinstance(node.value.slice.value, STR_TYPES)
472 ) 572 ) or
473 or isinstance(node.value.slice, ast.Constant) 573 isinstance(node.value.slice, ast.Constant)
474 ) 574 )
475 ) 575 )
476 if isIndexCall: 576 if isIndexCall:
477 subscript = node.value 577 subscript = node.value
478 slice_ = subscript.slice 578 slice_ = subscript.slice
489 589
490 # Check if this has a change 590 # Check if this has a change
491 hasChange = envName != envName.upper() 591 hasChange = envName != envName.upper()
492 592
493 isGetCall = ( 593 isGetCall = (
494 isinstance(node.value, ast.Call) 594 isinstance(node.value, ast.Call) and
495 and isinstance(node.value.func, ast.Attribute) 595 isinstance(node.value.func, ast.Attribute) and
496 and isinstance(node.value.func.value, ast.Attribute) 596 isinstance(node.value.func.value, ast.Attribute) and
497 and isinstance(node.value.func.value.value, ast.Name) 597 isinstance(node.value.func.value.value, ast.Name) and
498 and node.value.func.value.value.id == "os" 598 node.value.func.value.value.id == "os" and
499 and node.value.func.value.attr == "environ" 599 node.value.func.value.attr == "environ" and
500 and node.value.func.attr == "get" 600 node.value.func.attr == "get" and
501 and len(node.value.args) in [1, 2] 601 len(node.value.args) in [1, 2] and
502 and isinstance(node.value.args[0], STR_TYPES) 602 isinstance(node.value.args[0], STR_TYPES)
503 ) 603 )
504 if isGetCall: 604 if isGetCall:
505 call = node.value 605 call = node.value
506 stringPart = call.args[0] 606 stringPart = call.args[0]
507 if isinstance(stringPart, ast.Str): 607 if isinstance(stringPart, ast.Str):
527 else: 627 else:
528 return 628 return
529 629
530 self.__error(node.lineno - 1, node.col_offset, "Y112", expected, 630 self.__error(node.lineno - 1, node.col_offset, "Y112", expected,
531 original) 631 original)
632
633 def __check113(self, node):
634 """
635 Private method to check for loops in which "enumerate" should be
636 used.
637
638 @param node reference to the AST node to be checked
639 @type ast.For
640 """
641 # idx = 0
642 # for el in iterable:
643 # ...
644 # idx += 1
645 variableCandidates = []
646 for expression in node.body:
647 if (
648 isinstance(expression, ast.AugAssign) and
649 self.__isConstantIncrease(expression) and
650 isinstance(expression.target, ast.Name)
651 ):
652 variableCandidates.append(expression.target)
653
654 for candidate in variableCandidates:
655 self.__error(candidate.lineno - 1, candidate.col_offset, "Y113",
656 unparse(candidate))
657
658 def __check114(self, node):
659 """
660 Private method to check for alternative if clauses with identical
661 bodies.
662
663 @param node reference to the AST node to be checked
664 @type ast.If
665 """
666 # if a:
667 # b
668 # elif c:
669 # b
670 ifBodyPairs = self.__getIfBodyPairs(node)
671 errorPairs = []
672 for ifbody1, ifbody2 in itertools.combinations(ifBodyPairs, 2):
673 if self.__isSameBody(ifbody1[1], ifbody2[1]):
674 errorPairs.append((ifbody1, ifbody2))
675 for ifbody1, ifbody2 in errorPairs:
676 self.__error(ifbody1[0].lineno - 1, ifbody1[0].col_offset, "Y114",
677 unparse(ifbody1[0]), unparse(ifbody2[0]))
678
679 def __check115(self, node):
680 """
681 Private method to to check for places where open() is called without
682 a context handler.
683
684 @param node reference to the AST node to be checked
685 @type ast.Call
686 """
687 # f = open(...)
688 #. .. # (do something with f)
689 # f.close()
690 if (
691 isinstance(node.func, ast.Name) and
692 node.func.id == "open" and
693 not isinstance(node.parent, ast.withitem)
694 ):
695 self.__error(node.lineno - 1, node.col_offset, "Y115")
696
697 def __check116(self, node):
698 """
699 Private method to check for places with 3 or more consecutive
700 if-statements with direct returns.
701
702 * Each if-statement must be a check for equality with the
703 same variable
704 * Each if-statement must just have a "return"
705 * Else must also just have a return
706
707 @param node reference to the AST node to be checked
708 @type ast.If
709 """
710 # if a == "foo":
711 # return "bar"
712 # elif a == "bar":
713 # return "baz"
714 # elif a == "boo":
715 # return "ooh"
716 # else:
717 # return 42
718 if (
719 isinstance(node.test, ast.Compare) and
720 isinstance(node.test.left, ast.Name) and
721 len(node.test.ops) == 1 and
722 isinstance(node.test.ops[0], ast.Eq) and
723 len(node.test.comparators) == 1 and
724 isinstance(node.test.comparators[0], AST_CONST_TYPES) and
725 len(node.body) == 1 and
726 isinstance(node.body[0], ast.Return) and
727 len(node.orelse) == 1 and
728 isinstance(node.orelse[0], ast.If)
729 ):
730 variable = node.test.left
731 child = node.orelse[0]
732 elseValue = None
733 if isinstance(node.test.comparators[0], ast.Str):
734 keyValuePairs = {
735 node.test.comparators[0].s:
736 unparse(node.body[0].value).strip("'")
737 }
738 elif isinstance(node.test.comparators[0], ast.Num):
739 keyValuePairs = {
740 node.test.comparators[0].n:
741 unparse(node.body[0].value).strip("'")
742 }
743 else:
744 keyValuePairs = {
745 node.test.comparators[0].value:
746 unparse(node.body[0].value).strip("'")
747 }
748 while child:
749 if not (
750 isinstance(child.test, ast.Compare) and
751 isinstance(child.test.left, ast.Name) and
752 child.test.left.id == variable.id and
753 len(child.test.ops) == 1 and
754 isinstance(child.test.ops[0], ast.Eq) and
755 len(child.test.comparators) == 1 and
756 isinstance(child.test.comparators[0], AST_CONST_TYPES) and
757 len(child.body) == 1 and
758 isinstance(child.body[0], ast.Return) and
759 len(child.orelse) <= 1
760 ):
761 return
762
763 if isinstance(child.test.comparators[0], ast.Str):
764 key = child.test.comparators[0].s
765 elif isinstance(child.test.comparators[0], ast.Num):
766 key = child.test.comparators[0].n
767 else:
768 key = child.test.comparators[0].value
769 keyValuePairs[key] = unparse(child.body[0].value).strip("'")
770 if len(child.orelse) == 1:
771 if isinstance(child.orelse[0], ast.If):
772 child = child.orelse[0]
773 elif isinstance(child.orelse[0], ast.Return):
774 elseValue = unparse(child.orelse[0].value)
775 child = None
776 else:
777 return
778 else:
779 child = None
780
781 if len(keyValuePairs) < 3:
782 return
783
784 if elseValue:
785 ret = f"{keyValuePairs}.get({variable.id}, {elseValue})"
786 else:
787 ret = f"{keyValuePairs}.get({variable.id})"
788
789 self.__error(node.lineno - 1, node.col_offset, "Y116", ret)
532 790
533 # 791 #
534 # eflag: noqa = M891 792 # eflag: noqa = M891

eric ide

mercurial