mirror of https://github.com/crytic/slither
parent
176c85c092
commit
201b4dfcfe
@ -0,0 +1,166 @@ |
||||
""" |
||||
Halstead complexity metrics |
||||
https://en.wikipedia.org/wiki/Halstead_complexity_measures |
||||
|
||||
12 metrics based on the number of unique operators and operands: |
||||
|
||||
Core metrics: |
||||
n1 = the number of distinct operators |
||||
n2 = the number of distinct operands |
||||
N1 = the total number of operators |
||||
N2 = the total number of operands |
||||
|
||||
Extended metrics: |
||||
n = n1 + n2 # Program vocabulary |
||||
N = N1 + N2 # Program length |
||||
S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length |
||||
V = N * log2(n) # Volume |
||||
D = (n1 / 2) * (N2 / n2) # Difficulty |
||||
E = D * V # Effort |
||||
T = E / 18 seconds # Time required to program |
||||
B = (E^(2/3)) / 3000 # Number of delivered bugs |
||||
|
||||
|
||||
""" |
||||
import math |
||||
from collections import OrderedDict |
||||
from slither.printers.abstract_printer import AbstractPrinter |
||||
from slither.slithir.variables.temporary import TemporaryVariable |
||||
from slither.utils.myprettytable import make_pretty_table |
||||
|
||||
|
||||
def compute_halstead(contracts: list) -> tuple: |
||||
"""Used to compute the Halstead complexity metrics for a list of contracts. |
||||
Args: |
||||
contracts: list of contracts. |
||||
Returns: |
||||
Halstead metrics as a tuple of two OrderedDicts (core_metrics, extended_metrics) |
||||
which each contain one key per contract. The value of each key is a dict of metrics. |
||||
|
||||
In addition to one key per contract, there is a key for "ALL CONTRACTS" that contains |
||||
the metrics for ALL CONTRACTS combined. (Not the sums of the individual contracts!) |
||||
|
||||
core_metrics: |
||||
{"contract1 name": { |
||||
"n1_unique_operators": n1, |
||||
"n2_unique_operands": n1, |
||||
"N1_total_operators": N1, |
||||
"N2_total_operands": N2, |
||||
}} |
||||
|
||||
extended_metrics: |
||||
{"contract1 name": { |
||||
"n_vocabulary": n1 + n2, |
||||
"N_prog_length": N1 + N2, |
||||
"S_est_length": S, |
||||
"V_volume": V, |
||||
"D_difficulty": D, |
||||
"E_effort": E, |
||||
"T_time": T, |
||||
"B_bugs": B, |
||||
}} |
||||
|
||||
""" |
||||
core = OrderedDict() |
||||
extended = OrderedDict() |
||||
all_operators = [] |
||||
all_operands = [] |
||||
for contract in contracts: |
||||
operators = [] |
||||
operands = [] |
||||
for func in contract.functions: |
||||
for node in func.nodes: |
||||
for operation in node.irs: |
||||
# use operation.expression.type to get the unique operator type |
||||
operator_type = operation.expression.type |
||||
operators.append(operator_type) |
||||
all_operators.append(operator_type) |
||||
|
||||
# use operation.used to get the operands of the operation ignoring the temporary variables |
||||
new_operands = [ |
||||
op for op in operation.used if not isinstance(op, TemporaryVariable) |
||||
] |
||||
operands.extend(new_operands) |
||||
all_operands.extend(new_operands) |
||||
(core[contract.name], extended[contract.name]) = _calculate_metrics(operators, operands) |
||||
core["ALL CONTRACTS"] = OrderedDict() |
||||
extended["ALL CONTRACTS"] = OrderedDict() |
||||
(core["ALL CONTRACTS"], extended["ALL CONTRACTS"]) = _calculate_metrics( |
||||
all_operators, all_operands |
||||
) |
||||
return (core, extended) |
||||
|
||||
|
||||
# pylint: disable=too-many-locals |
||||
def _calculate_metrics(operators, operands): |
||||
"""Used to compute the Halstead complexity metrics for a list of operators and operands. |
||||
Args: |
||||
operators: list of operators. |
||||
operands: list of operands. |
||||
Returns: |
||||
Halstead metrics as a tuple of two OrderedDicts (core_metrics, extended_metrics) |
||||
which each contain one key per contract. The value of each key is a dict of metrics. |
||||
NOTE: The metric values are ints and floats that have been converted to formatted strings |
||||
""" |
||||
n1 = len(set(operators)) |
||||
n2 = len(set(operands)) |
||||
N1 = len(operators) |
||||
N2 = len(operands) |
||||
n = n1 + n2 |
||||
N = N1 + N2 |
||||
S = 0 if (n1 == 0 or n2 == 0) else n1 * math.log2(n1) + n2 * math.log2(n2) |
||||
V = N * math.log2(n) if n > 0 else 0 |
||||
D = (n1 / 2) * (N2 / n2) if n2 > 0 else 0 |
||||
E = D * V |
||||
T = E / 18 |
||||
B = (E ** (2 / 3)) / 3000 |
||||
core_metrics = { |
||||
"n1_unique_operators": n1, |
||||
"n2_unique_operands": n2, |
||||
"N1_total_operators": N1, |
||||
"N2_total_operands": N2, |
||||
} |
||||
extended_metrics = { |
||||
"n_vocabulary": str(n1 + n2), |
||||
"N_prog_length": str(N1 + N2), |
||||
"S_est_length": f"{S:.0f}", |
||||
"V_volume": f"{V:.0f}", |
||||
"D_difficulty": f"{D:.0f}", |
||||
"E_effort": f"{E:.0f}", |
||||
"T_time": f"{T:.0f}", |
||||
"B_bugs": f"{B:.3f}", |
||||
} |
||||
return (core_metrics, extended_metrics) |
||||
|
||||
|
||||
class Halstead(AbstractPrinter): |
||||
ARGUMENT = "halstead" |
||||
HELP = "Computes the Halstead complexity metrics for each contract" |
||||
|
||||
WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#halstead" |
||||
|
||||
def output(self, _filename): |
||||
if len(self.contracts) == 0: |
||||
return self.generate_output("No contract found") |
||||
|
||||
core, extended = compute_halstead(self.contracts) |
||||
|
||||
# Core metrics: operations and operands |
||||
txt = "\n\nHalstead complexity core metrics:\n" |
||||
keys = list(core[self.contracts[0].name].keys()) |
||||
table1 = make_pretty_table(["Contract", *keys], core, False) |
||||
txt += str(table1) + "\n" |
||||
|
||||
# Extended metrics: volume, difficulty, effort, time, bugs |
||||
# TODO: should we break this into 2 tables? currently 119 chars wide |
||||
txt += "\nHalstead complexity extended metrics:\n" |
||||
keys = list(extended[self.contracts[0].name].keys()) |
||||
table2 = make_pretty_table(["Contract", *keys], extended, False) |
||||
txt += str(table2) + "\n" |
||||
|
||||
res = self.generate_output(txt) |
||||
res.add_pretty_table(table1, "Halstead core metrics") |
||||
res.add_pretty_table(table2, "Halstead extended metrics") |
||||
self.info(txt) |
||||
|
||||
return res |
Loading…
Reference in new issue