From 201b4dfcfe1b645f34d65f5a87e7d5a095411431 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Tue, 2 May 2023 10:33:47 -0700 Subject: [PATCH 01/31] 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/31] 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 34a343e36c5005ac4f7cc2dfc0a5f4d66ff96121 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 5 May 2023 13:42:04 -0700 Subject: [PATCH 03/31] feat: ck printer --- slither/printers/all_printers.py | 1 + slither/printers/summary/ck.py | 266 +++++++++++++++++++++++++++++++ slither/utils/myprettytable.py | 80 +++++++++- 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 slither/printers/summary/ck.py diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 6dc8dddbdd..4e66351f85 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.ck import CKMetrics from .functions.cfg import CFG from .summary.function_ids import FunctionIds from .summary.variable_order import VariableOrder diff --git a/slither/printers/summary/ck.py b/slither/printers/summary/ck.py new file mode 100644 index 0000000000..d896015b24 --- /dev/null +++ b/slither/printers/summary/ck.py @@ -0,0 +1,266 @@ +""" + + # TODO: Add in other CK metrics (NOC, DIT) + # TODO: Don't display all the general function metrics, but add those to complexity-dashboard + CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994. + These metrics are used to measure the complexity of a class. + https://en.wikipedia.org/wiki/Programming_complexity + + - Response For a Class (RFC) is a metric that measures the number of unique method calls within a class. + - Number of Children (NOC) is a metric that measures the number of children a class has. + - Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has. + + Not implemented: + - Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods. + - Weighted Methods per Class (WMC) is a metric that measures the complexity of a class. + - Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to. + +""" +from typing import Tuple +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.myprettytable import make_pretty_table +from slither.slithir.operations.high_level_call import HighLevelCall +from slither.utils.colors import bold + + +def compute_dit(contract, depth=0): + """ + Recursively compute the depth of inheritance tree (DIT) of a contract + Args: + contract: Contract - the contract to compute the DIT for + depth: int - the depth of the contract in the inheritance tree + Returns: + the depth of the contract in the inheritance tree + """ + if not contract.inheritance: + return depth + max_dit = depth + for inherited_contract in contract.inheritance: + dit = compute_dit(inherited_contract, depth + 1) + max_dit = max(max_dit, dit) + return max_dit + + +# pylint: disable=too-many-locals +def compute_metrics(contracts): + """ + Compute CK metrics of a contract + Args: + contracts(list): list of contracts + Returns: + a tuple of (metrics1, metrics2, metrics3, metrics4, metrics5) + # Visbility + metrics1["contract name"] = { + "State variables":int, + "Constants":int, + "Immutables":int, + } + metrics2["contract name"] = { + "Public": int, + "External":int, + "Internal":int, + "Private":int, + } + # Mutability + metrics3["contract name"] = { + "Mutating":int, + "View":int, + "Pure":int, + } + # External facing, mutating: total / no auth / no modifiers + metrics4["contract name"] = { + "External mutating":int, + "No auth or onlyOwner":int, + "No modifiers":int, + } + metrics5["contract name"] = { + "Ext calls":int, + "Response For a Class":int, + "NOC":int, + "DIT":int, + } + + RFC is counted as follows: + +1 for each public or external fn + +1 for each public getter + +1 for each UNIQUE external call + + """ + metrics1 = {} + metrics2 = {} + metrics3 = {} + metrics4 = {} + metrics5 = {} + dependents = { + inherited.name: { + contract.name for contract in contracts if inherited.name in contract.inheritance + } + for inherited in contracts + } + + for c in contracts: + (state_variables, constants, immutables, public_getters) = count_variables(c) + rfc = public_getters # add 1 for each public getter + metrics1[c.name] = { + "State variables": state_variables, + "Constants": constants, + "Immutables": immutables, + } + metrics2[c.name] = { + "Public": 0, + "External": 0, + "Internal": 0, + "Private": 0, + } + metrics3[c.name] = { + "Mutating": 0, + "View": 0, + "Pure": 0, + } + metrics4[c.name] = { + "External mutating": 0, + "No auth or onlyOwner": 0, + "No modifiers": 0, + } + metrics5[c.name] = { + "Ext calls": 0, + "RFC": 0, + "NOC": len(dependents[c.name]), + "DIT": compute_dit(c), + } + for func in c.functions: + if func.name == "constructor": + continue + pure = func.pure + view = not pure and func.view + mutating = not pure and not view + external = func.visibility == "external" + public = func.visibility == "public" + internal = func.visibility == "internal" + private = func.visibility == "private" + external_public_mutating = external or public and mutating + external_no_auth = external_public_mutating and no_auth(func) + external_no_modifiers = external_public_mutating and len(func.modifiers) == 0 + if external or public: + rfc += 1 + + 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 = [] + for h in high_level_calls: + if hasattr(h.destination, "name"): + external_calls.append(f"{h.function_name}{h.destination.name}") + else: + external_calls.append(f"{h.function_name}{h.destination.type.type.name}") + + rfc += len(set(external_calls)) + + metrics2[c.name]["Public"] += 1 if public else 0 + metrics2[c.name]["External"] += 1 if external else 0 + metrics2[c.name]["Internal"] += 1 if internal else 0 + metrics2[c.name]["Private"] += 1 if private else 0 + + metrics3[c.name]["Mutating"] += 1 if mutating else 0 + metrics3[c.name]["View"] += 1 if view else 0 + metrics3[c.name]["Pure"] += 1 if pure else 0 + + metrics4[c.name]["External mutating"] += 1 if external_public_mutating else 0 + metrics4[c.name]["No auth or onlyOwner"] += 1 if external_no_auth else 0 + metrics4[c.name]["No modifiers"] += 1 if external_no_modifiers else 0 + + metrics5[c.name]["Ext calls"] += len(external_calls) + metrics5[c.name]["RFC"] = rfc + + return metrics1, metrics2, metrics3, metrics4, metrics5 + + +def count_variables(contract) -> Tuple[int, int, int, int]: + """Count the number of variables in a contract + Args: + contract(core.declarations.contract.Contract): contract to count variables + Returns: + Tuple of (state_variable_count, constant_count, immutable_count, public_getter) + """ + state_variable_count = 0 + constant_count = 0 + immutable_count = 0 + public_getter = 0 + for var in contract.variables: + if var.is_constant: + constant_count += 1 + elif var.is_immutable: + immutable_count += 1 + else: + state_variable_count += 1 + if var.visibility == "Public": + public_getter += 1 + return (state_variable_count, constant_count, immutable_count, public_getter) + + +def no_auth(func) -> bool: + """ + Check if a function has no auth or only_owner modifiers + Args: + func(core.declarations.function.Function): function to check + Returns: + bool + """ + for modifier in func.modifiers: + if "auth" in modifier.name or "only_owner" in modifier.name: + return False + return True + + +class CKMetrics(AbstractPrinter): + ARGUMENT = "ck" + HELP = "Computes the CK complexity metrics for each contract" + + WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#ck" + + def output(self, _filename): + if len(self.contracts) == 0: + return self.generate_output("No contract found") + metrics1, metrics2, metrics3, metrics4, metrics5 = compute_metrics(self.contracts) + txt = bold("\nCK complexity metrics\n") + # metrics2: variable counts + txt += bold("\nVariables\n") + keys = list(metrics1[self.contracts[0].name].keys()) + table0 = make_pretty_table(["Contract", *keys], metrics1, True) + txt += str(table0) + "\n" + + # metrics3: function visibility + txt += bold("\nFunction visibility\n") + keys = list(metrics2[self.contracts[0].name].keys()) + table1 = make_pretty_table(["Contract", *keys], metrics2, True) + txt += str(table1) + "\n" + + # metrics4: function mutability counts + txt += bold("\nFunction mutatability\n") + keys = list(metrics3[self.contracts[0].name].keys()) + table2 = make_pretty_table(["Contract", *keys], metrics3, True) + txt += str(table2) + "\n" + + # metrics5: external facing mutating functions + txt += bold("\nExternal/Public functions with modifiers\n") + keys = list(metrics4[self.contracts[0].name].keys()) + table3 = make_pretty_table(["Contract", *keys], metrics4, True) + txt += str(table3) + "\n" + + # metrics5: ext calls and rfc + txt += bold("\nExt calls and RFC\n") + keys = list(metrics5[self.contracts[0].name].keys()) + table4 = make_pretty_table(["Contract", *keys], metrics5, False) + txt += str(table4) + "\n" + + res = self.generate_output(txt) + res.add_pretty_table(table0, "CK complexity core metrics 1/5") + res.add_pretty_table(table1, "CK complexity core metrics 2/5") + res.add_pretty_table(table2, "CK complexity core metrics 3/5") + res.add_pretty_table(table3, "CK complexity core metrics 4/5") + res.add_pretty_table(table4, "CK complexity core metrics 5/5") + self.info(txt) + + return res diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index af10a6ff25..379d6699de 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -4,9 +4,17 @@ class MyPrettyTable: - def __init__(self, field_names: List[str]): + def __init__(self, field_names: List[str], pretty_align: bool = True): # TODO: True by default? self._field_names = field_names self._rows: List = [] + self._options: Dict = {} + if pretty_align: + self._options["set_alignment"] = [] + self._options["set_alignment"] += [(field_names[0], "l")] + for field_name in field_names[1:]: + self._options["set_alignment"] += [(field_name, "r")] + else: + self._options["set_alignment"] = [] def add_row(self, row: List[Union[str, List[str]]]) -> None: self._rows.append(row) @@ -15,6 +23,9 @@ def to_pretty_table(self) -> PrettyTable: table = PrettyTable(self._field_names) for row in self._rows: table.add_row(row) + if len(self._options["set_alignment"]): + for column_header, value in self._options["set_alignment"]: + table.align[column_header] = value return table def to_json(self) -> Dict: @@ -22,3 +33,70 @@ def to_json(self) -> Dict: def __str__(self) -> str: return str(self.to_pretty_table()) + + +# **Dict to MyPrettyTable utility functions** + + +def make_pretty_table( + headers: list, body: dict, totals: bool = False, total_header="TOTAL" +) -> MyPrettyTable: + """ + Converts a dict to a MyPrettyTable. Dict keys are the row headers. + Args: + headers: str[] of column names + body: dict of row headers with a dict of the values + totals: bool optional add Totals row + total_header: str optional if totals is set to True this will override the default "TOTAL" header + Returns: + 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_header] + [sum([body[row][key] for row in body]) for key in headers[1:]] + ) + return table + + +def make_pretty_table_simple( + data: dict, first_column_header, second_column_header="" +) -> 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([first_column_header, second_column_header]) + for k, v in data.items(): + table.add_row([k] + [v]) + 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 16b57263f47b1628d8bf4a4ca08ae9d674be9375 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Tue, 2 May 2023 09:54:51 -0700 Subject: [PATCH 04/31] 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 05/31] 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 314364eeb218d48c9fff6032dd2dd633fd4312f8 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 15 Jun 2023 14:13:25 -0700 Subject: [PATCH 06/31] chore: remove comments --- slither/printers/summary/ck.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/slither/printers/summary/ck.py b/slither/printers/summary/ck.py index d896015b24..6f604790f0 100644 --- a/slither/printers/summary/ck.py +++ b/slither/printers/summary/ck.py @@ -1,7 +1,4 @@ """ - - # TODO: Add in other CK metrics (NOC, DIT) - # TODO: Don't display all the general function metrics, but add those to complexity-dashboard CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994. These metrics are used to measure the complexity of a class. https://en.wikipedia.org/wiki/Programming_complexity From 975da91d333182b7a10d1be29993368073c1edb3 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 15 Jun 2023 14:20:18 -0700 Subject: [PATCH 07/31] 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 08/31] 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 14c9761da2bb2c85ac08d12327ead1d4f77b2414 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 15 Jun 2023 15:26:29 -0700 Subject: [PATCH 09/31] feat: add CBO --- slither/printers/all_printers.py | 1 + slither/printers/summary/ck.py | 77 +++++++++++++++++++------------- slither/utils/myprettytable.py | 22 --------- 3 files changed, 46 insertions(+), 54 deletions(-) diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 4e66351f85..7ac92327ef 100644 --- a/slither/printers/all_printers.py +++ b/slither/printers/all_printers.py @@ -21,3 +21,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/ck.py b/slither/printers/summary/ck.py index 6f604790f0..77a8c420cf 100644 --- a/slither/printers/summary/ck.py +++ b/slither/printers/summary/ck.py @@ -6,18 +6,19 @@ - Response For a Class (RFC) is a metric that measures the number of unique method calls within a class. - Number of Children (NOC) is a metric that measures the number of children a class has. - Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has. + - Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to. Not implemented: - Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods. - Weighted Methods per Class (WMC) is a metric that measures the complexity of a class. - - Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to. """ from typing import Tuple -from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.colors import bold from slither.utils.myprettytable import make_pretty_table from slither.slithir.operations.high_level_call import HighLevelCall -from slither.utils.colors import bold +from slither.printers.abstract_printer import AbstractPrinter +from slither.printers.summary.martin import compute_coupling def compute_dit(contract, depth=0): @@ -95,37 +96,41 @@ def compute_metrics(contracts): for inherited in contracts } - for c in contracts: - (state_variables, constants, immutables, public_getters) = count_variables(c) + # We pass 0 for the 2nd arg (abstractness) because we only care about the coupling metrics (Ca and Ce) + coupling = compute_coupling(contracts, 0) + + for contract in contracts: + (state_variables, constants, immutables, public_getters) = count_variables(contract) rfc = public_getters # add 1 for each public getter - metrics1[c.name] = { + metrics1[contract.name] = { "State variables": state_variables, "Constants": constants, "Immutables": immutables, } - metrics2[c.name] = { + metrics2[contract.name] = { "Public": 0, "External": 0, "Internal": 0, "Private": 0, } - metrics3[c.name] = { + metrics3[contract.name] = { "Mutating": 0, "View": 0, "Pure": 0, } - metrics4[c.name] = { + metrics4[contract.name] = { "External mutating": 0, "No auth or onlyOwner": 0, "No modifiers": 0, } - metrics5[c.name] = { + metrics5[contract.name] = { "Ext calls": 0, "RFC": 0, - "NOC": len(dependents[c.name]), - "DIT": compute_dit(c), + "NOC": len(dependents[contract.name]), + "DIT": compute_dit(contract), + "CBO": coupling[contract.name]["Dependents"] + coupling[contract.name]["Dependencies"], } - for func in c.functions: + for func in contract.functions: if func.name == "constructor": continue pure = func.pure @@ -147,29 +152,33 @@ def compute_metrics(contracts): # convert irs to string with target function and contract name external_calls = [] - for h in high_level_calls: - if hasattr(h.destination, "name"): - external_calls.append(f"{h.function_name}{h.destination.name}") + for high_level_call in high_level_calls: + if hasattr(high_level_call.destination, "name"): + external_calls.append( + f"{high_level_call.function_name}{high_level_call.destination.name}" + ) else: - external_calls.append(f"{h.function_name}{h.destination.type.type.name}") + external_calls.append( + f"{high_level_call.function_name}{high_level_call.destination.type.type.name}" + ) rfc += len(set(external_calls)) - metrics2[c.name]["Public"] += 1 if public else 0 - metrics2[c.name]["External"] += 1 if external else 0 - metrics2[c.name]["Internal"] += 1 if internal else 0 - metrics2[c.name]["Private"] += 1 if private else 0 + metrics2[contract.name]["Public"] += 1 if public else 0 + metrics2[contract.name]["External"] += 1 if external else 0 + metrics2[contract.name]["Internal"] += 1 if internal else 0 + metrics2[contract.name]["Private"] += 1 if private else 0 - metrics3[c.name]["Mutating"] += 1 if mutating else 0 - metrics3[c.name]["View"] += 1 if view else 0 - metrics3[c.name]["Pure"] += 1 if pure else 0 + metrics3[contract.name]["Mutating"] += 1 if mutating else 0 + metrics3[contract.name]["View"] += 1 if view else 0 + metrics3[contract.name]["Pure"] += 1 if pure else 0 - metrics4[c.name]["External mutating"] += 1 if external_public_mutating else 0 - metrics4[c.name]["No auth or onlyOwner"] += 1 if external_no_auth else 0 - metrics4[c.name]["No modifiers"] += 1 if external_no_modifiers else 0 + metrics4[contract.name]["External mutating"] += 1 if external_public_mutating else 0 + metrics4[contract.name]["No auth or onlyOwner"] += 1 if external_no_auth else 0 + metrics4[contract.name]["No modifiers"] += 1 if external_no_modifiers else 0 - metrics5[c.name]["Ext calls"] += len(external_calls) - metrics5[c.name]["RFC"] = rfc + metrics5[contract.name]["Ext calls"] += len(external_calls) + metrics5[contract.name]["RFC"] = rfc return metrics1, metrics2, metrics3, metrics4, metrics5 @@ -213,7 +222,7 @@ def no_auth(func) -> bool: class CKMetrics(AbstractPrinter): ARGUMENT = "ck" - HELP = "Computes the CK complexity metrics for each contract" + HELP = "Chidamber and Kemerer (CK) complexity metrics and related function attributes" WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#ck" @@ -246,8 +255,12 @@ def output(self, _filename): table3 = make_pretty_table(["Contract", *keys], metrics4, True) txt += str(table3) + "\n" - # metrics5: ext calls and rfc - txt += bold("\nExt calls and RFC\n") + # metrics5: ext calls and ck metrics + txt += bold("\nExternal calls and CK Metrics:\n") + txt += bold("Response For a Class (RFC)\n") + txt += bold("Number of Children (NOC)\n") + txt += bold("Depth of Inheritance Tree (DIT)\n") + txt += bold("Coupling Between Object Classes (CBO)\n") keys = list(metrics5[self.contracts[0].name].keys()) table4 = make_pretty_table(["Contract", *keys], metrics5, False) txt += str(table4) + "\n" diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index 6edb67def2..8c5d1af332 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -78,25 +78,3 @@ def make_pretty_table_simple( for k, v in data.items(): table.add_row([k] + [v]) 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 fa22c3460eab1f0e25f77d3767e1ddc193b0df31 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 15 Jun 2023 15:57:21 -0700 Subject: [PATCH 10/31] chore: update label --- slither/printers/summary/ck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/printers/summary/ck.py b/slither/printers/summary/ck.py index 77a8c420cf..a9f80b3544 100644 --- a/slither/printers/summary/ck.py +++ b/slither/printers/summary/ck.py @@ -244,7 +244,7 @@ def output(self, _filename): txt += str(table1) + "\n" # metrics4: function mutability counts - txt += bold("\nFunction mutatability\n") + txt += bold("\nState mutability\n") keys = list(metrics3[self.contracts[0].name].keys()) table2 = make_pretty_table(["Contract", *keys], metrics3, True) txt += str(table2) + "\n" From 3791467fb9a9460b4af6aa2b8324726511ba2ce2 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 16 Jun 2023 11:04:36 -0700 Subject: [PATCH 11/31] 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 12/31] 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 13/31] 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 14/31] 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 15/31] 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 16/31] 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 17/31] 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 fb25cbb26e4460305b5498ffe0bbfad9d8bb3044 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 6 Jul 2023 15:40:51 -0700 Subject: [PATCH 18/31] refactor: initial --- slither/printers/summary/ck.py | 1 + slither/utils/ck.py | 166 +++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 slither/utils/ck.py diff --git a/slither/printers/summary/ck.py b/slither/printers/summary/ck.py index a9f80b3544..fc199dc875 100644 --- a/slither/printers/summary/ck.py +++ b/slither/printers/summary/ck.py @@ -19,6 +19,7 @@ from slither.slithir.operations.high_level_call import HighLevelCall from slither.printers.abstract_printer import AbstractPrinter from slither.printers.summary.martin import compute_coupling +from slither.utils.ck import CKMetrics def compute_dit(contract, depth=0): diff --git a/slither/utils/ck.py b/slither/utils/ck.py new file mode 100644 index 0000000000..d3c5735216 --- /dev/null +++ b/slither/utils/ck.py @@ -0,0 +1,166 @@ +""" +Description + +""" +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 +# pylint: disable=too-many-instance-attributes +class TEMPLATEContractMetrics: + """Class to hold the TEMPLATE metrics for a single contract.""" + + contract: Contract + + 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() + pass + + 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 compute_metrics(self): + # """Compute the Halstead metrics.""" + # if all_operators is None: + # 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 + pass + + +@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 TEMPLATEMetrics: + """Class to hold the TEMPLATE 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 = 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( + # None, all_operators=all_operators, all_operands=all_operands + # ) + pass + + # 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 8d2e7c40ee58a4f46e8cb69d659cbef4a388bd6f Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 6 Jul 2023 16:51:38 -0700 Subject: [PATCH 19/31] 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 20/31] 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 21/31] 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 22/31] 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 23/31] 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 24/31] 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 25/31] 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 8d483ed471a7c630f8a4c0b694a2d7e2d196f4c2 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 7 Jul 2023 12:37:07 -0700 Subject: [PATCH 26/31] fix: external calls --- slither/utils/martin.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/slither/utils/martin.py b/slither/utils/martin.py index ded8c0efa3..16e3e13e80 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,27 @@ 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 + if isinstance(high_level_call.destination.type.type, Contract): + new_external_call = high_level_call.destination.type.type.name + if isinstance(high_level_call.destination.type.type, str): + new_external_call = high_level_call.destination.type.type + 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(): From 6843d03da43a984698ad493efe3de262246520e4 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 7 Jul 2023 12:37:07 -0700 Subject: [PATCH 27/31] 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(): From a35dff3a15b47ca75391029838aeddf5a6175ce2 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Fri, 7 Jul 2023 16:31:36 -0700 Subject: [PATCH 28/31] refactor: ck --- slither/printers/summary/ck.py | 269 ++------------------- slither/utils/ck.py | 413 ++++++++++++++++++++++++--------- slither/utils/halstead.py | 18 +- slither/utils/martin.py | 6 +- slither/utils/myprettytable.py | 1 + 5 files changed, 334 insertions(+), 373 deletions(-) diff --git a/slither/printers/summary/ck.py b/slither/printers/summary/ck.py index 27f6dd06d5..f7a8510391 100644 --- a/slither/printers/summary/ck.py +++ b/slither/printers/summary/ck.py @@ -14,214 +14,24 @@ During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated. These are also included in the output: - TODO!!! + - State variables: total number of state variables + - Constants: total number of constants + - Immutables: total number of immutables + - Public: total number of public functions + - External: total number of external functions + - Internal: total number of internal functions + - Private: total number of private functions + - Mutating: total number of state mutating functions + - View: total number of view functions + - Pure: total number of pure functions + - External mutating: total number of external mutating functions + - No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers + - No modifiers: total number of functions without modifiers + - Ext calls: total number of external calls """ -from typing import Tuple -from slither.utils.colors import bold -from slither.utils.myprettytable import make_pretty_table -from slither.slithir.operations.high_level_call import HighLevelCall from slither.printers.abstract_printer import AbstractPrinter -from slither.utils.martin import MartinMetrics - - -def compute_dit(contract, depth=0): - """ - Recursively compute the depth of inheritance tree (DIT) of a contract - Args: - contract: Contract - the contract to compute the DIT for - depth: int - the depth of the contract in the inheritance tree - Returns: - the depth of the contract in the inheritance tree - """ - if not contract.inheritance: - return depth - max_dit = depth - for inherited_contract in contract.inheritance: - dit = compute_dit(inherited_contract, depth + 1) - max_dit = max(max_dit, dit) - return max_dit - - -# pylint: disable=too-many-locals -def compute_metrics(contracts): - """ - Compute CK metrics of a contract - Args: - contracts(list): list of contracts - Returns: - a tuple of (metrics1, metrics2, metrics3, metrics4, metrics5) - # Visbility - metrics1["contract name"] = { - "State variables":int, - "Constants":int, - "Immutables":int, - } - metrics2["contract name"] = { - "Public": int, - "External":int, - "Internal":int, - "Private":int, - } - # Mutability - metrics3["contract name"] = { - "Mutating":int, - "View":int, - "Pure":int, - } - # External facing, mutating: total / no auth / no modifiers - metrics4["contract name"] = { - "External mutating":int, - "No auth or onlyOwner":int, - "No modifiers":int, - } - metrics5["contract name"] = { - "Ext calls":int, - "Response For a Class":int, - "NOC":int, - "DIT":int, - } - - RFC is counted as follows: - +1 for each public or external fn - +1 for each public getter - +1 for each UNIQUE external call - - """ - metrics1 = {} - metrics2 = {} - metrics3 = {} - metrics4 = {} - metrics5 = {} - dependents = { - inherited.name: { - contract.name for contract in contracts if inherited.name in contract.inheritance - } - for inherited in contracts - } - - # Use MartinMetrics to compute Ca and Ce - martin = MartinMetrics(contracts) - - for contract in contracts: - (state_variables, constants, immutables, public_getters) = count_variables(contract) - rfc = public_getters # add 1 for each public getter - metrics1[contract.name] = { - "State variables": state_variables, - "Constants": constants, - "Immutables": immutables, - } - metrics2[contract.name] = { - "Public": 0, - "External": 0, - "Internal": 0, - "Private": 0, - } - metrics3[contract.name] = { - "Mutating": 0, - "View": 0, - "Pure": 0, - } - metrics4[contract.name] = { - "External mutating": 0, - "No auth or onlyOwner": 0, - "No modifiers": 0, - } - metrics5[contract.name] = { - "Ext calls": 0, - "RFC": 0, - "NOC": len(dependents[contract.name]), - "DIT": compute_dit(contract), - "CBO": coupling[contract.name]["Dependents"] + coupling[contract.name]["Dependencies"], - } - for func in contract.functions: - if func.name == "constructor": - continue - pure = func.pure - view = not pure and func.view - mutating = not pure and not view - external = func.visibility == "external" - public = func.visibility == "public" - internal = func.visibility == "internal" - private = func.visibility == "private" - external_public_mutating = external or public and mutating - external_no_auth = external_public_mutating and no_auth(func) - external_no_modifiers = external_public_mutating and len(func.modifiers) == 0 - if external or public: - rfc += 1 - - 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 = [] - for high_level_call in high_level_calls: - if hasattr(high_level_call.destination, "name"): - external_calls.append( - f"{high_level_call.function_name}{high_level_call.destination.name}" - ) - else: - external_calls.append( - f"{high_level_call.function_name}{high_level_call.destination.type.type.name}" - ) - - rfc += len(set(external_calls)) - - metrics2[contract.name]["Public"] += 1 if public else 0 - metrics2[contract.name]["External"] += 1 if external else 0 - metrics2[contract.name]["Internal"] += 1 if internal else 0 - metrics2[contract.name]["Private"] += 1 if private else 0 - - metrics3[contract.name]["Mutating"] += 1 if mutating else 0 - metrics3[contract.name]["View"] += 1 if view else 0 - metrics3[contract.name]["Pure"] += 1 if pure else 0 - - metrics4[contract.name]["External mutating"] += 1 if external_public_mutating else 0 - metrics4[contract.name]["No auth or onlyOwner"] += 1 if external_no_auth else 0 - metrics4[contract.name]["No modifiers"] += 1 if external_no_modifiers else 0 - - metrics5[contract.name]["Ext calls"] += len(external_calls) - metrics5[contract.name]["RFC"] = rfc - - return metrics1, metrics2, metrics3, metrics4, metrics5 - - -def count_variables(contract) -> Tuple[int, int, int, int]: - """Count the number of variables in a contract - Args: - contract(core.declarations.contract.Contract): contract to count variables - Returns: - Tuple of (state_variable_count, constant_count, immutable_count, public_getter) - """ - state_variable_count = 0 - constant_count = 0 - immutable_count = 0 - public_getter = 0 - for var in contract.variables: - if var.is_constant: - constant_count += 1 - elif var.is_immutable: - immutable_count += 1 - else: - state_variable_count += 1 - if var.visibility == "Public": - public_getter += 1 - return (state_variable_count, constant_count, immutable_count, public_getter) - - -def no_auth(func) -> bool: - """ - Check if a function has no auth or only_owner modifiers - Args: - func(core.declarations.function.Function): function to check - Returns: - bool - """ - for modifier in func.modifiers: - if "auth" in modifier.name or "only_owner" in modifier.name: - return False - return True +from slither.utils.ck import CKMetrics class CK(AbstractPrinter): @@ -233,48 +43,15 @@ class CK(AbstractPrinter): def output(self, _filename): if len(self.contracts) == 0: return self.generate_output("No contract found") - metrics1, metrics2, metrics3, metrics4, metrics5 = compute_metrics(self.contracts) - txt = bold("\nCK complexity metrics\n") - # metrics2: variable counts - txt += bold("\nVariables\n") - keys = list(metrics1[self.contracts[0].name].keys()) - table0 = make_pretty_table(["Contract", *keys], metrics1, True) - txt += str(table0) + "\n" - - # metrics3: function visibility - txt += bold("\nFunction visibility\n") - keys = list(metrics2[self.contracts[0].name].keys()) - table1 = make_pretty_table(["Contract", *keys], metrics2, True) - txt += str(table1) + "\n" - - # metrics4: function mutability counts - txt += bold("\nState mutability\n") - keys = list(metrics3[self.contracts[0].name].keys()) - table2 = make_pretty_table(["Contract", *keys], metrics3, True) - txt += str(table2) + "\n" - - # metrics5: external facing mutating functions - txt += bold("\nExternal/Public functions with modifiers\n") - keys = list(metrics4[self.contracts[0].name].keys()) - table3 = make_pretty_table(["Contract", *keys], metrics4, True) - txt += str(table3) + "\n" - # metrics5: ext calls and ck metrics - txt += bold("\nExternal calls and CK Metrics:\n") - txt += bold("Response For a Class (RFC)\n") - txt += bold("Number of Children (NOC)\n") - txt += bold("Depth of Inheritance Tree (DIT)\n") - txt += bold("Coupling Between Object Classes (CBO)\n") - keys = list(metrics5[self.contracts[0].name].keys()) - table4 = make_pretty_table(["Contract", *keys], metrics5, False) - txt += str(table4) + "\n" + ck = CKMetrics(self.contracts) - res = self.generate_output(txt) - res.add_pretty_table(table0, "CK complexity core metrics 1/5") - res.add_pretty_table(table1, "CK complexity core metrics 2/5") - res.add_pretty_table(table2, "CK complexity core metrics 3/5") - res.add_pretty_table(table3, "CK complexity core metrics 4/5") - res.add_pretty_table(table4, "CK complexity core metrics 5/5") - self.info(txt) + res = self.generate_output(ck.full_text) + res.add_pretty_table(ck.auxiliary1.pretty_table, ck.auxiliary1.title) + res.add_pretty_table(ck.auxiliary2.pretty_table, ck.auxiliary2.title) + res.add_pretty_table(ck.auxiliary3.pretty_table, ck.auxiliary3.title) + res.add_pretty_table(ck.auxiliary4.pretty_table, ck.auxiliary4.title) + res.add_pretty_table(ck.core.pretty_table, ck.core.title) + self.info(ck.full_text) return res diff --git a/slither/utils/ck.py b/slither/utils/ck.py index d3c5735216..7b0d1afd90 100644 --- a/slither/utils/ck.py +++ b/slither/utils/ck.py @@ -1,79 +1,239 @@ """ -Description + CK Metrics are a suite of six software metrics proposed by Chidamber and Kemerer in 1994. + These metrics are used to measure the complexity of a class. + https://en.wikipedia.org/wiki/Programming_complexity + + - Response For a Class (RFC) is a metric that measures the number of unique method calls within a class. + - Number of Children (NOC) is a metric that measures the number of children a class has. + - Depth of Inheritance Tree (DIT) is a metric that measures the number of parent classes a class has. + - Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to. + + Not implemented: + - Lack of Cohesion of Methods (LCOM) is a metric that measures the lack of cohesion in methods. + - Weighted Methods per Class (WMC) is a metric that measures the complexity of a class. + + During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated. + These are also included in the output: + - State variables: total number of state variables + - Constants: total number of constants + - Immutables: total number of immutables + - Public: total number of public functions + - External: total number of external functions + - Internal: total number of internal functions + - Private: total number of private functions + - Mutating: total number of state mutating functions + - View: total number of view functions + - Pure: total number of pure functions + - External mutating: total number of external mutating functions + - No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers + - No modifiers: total number of functions without modifiers + - Ext calls: total number of external calls """ -import math -from dataclasses import dataclass, field -from typing import Tuple, List, Dict from collections import OrderedDict +from typing import Tuple, List, Dict +from dataclasses import dataclass, field +from slither.utils.colors import bold 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 +from slither.utils.martin import MartinMetrics +from slither.slithir.operations.high_level_call import HighLevelCall + + +# Utility functions + + +def compute_dit(contract: Contract, depth: int = 0) -> int: + """ + Recursively compute the depth of inheritance tree (DIT) of a contract + Args: + contract(core.declarations.contract.Contract): contract to compute DIT for + depth(int): current depth of the contract + Returns: + int: depth of the contract + """ + if not contract.inheritance: + return depth + max_dit = depth + for inherited_contract in contract.inheritance: + dit = compute_dit(inherited_contract, depth + 1) + max_dit = max(max_dit, dit) + return max_dit + + +def has_auth(func) -> bool: + """ + Check if a function has no auth or only_owner modifiers + Args: + func(core.declarations.function.Function): function to check + Returns: + bool True if it does have auth or only_owner modifiers + """ + for modifier in func.modifiers: + if "auth" in modifier.name or "only_owner" in modifier.name: + return True + return False + + +# Utility classes for calculating CK metrics @dataclass # pylint: disable=too-many-instance-attributes -class TEMPLATEContractMetrics: - """Class to hold the TEMPLATE metrics for a single contract.""" +class CKContractMetrics: + """Class to hold the CK metrics for a single contract.""" contract: Contract + # Used to calculate CBO - should be passed in as a constructor arg + martin_metrics: Dict + + # Used to calculate NOC + dependents: Dict + + state_variables: int = 0 + constants: int = 0 + immutables: int = 0 + public: int = 0 + external: int = 0 + internal: int = 0 + private: int = 0 + mutating: int = 0 + view: int = 0 + pure: int = 0 + external_mutating: int = 0 + no_auth_or_only_owner: int = 0 + no_modifiers: int = 0 + ext_calls: int = 0 + rfc: int = 0 + noc: int = 0 + dit: int = 0 + cbo: int = 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() - pass + if not hasattr(self.contract, "functions"): + return + self.count_variables() + self.noc = len(self.dependents[self.contract.name]) + self.dit = compute_dit(self.contract) + self.cbo = ( + self.martin_metrics[self.contract.name].ca + self.martin_metrics[self.contract.name].ce + ) + self.calculate_metrics() + + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + def calculate_metrics(self): + """Calculate the metrics for a contract""" + rfc = self.public # initialize with public getter count + for func in self.contract.functions: + if func.name == "constructor": + continue + pure = func.pure + view = not pure and func.view + mutating = not pure and not view + external = func.visibility == "external" + public = func.visibility == "public" + internal = func.visibility == "internal" + private = func.visibility == "private" + external_public_mutating = external or public and mutating + external_no_auth = external_public_mutating and not has_auth(func) + external_no_modifiers = external_public_mutating and len(func.modifiers) == 0 + if external or public: + rfc += 1 + + 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 = [] + for high_level_call in high_level_calls: + if isinstance(high_level_call.destination, Contract): + destination_contract = high_level_call.destination.name + elif isinstance(high_level_call.destination, str): + destination_contract = high_level_call.destination + elif not hasattr(high_level_call.destination, "type"): + continue + elif isinstance(high_level_call.destination.type, Contract): + destination_contract = high_level_call.destination.type.name + elif isinstance(high_level_call.destination.type, str): + destination_contract = high_level_call.destination.type + elif not hasattr(high_level_call.destination.type, "type"): + continue + elif isinstance(high_level_call.destination.type.type, Contract): + destination_contract = high_level_call.destination.type.type.name + elif isinstance(high_level_call.destination.type.type, str): + destination_contract = high_level_call.destination.type.type + else: + continue + external_calls.append(f"{high_level_call.function_name}{destination_contract}") + rfc += len(set(external_calls)) + + self.public += public + self.external += external + self.internal += internal + self.private += private + + self.mutating += mutating + self.view += view + self.pure += pure + + self.external_mutating += external_public_mutating + self.no_auth_or_only_owner += external_no_auth + self.no_modifiers += external_no_modifiers + + self.ext_calls += len(external_calls) + self.rfc = rfc + + def count_variables(self): + """Count the number of variables in a contract""" + state_variable_count = 0 + constant_count = 0 + immutable_count = 0 + public_getter_count = 0 + for variable in self.contract.variables: + if variable.is_constant: + constant_count += 1 + elif variable.is_immutable: + immutable_count += 1 + else: + state_variable_count += 1 + if variable.visibility == "Public": + public_getter_count += 1 + self.state_variables = state_variable_count + self.constants = constant_count + self.immutables = immutable_count + + # initialize RFC with public getter count + # self.public is used count public functions not public variables + self.rfc = public_getter_count 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}", - # } + { + "State variables": self.state_variables, + "Constants": self.constants, + "Immutables": self.immutables, + "Public": self.public, + "External": self.external, + "Internal": self.internal, + "Private": self.private, + "Mutating": self.mutating, + "View": self.view, + "Pure": self.pure, + "External mutating": self.external_mutating, + "No auth or onlyOwner": self.no_auth_or_only_owner, + "No modifiers": self.no_modifiers, + "Ext calls": self.ext_calls, + "RFC": self.rfc, + "NOC": self.noc, + "DIT": self.dit, + "CBO": self.cbo, + } ) - def compute_metrics(self): - # """Compute the Halstead metrics.""" - # if all_operators is None: - # 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 - pass - @dataclass class SectionInfo: @@ -86,81 +246,102 @@ class SectionInfo: @dataclass # pylint: disable=too-many-instance-attributes -class TEMPLATEMetrics: - """Class to hold the TEMPLATE 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) +class CKMetrics: + """Class to hold the CK metrics for all contracts. Contains methods useful for reporting. + There are 5 sections in the report: + 1. Variable count by type (state, constant, immutable) + 2. Function count by visibility (public, external, internal, private) + 3. Function count by mutability (mutating, view, pure) + 4. External mutating function count by modifier (external mutating, no auth or onlyOwner, no modifiers) + 5. CK metrics (RFC, NOC, DIT, CBO) """ contracts: List[Contract] = field(default_factory=list) contract_metrics: OrderedDict = 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), - # ) + title: str = "CK complexity metrics" + full_text: str = "" + auxiliary1: SectionInfo = field(default=SectionInfo) + auxiliary2: SectionInfo = field(default=SectionInfo) + auxiliary3: SectionInfo = field(default=SectionInfo) + auxiliary4: SectionInfo = field(default=SectionInfo) + core: SectionInfo = field(default=SectionInfo) + AUXILIARY1_KEYS = ( + "State variables", + "Constants", + "Immutables", + ) + AUXILIARY2_KEYS = ( + "Public", + "External", + "Internal", + "Private", + ) + AUXILIARY3_KEYS = ( + "Mutating", + "View", + "Pure", + ) + AUXILIARY4_KEYS = ( + "External mutating", + "No auth or onlyOwner", + "No modifiers", + ) + CORE_KEYS = ( + "Ext calls", + "RFC", + "NOC", + "DIT", + "CBO", + ) + SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = ( + ("Variables", "auxiliary1", AUXILIARY1_KEYS), + ("Function visibility", "auxiliary2", AUXILIARY2_KEYS), + ("State mutability", "auxiliary3", AUXILIARY3_KEYS), + ("External mutating functions", "auxiliary4", AUXILIARY4_KEYS), + ("Core", "core", CORE_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( - # None, all_operators=all_operators, all_operands=all_operands - # ) - pass + martin_metrics = MartinMetrics(self.contracts).contract_metrics + dependents = { + inherited.name: { + contract.name + for contract in self.contracts + if inherited.name in contract.inheritance + } + for inherited in self.contracts + } + for contract in self.contracts: + self.contract_metrics[contract.name] = CKContractMetrics( + contract=contract, martin_metrics=martin_metrics, dependents=dependents + ) # 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) + + # Update each section + for (title, attr, keys) in self.SECTIONS: + if attr == "core": + # Special handling for core section + totals_enabled = False + subtitle += bold("RFC: Response For a Class\n") + subtitle += bold("NOC: Number of Children\n") + subtitle += bold("DIT: Depth of Inheritance Tree\n") + subtitle += bold("CBO: Coupling Between Object Classes\n") + else: + totals_enabled = True + subtitle = "" + + pretty_table = make_pretty_table(["Contract", *keys], data, totals=totals_enabled) section_title = f"{self.title} ({title})" - txt = f"\n\n{section_title}:\n{pretty_table}\n" - self.full_txt += txt + txt = f"\n\n{section_title}:\n{subtitle}{pretty_table}\n" + self.full_text += txt setattr( self, - title.lower(), + attr, SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), ) diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py index a5ab41cc9b..64dd1f6a1a 100644 --- a/slither/utils/halstead.py +++ b/slither/utils/halstead.py @@ -58,7 +58,10 @@ class HalsteadContractMetrics: 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: + if not hasattr(self.contract, "functions"): + return self.populate_operators_and_operands() if len(self.all_operators) > 0: self.compute_metrics() @@ -86,8 +89,7 @@ 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: @@ -175,10 +177,10 @@ class HalsteadMetrics: "Time", "Estimated Bugs", ) - SECTIONS: Tuple[Tuple[str, Tuple[str]]] = ( - ("Core", CORE_KEYS), - ("Extended1", EXTENDED1_KEYS), - ("Extended2", EXTENDED2_KEYS), + SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = ( + ("Core", "core", CORE_KEYS), + ("Extended 1/2", "extended1", EXTENDED1_KEYS), + ("Extended 2/2", "extended2", EXTENDED2_KEYS), ) def __post_init__(self): @@ -215,13 +217,13 @@ def update_reporting_sections(self): contract.name: self.contract_metrics[contract.name].to_dict() for contract in self.contracts } - for (title, keys) in self.SECTIONS: + for (title, attr, 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_text += txt setattr( self, - title.lower(), + attr, SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), ) diff --git a/slither/utils/martin.py b/slither/utils/martin.py index 7d39b2c14a..fb62a4c584 100644 --- a/slither/utils/martin.py +++ b/slither/utils/martin.py @@ -63,7 +63,7 @@ class MartinMetrics: "Instability", "Distance from main sequence", ) - SECTIONS: Tuple[Tuple[str, Tuple[str]]] = (("Core", CORE_KEYS),) + SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (("Core", "core", CORE_KEYS),) def __post_init__(self): self.update_abstractness() @@ -76,7 +76,7 @@ def update_reporting_sections(self): contract.name: self.contract_metrics[contract.name].to_dict() for contract in self.contracts } - for (title, keys) in self.SECTIONS: + for (title, attr, 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" @@ -94,7 +94,7 @@ def update_reporting_sections(self): self.full_text += txt setattr( self, - title.lower(), + attr, SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), ) diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index 2763470c4e..d67f570c0a 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -37,6 +37,7 @@ def __str__(self) -> str: # UTILITY FUNCTIONS + def make_pretty_table( headers: list, body: dict, totals: bool = False, total_header="TOTAL" ) -> MyPrettyTable: From 4053c9b9e35949f1293f6d824aafbaed8381f44e Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Fri, 15 Sep 2023 11:10:12 +0200 Subject: [PATCH 29/31] minor improvements --- slither/core/variables/__init__.py | 6 + slither/printers/summary/ck.py | 3 +- slither/printers/summary/halstead.py | 3 +- slither/printers/summary/martin.py | 3 +- slither/utils/ck.py | 9 +- slither/utils/encoding.py | 202 +++++++++++++++++++++ slither/utils/halstead.py | 22 ++- slither/utils/martin.py | 12 +- slither/utils/upgradeability.py | 254 ++------------------------- 9 files changed, 252 insertions(+), 262 deletions(-) create mode 100644 slither/utils/encoding.py diff --git a/slither/core/variables/__init__.py b/slither/core/variables/__init__.py index 638f0f3a4e..53872853aa 100644 --- a/slither/core/variables/__init__.py +++ b/slither/core/variables/__init__.py @@ -1,2 +1,8 @@ from .state_variable import StateVariable from .variable import Variable +from .local_variable_init_from_tuple import LocalVariableInitFromTuple +from .local_variable import LocalVariable +from .top_level_variable import TopLevelVariable +from .event_variable import EventVariable +from .function_type_variable import FunctionTypeVariable +from .structure_variable import StructureVariable diff --git a/slither/printers/summary/ck.py b/slither/printers/summary/ck.py index f7a8510391..78da23756b 100644 --- a/slither/printers/summary/ck.py +++ b/slither/printers/summary/ck.py @@ -32,6 +32,7 @@ """ from slither.printers.abstract_printer import AbstractPrinter from slither.utils.ck import CKMetrics +from slither.utils.output import Output class CK(AbstractPrinter): @@ -40,7 +41,7 @@ class CK(AbstractPrinter): WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#ck" - def output(self, _filename): + def output(self, _filename: str) -> Output: if len(self.contracts) == 0: return self.generate_output("No contract found") diff --git a/slither/printers/summary/halstead.py b/slither/printers/summary/halstead.py index 8144e467f7..d3c3557db9 100644 --- a/slither/printers/summary/halstead.py +++ b/slither/printers/summary/halstead.py @@ -25,6 +25,7 @@ """ from slither.printers.abstract_printer import AbstractPrinter from slither.utils.halstead import HalsteadMetrics +from slither.utils.output import Output class Halstead(AbstractPrinter): @@ -33,7 +34,7 @@ class Halstead(AbstractPrinter): WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#halstead" - def output(self, _filename): + def output(self, _filename: str) -> Output: if len(self.contracts) == 0: return self.generate_output("No contract found") diff --git a/slither/printers/summary/martin.py b/slither/printers/summary/martin.py index c49e63fcb5..a0f1bbfcb1 100644 --- a/slither/printers/summary/martin.py +++ b/slither/printers/summary/martin.py @@ -11,6 +11,7 @@ """ from slither.printers.abstract_printer import AbstractPrinter from slither.utils.martin import MartinMetrics +from slither.utils.output import Output class Martin(AbstractPrinter): @@ -19,7 +20,7 @@ class Martin(AbstractPrinter): WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#martin" - def output(self, _filename): + def output(self, _filename: str) -> Output: if len(self.contracts) == 0: return self.generate_output("No contract found") diff --git a/slither/utils/ck.py b/slither/utils/ck.py index 7b0d1afd90..ffba663ad2 100644 --- a/slither/utils/ck.py +++ b/slither/utils/ck.py @@ -110,7 +110,7 @@ class CKContractMetrics: dit: int = 0 cbo: int = 0 - def __post_init__(self): + def __post_init__(self) -> None: if not hasattr(self.contract, "functions"): return self.count_variables() @@ -123,7 +123,7 @@ def __post_init__(self): # pylint: disable=too-many-locals # pylint: disable=too-many-branches - def calculate_metrics(self): + def calculate_metrics(self) -> None: """Calculate the metrics for a contract""" rfc = self.public # initialize with public getter count for func in self.contract.functions: @@ -186,7 +186,7 @@ def calculate_metrics(self): self.ext_calls += len(external_calls) self.rfc = rfc - def count_variables(self): + def count_variables(self) -> None: """Count the number of variables in a contract""" state_variable_count = 0 constant_count = 0 @@ -302,7 +302,7 @@ class CKMetrics: ("Core", "core", CORE_KEYS), ) - def __post_init__(self): + def __post_init__(self) -> None: martin_metrics = MartinMetrics(self.contracts).contract_metrics dependents = { inherited.name: { @@ -323,6 +323,7 @@ def __post_init__(self): for contract in self.contracts } + subtitle = "" # Update each section for (title, attr, keys) in self.SECTIONS: if attr == "core": diff --git a/slither/utils/encoding.py b/slither/utils/encoding.py new file mode 100644 index 0000000000..288b581505 --- /dev/null +++ b/slither/utils/encoding.py @@ -0,0 +1,202 @@ +from typing import Union + +from slither.core import variables +from slither.core.declarations import ( + SolidityVariable, + SolidityVariableComposed, + Structure, + Enum, + Contract, +) +from slither.core import solidity_types +from slither.slithir import operations +from slither.slithir import variables as SlitherIRVariable + + +# pylint: disable=too-many-branches +def ntype(_type: Union[solidity_types.Type, str]) -> str: + if isinstance(_type, solidity_types.ElementaryType): + _type = str(_type) + elif isinstance(_type, solidity_types.ArrayType): + if isinstance(_type.type, solidity_types.ElementaryType): + _type = str(_type) + else: + _type = "user_defined_array" + elif isinstance(_type, Structure): + _type = str(_type) + elif isinstance(_type, Enum): + _type = str(_type) + elif isinstance(_type, solidity_types.MappingType): + _type = str(_type) + elif isinstance(_type, solidity_types.UserDefinedType): + if isinstance(_type.type, Contract): + _type = f"contract({_type.type.name})" + elif isinstance(_type.type, Structure): + _type = f"struct({_type.type.name})" + elif isinstance(_type.type, Enum): + _type = f"enum({_type.type.name})" + else: + _type = str(_type) + + _type = _type.replace(" memory", "") + _type = _type.replace(" storage ref", "") + + if "struct" in _type: + return "struct" + if "enum" in _type: + return "enum" + if "tuple" in _type: + return "tuple" + if "contract" in _type: + return "contract" + if "mapping" in _type: + return "mapping" + return _type.replace(" ", "_") + + +# pylint: disable=too-many-branches +def encode_var_for_compare(var: Union[variables.Variable, SolidityVariable]) -> str: + + # variables + if isinstance(var, SlitherIRVariable.Constant): + return f"constant({ntype(var.type)},{var.value})" + if isinstance(var, SolidityVariableComposed): + return f"solidity_variable_composed({var.name})" + if isinstance(var, SolidityVariable): + return f"solidity_variable{var.name}" + if isinstance(var, SlitherIRVariable.TemporaryVariable): + return "temporary_variable" + if isinstance(var, SlitherIRVariable.ReferenceVariable): + return f"reference({ntype(var.type)})" + if isinstance(var, variables.LocalVariable): + return f"local_solc_variable({ntype(var.type)},{var.location})" + if isinstance(var, variables.StateVariable): + if not (var.is_constant or var.is_immutable): + try: + slot, _ = var.contract.compilation_unit.storage_layout_of(var.contract, var) + except KeyError: + slot = var.name + else: + slot = var.name + return f"state_solc_variable({ntype(var.type)},{slot})" + if isinstance(var, variables.LocalVariableInitFromTuple): + return "local_variable_init_tuple" + if isinstance(var, SlitherIRVariable.TupleVariable): + return "tuple_variable" + + # default + return "" + + +# pylint: disable=too-many-branches +def encode_ir_for_upgradeability_compare(ir: operations.Operation) -> str: + # operations + if isinstance(ir, operations.Assignment): + return f"({encode_var_for_compare(ir.lvalue)}):=({encode_var_for_compare(ir.rvalue)})" + if isinstance(ir, operations.Index): + return f"index({ntype(ir.variable_right.type)})" + if isinstance(ir, operations.Member): + return "member" # .format(ntype(ir._type)) + if isinstance(ir, operations.Length): + return "length" + if isinstance(ir, operations.Binary): + return f"binary({encode_var_for_compare(ir.variable_left)}{ir.type}{encode_var_for_compare(ir.variable_right)})" + if isinstance(ir, operations.Unary): + return f"unary({str(ir.type)})" + if isinstance(ir, operations.Condition): + return f"condition({encode_var_for_compare(ir.value)})" + if isinstance(ir, operations.NewStructure): + return "new_structure" + if isinstance(ir, operations.NewContract): + return "new_contract" + if isinstance(ir, operations.NewArray): + return f"new_array({ntype(ir.array_type)})" + if isinstance(ir, operations.NewElementaryType): + return f"new_elementary({ntype(ir.type)})" + if isinstance(ir, operations.Delete): + return f"delete({encode_var_for_compare(ir.lvalue)},{encode_var_for_compare(ir.variable)})" + if isinstance(ir, operations.SolidityCall): + return f"solidity_call({ir.function.full_name})" + if isinstance(ir, operations.InternalCall): + return f"internal_call({ntype(ir.type_call)})" + if isinstance(ir, operations.EventCall): # is this useful? + return "event" + if isinstance(ir, operations.LibraryCall): + return "library_call" + if isinstance(ir, operations.InternalDynamicCall): + return "internal_dynamic_call" + if isinstance(ir, operations.HighLevelCall): # TODO: improve + return "high_level_call" + if isinstance(ir, operations.LowLevelCall): # TODO: improve + return "low_level_call" + if isinstance(ir, operations.TypeConversion): + return f"type_conversion({ntype(ir.type)})" + if isinstance(ir, operations.Return): # this can be improved using values + return "return" # .format(ntype(ir.type)) + if isinstance(ir, operations.Transfer): + return f"transfer({encode_var_for_compare(ir.call_value)})" + if isinstance(ir, operations.Send): + return f"send({encode_var_for_compare(ir.call_value)})" + if isinstance(ir, operations.Unpack): # TODO: improve + return "unpack" + if isinstance(ir, operations.InitArray): # TODO: improve + return "init_array" + + # default + return "" + + +def encode_ir_for_halstead(ir: operations.Operation) -> str: + # operations + if isinstance(ir, operations.Assignment): + return "assignment" + if isinstance(ir, operations.Index): + return "index" + if isinstance(ir, operations.Member): + return "member" # .format(ntype(ir._type)) + if isinstance(ir, operations.Length): + return "length" + if isinstance(ir, operations.Binary): + return f"binary({str(ir.type)})" + if isinstance(ir, operations.Unary): + return f"unary({str(ir.type)})" + if isinstance(ir, operations.Condition): + return f"condition({encode_var_for_compare(ir.value)})" + if isinstance(ir, operations.NewStructure): + return "new_structure" + if isinstance(ir, operations.NewContract): + return "new_contract" + if isinstance(ir, operations.NewArray): + return f"new_array({ntype(ir.array_type)})" + if isinstance(ir, operations.NewElementaryType): + return f"new_elementary({ntype(ir.type)})" + if isinstance(ir, operations.Delete): + return "delete" + if isinstance(ir, operations.SolidityCall): + return f"solidity_call({ir.function.full_name})" + if isinstance(ir, operations.InternalCall): + return f"internal_call({ntype(ir.type_call)})" + if isinstance(ir, operations.EventCall): # is this useful? + return "event" + if isinstance(ir, operations.LibraryCall): + return "library_call" + if isinstance(ir, operations.InternalDynamicCall): + return "internal_dynamic_call" + if isinstance(ir, operations.HighLevelCall): # TODO: improve + return "high_level_call" + if isinstance(ir, operations.LowLevelCall): # TODO: improve + return "low_level_call" + if isinstance(ir, operations.TypeConversion): + return f"type_conversion({ntype(ir.type)})" + if isinstance(ir, operations.Return): # this can be improved using values + return "return" # .format(ntype(ir.type)) + if isinstance(ir, operations.Transfer): + return "transfer" + if isinstance(ir, operations.Send): + return "send" + if isinstance(ir, operations.Unpack): # TODO: improve + return "unpack" + if isinstance(ir, operations.InitArray): # TODO: improve + return "init_array" + # default + raise NotImplementedError(f"encode_ir_for_halstead: {ir}") diff --git a/slither/utils/halstead.py b/slither/utils/halstead.py index 64dd1f6a1a..9ec952e486 100644 --- a/slither/utils/halstead.py +++ b/slither/utils/halstead.py @@ -25,13 +25,17 @@ """ import math +from collections import OrderedDict 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.encoding import encode_ir_for_halstead from slither.utils.myprettytable import make_pretty_table, MyPrettyTable -from slither.utils.upgradeability import encode_ir_for_halstead + + +# pylint: disable=too-many-branches @dataclass @@ -55,7 +59,7 @@ class HalsteadContractMetrics: T: float = 0 B: float = 0 - def __post_init__(self): + def __post_init__(self) -> None: """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""" @@ -85,7 +89,7 @@ def to_dict(self) -> Dict[str, float]: } ) - def populate_operators_and_operands(self): + def populate_operators_and_operands(self) -> None: """Populate the operators and operands lists.""" operators = [] operands = [] @@ -104,7 +108,7 @@ def populate_operators_and_operands(self): self.all_operators.extend(operators) self.all_operands.extend(operands) - def compute_metrics(self, all_operators=None, all_operands=None): + def compute_metrics(self, all_operators=None, all_operands=None) -> None: """Compute the Halstead metrics.""" if all_operators is None: all_operators = self.all_operators @@ -183,17 +187,17 @@ class HalsteadMetrics: ("Extended 2/2", "extended2", EXTENDED2_KEYS), ) - def __post_init__(self): + def __post_init__(self) -> None: # 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): + def update_contract_metrics(self) -> None: for contract in self.contracts: self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract) - def add_all_contracts_metrics(self): + def add_all_contracts_metrics(self) -> None: # If there are more than 1 contract, compute the metrics for all contracts. if len(self.contracts) <= 1: return @@ -211,7 +215,7 @@ def add_all_contracts_metrics(self): None, all_operators=all_operators, all_operands=all_operands ) - def update_reporting_sections(self): + def update_reporting_sections(self) -> None: # Create the table and text for each section. data = { contract.name: self.contract_metrics[contract.name].to_dict() diff --git a/slither/utils/martin.py b/slither/utils/martin.py index fb62a4c584..c336227fd2 100644 --- a/slither/utils/martin.py +++ b/slither/utils/martin.py @@ -26,12 +26,12 @@ class MartinContractMetrics: i: float = 0.0 d: float = 0.0 - def __post_init__(self): + def __post_init__(self) -> None: 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): + def to_dict(self) -> Dict: return { "Dependents": self.ca, "Dependencies": self.ce, @@ -65,12 +65,12 @@ class MartinMetrics: ) SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (("Core", "core", CORE_KEYS),) - def __post_init__(self): + def __post_init__(self) -> None: self.update_abstractness() self.update_coupling() self.update_reporting_sections() - def update_reporting_sections(self): + def update_reporting_sections(self) -> None: # Create the table and text for each section. data = { contract.name: self.contract_metrics[contract.name].to_dict() @@ -98,7 +98,7 @@ def update_reporting_sections(self): SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), ) - def update_abstractness(self) -> float: + def update_abstractness(self) -> None: abstract_contract_count = 0 for c in self.contracts: if not c.is_fully_implemented: @@ -106,7 +106,7 @@ def update_abstractness(self) -> float: self.abstractness = float(abstract_contract_count / len(self.contracts)) # pylint: disable=too-many-branches - def update_coupling(self) -> Dict: + def update_coupling(self) -> None: dependencies = {} for contract in self.contracts: external_calls = [] diff --git a/slither/utils/upgradeability.py b/slither/utils/upgradeability.py index 5cc3dba08c..bbb253175e 100644 --- a/slither/utils/upgradeability.py +++ b/slither/utils/upgradeability.py @@ -1,66 +1,28 @@ -from typing import Optional, Tuple, List, Union +from typing import Optional, Tuple, List + +from slither.analyses.data_dependency.data_dependency import get_dependencies +from slither.core.cfg.node import Node, NodeType from slither.core.declarations import ( Contract, - Structure, - Enum, - SolidityVariableComposed, - SolidityVariable, Function, ) -from slither.core.solidity_types import ( - Type, - ElementaryType, - ArrayType, - MappingType, - UserDefinedType, -) -from slither.core.variables.local_variable import LocalVariable -from slither.core.variables.local_variable_init_from_tuple import LocalVariableInitFromTuple -from slither.core.variables.state_variable import StateVariable -from slither.analyses.data_dependency.data_dependency import get_dependencies -from slither.core.variables.variable import Variable from slither.core.expressions import ( Literal, Identifier, CallExpression, AssignmentOperation, ) -from slither.core.cfg.node import Node, NodeType +from slither.core.solidity_types import ( + ElementaryType, +) +from slither.core.variables.local_variable import LocalVariable +from slither.core.variables.state_variable import StateVariable +from slither.core.variables.variable import Variable from slither.slithir.operations import ( - Operation, - Assignment, - Index, - Member, - Length, - Binary, - Unary, - Condition, - NewArray, - NewStructure, - NewContract, - NewElementaryType, - SolidityCall, - Delete, - EventCall, - LibraryCall, - InternalDynamicCall, - HighLevelCall, LowLevelCall, - TypeConversion, - Return, - Transfer, - Send, - Unpack, - InitArray, - InternalCall, -) -from slither.slithir.variables import ( - TemporaryVariable, - TupleVariable, - Constant, - ReferenceVariable, ) from slither.tools.read_storage.read_storage import SlotInfo, SlitherReadStorage +from slither.utils.encoding import encode_ir_for_upgradeability_compare class TaintedExternalContract: @@ -385,201 +347,13 @@ def is_function_modified(f1: Function, f2: Function) -> bool: if len(node_f1.irs) != len(node_f2.irs): return True for i, ir in enumerate(node_f1.irs): - if encode_ir_for_compare(ir) != encode_ir_for_compare(node_f2.irs[i]): + if encode_ir_for_upgradeability_compare(ir) != encode_ir_for_upgradeability_compare( + node_f2.irs[i] + ): return True return False -# pylint: disable=too-many-branches -def ntype(_type: Union[Type, str]) -> str: - if isinstance(_type, ElementaryType): - _type = str(_type) - elif isinstance(_type, ArrayType): - if isinstance(_type.type, ElementaryType): - _type = str(_type) - else: - _type = "user_defined_array" - elif isinstance(_type, Structure): - _type = str(_type) - elif isinstance(_type, Enum): - _type = str(_type) - elif isinstance(_type, MappingType): - _type = str(_type) - elif isinstance(_type, UserDefinedType): - if isinstance(_type.type, Contract): - _type = f"contract({_type.type.name})" - elif isinstance(_type.type, Structure): - _type = f"struct({_type.type.name})" - elif isinstance(_type.type, Enum): - _type = f"enum({_type.type.name})" - else: - _type = str(_type) - - _type = _type.replace(" memory", "") - _type = _type.replace(" storage ref", "") - - if "struct" in _type: - return "struct" - if "enum" in _type: - return "enum" - if "tuple" in _type: - return "tuple" - if "contract" in _type: - return "contract" - if "mapping" in _type: - return "mapping" - return _type.replace(" ", "_") - - -# pylint: disable=too-many-branches -def encode_ir_for_compare(ir: Operation) -> str: - # operations - if isinstance(ir, Assignment): - return f"({encode_var_for_compare(ir.lvalue)}):=({encode_var_for_compare(ir.rvalue)})" - if isinstance(ir, Index): - return f"index({ntype(ir.variable_right.type)})" - if isinstance(ir, Member): - return "member" # .format(ntype(ir._type)) - if isinstance(ir, Length): - return "length" - if isinstance(ir, Binary): - return f"binary({encode_var_for_compare(ir.variable_left)}{ir.type}{encode_var_for_compare(ir.variable_right)})" - 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 f"delete({encode_var_for_compare(ir.lvalue)},{encode_var_for_compare(ir.variable)})" - 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 f"transfer({encode_var_for_compare(ir.call_value)})" - if isinstance(ir, Send): - return f"send({encode_var_for_compare(ir.call_value)})" - if isinstance(ir, Unpack): # TODO: improve - return "unpack" - if isinstance(ir, InitArray): # TODO: improve - return "init_array" - - # default - 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: - - # variables - if isinstance(var, Constant): - return f"constant({ntype(var.type)},{var.value})" - if isinstance(var, SolidityVariableComposed): - return f"solidity_variable_composed({var.name})" - if isinstance(var, SolidityVariable): - return f"solidity_variable{var.name}" - if isinstance(var, TemporaryVariable): - return "temporary_variable" - if isinstance(var, ReferenceVariable): - return f"reference({ntype(var.type)})" - if isinstance(var, LocalVariable): - return f"local_solc_variable({ntype(var.type)},{var.location})" - if isinstance(var, StateVariable): - if not (var.is_constant or var.is_immutable): - try: - slot, _ = var.contract.compilation_unit.storage_layout_of(var.contract, var) - except KeyError: - slot = var.name - else: - slot = var.name - return f"state_solc_variable({ntype(var.type)},{slot})" - if isinstance(var, LocalVariableInitFromTuple): - return "local_variable_init_tuple" - if isinstance(var, TupleVariable): - return "tuple_variable" - - # default - return "" - - def get_proxy_implementation_slot(proxy: Contract) -> Optional[SlotInfo]: """ Gets information about the storage slot where a proxy's implementation address is stored. From c8bd72ed9fd00f8d42c8b1de3419a2d1bdc779b7 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Fri, 15 Sep 2023 11:16:32 +0200 Subject: [PATCH 30/31] Fix circular dep --- slither/core/variables/local_variable.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/slither/core/variables/local_variable.py b/slither/core/variables/local_variable.py index 427f004082..9baf804457 100644 --- a/slither/core/variables/local_variable.py +++ b/slither/core/variables/local_variable.py @@ -2,7 +2,6 @@ from slither.core.variables.variable import Variable from slither.core.solidity_types.user_defined_type import UserDefinedType -from slither.core.solidity_types.array_type import ArrayType from slither.core.solidity_types.mapping_type import MappingType from slither.core.solidity_types.elementary_type import ElementaryType @@ -51,6 +50,9 @@ def is_storage(self) -> bool: Returns: (bool) """ + # pylint: disable=import-outside-toplevel + from slither.core.solidity_types.array_type import ArrayType + if self.location == "memory": return False if self.location == "calldata": From bcbe4ffe93ab0c1968c2f212d5b7d437126339b7 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Fri, 15 Sep 2023 11:30:47 +0200 Subject: [PATCH 31/31] Update ci_test_printers.sh --- 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 e7310700e5..3306c134e2 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,martin,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,ck" # Only test 0.5.17 to limit test time for file in *0.5.17-compact.zip; do