diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 6dc8dddbd..c836b98d2 100644 --- a/slither/printers/all_printers.py +++ b/slither/printers/all_printers.py @@ -20,3 +20,4 @@ from .summary.evm import PrinterEVM 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 000000000..1bb59c4ff --- /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 af10a6ff2..efdb96504 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -22,3 +22,42 @@ class MyPrettyTable: 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] + }