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

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9473
3f23dbf37dbe
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
15 15
16 class ComplexityChecker: 16 class ComplexityChecker:
17 """ 17 """
18 Class implementing a checker for code complexity. 18 Class implementing a checker for code complexity.
19 """ 19 """
20
20 Codes = [ 21 Codes = [
21 "C101", 22 "C101",
22 "C111", "C112", 23 "C111",
24 "C112",
23 ] 25 ]
24 26
25 def __init__(self, source, filename, tree, select, ignore, args): 27 def __init__(self, source, filename, tree, select, ignore, args):
26 """ 28 """
27 Constructor 29 Constructor
28 30
29 @param source source code to be checked 31 @param source source code to be checked
30 @type list of str 32 @type list of str
31 @param filename name of the source file 33 @param filename name of the source file
32 @type str 34 @type str
33 @param tree AST tree of the source code 35 @param tree AST tree of the source code
41 """ 43 """
42 self.__filename = filename 44 self.__filename = filename
43 self.__source = source[:] 45 self.__source = source[:]
44 self.__tree = copy.deepcopy(tree) 46 self.__tree = copy.deepcopy(tree)
45 self.__select = tuple(select) 47 self.__select = tuple(select)
46 self.__ignore = ('',) if select else tuple(ignore) 48 self.__ignore = ("",) if select else tuple(ignore)
47 self.__args = args 49 self.__args = args
48 50
49 self.__defaultArgs = { 51 self.__defaultArgs = {
50 "McCabeComplexity": 10, 52 "McCabeComplexity": 10,
51 "LineComplexity": 15, 53 "LineComplexity": 15,
52 "LineComplexityScore": 10, 54 "LineComplexityScore": 10,
53 } 55 }
54 56
55 # statistics counters 57 # statistics counters
56 self.counters = {} 58 self.counters = {}
57 59
58 # collection of detected errors 60 # collection of detected errors
59 self.errors = [] 61 self.errors = []
60 62
61 checkersWithCodes = [ 63 checkersWithCodes = [
62 (self.__checkMcCabeComplexity, ("C101",)), 64 (self.__checkMcCabeComplexity, ("C101",)),
63 (self.__checkLineComplexity, ("C111", "C112")), 65 (self.__checkLineComplexity, ("C111", "C112")),
64 ] 66 ]
65 67
66 self.__checkers = [] 68 self.__checkers = []
67 for checker, codes in checkersWithCodes: 69 for checker, codes in checkersWithCodes:
68 if any(not (code and self.__ignoreCode(code)) 70 if any(not (code and self.__ignoreCode(code)) for code in codes):
69 for code in codes):
70 self.__checkers.append(checker) 71 self.__checkers.append(checker)
71 72
72 def __ignoreCode(self, code): 73 def __ignoreCode(self, code):
73 """ 74 """
74 Private method to check if the message code should be ignored. 75 Private method to check if the message code should be ignored.
75 76
76 @param code message code to check for 77 @param code message code to check for
77 @type str 78 @type str
78 @return flag indicating to ignore the given code 79 @return flag indicating to ignore the given code
79 @rtype bool 80 @rtype bool
80 """ 81 """
81 return (code.startswith(self.__ignore) and 82 return code.startswith(self.__ignore) and not code.startswith(self.__select)
82 not code.startswith(self.__select)) 83
83
84 def __error(self, lineNumber, offset, code, *args): 84 def __error(self, lineNumber, offset, code, *args):
85 """ 85 """
86 Private method to record an issue. 86 Private method to record an issue.
87 87
88 @param lineNumber line number of the issue 88 @param lineNumber line number of the issue
89 @type int 89 @type int
90 @param offset position within line of the issue 90 @param offset position within line of the issue
91 @type int 91 @type int
92 @param code message code 92 @param code message code
94 @param args arguments for the message 94 @param args arguments for the message
95 @type list 95 @type list
96 """ 96 """
97 if self.__ignoreCode(code): 97 if self.__ignoreCode(code):
98 return 98 return
99 99
100 if code in self.counters: 100 if code in self.counters:
101 self.counters[code] += 1 101 self.counters[code] += 1
102 else: 102 else:
103 self.counters[code] = 1 103 self.counters[code] = 1
104 104
105 if code: 105 if code:
106 # record the issue with one based line number 106 # record the issue with one based line number
107 self.errors.append( 107 self.errors.append(
108 { 108 {
109 "file": self.__filename, 109 "file": self.__filename,
111 "offset": offset, 111 "offset": offset,
112 "code": code, 112 "code": code,
113 "args": args, 113 "args": args,
114 } 114 }
115 ) 115 )
116 116
117 def run(self): 117 def run(self):
118 """ 118 """
119 Public method to check the given source for code complexity. 119 Public method to check the given source for code complexity.
120 """ 120 """
121 if not self.__filename or not self.__source: 121 if not self.__filename or not self.__source:
122 # don't do anything, if essential data is missing 122 # don't do anything, if essential data is missing
123 return 123 return
124 124
125 if not self.__checkers: 125 if not self.__checkers:
126 # don't do anything, if no codes were selected 126 # don't do anything, if no codes were selected
127 return 127 return
128 128
129 for check in self.__checkers: 129 for check in self.__checkers:
130 check() 130 check()
131 131
132 def __checkMcCabeComplexity(self): 132 def __checkMcCabeComplexity(self):
133 """ 133 """
134 Private method to check the McCabe code complexity. 134 Private method to check the McCabe code complexity.
135 """ 135 """
136 try: 136 try:
137 # create the AST again because it is modified by the checker 137 # create the AST again because it is modified by the checker
138 tree = compile(''.join(self.__source), self.__filename, 'exec', 138 tree = compile(
139 ast.PyCF_ONLY_AST) 139 "".join(self.__source), self.__filename, "exec", ast.PyCF_ONLY_AST
140 )
140 except (SyntaxError, TypeError): 141 except (SyntaxError, TypeError):
141 # compile errors are already reported by the run() method 142 # compile errors are already reported by the run() method
142 return 143 return
143 144
144 maxComplexity = self.__args.get("McCabeComplexity", 145 maxComplexity = self.__args.get(
145 self.__defaultArgs["McCabeComplexity"]) 146 "McCabeComplexity", self.__defaultArgs["McCabeComplexity"]
146 147 )
148
147 visitor = PathGraphingAstVisitor() 149 visitor = PathGraphingAstVisitor()
148 visitor.preorder(tree, visitor) 150 visitor.preorder(tree, visitor)
149 for graph in visitor.graphs.values(): 151 for graph in visitor.graphs.values():
150 if graph.complexity() > maxComplexity: 152 if graph.complexity() > maxComplexity:
151 self.__error(graph.lineno, 0, "C101", 153 self.__error(graph.lineno, 0, "C101", graph.entity, graph.complexity())
152 graph.entity, graph.complexity()) 154
153
154 def __checkLineComplexity(self): 155 def __checkLineComplexity(self):
155 """ 156 """
156 Private method to check the complexity of a single line of code and 157 Private method to check the complexity of a single line of code and
157 the median line complexity of the source code. 158 the median line complexity of the source code.
158 159
159 Complexity is defined as the number of AST nodes produced by a line 160 Complexity is defined as the number of AST nodes produced by a line
160 of code. 161 of code.
161 """ 162 """
162 maxLineComplexity = self.__args.get( 163 maxLineComplexity = self.__args.get(
163 "LineComplexity", self.__defaultArgs["LineComplexity"]) 164 "LineComplexity", self.__defaultArgs["LineComplexity"]
165 )
164 maxLineComplexityScore = self.__args.get( 166 maxLineComplexityScore = self.__args.get(
165 "LineComplexityScore", self.__defaultArgs["LineComplexityScore"]) 167 "LineComplexityScore", self.__defaultArgs["LineComplexityScore"]
166 168 )
169
167 visitor = LineComplexityVisitor() 170 visitor = LineComplexityVisitor()
168 visitor.visit(self.__tree) 171 visitor.visit(self.__tree)
169 172
170 sortedItems = visitor.sortedList() 173 sortedItems = visitor.sortedList()
171 score = visitor.score() 174 score = visitor.score()
172 175
173 for line, complexity in sortedItems: 176 for line, complexity in sortedItems:
174 if complexity > maxLineComplexity: 177 if complexity > maxLineComplexity:
175 self.__error(line, 0, "C111", complexity) 178 self.__error(line, 0, "C111", complexity)
176 179
177 if score > maxLineComplexityScore: 180 if score > maxLineComplexityScore:
178 self.__error(0, 0, "C112", score) 181 self.__error(0, 0, "C112", score)
179 182
180 183
181 class LineComplexityVisitor(ast.NodeVisitor): 184 class LineComplexityVisitor(ast.NodeVisitor):
182 """ 185 """
183 Class calculating the number of AST nodes per line of code 186 Class calculating the number of AST nodes per line of code
184 and the median nodes/line score. 187 and the median nodes/line score.
185 """ 188 """
189
186 def __init__(self): 190 def __init__(self):
187 """ 191 """
188 Constructor 192 Constructor
189 """ 193 """
190 super().__init__() 194 super().__init__()
191 self.__count = {} 195 self.__count = {}
192 196
193 def visit(self, node): 197 def visit(self, node):
194 """ 198 """
195 Public method to recursively visit all the nodes and add up the 199 Public method to recursively visit all the nodes and add up the
196 instructions. 200 instructions.
197 201
198 @param node reference to the node 202 @param node reference to the node
199 @type ast.AST 203 @type ast.AST
200 """ 204 """
201 if hasattr(node, 'lineno'): 205 if hasattr(node, "lineno"):
202 self.__count[node.lineno] = self.__count.get(node.lineno, 0) + 1 206 self.__count[node.lineno] = self.__count.get(node.lineno, 0) + 1
203 self.generic_visit(node) 207 self.generic_visit(node)
204 208
205 def sortedList(self): 209 def sortedList(self):
206 """ 210 """
207 Public method to get a sorted list of (line, nodes) tuples. 211 Public method to get a sorted list of (line, nodes) tuples.
208 212
209 @return sorted list of (line, nodes) tuples 213 @return sorted list of (line, nodes) tuples
210 @rtype list of tuple of (int,int) 214 @rtype list of tuple of (int,int)
211 """ 215 """
212 lst = [(line, self.__count[line]) 216 lst = [(line, self.__count[line]) for line in sorted(self.__count.keys())]
213 for line in sorted(self.__count.keys())]
214 return lst 217 return lst
215 218
216 def score(self): 219 def score(self):
217 """ 220 """
218 Public method to calculate the median. 221 Public method to calculate the median.
219 222
220 @return median line complexity value 223 @return median line complexity value
221 @rtype float 224 @rtype float
222 """ 225 """
223 lst = self.__count.values() 226 lst = self.__count.values()
224 sortedList = sorted(lst) 227 sortedList = sorted(lst)
225 listLength = len(lst) 228 listLength = len(lst)
226 medianIndex = (listLength - 1) // 2 229 medianIndex = (listLength - 1) // 2
227 230
228 if listLength == 0: 231 if listLength == 0:
229 return 0.0 232 return 0.0
230 elif (listLength % 2): 233 elif listLength % 2:
231 return float(sortedList[medianIndex]) 234 return float(sortedList[medianIndex])
232 else: 235 else:
233 return ( 236 return (sortedList[medianIndex] + sortedList[medianIndex + 1]) / 2.0
234 (sortedList[medianIndex] + sortedList[medianIndex + 1]) / 2.0
235 )

eric ide

mercurial