From 201b4dfcfe1b645f34d65f5a87e7d5a095411431 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Tue, 2 May 2023 10:33:47 -0700 Subject: [PATCH 01/21] feat: halstead printer --- slither/printers/all_printers.py | 1 + slither/printers/summary/halstead.py | 166 +++++++++++++++++++++++++++ slither/utils/myprettytable.py | 39 +++++++ 3 files changed, 206 insertions(+) create mode 100644 slither/printers/summary/halstead.py diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 6dc8dddbdd..1202794039 100644 --- a/slither/printers/all_printers.py +++ b/slither/printers/all_printers.py @@ -8,6 +8,7 @@ from .summary.slithir import PrinterSlithIR from .summary.slithir_ssa import PrinterSlithIRSSA from .summary.human_summary import PrinterHumanSummary +from .summary.halstead import Halstead from .functions.cfg import CFG from .summary.function_ids import FunctionIds from .summary.variable_order import VariableOrder diff --git a/slither/printers/summary/halstead.py b/slither/printers/summary/halstead.py new file mode 100644 index 0000000000..b50575ab63 --- /dev/null +++ b/slither/printers/summary/halstead.py @@ -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 diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index af10a6ff25..efdb965048 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -22,3 +22,42 @@ def to_json(self) -> Dict: def __str__(self) -> str: return str(self.to_pretty_table()) + + +# **Dict to MyPrettyTable utility functions** + + +# Converts a dict to a MyPrettyTable. Dict keys are the row headers. +# @param headers str[] of column names +# @param body dict of row headers with a dict of the values +# @param totals bool optional add Totals row +def make_pretty_table(headers: list, body: dict, totals: bool = False) -> MyPrettyTable: + table = MyPrettyTable(headers) + for row in body: + table_row = [row] + [body[row][key] for key in headers[1:]] + table.add_row(table_row) + if totals: + table.add_row(["Total"] + [sum([body[row][key] for row in body]) for key in headers[1:]]) + return table + + +# takes a dict of dicts and returns a dict of dicts with the keys transposed +# example: +# in: +# { +# "dep": {"loc": 0, "sloc": 0, "cloc": 0}, +# "test": {"loc": 0, "sloc": 0, "cloc": 0}, +# "src": {"loc": 0, "sloc": 0, "cloc": 0}, +# } +# out: +# { +# 'loc': {'dep': 0, 'test': 0, 'src': 0}, +# 'sloc': {'dep': 0, 'test': 0, 'src': 0}, +# 'cloc': {'dep': 0, 'test': 0, 'src': 0}, +# } +def transpose(table): + any_key = list(table.keys())[0] + return { + inner_key: {outer_key: table[outer_key][inner_key] for outer_key in table} + for inner_key in table[any_key] + } From 7eb270adeffffe08a3eb496ff62a298e7b5ccf8d Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 4 May 2023 12:53:54 -0700 Subject: [PATCH 02/21] feat: halstead printer --- slither/printers/summary/halstead.py | 103 ++++++++++++++++----------- slither/utils/upgradeability.py | 57 +++++++++++++++ 2 files changed, 120 insertions(+), 40 deletions(-) diff --git a/slither/printers/summary/halstead.py b/slither/printers/summary/halstead.py index b50575ab63..12e422fb08 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 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 @@ def output(self, _filename): 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 7b4e8493a7..910ba6f087 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: From 16b57263f47b1628d8bf4a4ca08ae9d674be9375 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Tue, 2 May 2023 09:54:51 -0700 Subject: [PATCH 03/21] feat: martin printer --- slither/printers/all_printers.py | 1 + slither/printers/summary/martin.py | 114 +++++++++++++++++++++++++++++ slither/utils/myprettytable.py | 39 ++++++++++ 3 files changed, 154 insertions(+) create mode 100644 slither/printers/summary/martin.py diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 6dc8dddbdd..c836b98d28 100644 --- a/slither/printers/all_printers.py +++ b/slither/printers/all_printers.py @@ -20,3 +20,4 @@ from .summary.when_not_paused import PrinterWhenNotPaused from .summary.declaration import Declaration from .functions.dominator import Dominator +from .summary.martin import Martin diff --git a/slither/printers/summary/martin.py b/slither/printers/summary/martin.py new file mode 100644 index 0000000000..1bb59c4ffe --- /dev/null +++ b/slither/printers/summary/martin.py @@ -0,0 +1,114 @@ +""" + Robert "Uncle Bob" Martin - Agile software metrics + https://en.wikipedia.org/wiki/Software_package_metrics + + Efferent Coupling (Ce): Number of contracts that the contract depends on + Afferent Coupling (Ca): Number of contracts that depend on a contract + Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca)) + Abstractness (A): Number of abstract contracts / total number of contracts + Distance from the Main Sequence (D): abs(A + I - 1) + +""" +from slither.printers.abstract_printer import AbstractPrinter +from slither.slithir.operations.high_level_call import HighLevelCall +from slither.utils.myprettytable import make_pretty_table + + +def count_abstracts(contracts): + """ + Count the number of abstract contracts + Args: + contracts(list): list of contracts + Returns: + a tuple of (abstract_contract_count, total_contract_count) + """ + abstract_contract_count = 0 + for c in contracts: + if not c.is_fully_implemented: + abstract_contract_count += 1 + return (abstract_contract_count, len(contracts)) + + +def compute_coupling(contracts: list, abstractness: float) -> dict: + """ + Used to compute the coupling between contracts external calls made to internal contracts + Args: + contracts: list of contracts + Returns: + dict of contract names with dicts of the coupling metrics: + { + "contract_name1": { + "Dependents": 0, + "Dependencies": 3 + "Instability": 1.0, + "Abstractness": 0.0, + "Distance from main sequence": 1.0, + }, + "contract_name2": { + "Dependents": 1, + "Dependencies": 0 + "Instability": 0.0, + "Abstractness": 1.0, + "Distance from main sequence": 0.0, + } + """ + dependencies = {} + for contract in contracts: + for func in contract.functions: + high_level_calls = [ + ir for node in func.nodes for ir in node.irs_ssa if isinstance(ir, HighLevelCall) + ] + # convert irs to string with target function and contract name + external_calls = [h.destination.type.type.name for h in high_level_calls] + dependencies[contract.name] = set(external_calls) + dependents = {} + for contract, deps in dependencies.items(): + for dep in deps: + if dep not in dependents: + dependents[dep] = set() + dependents[dep].add(contract) + + coupling_dict = {} + for contract in contracts: + ce = len(dependencies.get(contract.name, [])) + ca = len(dependents.get(contract.name, [])) + i = 0.0 + d = 0.0 + if ce + ca > 0: + i = float(ce / (ce + ca)) + d = float(abs(i - abstractness)) + coupling_dict[contract.name] = { + "Dependents": ca, + "Dependencies": ce, + "Instability": f"{i:.2f}", + "Distance from main sequence": f"{d:.2f}", + } + return coupling_dict + + +class Martin(AbstractPrinter): + ARGUMENT = "martin" + HELP = "Martin agile software metrics (Ca, Ce, I, A, D)" + + WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#martin" + + def output(self, _filename): + (abstract_contract_count, total_contract_count) = count_abstracts(self.contracts) + abstractness = float(abstract_contract_count / total_contract_count) + coupling_dict = compute_coupling(self.contracts, abstractness) + + table = make_pretty_table( + ["Contract", *list(coupling_dict[self.contracts[0].name].keys())], coupling_dict + ) + txt = "Martin agile software metrics\n" + txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" + txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" + txt += "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" + txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" + txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" + txt += "\n" + txt += f"Abstractness (overall): {round(abstractness, 2)}\n" + str(table) + self.info(txt) + res = self.generate_output(txt) + res.add_pretty_table(table, "Code Lines") + return res diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index af10a6ff25..efdb965048 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -22,3 +22,42 @@ def to_json(self) -> Dict: def __str__(self) -> str: return str(self.to_pretty_table()) + + +# **Dict to MyPrettyTable utility functions** + + +# Converts a dict to a MyPrettyTable. Dict keys are the row headers. +# @param headers str[] of column names +# @param body dict of row headers with a dict of the values +# @param totals bool optional add Totals row +def make_pretty_table(headers: list, body: dict, totals: bool = False) -> MyPrettyTable: + table = MyPrettyTable(headers) + for row in body: + table_row = [row] + [body[row][key] for key in headers[1:]] + table.add_row(table_row) + if totals: + table.add_row(["Total"] + [sum([body[row][key] for row in body]) for key in headers[1:]]) + return table + + +# takes a dict of dicts and returns a dict of dicts with the keys transposed +# example: +# in: +# { +# "dep": {"loc": 0, "sloc": 0, "cloc": 0}, +# "test": {"loc": 0, "sloc": 0, "cloc": 0}, +# "src": {"loc": 0, "sloc": 0, "cloc": 0}, +# } +# out: +# { +# 'loc': {'dep': 0, 'test': 0, 'src': 0}, +# 'sloc': {'dep': 0, 'test': 0, 'src': 0}, +# 'cloc': {'dep': 0, 'test': 0, 'src': 0}, +# } +def transpose(table): + any_key = list(table.keys())[0] + return { + inner_key: {outer_key: table[outer_key][inner_key] for outer_key in table} + for inner_key in table[any_key] + } From d682232b46df040fb799df56f3b51e5804d3af9d Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 11 May 2023 11:01:12 -0700 Subject: [PATCH 04/21] chore: add to scripts/ci_test_printers.sh --- scripts/ci_test_printers.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ci_test_printers.sh b/scripts/ci_test_printers.sh index 61994b337d..7ed2b62026 100755 --- a/scripts/ci_test_printers.sh +++ b/scripts/ci_test_printers.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -### Test printer +### Test printer cd tests/e2e/solc_parsing/test_data/compile/ || exit # Do not test the evm printer,as it needs a refactoring -ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,human-summary,inheritance,inheritance-graph,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration" +ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration" # Only test 0.5.17 to limit test time for file in *0.5.17-compact.zip; do From 975da91d333182b7a10d1be29993368073c1edb3 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 15 Jun 2023 14:20:18 -0700 Subject: [PATCH 05/21] chore: add type --- slither/printers/summary/martin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slither/printers/summary/martin.py b/slither/printers/summary/martin.py index 1bb59c4ffe..b289a21f99 100644 --- a/slither/printers/summary/martin.py +++ b/slither/printers/summary/martin.py @@ -12,9 +12,9 @@ from slither.printers.abstract_printer import AbstractPrinter from slither.slithir.operations.high_level_call import HighLevelCall from slither.utils.myprettytable import make_pretty_table +from typing import Tuple - -def count_abstracts(contracts): +def count_abstracts(contracts) -> Tuple[int, int]: """ Count the number of abstract contracts Args: From 6f280c18b98c6ccee39f09a3f0b9381e0816ce5b Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 15 Jun 2023 14:36:19 -0700 Subject: [PATCH 06/21] chore: pylint --- slither/printers/summary/martin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/slither/printers/summary/martin.py b/slither/printers/summary/martin.py index b289a21f99..693ec15759 100644 --- a/slither/printers/summary/martin.py +++ b/slither/printers/summary/martin.py @@ -9,10 +9,11 @@ Distance from the Main Sequence (D): abs(A + I - 1) """ -from slither.printers.abstract_printer import AbstractPrinter +from typing import Tuple from slither.slithir.operations.high_level_call import HighLevelCall from slither.utils.myprettytable import make_pretty_table -from typing import Tuple +from slither.printers.abstract_printer import AbstractPrinter + def count_abstracts(contracts) -> Tuple[int, int]: """ From 3791467fb9a9460b4af6aa2b8324726511ba2ce2 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 16 Jun 2023 11:04:36 -0700 Subject: [PATCH 07/21] docs: fix docstring --- slither/utils/myprettytable.py | 44 ++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index 57e1308840..dd3672f848 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -32,6 +32,14 @@ def __str__(self) -> str: # @param body dict of row headers with a dict of the values # @param totals bool optional add Totals row def make_pretty_table(headers: list, body: dict, totals: bool = False) -> MyPrettyTable: + """ + Converts a dict to a MyPrettyTable. Dict keys are the row headers. + Args: + data: dict of row headers with a dict of the values + column_header: str of column name for 1st column + Returns: + MyPrettyTable + """ table = MyPrettyTable(headers) for row in body: table_row = [row] + [body[row][key] for key in headers[1:]] @@ -40,22 +48,28 @@ def make_pretty_table(headers: list, body: dict, totals: bool = False) -> MyPret table.add_row(["Total"] + [sum([body[row][key] for row in body]) for key in headers[1:]]) return table - -# takes a dict of dicts and returns a dict of dicts with the keys transposed -# example: -# in: -# { -# "dep": {"loc": 0, "sloc": 0, "cloc": 0}, -# "test": {"loc": 0, "sloc": 0, "cloc": 0}, -# "src": {"loc": 0, "sloc": 0, "cloc": 0}, -# } -# out: -# { -# 'loc': {'dep': 0, 'test': 0, 'src': 0}, -# 'sloc': {'dep': 0, 'test': 0, 'src': 0}, -# 'cloc': {'dep': 0, 'test': 0, 'src': 0}, -# } def transpose(table): + """ + Converts a dict of dicts to a dict of dicts with the keys transposed + Args: + table: dict of dicts + Returns: + dict of dicts + + Example: + in: + { + "dep": {"loc": 0, "sloc": 0, "cloc": 0}, + "test": {"loc": 0, "sloc": 0, "cloc": 0}, + "src": {"loc": 0, "sloc": 0, "cloc": 0}, + } + out: + { + 'loc': {'dep': 0, 'test': 0, 'src': 0}, + 'sloc': {'dep': 0, 'test': 0, 'src': 0}, + 'cloc': {'dep': 0, 'test': 0, 'src': 0}, + } + """ any_key = list(table.keys())[0] return { inner_key: {outer_key: table[outer_key][inner_key] for outer_key in table} From c3a674acdc874cd97aeb20fcd464b34ccf873626 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 16 Jun 2023 11:13:47 -0700 Subject: [PATCH 08/21] chore: black --- slither/utils/myprettytable.py | 1 + 1 file changed, 1 insertion(+) diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index dd3672f848..fec84ef0bb 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -48,6 +48,7 @@ def make_pretty_table(headers: list, body: dict, totals: bool = False) -> MyPret table.add_row(["Total"] + [sum([body[row][key] for row in body]) for key in headers[1:]]) return table + def transpose(table): """ Converts a dict of dicts to a dict of dicts with the keys transposed From 06e218c822adcf83f96c7996b1c1683dd6a4d27d Mon Sep 17 00:00:00 2001 From: devtooligan Date: Wed, 5 Jul 2023 18:00:13 -0700 Subject: [PATCH 09/21] refactor: prefer custom classes to dicts --- slither/printers/summary/halstead.py | 177 ++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 4 deletions(-) diff --git a/slither/printers/summary/halstead.py b/slither/printers/summary/halstead.py index 12e422fb08..5741db1231 100644 --- a/slither/printers/summary/halstead.py +++ b/slither/printers/summary/halstead.py @@ -25,19 +25,182 @@ """ import math +from dataclasses import dataclass, field +from typing import Tuple, List, Dict from collections import OrderedDict +from slither.core.declarations import ( + Contract, + Pragma, + Import, + Function, + Modifier, +) 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 +from slither.utils.myprettytable import make_pretty_table, MyPrettyTable +from slither.utils.upgradeability import encode_ir_for_halstead # TODO: Add to slither/utils/halstead -def compute_halstead(contracts: list) -> tuple: +@dataclass +class HalsteadContractMetrics: + """Class to hold the Halstead metrics for a single contract.""" + # TODO: Add to slither/utils/halstead + contract: Contract + all_operators: List[str] = field(default_factory=list) + all_operands: List[str] = field(default_factory=list) + n1: int = 0 + n2: int = 0 + N1: int = 0 + N2: int = 0 + n: int = 0 + N: int = 0 + S: float = 0 + V: float = 0 + D: float = 0 + E: float = 0 + T: float = 0 + B: float = 0 + + def __post_init__(self): + if (len(self.all_operators) == 0): + self.populate_operators_and_operands() + self.compute_metrics() + + def to_dict(self) -> Dict[str, float]: + """Return the metrics as a dictionary.""" + return OrderedDict({ + "Total Operators": self.N1, + "Unique Operators": self.n1, + "Total Operands": self.N2, + "Unique Operands": self.n2, + "Vocabulary": str(self.n1 + self.n2), + "Program Length": str(self.N1 + self.N2), + "Estimated Length": f"{self.S:.0f}", + "Volume": f"{self.V:.0f}", + "Difficulty": f"{self.D:.0f}", + "Effort": f"{self.E:.0f}", + "Time": f"{self.T:.0f}", + "Estimated Bugs": f"{self.B:.3f}", + }) + + def populate_operators_and_operands(self): + """Populate the operators and operands lists.""" + operators = [] + operands = [] + for func in self.contract.functions: + for node in func.nodes: + for operation in node.irs: + # use operation.expression.type to get the unique operator type + encoded_operator = encode_ir_for_halstead(operation) + operators.append(encoded_operator) + + # use operation.used to get the operands of the operation ignoring the temporary variables + operands.extend([ + op for op in operation.used if not isinstance(op, TemporaryVariable) + ]) + # import pdb; pdb.set_trace() + self.all_operators.extend(operators) + self.all_operands.extend(operands) + + def compute_metrics(self, all_operators=[], all_operands=[]): + """Compute the Halstead metrics.""" + if len(all_operators) == 0: + all_operators = self.all_operators + all_operands = self.all_operands + + self.n1 = len(set(all_operators)) + self.n2 = len(set(all_operands)) + self.N1 = len(all_operators) + self.N2 = len(all_operands) + if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]): + raise ValueError("n1 and n2 must be greater than 0") + + self.n = self.n1 + self.n2 + self.N = self.N1 + self.N2 + self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2) + self.V = self.N * math.log2(self.n) + self.D = (self.n1 / 2) * (self.N2 / self.n2) + self.E = self.D * self.V + self.T = self.E / 18 + self.B = (self.E ** (2 / 3)) / 3000 + + +@dataclass +class SectionInfo: + title: str + pretty_table: MyPrettyTable + txt: str + + +@dataclass +class HalsteadMetrics: + """Class to hold the Halstead metrics for all contracts and methods for reporting.""" + contracts: List[Contract] = field(default_factory=list) + contract_metrics: OrderedDict[Contract, HalsteadContractMetrics] = field(default_factory=OrderedDict) + title: str = "Halstead complexity metrics" + full_txt: str = "" + core: SectionInfo = field(default=SectionInfo) + extended1: SectionInfo = field(default=SectionInfo) + extended2: SectionInfo = field(default=SectionInfo) + CORE_KEYS = ( + "Total Operators", + "Unique Operators", + "Total Operands", + "Unique Operands", + ) + EXTENDED1_KEYS = ( + "Vocabulary", + "Program Length", + "Estimated Length", + "Volume", + ) + EXTENDED2_KEYS = ( + "Difficulty", + "Effort", + "Time", + "Estimated Bugs", + ) + SECTIONS: Tuple[Tuple[str, Tuple[str]]] = ( + ("Core", CORE_KEYS), + ("Extended1", EXTENDED1_KEYS), + ("Extended2", EXTENDED2_KEYS), + ) + + + def __post_init__(self): + for contract in self.contracts: + self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract) + + if len(self.contracts) > 1: + all_operators = [ + operator for contract in self.contracts + for operator in self.contract_metrics[contract.name].all_operators + ] + + all_operands = [ + operand for contract in self.contracts + for operand in self.contract_metrics[contract.name].all_operands + ] + + self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics(all_operators=all_operators, all_operands=all_operands) + + data = { + contract.name: self.contract_metrics[contract.name].to_dict() + for contract in self.contracts + } + for (title, keys) in self.SECTIONS: + pretty_table = make_pretty_table(["Contract", *keys], data, False) + section_title = f"{self.title} - {title}" + txt = f"\n\n{section_title}:\n{pretty_table}\n" + self.full_txt += txt + setattr(self, title.lower(), SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt)) + +def compute_halstead(contracts: list) -> Tuple[Dict, Dict, Dict]: """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) + Halstead metrics as a tuple of three OrderedDicts (core_metrics, extended_metrics1, extended_metrics2) 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 @@ -162,6 +325,8 @@ def output(self, _filename): core, extended1, extended2 = compute_halstead(self.contracts) + halstead = HalsteadMetrics(self.contracts) + # Core metrics: operations and operands txt = "\n\nHalstead complexity core metrics:\n" keys = list(core[self.contracts[0].name].keys()) @@ -185,5 +350,9 @@ def output(self, _filename): res.add_pretty_table(table_extended1, "Halstead extended metrics1") res.add_pretty_table(table_extended2, "Halstead extended metrics2") self.info(txt) + self.info("*****************************************************************") + self.info("new one") + self.info("*****************************************************************") + self.info(halstead.full_txt) return res From db5ec712de134cef84c9bde4e2a57125a2336a13 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 6 Jul 2023 12:06:58 -0700 Subject: [PATCH 10/21] chore: move halstead utilities to utils folder --- slither/printers/summary/halstead.py | 321 +-------------------------- slither/utils/halstead.py | 203 +++++++++++++++++ 2 files changed, 208 insertions(+), 316 deletions(-) create mode 100644 slither/utils/halstead.py diff --git a/slither/printers/summary/halstead.py b/slither/printers/summary/halstead.py index 5741db1231..ef446e3896 100644 --- a/slither/printers/summary/halstead.py +++ b/slither/printers/summary/halstead.py @@ -22,296 +22,9 @@ T = E / 18 seconds # Time required to program B = (E^(2/3)) / 3000 # Number of delivered bugs - """ -import math -from dataclasses import dataclass, field -from typing import Tuple, List, Dict -from collections import OrderedDict -from slither.core.declarations import ( - Contract, - Pragma, - Import, - Function, - Modifier, -) from slither.printers.abstract_printer import AbstractPrinter -from slither.slithir.variables.temporary import TemporaryVariable -from slither.utils.myprettytable import make_pretty_table, MyPrettyTable -from slither.utils.upgradeability import encode_ir_for_halstead # TODO: Add to slither/utils/halstead - - -@dataclass -class HalsteadContractMetrics: - """Class to hold the Halstead metrics for a single contract.""" - # TODO: Add to slither/utils/halstead - contract: Contract - all_operators: List[str] = field(default_factory=list) - all_operands: List[str] = field(default_factory=list) - n1: int = 0 - n2: int = 0 - N1: int = 0 - N2: int = 0 - n: int = 0 - N: int = 0 - S: float = 0 - V: float = 0 - D: float = 0 - E: float = 0 - T: float = 0 - B: float = 0 - - def __post_init__(self): - if (len(self.all_operators) == 0): - self.populate_operators_and_operands() - self.compute_metrics() - - def to_dict(self) -> Dict[str, float]: - """Return the metrics as a dictionary.""" - return OrderedDict({ - "Total Operators": self.N1, - "Unique Operators": self.n1, - "Total Operands": self.N2, - "Unique Operands": self.n2, - "Vocabulary": str(self.n1 + self.n2), - "Program Length": str(self.N1 + self.N2), - "Estimated Length": f"{self.S:.0f}", - "Volume": f"{self.V:.0f}", - "Difficulty": f"{self.D:.0f}", - "Effort": f"{self.E:.0f}", - "Time": f"{self.T:.0f}", - "Estimated Bugs": f"{self.B:.3f}", - }) - - def populate_operators_and_operands(self): - """Populate the operators and operands lists.""" - operators = [] - operands = [] - for func in self.contract.functions: - for node in func.nodes: - for operation in node.irs: - # use operation.expression.type to get the unique operator type - encoded_operator = encode_ir_for_halstead(operation) - operators.append(encoded_operator) - - # use operation.used to get the operands of the operation ignoring the temporary variables - operands.extend([ - op for op in operation.used if not isinstance(op, TemporaryVariable) - ]) - # import pdb; pdb.set_trace() - self.all_operators.extend(operators) - self.all_operands.extend(operands) - - def compute_metrics(self, all_operators=[], all_operands=[]): - """Compute the Halstead metrics.""" - if len(all_operators) == 0: - all_operators = self.all_operators - all_operands = self.all_operands - - self.n1 = len(set(all_operators)) - self.n2 = len(set(all_operands)) - self.N1 = len(all_operators) - self.N2 = len(all_operands) - if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]): - raise ValueError("n1 and n2 must be greater than 0") - - self.n = self.n1 + self.n2 - self.N = self.N1 + self.N2 - self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2) - self.V = self.N * math.log2(self.n) - self.D = (self.n1 / 2) * (self.N2 / self.n2) - self.E = self.D * self.V - self.T = self.E / 18 - self.B = (self.E ** (2 / 3)) / 3000 - - -@dataclass -class SectionInfo: - title: str - pretty_table: MyPrettyTable - txt: str - - -@dataclass -class HalsteadMetrics: - """Class to hold the Halstead metrics for all contracts and methods for reporting.""" - contracts: List[Contract] = field(default_factory=list) - contract_metrics: OrderedDict[Contract, HalsteadContractMetrics] = field(default_factory=OrderedDict) - title: str = "Halstead complexity metrics" - full_txt: str = "" - core: SectionInfo = field(default=SectionInfo) - extended1: SectionInfo = field(default=SectionInfo) - extended2: SectionInfo = field(default=SectionInfo) - CORE_KEYS = ( - "Total Operators", - "Unique Operators", - "Total Operands", - "Unique Operands", - ) - EXTENDED1_KEYS = ( - "Vocabulary", - "Program Length", - "Estimated Length", - "Volume", - ) - EXTENDED2_KEYS = ( - "Difficulty", - "Effort", - "Time", - "Estimated Bugs", - ) - SECTIONS: Tuple[Tuple[str, Tuple[str]]] = ( - ("Core", CORE_KEYS), - ("Extended1", EXTENDED1_KEYS), - ("Extended2", EXTENDED2_KEYS), - ) - - - def __post_init__(self): - for contract in self.contracts: - self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract) - - if len(self.contracts) > 1: - all_operators = [ - operator for contract in self.contracts - for operator in self.contract_metrics[contract.name].all_operators - ] - - all_operands = [ - operand for contract in self.contracts - for operand in self.contract_metrics[contract.name].all_operands - ] - - self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics(all_operators=all_operators, all_operands=all_operands) - - data = { - contract.name: self.contract_metrics[contract.name].to_dict() - for contract in self.contracts - } - for (title, keys) in self.SECTIONS: - pretty_table = make_pretty_table(["Contract", *keys], data, False) - section_title = f"{self.title} - {title}" - txt = f"\n\n{section_title}:\n{pretty_table}\n" - self.full_txt += txt - setattr(self, title.lower(), SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt)) - -def compute_halstead(contracts: list) -> Tuple[Dict, Dict, Dict]: - """Used to compute the Halstead complexity metrics for a list of contracts. - Args: - contracts: list of contracts. - Returns: - Halstead metrics as a tuple of three OrderedDicts (core_metrics, extended_metrics1, extended_metrics2) - 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_total_operators": N1, - "n1_unique_operators": n1, - "N2_total_operands": N2, - "n2_unique_operands": n1, - }} - - 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, - "B_bugs": B, - }} - - """ - core = OrderedDict() - extended1 = OrderedDict() - extended2 = 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 - 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 = [ - op for op in operation.used if not isinstance(op, TemporaryVariable) - ] - operands.extend(new_operands) - all_operands.extend(new_operands) - ( - 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 -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 = { - "Total Operators": N1, - "Unique Operators": n1, - "Total Operands": N2, - "Unique Operands": n2, - } - extended_metrics1 = { - "Vocabulary": str(n1 + n2), - "Program Length": str(N1 + N2), - "Estimated Length": f"{S:.0f}", - "Volume": f"{V:.0f}", - } - 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) - +from slither.utils.halstead import HalsteadMetrics class Halstead(AbstractPrinter): ARGUMENT = "halstead" @@ -323,36 +36,12 @@ def output(self, _filename): if len(self.contracts) == 0: return self.generate_output("No contract found") - core, extended1, extended2 = compute_halstead(self.contracts) - halstead = HalsteadMetrics(self.contracts) - # Core metrics: operations and operands - txt = "\n\nHalstead complexity core metrics:\n" - keys = list(core[self.contracts[0].name].keys()) - 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 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(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) - self.info("*****************************************************************") - self.info("new one") - self.info("*****************************************************************") + res = self.generate_output(halstead.full_txt) + res.add_pretty_table(halstead.core.pretty_table, halstead.core.title) + res.add_pretty_table(halstead.extended1.pretty_table, halstead.extended1.title) + res.add_pretty_table(halstead.extended2.pretty_table, halstead.extended2.title) self.info(halstead.full_txt) return res diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py new file mode 100644 index 0000000000..7417fb4e10 --- /dev/null +++ b/slither/utils/halstead.py @@ -0,0 +1,203 @@ +""" + 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 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 + B = (E^(2/3)) / 3000 # Number of delivered bugs + + +""" +import math +from dataclasses import dataclass, field +from typing import Tuple, List, Dict +from collections import OrderedDict +from slither.core.declarations import Contract +from slither.slithir.variables.temporary import TemporaryVariable +from slither.utils.myprettytable import make_pretty_table, MyPrettyTable +from slither.utils.upgradeability import encode_ir_for_halstead + + +@dataclass +class HalsteadContractMetrics: + """Class to hold the Halstead metrics for a single contract.""" + contract: Contract + all_operators: List[str] = field(default_factory=list) + all_operands: List[str] = field(default_factory=list) + n1: int = 0 + n2: int = 0 + N1: int = 0 + N2: int = 0 + n: int = 0 + N: int = 0 + S: float = 0 + V: float = 0 + D: float = 0 + E: float = 0 + T: float = 0 + B: float = 0 + + def __post_init__(self): + """ Operators and operands can be passed in as constructor args to avoid computing + them based on the contract. Useful for computing metrics for ALL_CONTRACTS""" + if (len(self.all_operators) == 0): + self.populate_operators_and_operands() + if (len(self.all_operators) > 0): + self.compute_metrics() + + def to_dict(self) -> Dict[str, float]: + """Return the metrics as a dictionary.""" + return OrderedDict({ + "Total Operators": self.N1, + "Unique Operators": self.n1, + "Total Operands": self.N2, + "Unique Operands": self.n2, + "Vocabulary": str(self.n1 + self.n2), + "Program Length": str(self.N1 + self.N2), + "Estimated Length": f"{self.S:.0f}", + "Volume": f"{self.V:.0f}", + "Difficulty": f"{self.D:.0f}", + "Effort": f"{self.E:.0f}", + "Time": f"{self.T:.0f}", + "Estimated Bugs": f"{self.B:.3f}", + }) + + def populate_operators_and_operands(self): + """Populate the operators and operands lists.""" + operators = [] + operands = [] + for func in self.contract.functions: + for node in func.nodes: + for operation in node.irs: + # use operation.expression.type to get the unique operator type + encoded_operator = encode_ir_for_halstead(operation) + operators.append(encoded_operator) + + # use operation.used to get the operands of the operation ignoring the temporary variables + operands.extend([ + op for op in operation.used if not isinstance(op, TemporaryVariable) + ]) + self.all_operators.extend(operators) + self.all_operands.extend(operands) + + def compute_metrics(self, all_operators=[], all_operands=[]): + """Compute the Halstead metrics.""" + if len(all_operators) == 0: + all_operators = self.all_operators + all_operands = self.all_operands + + # core metrics + self.n1 = len(set(all_operators)) + self.n2 = len(set(all_operands)) + self.N1 = len(all_operators) + self.N2 = len(all_operands) + if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]): + raise ValueError("n1 and n2 must be greater than 0") + + # extended metrics 1 + self.n = self.n1 + self.n2 + self.N = self.N1 + self.N2 + self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2) + self.V = self.N * math.log2(self.n) + + # extended metrics 2 + self.D = (self.n1 / 2) * (self.N2 / self.n2) + self.E = self.D * self.V + self.T = self.E / 18 + self.B = (self.E ** (2 / 3)) / 3000 + + +@dataclass +class SectionInfo: + """Class to hold the information for a section of the report.""" + title: str + pretty_table: MyPrettyTable + txt: str + + +@dataclass +class HalsteadMetrics: + """Class to hold the Halstead metrics for all contracts. Contains methods useful for reporting. + + There are 3 sections in the report: + 1. Core metrics (n1, n2, N1, N2) + 2. Extended metrics 1 (n, N, S, V) + 3. Extended metrics 2 (D, E, T, B) + + """ + contracts: List[Contract] = field(default_factory=list) + contract_metrics: OrderedDict[Contract, HalsteadContractMetrics] = field(default_factory=OrderedDict) + title: str = "Halstead complexity metrics" + full_txt: str = "" + core: SectionInfo = field(default=SectionInfo) + extended1: SectionInfo = field(default=SectionInfo) + extended2: SectionInfo = field(default=SectionInfo) + CORE_KEYS = ( + "Total Operators", + "Unique Operators", + "Total Operands", + "Unique Operands", + ) + EXTENDED1_KEYS = ( + "Vocabulary", + "Program Length", + "Estimated Length", + "Volume", + ) + EXTENDED2_KEYS = ( + "Difficulty", + "Effort", + "Time", + "Estimated Bugs", + ) + SECTIONS: Tuple[Tuple[str, Tuple[str]]] = ( + ("Core", CORE_KEYS), + ("Extended1", EXTENDED1_KEYS), + ("Extended2", EXTENDED2_KEYS), + ) + + + def __post_init__(self): + # Compute the metrics for each contract and for all contracts. + for contract in self.contracts: + self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract) + + # If there are more than 1 contract, compute the metrics for all contracts. + if len(self.contracts) > 1: + all_operators = [ + operator for contract in self.contracts + for operator in self.contract_metrics[contract.name].all_operators + ] + all_operands = [ + operand for contract in self.contracts + for operand in self.contract_metrics[contract.name].all_operands + ] + self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics(all_operators=all_operators, all_operands=all_operands) + + # Create the table and text for each section. + data = { + contract.name: self.contract_metrics[contract.name].to_dict() + for contract in self.contracts + } + for (title, keys) in self.SECTIONS: + pretty_table = make_pretty_table(["Contract", *keys], data, False) + section_title = f"{self.title} ({title})" + txt = f"\n\n{section_title}:\n{pretty_table}\n" + self.full_txt += txt + setattr(self, title.lower(), SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt)) From 0fb6e42a9d97833387dc23624f4763b33e9fb8ab Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 6 Jul 2023 12:12:40 -0700 Subject: [PATCH 11/21] chore: lint --- slither/printers/summary/halstead.py | 1 + slither/utils/halstead.py | 72 +++++++++++++++++----------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/slither/printers/summary/halstead.py b/slither/printers/summary/halstead.py index ef446e3896..f5e4f0a90b 100644 --- a/slither/printers/summary/halstead.py +++ b/slither/printers/summary/halstead.py @@ -26,6 +26,7 @@ from slither.printers.abstract_printer import AbstractPrinter from slither.utils.halstead import HalsteadMetrics + class Halstead(AbstractPrinter): ARGUMENT = "halstead" HELP = "Computes the Halstead complexity metrics for each contract" diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py index 7417fb4e10..d9835b6be2 100644 --- a/slither/utils/halstead.py +++ b/slither/utils/halstead.py @@ -35,8 +35,10 @@ @dataclass +# pylint: disable=too-many-instance-attributes class HalsteadContractMetrics: """Class to hold the Halstead metrics for a single contract.""" + contract: Contract all_operators: List[str] = field(default_factory=list) all_operands: List[str] = field(default_factory=list) @@ -54,29 +56,31 @@ class HalsteadContractMetrics: B: float = 0 def __post_init__(self): - """ Operators and operands can be passed in as constructor args to avoid computing + """Operators and operands can be passed in as constructor args to avoid computing them based on the contract. Useful for computing metrics for ALL_CONTRACTS""" - if (len(self.all_operators) == 0): + if len(self.all_operators) == 0: self.populate_operators_and_operands() - if (len(self.all_operators) > 0): + if len(self.all_operators) > 0: self.compute_metrics() def to_dict(self) -> Dict[str, float]: """Return the metrics as a dictionary.""" - return OrderedDict({ - "Total Operators": self.N1, - "Unique Operators": self.n1, - "Total Operands": self.N2, - "Unique Operands": self.n2, - "Vocabulary": str(self.n1 + self.n2), - "Program Length": str(self.N1 + self.N2), - "Estimated Length": f"{self.S:.0f}", - "Volume": f"{self.V:.0f}", - "Difficulty": f"{self.D:.0f}", - "Effort": f"{self.E:.0f}", - "Time": f"{self.T:.0f}", - "Estimated Bugs": f"{self.B:.3f}", - }) + return OrderedDict( + { + "Total Operators": self.N1, + "Unique Operators": self.n1, + "Total Operands": self.N2, + "Unique Operands": self.n2, + "Vocabulary": str(self.n1 + self.n2), + "Program Length": str(self.N1 + self.N2), + "Estimated Length": f"{self.S:.0f}", + "Volume": f"{self.V:.0f}", + "Difficulty": f"{self.D:.0f}", + "Effort": f"{self.E:.0f}", + "Time": f"{self.T:.0f}", + "Estimated Bugs": f"{self.B:.3f}", + } + ) def populate_operators_and_operands(self): """Populate the operators and operands lists.""" @@ -90,15 +94,15 @@ def populate_operators_and_operands(self): operators.append(encoded_operator) # use operation.used to get the operands of the operation ignoring the temporary variables - operands.extend([ - op for op in operation.used if not isinstance(op, TemporaryVariable) - ]) + operands.extend( + [op for op in operation.used if not isinstance(op, TemporaryVariable)] + ) self.all_operators.extend(operators) self.all_operands.extend(operands) - def compute_metrics(self, all_operators=[], all_operands=[]): + def compute_metrics(self, all_operators=None, all_operands=None): """Compute the Halstead metrics.""" - if len(all_operators) == 0: + if all_operators is None: all_operators = self.all_operators all_operands = self.all_operands @@ -126,12 +130,14 @@ def compute_metrics(self, all_operators=[], all_operands=[]): @dataclass class SectionInfo: """Class to hold the information for a section of the report.""" + title: str pretty_table: MyPrettyTable txt: str @dataclass +# pylint: disable=too-many-instance-attributes class HalsteadMetrics: """Class to hold the Halstead metrics for all contracts. Contains methods useful for reporting. @@ -141,8 +147,11 @@ class HalsteadMetrics: 3. Extended metrics 2 (D, E, T, B) """ + contracts: List[Contract] = field(default_factory=list) - contract_metrics: OrderedDict[Contract, HalsteadContractMetrics] = field(default_factory=OrderedDict) + contract_metrics: OrderedDict[Contract, HalsteadContractMetrics] = field( + default_factory=OrderedDict + ) title: str = "Halstead complexity metrics" full_txt: str = "" core: SectionInfo = field(default=SectionInfo) @@ -172,7 +181,6 @@ class HalsteadMetrics: ("Extended2", EXTENDED2_KEYS), ) - def __post_init__(self): # Compute the metrics for each contract and for all contracts. for contract in self.contracts: @@ -181,14 +189,18 @@ def __post_init__(self): # If there are more than 1 contract, compute the metrics for all contracts. if len(self.contracts) > 1: all_operators = [ - operator for contract in self.contracts + operator + for contract in self.contracts for operator in self.contract_metrics[contract.name].all_operators ] all_operands = [ - operand for contract in self.contracts + operand + for contract in self.contracts for operand in self.contract_metrics[contract.name].all_operands ] - self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics(all_operators=all_operators, all_operands=all_operands) + self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics( + None, all_operators=all_operators, all_operands=all_operands + ) # Create the table and text for each section. data = { @@ -200,4 +212,8 @@ def __post_init__(self): section_title = f"{self.title} ({title})" txt = f"\n\n{section_title}:\n{pretty_table}\n" self.full_txt += txt - setattr(self, title.lower(), SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt)) + setattr( + self, + title.lower(), + SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), + ) From 8f831ada952e8af8c084712282d6f73ec08afa1d Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 6 Jul 2023 15:00:09 -0700 Subject: [PATCH 12/21] fix: 'type' object is not subscriptable --- slither/utils/halstead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py index d9835b6be2..d781e3c39b 100644 --- a/slither/utils/halstead.py +++ b/slither/utils/halstead.py @@ -149,7 +149,7 @@ class HalsteadMetrics: """ contracts: List[Contract] = field(default_factory=list) - contract_metrics: OrderedDict[Contract, HalsteadContractMetrics] = field( + contract_metrics: OrderedDict = field( default_factory=OrderedDict ) title: str = "Halstead complexity metrics" From 61e3076647bce3dbd2940986fce9863abe63eb38 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 6 Jul 2023 15:15:59 -0700 Subject: [PATCH 13/21] chore: lint --- slither/utils/halstead.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py index d781e3c39b..f4426f60a9 100644 --- a/slither/utils/halstead.py +++ b/slither/utils/halstead.py @@ -149,9 +149,7 @@ class HalsteadMetrics: """ contracts: List[Contract] = field(default_factory=list) - contract_metrics: OrderedDict = field( - default_factory=OrderedDict - ) + contract_metrics: OrderedDict = field(default_factory=OrderedDict) title: str = "Halstead complexity metrics" full_txt: str = "" core: SectionInfo = field(default=SectionInfo) From 8d2e7c40ee58a4f46e8cb69d659cbef4a388bd6f Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 6 Jul 2023 16:51:38 -0700 Subject: [PATCH 14/21] refactor: prefer custom classes to dicts --- slither/printers/summary/martin.py | 128 ++++++++++++++++++++++++++++- slither/utils/halstead.py | 41 +++++---- 2 files changed, 151 insertions(+), 18 deletions(-) diff --git a/slither/printers/summary/martin.py b/slither/printers/summary/martin.py index 693ec15759..56a04f341f 100644 --- a/slither/printers/summary/martin.py +++ b/slither/printers/summary/martin.py @@ -9,9 +9,12 @@ Distance from the Main Sequence (D): abs(A + I - 1) """ -from typing import Tuple +from typing import Tuple, List, Dict +from dataclasses import dataclass, field +from collections import OrderedDict from slither.slithir.operations.high_level_call import HighLevelCall -from slither.utils.myprettytable import make_pretty_table +from slither.core.declarations import Contract +from slither.utils.myprettytable import make_pretty_table, MyPrettyTable from slither.printers.abstract_printer import AbstractPrinter @@ -86,6 +89,117 @@ def compute_coupling(contracts: list, abstractness: float) -> dict: } return coupling_dict +@dataclass +class MartinContractMetrics: + contract: Contract + ca: int + ce: int + abstractness: float + i: float = 0.0 + d: float = 0.0 + + def __post_init__(self): + if self.ce + self.ca > 0: + self.i = float(self.ce / (self.ce + self.ca)) + self.d = float(abs(self.i - self.abstractness)) + + def to_dict(self): + return { + "Dependents": self.ca, + "Dependencies": self.ce, + "Instability": f"{self.i:.2f}", + "Distance from main sequence": f"{self.d:.2f}", + } + +@dataclass +class SectionInfo: + """Class to hold the information for a section of the report.""" + + title: str + pretty_table: MyPrettyTable + txt: str + + +@dataclass +class MartinMetrics: + contracts: List[Contract] = field(default_factory=list) + abstractness: float = 0.0 + contract_metrics: OrderedDict = field(default_factory=OrderedDict) + title: str = "Martin complexity metrics" + full_text: str = "" + core: SectionInfo = field(default=SectionInfo) + CORE_KEYS = ( + "Dependents", + "Dependencies", + "Instability", + "Distance from main sequence", + ) + SECTIONS: Tuple[Tuple[str, Tuple[str]]] = ( + ("Core", CORE_KEYS), + ) + + def __post_init__(self): + self.update_abstractness() + self.update_coupling() + self.update_reporting_sections() + + def update_reporting_sections(self): + # Create the table and text for each section. + data = { + contract.name: self.contract_metrics[contract.name].to_dict() + for contract in self.contracts + } + for (title, keys) in self.SECTIONS: + pretty_table = make_pretty_table(["Contract", *keys], data, False) + section_title = f"{self.title} ({title})" + txt = f"\n\n{section_title}:\n" + txt = "Martin agile software metrics\n" + txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" + txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" + txt += "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" + txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" + txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" + txt += "\n" + txt += f"Abstractness (overall): {round(self.abstractness, 2)}\n" + txt += f"{pretty_table}\n" + self.full_text += txt + setattr( + self, + title.lower(), + SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), + ) + + def update_abstractness(self) -> float: + abstract_contract_count = 0 + for c in self.contracts: + if not c.is_fully_implemented: + abstract_contract_count += 1 + self.abstractness = float(abstract_contract_count / len(self.contracts)) + + + def update_coupling(self) -> Dict: + dependencies = {} + for contract in self.contracts: + for func in contract.functions: + high_level_calls = [ + ir for node in func.nodes for ir in node.irs_ssa if isinstance(ir, HighLevelCall) + ] + # convert irs to string with target function and contract name + external_calls = [h.destination.type.type.name for h in high_level_calls] + dependencies[contract.name] = set(external_calls) + dependents = {} + for contract, deps in dependencies.items(): + for dep in deps: + if dep not in dependents: + dependents[dep] = set() + dependents[dep].add(contract) + + coupling_dict = {} + for contract in self.contracts: + ce = len(dependencies.get(contract.name, [])) + ca = len(dependents.get(contract.name, [])) + self.contract_metrics[contract.name] = MartinContractMetrics(contract, ca, ce, self.abstractness) + class Martin(AbstractPrinter): ARGUMENT = "martin" @@ -94,6 +208,16 @@ class Martin(AbstractPrinter): WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#martin" def output(self, _filename): + if len(self.contracts) == 0: + return self.generate_output("No contract found") + + martin = MartinMetrics(self.contracts) + + res = self.generate_output(martin.full_text) + res.add_pretty_table(martin.core.pretty_table, martin.core.title) + self.info(martin.full_text) + + (abstract_contract_count, total_contract_count) = count_abstracts(self.contracts) abstractness = float(abstract_contract_count / total_contract_count) coupling_dict = compute_coupling(self.contracts, abstractness) diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py index f4426f60a9..a152474d08 100644 --- a/slither/utils/halstead.py +++ b/slither/utils/halstead.py @@ -151,7 +151,7 @@ class HalsteadMetrics: contracts: List[Contract] = field(default_factory=list) contract_metrics: OrderedDict = field(default_factory=OrderedDict) title: str = "Halstead complexity metrics" - full_txt: str = "" + full_text: str = "" core: SectionInfo = field(default=SectionInfo) extended1: SectionInfo = field(default=SectionInfo) extended2: SectionInfo = field(default=SectionInfo) @@ -181,25 +181,34 @@ class HalsteadMetrics: def __post_init__(self): # Compute the metrics for each contract and for all contracts. + self.update_contract_metrics() + self.add_all_contracts_metrics() + self.update_reporting_sections() + + def update_contract_metrics(self): for contract in self.contracts: self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract) + def add_all_contracts_metrics(self): # If there are more than 1 contract, compute the metrics for all contracts. - if len(self.contracts) > 1: - all_operators = [ - operator - for contract in self.contracts - for operator in self.contract_metrics[contract.name].all_operators - ] - all_operands = [ - operand - for contract in self.contracts - for operand in self.contract_metrics[contract.name].all_operands - ] - self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics( - None, all_operators=all_operators, all_operands=all_operands - ) + if len(self.contracts) <= 1: + return + all_operators = [ + operator + for contract in self.contracts + for operator in self.contract_metrics[contract.name].all_operators + ] + all_operands = [ + operand + for contract in self.contracts + for operand in self.contract_metrics[contract.name].all_operands + ] + self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics( + None, all_operators=all_operators, all_operands=all_operands + ) + + def update_reporting_sections(self): # Create the table and text for each section. data = { contract.name: self.contract_metrics[contract.name].to_dict() @@ -209,7 +218,7 @@ def __post_init__(self): pretty_table = make_pretty_table(["Contract", *keys], data, False) section_title = f"{self.title} ({title})" txt = f"\n\n{section_title}:\n{pretty_table}\n" - self.full_txt += txt + self.full_text += txt setattr( self, title.lower(), From c787fb4fbeef2296e1329f01ac32493fbaaac523 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 6 Jul 2023 16:55:37 -0700 Subject: [PATCH 15/21] chore: move Martin logic to utils --- slither/printers/summary/halstead.py | 4 +- slither/printers/summary/martin.py | 211 +-------------------------- slither/utils/martin.py | 130 +++++++++++++++++ 3 files changed, 133 insertions(+), 212 deletions(-) create mode 100644 slither/utils/martin.py diff --git a/slither/printers/summary/halstead.py b/slither/printers/summary/halstead.py index f5e4f0a90b..8144e467f7 100644 --- a/slither/printers/summary/halstead.py +++ b/slither/printers/summary/halstead.py @@ -39,10 +39,10 @@ def output(self, _filename): halstead = HalsteadMetrics(self.contracts) - res = self.generate_output(halstead.full_txt) + res = self.generate_output(halstead.full_text) res.add_pretty_table(halstead.core.pretty_table, halstead.core.title) res.add_pretty_table(halstead.extended1.pretty_table, halstead.extended1.title) res.add_pretty_table(halstead.extended2.pretty_table, halstead.extended2.title) - self.info(halstead.full_txt) + self.info(halstead.full_text) return res diff --git a/slither/printers/summary/martin.py b/slither/printers/summary/martin.py index 56a04f341f..66b14fb90c 100644 --- a/slither/printers/summary/martin.py +++ b/slither/printers/summary/martin.py @@ -9,197 +9,8 @@ Distance from the Main Sequence (D): abs(A + I - 1) """ -from typing import Tuple, List, Dict -from dataclasses import dataclass, field -from collections import OrderedDict -from slither.slithir.operations.high_level_call import HighLevelCall -from slither.core.declarations import Contract -from slither.utils.myprettytable import make_pretty_table, MyPrettyTable from slither.printers.abstract_printer import AbstractPrinter - - -def count_abstracts(contracts) -> Tuple[int, int]: - """ - Count the number of abstract contracts - Args: - contracts(list): list of contracts - Returns: - a tuple of (abstract_contract_count, total_contract_count) - """ - abstract_contract_count = 0 - for c in contracts: - if not c.is_fully_implemented: - abstract_contract_count += 1 - return (abstract_contract_count, len(contracts)) - - -def compute_coupling(contracts: list, abstractness: float) -> dict: - """ - Used to compute the coupling between contracts external calls made to internal contracts - Args: - contracts: list of contracts - Returns: - dict of contract names with dicts of the coupling metrics: - { - "contract_name1": { - "Dependents": 0, - "Dependencies": 3 - "Instability": 1.0, - "Abstractness": 0.0, - "Distance from main sequence": 1.0, - }, - "contract_name2": { - "Dependents": 1, - "Dependencies": 0 - "Instability": 0.0, - "Abstractness": 1.0, - "Distance from main sequence": 0.0, - } - """ - dependencies = {} - for contract in contracts: - for func in contract.functions: - high_level_calls = [ - ir for node in func.nodes for ir in node.irs_ssa if isinstance(ir, HighLevelCall) - ] - # convert irs to string with target function and contract name - external_calls = [h.destination.type.type.name for h in high_level_calls] - dependencies[contract.name] = set(external_calls) - dependents = {} - for contract, deps in dependencies.items(): - for dep in deps: - if dep not in dependents: - dependents[dep] = set() - dependents[dep].add(contract) - - coupling_dict = {} - for contract in contracts: - ce = len(dependencies.get(contract.name, [])) - ca = len(dependents.get(contract.name, [])) - i = 0.0 - d = 0.0 - if ce + ca > 0: - i = float(ce / (ce + ca)) - d = float(abs(i - abstractness)) - coupling_dict[contract.name] = { - "Dependents": ca, - "Dependencies": ce, - "Instability": f"{i:.2f}", - "Distance from main sequence": f"{d:.2f}", - } - return coupling_dict - -@dataclass -class MartinContractMetrics: - contract: Contract - ca: int - ce: int - abstractness: float - i: float = 0.0 - d: float = 0.0 - - def __post_init__(self): - if self.ce + self.ca > 0: - self.i = float(self.ce / (self.ce + self.ca)) - self.d = float(abs(self.i - self.abstractness)) - - def to_dict(self): - return { - "Dependents": self.ca, - "Dependencies": self.ce, - "Instability": f"{self.i:.2f}", - "Distance from main sequence": f"{self.d:.2f}", - } - -@dataclass -class SectionInfo: - """Class to hold the information for a section of the report.""" - - title: str - pretty_table: MyPrettyTable - txt: str - - -@dataclass -class MartinMetrics: - contracts: List[Contract] = field(default_factory=list) - abstractness: float = 0.0 - contract_metrics: OrderedDict = field(default_factory=OrderedDict) - title: str = "Martin complexity metrics" - full_text: str = "" - core: SectionInfo = field(default=SectionInfo) - CORE_KEYS = ( - "Dependents", - "Dependencies", - "Instability", - "Distance from main sequence", - ) - SECTIONS: Tuple[Tuple[str, Tuple[str]]] = ( - ("Core", CORE_KEYS), - ) - - def __post_init__(self): - self.update_abstractness() - self.update_coupling() - self.update_reporting_sections() - - def update_reporting_sections(self): - # Create the table and text for each section. - data = { - contract.name: self.contract_metrics[contract.name].to_dict() - for contract in self.contracts - } - for (title, keys) in self.SECTIONS: - pretty_table = make_pretty_table(["Contract", *keys], data, False) - section_title = f"{self.title} ({title})" - txt = f"\n\n{section_title}:\n" - txt = "Martin agile software metrics\n" - txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" - txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" - txt += "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" - txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" - txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" - txt += "\n" - txt += f"Abstractness (overall): {round(self.abstractness, 2)}\n" - txt += f"{pretty_table}\n" - self.full_text += txt - setattr( - self, - title.lower(), - SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), - ) - - def update_abstractness(self) -> float: - abstract_contract_count = 0 - for c in self.contracts: - if not c.is_fully_implemented: - abstract_contract_count += 1 - self.abstractness = float(abstract_contract_count / len(self.contracts)) - - - def update_coupling(self) -> Dict: - dependencies = {} - for contract in self.contracts: - for func in contract.functions: - high_level_calls = [ - ir for node in func.nodes for ir in node.irs_ssa if isinstance(ir, HighLevelCall) - ] - # convert irs to string with target function and contract name - external_calls = [h.destination.type.type.name for h in high_level_calls] - dependencies[contract.name] = set(external_calls) - dependents = {} - for contract, deps in dependencies.items(): - for dep in deps: - if dep not in dependents: - dependents[dep] = set() - dependents[dep].add(contract) - - coupling_dict = {} - for contract in self.contracts: - ce = len(dependencies.get(contract.name, [])) - ca = len(dependents.get(contract.name, [])) - self.contract_metrics[contract.name] = MartinContractMetrics(contract, ca, ce, self.abstractness) - +from slither.utils.martin import MartinMetrics class Martin(AbstractPrinter): ARGUMENT = "martin" @@ -216,24 +27,4 @@ def output(self, _filename): res = self.generate_output(martin.full_text) res.add_pretty_table(martin.core.pretty_table, martin.core.title) self.info(martin.full_text) - - - (abstract_contract_count, total_contract_count) = count_abstracts(self.contracts) - abstractness = float(abstract_contract_count / total_contract_count) - coupling_dict = compute_coupling(self.contracts, abstractness) - - table = make_pretty_table( - ["Contract", *list(coupling_dict[self.contracts[0].name].keys())], coupling_dict - ) - txt = "Martin agile software metrics\n" - txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" - txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" - txt += "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" - txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" - txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" - txt += "\n" - txt += f"Abstractness (overall): {round(abstractness, 2)}\n" + str(table) - self.info(txt) - res = self.generate_output(txt) - res.add_pretty_table(table, "Code Lines") return res diff --git a/slither/utils/martin.py b/slither/utils/martin.py new file mode 100644 index 0000000000..2ca38473d1 --- /dev/null +++ b/slither/utils/martin.py @@ -0,0 +1,130 @@ +""" + Robert "Uncle Bob" Martin - Agile software metrics + https://en.wikipedia.org/wiki/Software_package_metrics + + Efferent Coupling (Ce): Number of contracts that the contract depends on + Afferent Coupling (Ca): Number of contracts that depend on a contract + Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca)) + Abstractness (A): Number of abstract contracts / total number of contracts + Distance from the Main Sequence (D): abs(A + I - 1) + +""" +from typing import Tuple, List, Dict +from dataclasses import dataclass, field +from collections import OrderedDict +from slither.slithir.operations.high_level_call import HighLevelCall +from slither.core.declarations import Contract +from slither.utils.myprettytable import make_pretty_table, MyPrettyTable + + +@dataclass +class MartinContractMetrics: + contract: Contract + ca: int + ce: int + abstractness: float + i: float = 0.0 + d: float = 0.0 + + def __post_init__(self): + if self.ce + self.ca > 0: + self.i = float(self.ce / (self.ce + self.ca)) + self.d = float(abs(self.i - self.abstractness)) + + def to_dict(self): + return { + "Dependents": self.ca, + "Dependencies": self.ce, + "Instability": f"{self.i:.2f}", + "Distance from main sequence": f"{self.d:.2f}", + } + +@dataclass +class SectionInfo: + """Class to hold the information for a section of the report.""" + + title: str + pretty_table: MyPrettyTable + txt: str + + +@dataclass +class MartinMetrics: + contracts: List[Contract] = field(default_factory=list) + abstractness: float = 0.0 + contract_metrics: OrderedDict = field(default_factory=OrderedDict) + title: str = "Martin complexity metrics" + full_text: str = "" + core: SectionInfo = field(default=SectionInfo) + CORE_KEYS = ( + "Dependents", + "Dependencies", + "Instability", + "Distance from main sequence", + ) + SECTIONS: Tuple[Tuple[str, Tuple[str]]] = ( + ("Core", CORE_KEYS), + ) + + def __post_init__(self): + self.update_abstractness() + self.update_coupling() + self.update_reporting_sections() + + def update_reporting_sections(self): + # Create the table and text for each section. + data = { + contract.name: self.contract_metrics[contract.name].to_dict() + for contract in self.contracts + } + for (title, keys) in self.SECTIONS: + pretty_table = make_pretty_table(["Contract", *keys], data, False) + section_title = f"{self.title} ({title})" + txt = f"\n\n{section_title}:\n" + txt = "Martin agile software metrics\n" + txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" + txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" + txt += "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" + txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" + txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" + txt += "\n" + txt += f"Abstractness (overall): {round(self.abstractness, 2)}\n" + txt += f"{pretty_table}\n" + self.full_text += txt + setattr( + self, + title.lower(), + SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), + ) + + def update_abstractness(self) -> float: + abstract_contract_count = 0 + for c in self.contracts: + if not c.is_fully_implemented: + abstract_contract_count += 1 + self.abstractness = float(abstract_contract_count / len(self.contracts)) + + + def update_coupling(self) -> Dict: + dependencies = {} + for contract in self.contracts: + for func in contract.functions: + high_level_calls = [ + ir for node in func.nodes for ir in node.irs_ssa if isinstance(ir, HighLevelCall) + ] + # convert irs to string with target function and contract name + external_calls = [h.destination.type.type.name for h in high_level_calls] + dependencies[contract.name] = set(external_calls) + dependents = {} + for contract, deps in dependencies.items(): + for dep in deps: + if dep not in dependents: + dependents[dep] = set() + dependents[dep].add(contract) + + coupling_dict = {} + for contract in self.contracts: + ce = len(dependencies.get(contract.name, [])) + ca = len(dependents.get(contract.name, [])) + self.contract_metrics[contract.name] = MartinContractMetrics(contract, ca, ce, self.abstractness) + From 2e99e498f0d82d01de92ad38e763773d7e365beb Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 6 Jul 2023 16:59:57 -0700 Subject: [PATCH 16/21] chore: lint --- slither/printers/summary/martin.py | 1 + slither/utils/halstead.py | 1 - slither/utils/martin.py | 21 ++++++++++++--------- slither/utils/myprettytable.py | 1 + 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/slither/printers/summary/martin.py b/slither/printers/summary/martin.py index 66b14fb90c..c49e63fcb5 100644 --- a/slither/printers/summary/martin.py +++ b/slither/printers/summary/martin.py @@ -12,6 +12,7 @@ from slither.printers.abstract_printer import AbstractPrinter from slither.utils.martin import MartinMetrics + class Martin(AbstractPrinter): ARGUMENT = "martin" HELP = "Martin agile software metrics (Ca, Ce, I, A, D)" diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py index a152474d08..93552c9cd3 100644 --- a/slither/utils/halstead.py +++ b/slither/utils/halstead.py @@ -207,7 +207,6 @@ def add_all_contracts_metrics(self): None, all_operators=all_operators, all_operands=all_operands ) - def update_reporting_sections(self): # Create the table and text for each section. data = { diff --git a/slither/utils/martin.py b/slither/utils/martin.py index 2ca38473d1..ded8c0efa3 100644 --- a/slither/utils/martin.py +++ b/slither/utils/martin.py @@ -39,6 +39,7 @@ def to_dict(self): "Distance from main sequence": f"{self.d:.2f}", } + @dataclass class SectionInfo: """Class to hold the information for a section of the report.""" @@ -62,9 +63,7 @@ class MartinMetrics: "Instability", "Distance from main sequence", ) - SECTIONS: Tuple[Tuple[str, Tuple[str]]] = ( - ("Core", CORE_KEYS), - ) + SECTIONS: Tuple[Tuple[str, Tuple[str]]] = (("Core", CORE_KEYS),) def __post_init__(self): self.update_abstractness() @@ -84,7 +83,9 @@ def update_reporting_sections(self): txt = "Martin agile software metrics\n" txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" - txt += "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" + txt += ( + "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" + ) txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" txt += "\n" @@ -104,13 +105,15 @@ def update_abstractness(self) -> float: abstract_contract_count += 1 self.abstractness = float(abstract_contract_count / len(self.contracts)) - def update_coupling(self) -> Dict: dependencies = {} for contract in self.contracts: for func in contract.functions: high_level_calls = [ - ir for node in func.nodes for ir in node.irs_ssa if isinstance(ir, HighLevelCall) + ir + for node in func.nodes + for ir in node.irs_ssa + if isinstance(ir, HighLevelCall) ] # convert irs to string with target function and contract name external_calls = [h.destination.type.type.name for h in high_level_calls] @@ -122,9 +125,9 @@ def update_coupling(self) -> Dict: dependents[dep] = set() dependents[dep].add(contract) - coupling_dict = {} for contract in self.contracts: ce = len(dependencies.get(contract.name, [])) ca = len(dependents.get(contract.name, [])) - self.contract_metrics[contract.name] = MartinContractMetrics(contract, ca, ce, self.abstractness) - + self.contract_metrics[contract.name] = MartinContractMetrics( + contract, ca, ce, self.abstractness + ) diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index b33164894a..e45edeaf13 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -26,6 +26,7 @@ def __str__(self) -> str: # **Dict to MyPrettyTable utility functions** + def make_pretty_table(headers: list, body: dict, totals: bool = False) -> MyPrettyTable: """ Converts a dict to a MyPrettyTable. Dict keys are the row headers. From 8c51e47a42d25d00379d3c883f4a876c62122315 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 7 Jul 2023 11:25:51 -0700 Subject: [PATCH 17/21] Update scripts/ci_test_printers.sh Co-authored-by: alpharush <0xalpharush@protonmail.com> --- scripts/ci_test_printers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci_test_printers.sh b/scripts/ci_test_printers.sh index 90d1b0e038..329c415c6e 100755 --- a/scripts/ci_test_printers.sh +++ b/scripts/ci_test_printers.sh @@ -5,7 +5,7 @@ cd tests/e2e/solc_parsing/test_data/compile/ || exit # Do not test the evm printer,as it needs a refactoring -ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc, slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration" +ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration" # Only test 0.5.17 to limit test time for file in *0.5.17-compact.zip; do From 56993490dd1de85a2fa1bde1d97cd17989beb1c5 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 7 Jul 2023 11:29:22 -0700 Subject: [PATCH 18/21] fix: typo --- scripts/ci_test_printers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci_test_printers.sh b/scripts/ci_test_printers.sh index 90d1b0e038..329c415c6e 100755 --- a/scripts/ci_test_printers.sh +++ b/scripts/ci_test_printers.sh @@ -5,7 +5,7 @@ cd tests/e2e/solc_parsing/test_data/compile/ || exit # Do not test the evm printer,as it needs a refactoring -ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc, slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration" +ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration" # Only test 0.5.17 to limit test time for file in *0.5.17-compact.zip; do From 4a3ab0a6538e0ecd6f562449a095722fe95a91c8 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 7 Jul 2023 11:30:39 -0700 Subject: [PATCH 19/21] fix: add martin printer to testing printer list --- scripts/ci_test_printers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci_test_printers.sh b/scripts/ci_test_printers.sh index 329c415c6e..e7310700e5 100755 --- a/scripts/ci_test_printers.sh +++ b/scripts/ci_test_printers.sh @@ -5,7 +5,7 @@ cd tests/e2e/solc_parsing/test_data/compile/ || exit # Do not test the evm printer,as it needs a refactoring -ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration" +ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc,martin,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration" # Only test 0.5.17 to limit test time for file in *0.5.17-compact.zip; do From 42cd6e0ecfae92f31d57ae1316b62925e3007ede Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 7 Jul 2023 12:13:22 -0700 Subject: [PATCH 20/21] fix: account for case w no functions --- slither/utils/halstead.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py index f4426f60a9..829dc8035c 100644 --- a/slither/utils/halstead.py +++ b/slither/utils/halstead.py @@ -86,6 +86,8 @@ def populate_operators_and_operands(self): """Populate the operators and operands lists.""" operators = [] operands = [] + if not hasattr(self.contract, "functions"): + return for func in self.contract.functions: for node in func.nodes: for operation in node.irs: From 6843d03da43a984698ad493efe3de262246520e4 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 7 Jul 2023 12:37:07 -0700 Subject: [PATCH 21/21] fix: external calls --- slither/utils/martin.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/slither/utils/martin.py b/slither/utils/martin.py index ded8c0efa3..7d39b2c14a 100644 --- a/slither/utils/martin.py +++ b/slither/utils/martin.py @@ -105,9 +105,11 @@ def update_abstractness(self) -> float: abstract_contract_count += 1 self.abstractness = float(abstract_contract_count / len(self.contracts)) + # pylint: disable=too-many-branches def update_coupling(self) -> Dict: dependencies = {} for contract in self.contracts: + external_calls = [] for func in contract.functions: high_level_calls = [ ir @@ -116,7 +118,29 @@ def update_coupling(self) -> Dict: if isinstance(ir, HighLevelCall) ] # convert irs to string with target function and contract name - external_calls = [h.destination.type.type.name for h in high_level_calls] + # Get the target contract name for each high level call + new_external_calls = [] + for high_level_call in high_level_calls: + if isinstance(high_level_call.destination, Contract): + new_external_call = high_level_call.destination.name + elif isinstance(high_level_call.destination, str): + new_external_call = high_level_call.destination + elif not hasattr(high_level_call.destination, "type"): + continue + elif isinstance(high_level_call.destination.type, Contract): + new_external_call = high_level_call.destination.type.name + elif isinstance(high_level_call.destination.type, str): + new_external_call = high_level_call.destination.type + elif not hasattr(high_level_call.destination.type, "type"): + continue + elif isinstance(high_level_call.destination.type.type, Contract): + new_external_call = high_level_call.destination.type.type.name + elif isinstance(high_level_call.destination.type.type, str): + new_external_call = high_level_call.destination.type.type + else: + continue + new_external_calls.append(new_external_call) + external_calls.extend(new_external_calls) dependencies[contract.name] = set(external_calls) dependents = {} for contract, deps in dependencies.items():