src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/ComplexityChecker.py

branch
eric7
changeset 11150
73d80859079c
parent 11147
dee6e106b4d3
equal deleted inserted replaced
11149:fc45672fae42 11150:73d80859079c
6 """ 6 """
7 Module implementing a checker for code complexity. 7 Module implementing a checker for code complexity.
8 """ 8 """
9 9
10 import ast 10 import ast
11 import copy 11
12 from CodeStyleTopicChecker import CodeStyleTopicChecker
12 13
13 from .mccabe import PathGraphingAstVisitor 14 from .mccabe import PathGraphingAstVisitor
14 15
15 16
16 class ComplexityChecker: 17 class ComplexityChecker(CodeStyleTopicChecker):
17 """ 18 """
18 Class implementing a checker for code complexity. 19 Class implementing a checker for code complexity.
19 """ 20 """
20 21
21 Codes = [ 22 Codes = [
22 "C-101", 23 "C-101",
23 "C-111", 24 "C-111",
24 "C-112", 25 "C-112",
25 ] 26 ]
27 Category = "C"
26 28
27 def __init__(self, source, filename, tree, select, ignore, args): 29 def __init__(self, source, filename, tree, select, ignore, args):
28 """ 30 """
29 Constructor 31 Constructor
30 32
39 @param ignore list of codes to be ignored 41 @param ignore list of codes to be ignored
40 @type list of str 42 @type list of str
41 @param args dictionary of arguments for the miscellaneous checks 43 @param args dictionary of arguments for the miscellaneous checks
42 @type dict 44 @type dict
43 """ 45 """
44 self.__filename = filename 46 super().__init__(
45 self.__source = source[:] 47 ComplexityChecker.Category,
46 self.__tree = copy.deepcopy(tree) 48 source,
47 self.__select = tuple(select) 49 filename,
48 self.__ignore = tuple(ignore) 50 tree,
49 self.__args = args 51 select,
52 ignore,
53 [],
54 True,
55 args,
56 )
50 57
51 self.__defaultArgs = { 58 self.__defaultArgs = {
52 "McCabeComplexity": 10, 59 "McCabeComplexity": 10,
53 "LineComplexity": 15, 60 "LineComplexity": 15,
54 "LineComplexityScore": 10, 61 "LineComplexityScore": 10,
55 } 62 }
56 63
57 # statistics counters
58 self.counters = {}
59
60 # collection of detected errors
61 self.errors = []
62
63 checkersWithCodes = [ 64 checkersWithCodes = [
64 (self.__checkMcCabeComplexity, ("C-101",)), 65 (self.__checkMcCabeComplexity, ("C-101",)),
65 (self.__checkLineComplexity, ("C-111", "C-112")), 66 (self.__checkLineComplexity, ("C-111", "C-112")),
66 ] 67 ]
67 68 self._initializeCheckers(checkersWithCodes)
68 self.__checkers = []
69 for checker, codes in checkersWithCodes:
70 if any(not (code and self.__ignoreCode(code)) for code in codes):
71 self.__checkers.append(checker)
72
73 def __ignoreCode(self, code):
74 """
75 Private method to check if the message code should be ignored.
76
77 @param code message code to check for
78 @type str
79 @return flag indicating to ignore the given code
80 @rtype bool
81 """
82 return code in self.__ignore or (
83 code.startswith(self.__ignore) and not code.startswith(self.__select)
84 )
85
86 def __error(self, lineNumber, offset, code, *args):
87 """
88 Private method to record an issue.
89
90 @param lineNumber line number of the issue
91 @type int
92 @param offset position within line of the issue
93 @type int
94 @param code message code
95 @type str
96 @param args arguments for the message
97 @type list
98 """
99 if self.__ignoreCode(code):
100 return
101
102 if code in self.counters:
103 self.counters[code] += 1
104 else:
105 self.counters[code] = 1
106
107 if code:
108 # record the issue with one based line number
109 self.errors.append(
110 {
111 "file": self.__filename,
112 "line": lineNumber,
113 "offset": offset,
114 "code": code,
115 "args": args,
116 }
117 )
118
119 def run(self):
120 """
121 Public method to check the given source for code complexity.
122 """
123 if not self.__filename or not self.__source:
124 # don't do anything, if essential data is missing
125 return
126
127 if not self.__checkers:
128 # don't do anything, if no codes were selected
129 return
130
131 for check in self.__checkers:
132 check()
133 69
134 def __checkMcCabeComplexity(self): 70 def __checkMcCabeComplexity(self):
135 """ 71 """
136 Private method to check the McCabe code complexity. 72 Private method to check the McCabe code complexity.
137 """ 73 """
138 try: 74 try:
139 # create the AST again because it is modified by the checker 75 # create the AST again because it is modified by the checker
140 tree = compile( 76 tree = compile(
141 "".join(self.__source), self.__filename, "exec", ast.PyCF_ONLY_AST 77 "".join(self.source), self.filename, "exec", ast.PyCF_ONLY_AST
142 ) 78 )
143 except (SyntaxError, TypeError): 79 except (SyntaxError, TypeError):
144 # compile errors are already reported by the run() method 80 # compile errors are already reported by the run() method
145 return 81 return
146 82
147 maxComplexity = self.__args.get( 83 maxComplexity = self.args.get(
148 "McCabeComplexity", self.__defaultArgs["McCabeComplexity"] 84 "McCabeComplexity", self.__defaultArgs["McCabeComplexity"]
149 ) 85 )
150 86
151 visitor = PathGraphingAstVisitor() 87 visitor = PathGraphingAstVisitor()
152 visitor.preorder(tree, visitor) 88 visitor.preorder(tree, visitor)
153 for graph in visitor.graphs.values(): 89 for graph in visitor.graphs.values():
154 if graph.complexity() > maxComplexity: 90 if graph.complexity() > maxComplexity:
155 self.__error(graph.lineno, 0, "C-101", graph.entity, graph.complexity()) 91 self.addError(
92 graph.lineno + 1, 0, "C-101", graph.entity, graph.complexity()
93 )
156 94
157 def __checkLineComplexity(self): 95 def __checkLineComplexity(self):
158 """ 96 """
159 Private method to check the complexity of a single line of code and 97 Private method to check the complexity of a single line of code and
160 the median line complexity of the source code. 98 the median line complexity of the source code.
161 99
162 Complexity is defined as the number of AST nodes produced by a line 100 Complexity is defined as the number of AST nodes produced by a line
163 of code. 101 of code.
164 """ 102 """
165 maxLineComplexity = self.__args.get( 103 maxLineComplexity = self.args.get(
166 "LineComplexity", self.__defaultArgs["LineComplexity"] 104 "LineComplexity", self.__defaultArgs["LineComplexity"]
167 ) 105 )
168 maxLineComplexityScore = self.__args.get( 106 maxLineComplexityScore = self.args.get(
169 "LineComplexityScore", self.__defaultArgs["LineComplexityScore"] 107 "LineComplexityScore", self.__defaultArgs["LineComplexityScore"]
170 ) 108 )
171 109
172 visitor = LineComplexityVisitor() 110 visitor = LineComplexityVisitor()
173 visitor.visit(self.__tree) 111 visitor.visit(self.tree)
174 112
175 sortedItems = visitor.sortedList() 113 sortedItems = visitor.sortedList()
176 score = visitor.score() 114 score = visitor.score()
177 115
178 for line, complexity in sortedItems: 116 for line, complexity in sortedItems:
179 if complexity > maxLineComplexity: 117 if complexity > maxLineComplexity:
180 self.__error(line, 0, "C-111", complexity) 118 self.addError(line + 1, 0, "C-111", complexity)
181 119
182 if score > maxLineComplexityScore: 120 if score > maxLineComplexityScore:
183 self.__error(0, 0, "C-112", score) 121 self.addError(1, 0, "C-112", score)
184 122
185 123
186 class LineComplexityVisitor(ast.NodeVisitor): 124 class LineComplexityVisitor(ast.NodeVisitor):
187 """ 125 """
188 Class calculating the number of AST nodes per line of code 126 Class calculating the number of AST nodes per line of code
204 @param node reference to the node 142 @param node reference to the node
205 @type ast.AST 143 @type ast.AST
206 """ 144 """
207 if hasattr(node, "lineno"): 145 if hasattr(node, "lineno"):
208 self.__count[node.lineno] = self.__count.get(node.lineno, 0) + 1 146 self.__count[node.lineno] = self.__count.get(node.lineno, 0) + 1
147
209 self.generic_visit(node) 148 self.generic_visit(node)
210 149
211 def sortedList(self): 150 def sortedList(self):
212 """ 151 """
213 Public method to get a sorted list of (line, nodes) tuples. 152 Public method to get a sorted list of (line, nodes) tuples.

eric ide

mercurial