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 |