feat: martin printer

pull/1889/head
devtooligan 2 years ago
parent 176c85c092
commit 16b57263f4
No known key found for this signature in database
GPG Key ID: A5606B287899E7FB
  1. 1
      slither/printers/all_printers.py
  2. 114
      slither/printers/summary/martin.py
  3. 39
      slither/utils/myprettytable.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

@ -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

@ -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]
}

Loading…
Cancel
Save