2 In this file we define a ProblemVault class where we store all the
3 exceptions and all the problems we find with the code.
5 The ProblemVault is capable of registering problems and also figuring out if a
6 problem is worse than a registered exception so that it only warns when things
10 # Future imports for Python 2.7, mandatory in 3.0
11 from __future__
import division
12 from __future__
import print_function
13 from __future__
import unicode_literals
23 class ProblemVault(object):
25 Singleton where we store the various new problems we
26 found in the code, and also the old problems we read from the exception
29 def __init__(self
, exception_fname
=None):
30 # Exception dictionary: { problem.key() : Problem object }
32 # Exception list: list of Problem objects, in the order added.
33 self
.exception_list
= []
34 # Exception dictionary: maps key to the problem it was used to
36 self
.used_exception_for
= {}
38 if exception_fname
== None:
42 with
open(exception_fname
, 'r') as exception_f
:
43 self
.register_exceptions(exception_f
)
45 print("No exception file provided", file=sys
.stderr
)
47 def register_exceptions(self
, exception_file
):
49 for lineno
, line
in enumerate(exception_file
, 1):
51 problem
= get_old_problem_from_exception_str(line
)
52 except ValueError as v
:
53 print("Exception file line {} not recognized: {}"
61 # Fail if we see dup exceptions. There is really no reason to have dup exceptions.
62 if problem
.key() in self
.exceptions
:
63 print("Duplicate exceptions lines found in exception file:\n\t{}\n\t{}\nAborting...".format(problem
, self
.exceptions
[problem
.key()]),
67 self
.exceptions
[problem
.key()] = problem
68 self
.exception_list
.append(problem
)
69 #print "Registering exception: %s" % problem
71 def register_problem(self
, problem
):
73 Register this problem to the problem value. Return true if it was a new
74 problem or it worsens an already existing problem. A true
75 value may be STATUS_ERR to indicate a hard violation, or STATUS_WARN
76 to indicate a warning.
78 # This is a new problem, print it
79 if problem
.key() not in self
.exceptions
:
82 # If it's an old problem, we don't warn if the situation got better
83 # (e.g. we went from 4k LoC to 3k LoC), but we do warn if the
84 # situation worsened (e.g. we went from 60 includes to 80).
85 status
= problem
.is_worse_than(self
.exceptions
[problem
.key()])
87 # Remember that we used this exception, so that we can later
88 # determine whether the exception was overbroad.
89 self
.used_exception_for
[problem
.key()] = problem
93 def list_overbroad_exceptions(self
):
94 """Return an iterator of tuples containing (ex,prob) where ex is an
95 exceptions in this vault that are stricter than it needs to be, and
96 prob is the worst problem (if any) that it covered.
98 for k
in self
.exceptions
:
99 e
= self
.exceptions
[k
]
100 p
= self
.used_exception_for
.get(k
)
101 if p
is None or e
.is_worse_than(p
):
104 def list_exceptions_without_overbroad(self
):
105 """Return an iterator of new problems, such that overbroad
106 exceptions are replaced with minimally broad versions, or removed.
108 for e
in self
.exception_list
:
109 p
= self
.used_exception_for
.get(e
.key())
111 # This exception wasn't needed at all.
113 if e
.is_worse_than(p
):
114 # The exception is worse than the problem we found.
115 # Yield the problem as the new exception value.
118 # The problem is as bad as the exception, or worse.
119 # Yield the exception.
122 def set_tolerances(self
, fns
):
123 """Adjust the tolerances for the exceptions in this vault. Takes
124 a map of problem type to a function that adjusts the permitted
125 function to its new maximum value."""
126 for k
in self
.exceptions
:
127 ex
= self
.exceptions
[k
]
128 fn
= fns
.get(ex
.problem_type
)
130 ex
.metric_value
= fn(ex
.metric_value
)
132 class ProblemFilter(object):
134 self
.thresholds
= dict()
136 def addThreshold(self
, item
):
137 self
.thresholds
[(item
.get_type(),item
.get_file_type())] = item
139 def matches(self
, item
):
140 key
= (item
.get_type(), item
.get_file_type())
141 filt
= self
.thresholds
.get(key
, None)
144 return item
.is_worse_than(filt
)
146 def filter(self
, sequence
):
147 for item
in iter(sequence
):
148 if self
.matches(item
):
153 A generic measurement about some aspect of our source code. See
154 the subclasses below for the specific problems we are trying to tackle.
156 def __init__(self
, problem_type
, problem_location
, metric_value
):
157 self
.problem_location
= problem_location
158 self
.metric_value
= int(metric_value
)
159 self
.warning_threshold
= self
.metric_value
160 self
.problem_type
= problem_type
162 def is_worse_than(self
, other_problem
):
163 """Return STATUS_ERR if this is a worse problem than other_problem.
164 Return STATUS_WARN if it is a little worse, but falls within the
165 warning threshold. Return STATUS_OK if this problem is not
166 at all worse than other_problem.
168 if self
.metric_value
> other_problem
.metric_value
:
170 elif self
.metric_value
> other_problem
.warning_threshold
:
176 """Generate a unique key that describes this problem that can be used as a dictionary key"""
177 # Item location is a filesystem path, so we need to normalize this
178 # across platforms otherwise same paths are not gonna match.
179 canonical_location
= os
.path
.normcase(self
.problem_location
)
180 return "%s:%s" % (canonical_location
, self
.problem_type
)
183 return "problem %s %s %s" % (self
.problem_type
, self
.problem_location
, self
.metric_value
)
186 return self
.problem_type
188 def get_file_type(self
):
189 if self
.problem_location
.endswith(".h"):
194 class FileSizeItem(Item
):
196 Denotes a problem with the size of a .c file.
198 The 'problem_location' is the filesystem path of the .c file, and the
199 'metric_value' is the number of lines in the .c file.
201 def __init__(self
, problem_location
, metric_value
):
202 super(FileSizeItem
, self
).__init
__("file-size", problem_location
, metric_value
)
204 class IncludeCountItem(Item
):
206 Denotes a problem with the number of #includes in a .c file.
208 The 'problem_location' is the filesystem path of the .c file, and the
209 'metric_value' is the number of #includes in the .c file.
211 def __init__(self
, problem_location
, metric_value
):
212 super(IncludeCountItem
, self
).__init
__("include-count", problem_location
, metric_value
)
214 class FunctionSizeItem(Item
):
216 Denotes a problem with a size of a function in a .c file.
218 The 'problem_location' is "<path>:<function>()" where <path> is the
219 filesystem path of the .c file and <function> is the name of the offending
222 The 'metric_value' is the size of the offending function in lines.
224 def __init__(self
, problem_location
, metric_value
):
225 super(FunctionSizeItem
, self
).__init
__("function-size", problem_location
, metric_value
)
227 class DependencyViolationItem(Item
):
229 Denotes a dependency violation in a .c or .h file. A dependency violation
230 occurs when a file includes a file from some module that is not listed
231 in its .may_include file.
233 The 'problem_location' is the file that contains the problem.
235 The 'metric_value' is the number of forbidden includes.
237 def __init__(self
, problem_location
, metric_value
):
238 super(DependencyViolationItem
, self
).__init
__("dependency-violation",
242 comment_re
= re
.compile(r
'#.*$')
244 def get_old_problem_from_exception_str(exception_str
):
245 orig_str
= exception_str
246 exception_str
= comment_re
.sub("", exception_str
)
247 fields
= exception_str
.split()
249 # empty line or comment
251 elif len(fields
) == 4:
253 _
, problem_type
, problem_location
, metric_value
= fields
255 raise ValueError("Misformatted line {!r}".format(orig_str
))
257 if problem_type
== "file-size":
258 return FileSizeItem(problem_location
, metric_value
)
259 elif problem_type
== "include-count":
260 return IncludeCountItem(problem_location
, metric_value
)
261 elif problem_type
== "function-size":
262 return FunctionSizeItem(problem_location
, metric_value
)
263 elif problem_type
== "dependency-violation":
264 return DependencyViolationItem(problem_location
, metric_value
)
266 raise ValueError("Unknown exception type {!r}".format(orig_str
))