Plugins/CheckerPlugins/CodeStyleChecker/ComplexityChecker.py

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

eric ide

mercurial