diff --git a/slither/printers/summary/halstead.py b/slither/printers/summary/halstead.py index b50575ab6..12e422fb0 100644 --- a/slither/printers/summary/halstead.py +++ b/slither/printers/summary/halstead.py @@ -10,11 +10,13 @@ N1 = the total number of operators N2 = the total number of operands - Extended metrics: + Extended metrics1: 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 + + Extended metrics2: D = (n1 / 2) * (N2 / n2) # Difficulty E = D * V # Effort 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.slithir.variables.temporary import TemporaryVariable from slither.utils.myprettytable import make_pretty_table +from slither.utils.upgradeability import encode_ir_for_halstead def compute_halstead(contracts: list) -> tuple: @@ -42,18 +45,21 @@ def compute_halstead(contracts: list) -> tuple: core_metrics: {"contract1 name": { - "n1_unique_operators": n1, - "n2_unique_operands": n1, "N1_total_operators": N1, + "n1_unique_operators": n1, "N2_total_operands": N2, + "n2_unique_operands": n1, }} - extended_metrics: + extended_metrics1: {"contract1 name": { "n_vocabulary": n1 + n2, "N_prog_length": N1 + N2, "S_est_length": S, "V_volume": V, + }} + extended_metrics2: + {"contract1 name": { "D_difficulty": D, "E_effort": E, "T_time": T, @@ -62,7 +68,8 @@ def compute_halstead(contracts: list) -> tuple: """ core = OrderedDict() - extended = OrderedDict() + extended1 = OrderedDict() + extended2 = OrderedDict() all_operators = [] all_operands = [] for contract in contracts: @@ -72,9 +79,9 @@ def compute_halstead(contracts: list) -> tuple: 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) + encoded_operator = encode_ir_for_halstead(operation) + operators.append(encoded_operator) + all_operators.append(encoded_operator) # use operation.used to get the operands of the operation ignoring the temporary variables new_operands = [ @@ -82,13 +89,21 @@ def compute_halstead(contracts: list) -> tuple: ] 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) + ( + core[contract.name], + extended1[contract.name], + extended2[contract.name], + ) = _calculate_metrics(operators, operands) + if len(contracts) > 1: + 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 @@ -115,22 +130,24 @@ def _calculate_metrics(operators, operands): 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, + "Total Operators": N1, + "Unique Operators": n1, + "Total Operands": N2, + "Unique 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}", + extended_metrics1 = { + "Vocabulary": str(n1 + n2), + "Program Length": str(N1 + N2), + "Estimated Length": f"{S:.0f}", + "Volume": f"{V:.0f}", } - 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): @@ -143,24 +160,30 @@ class Halstead(AbstractPrinter): if len(self.contracts) == 0: 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 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" + table_core = make_pretty_table(["Contract", *keys], core, False) + 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 - # 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" + # Extended metrics2: difficulty, effort, time, bugs + txt += "\nHalstead complexity extended metrics2:\n" + keys = list(extended2[self.contracts[0].name].keys()) + table_extended2 = make_pretty_table(["Contract", *keys], extended2, False) + txt += str(table_extended2) + "\n" res = self.generate_output(txt) - res.add_pretty_table(table1, "Halstead core metrics") - res.add_pretty_table(table2, "Halstead extended metrics") + res.add_pretty_table(table_core, "Halstead core metrics") + res.add_pretty_table(table_extended1, "Halstead extended metrics1") + res.add_pretty_table(table_extended2, "Halstead extended metrics2") self.info(txt) return res diff --git a/slither/utils/upgradeability.py b/slither/utils/upgradeability.py index 7b4e8493a..910ba6f08 100644 --- a/slither/utils/upgradeability.py +++ b/slither/utils/upgradeability.py @@ -325,6 +325,63 @@ def encode_ir_for_compare(ir: Operation) -> str: 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 def encode_var_for_compare(var: Variable) -> str: