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 |
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 ) |
|