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

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

eric ide

mercurial