feat: halstead printer

pull/1878/head
devtooligan 2 years ago
parent 201b4dfcfe
commit 7eb270adef
No known key found for this signature in database
GPG Key ID: A5606B287899E7FB
  1. 103
      slither/printers/summary/halstead.py
  2. 57
      slither/utils/upgradeability.py

@ -10,11 +10,13 @@
N1 = the total number of operators N1 = the total number of operators
N2 = the total number of operands N2 = the total number of operands
Extended metrics: Extended metrics1:
n = n1 + n2 # Program vocabulary n = n1 + n2 # Program vocabulary
N = N1 + N2 # Program length N = N1 + N2 # Program length
S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length
V = N * log2(n) # Volume V = N * log2(n) # Volume
Extended metrics2:
D = (n1 / 2) * (N2 / n2) # Difficulty D = (n1 / 2) * (N2 / n2) # Difficulty
E = D * V # Effort E = D * V # Effort
T = E / 18 seconds # Time required to program T = E / 18 seconds # Time required to program
@ -27,6 +29,7 @@ from collections import OrderedDict
from slither.printers.abstract_printer import AbstractPrinter from slither.printers.abstract_printer import AbstractPrinter
from slither.slithir.variables.temporary import TemporaryVariable from slither.slithir.variables.temporary import TemporaryVariable
from slither.utils.myprettytable import make_pretty_table from slither.utils.myprettytable import make_pretty_table
from slither.utils.upgradeability import encode_ir_for_halstead
def compute_halstead(contracts: list) -> tuple: def compute_halstead(contracts: list) -> tuple:
@ -42,18 +45,21 @@ def compute_halstead(contracts: list) -> tuple:
core_metrics: core_metrics:
{"contract1 name": { {"contract1 name": {
"n1_unique_operators": n1,
"n2_unique_operands": n1,
"N1_total_operators": N1, "N1_total_operators": N1,
"n1_unique_operators": n1,
"N2_total_operands": N2, "N2_total_operands": N2,
"n2_unique_operands": n1,
}} }}
extended_metrics: extended_metrics1:
{"contract1 name": { {"contract1 name": {
"n_vocabulary": n1 + n2, "n_vocabulary": n1 + n2,
"N_prog_length": N1 + N2, "N_prog_length": N1 + N2,
"S_est_length": S, "S_est_length": S,
"V_volume": V, "V_volume": V,
}}
extended_metrics2:
{"contract1 name": {
"D_difficulty": D, "D_difficulty": D,
"E_effort": E, "E_effort": E,
"T_time": T, "T_time": T,
@ -62,7 +68,8 @@ def compute_halstead(contracts: list) -> tuple:
""" """
core = OrderedDict() core = OrderedDict()
extended = OrderedDict() extended1 = OrderedDict()
extended2 = OrderedDict()
all_operators = [] all_operators = []
all_operands = [] all_operands = []
for contract in contracts: for contract in contracts:
@ -72,9 +79,9 @@ def compute_halstead(contracts: list) -> tuple:
for node in func.nodes: for node in func.nodes:
for operation in node.irs: for operation in node.irs:
# use operation.expression.type to get the unique operator type # use operation.expression.type to get the unique operator type
operator_type = operation.expression.type encoded_operator = encode_ir_for_halstead(operation)
operators.append(operator_type) operators.append(encoded_operator)
all_operators.append(operator_type) all_operators.append(encoded_operator)
# use operation.used to get the operands of the operation ignoring the temporary variables # use operation.used to get the operands of the operation ignoring the temporary variables
new_operands = [ new_operands = [
@ -82,13 +89,21 @@ def compute_halstead(contracts: list) -> tuple:
] ]
operands.extend(new_operands) operands.extend(new_operands)
all_operands.extend(new_operands) all_operands.extend(new_operands)
(core[contract.name], extended[contract.name]) = _calculate_metrics(operators, operands) (
core["ALL CONTRACTS"] = OrderedDict() core[contract.name],
extended["ALL CONTRACTS"] = OrderedDict() extended1[contract.name],
(core["ALL CONTRACTS"], extended["ALL CONTRACTS"]) = _calculate_metrics( extended2[contract.name],
all_operators, all_operands ) = _calculate_metrics(operators, operands)
) if len(contracts) > 1:
return (core, extended) core["ALL CONTRACTS"] = OrderedDict()
extended1["ALL CONTRACTS"] = OrderedDict()
extended2["ALL CONTRACTS"] = OrderedDict()
(
core["ALL CONTRACTS"],
extended1["ALL CONTRACTS"],
extended2["ALL CONTRACTS"],
) = _calculate_metrics(all_operators, all_operands)
return (core, extended1, extended2)
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
@ -115,22 +130,24 @@ def _calculate_metrics(operators, operands):
T = E / 18 T = E / 18
B = (E ** (2 / 3)) / 3000 B = (E ** (2 / 3)) / 3000
core_metrics = { core_metrics = {
"n1_unique_operators": n1, "Total Operators": N1,
"n2_unique_operands": n2, "Unique Operators": n1,
"N1_total_operators": N1, "Total Operands": N2,
"N2_total_operands": N2, "Unique Operands": n2,
} }
extended_metrics = { extended_metrics1 = {
"n_vocabulary": str(n1 + n2), "Vocabulary": str(n1 + n2),
"N_prog_length": str(N1 + N2), "Program Length": str(N1 + N2),
"S_est_length": f"{S:.0f}", "Estimated Length": f"{S:.0f}",
"V_volume": f"{V:.0f}", "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) extended_metrics2 = {
"Difficulty": f"{D:.0f}",
"Effort": f"{E:.0f}",
"Time": f"{T:.0f}",
"Estimated Bugs": f"{B:.3f}",
}
return (core_metrics, extended_metrics1, extended_metrics2)
class Halstead(AbstractPrinter): class Halstead(AbstractPrinter):
@ -143,24 +160,30 @@ class Halstead(AbstractPrinter):
if len(self.contracts) == 0: if len(self.contracts) == 0:
return self.generate_output("No contract found") return self.generate_output("No contract found")
core, extended = compute_halstead(self.contracts) core, extended1, extended2 = compute_halstead(self.contracts)
# Core metrics: operations and operands # Core metrics: operations and operands
txt = "\n\nHalstead complexity core metrics:\n" txt = "\n\nHalstead complexity core metrics:\n"
keys = list(core[self.contracts[0].name].keys()) keys = list(core[self.contracts[0].name].keys())
table1 = make_pretty_table(["Contract", *keys], core, False) table_core = make_pretty_table(["Contract", *keys], core, False)
txt += str(table1) + "\n" txt += str(table_core) + "\n"
# Extended metrics1: vocabulary, program length, estimated length, volume
txt += "\nHalstead complexity extended metrics1:\n"
keys = list(extended1[self.contracts[0].name].keys())
table_extended1 = make_pretty_table(["Contract", *keys], extended1, False)
txt += str(table_extended1) + "\n"
# Extended metrics: volume, difficulty, effort, time, bugs # Extended metrics2: difficulty, effort, time, bugs
# TODO: should we break this into 2 tables? currently 119 chars wide txt += "\nHalstead complexity extended metrics2:\n"
txt += "\nHalstead complexity extended metrics:\n" keys = list(extended2[self.contracts[0].name].keys())
keys = list(extended[self.contracts[0].name].keys()) table_extended2 = make_pretty_table(["Contract", *keys], extended2, False)
table2 = make_pretty_table(["Contract", *keys], extended, False) txt += str(table_extended2) + "\n"
txt += str(table2) + "\n"
res = self.generate_output(txt) res = self.generate_output(txt)
res.add_pretty_table(table1, "Halstead core metrics") res.add_pretty_table(table_core, "Halstead core metrics")
res.add_pretty_table(table2, "Halstead extended metrics") res.add_pretty_table(table_extended1, "Halstead extended metrics1")
res.add_pretty_table(table_extended2, "Halstead extended metrics2")
self.info(txt) self.info(txt)
return res return res

@ -325,6 +325,63 @@ def encode_ir_for_compare(ir: Operation) -> str:
return "" return ""
# pylint: disable=too-many-branches
def encode_ir_for_halstead(ir: Operation) -> str:
# operations
if isinstance(ir, Assignment):
return "assignment"
if isinstance(ir, Index):
return "index"
if isinstance(ir, Member):
return "member" # .format(ntype(ir._type))
if isinstance(ir, Length):
return "length"
if isinstance(ir, Binary):
return f"binary({str(ir.type)})"
if isinstance(ir, Unary):
return f"unary({str(ir.type)})"
if isinstance(ir, Condition):
return f"condition({encode_var_for_compare(ir.value)})"
if isinstance(ir, NewStructure):
return "new_structure"
if isinstance(ir, NewContract):
return "new_contract"
if isinstance(ir, NewArray):
return f"new_array({ntype(ir.array_type)})"
if isinstance(ir, NewElementaryType):
return f"new_elementary({ntype(ir.type)})"
if isinstance(ir, Delete):
return "delete"
if isinstance(ir, SolidityCall):
return f"solidity_call({ir.function.full_name})"
if isinstance(ir, InternalCall):
return f"internal_call({ntype(ir.type_call)})"
if isinstance(ir, EventCall): # is this useful?
return "event"
if isinstance(ir, LibraryCall):
return "library_call"
if isinstance(ir, InternalDynamicCall):
return "internal_dynamic_call"
if isinstance(ir, HighLevelCall): # TODO: improve
return "high_level_call"
if isinstance(ir, LowLevelCall): # TODO: improve
return "low_level_call"
if isinstance(ir, TypeConversion):
return f"type_conversion({ntype(ir.type)})"
if isinstance(ir, Return): # this can be improved using values
return "return" # .format(ntype(ir.type))
if isinstance(ir, Transfer):
return "transfer"
if isinstance(ir, Send):
return "send"
if isinstance(ir, Unpack): # TODO: improve
return "unpack"
if isinstance(ir, InitArray): # TODO: improve
return "init_array"
# default
raise NotImplementedError(f"encode_ir_for_halstead: {ir}")
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def encode_var_for_compare(var: Variable) -> str: def encode_var_for_compare(var: Variable) -> str:

Loading…
Cancel
Save