From 14c9761da2bb2c85ac08d12327ead1d4f77b2414 Mon Sep 17 00:00:00 2001 From: devtooligan Date: Thu, 15 Jun 2023 15:26:29 -0700 Subject: [PATCH] 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] - }