Skip to content

Commit

Permalink
feat: ck printer
Browse files Browse the repository at this point in the history
  • Loading branch information
devtooligan committed May 5, 2023
1 parent 176c85c commit 55a26f4
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 1 deletion.
1 change: 1 addition & 0 deletions slither/printers/all_printers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
238 changes: 238 additions & 0 deletions slither/printers/summary/ck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
"""
# 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
- 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:
- 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.
"""
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
from typing import TYPE_CHECKING, List, Tuple
from slither.slithir.operations.high_level_call import HighLevelCall
from slither.utils.colors import bold, green, magenta, red


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"] = {
"External calls":int,
"RFC":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 = {}
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] = {
"External calls": 0,
"RFC": 0,
}
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"
mutatability = "mutating" if mutating else "view" if view else "pure"
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 = [
f"{high_level_calls[0].function_name}{high_level_calls[0].destination.contract.name}"
for high_level_calls[0] in high_level_calls
]
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]["External 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: external calls and rfc
txt += bold("\nExternal calls and RFC\n")
keys = list(metrics5[self.contracts[0].name].keys())
table4 = make_pretty_table(["Contract", *keys], metrics5, True)
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
77 changes: 76 additions & 1 deletion slither/utils/myprettytable.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,96 @@


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)


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:
return {"fields_names": self._field_names, "rows": self._rows}

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:
try:
table_row = [row] + [body[row][key] for key in headers[1:]]
except:
print(row)
print(body)
print(headers[1:])
import pdb; pdb.set_trace()
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

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

0 comments on commit 55a26f4

Please sign in to comment.