ck-printer-old
devtooligan 2 years ago
parent 176c85c092
commit 92c09df93c
No known key found for this signature in database
GPG Key ID: A5606B287899E7FB
  1. 29
      examples/printers/devtooligan.sol
  2. 1
      slither/printers/all_printers.py
  3. 224
      slither/printers/summary/ck.py
  4. 168
      slither/printers/summary/halstead.py
  5. 19
      slither/utils/code_complexity.py
  6. 77
      slither/utils/myprettytable.py

@ -0,0 +1,29 @@
pragma solidity ^0.8.18;
contract BContract1{
function a() public pure {}
}
contract BContract2{
function b() public pure {}
}
contract CContract1 {
modifier auth {_;}
BContract1 public bc;
BContract2 public bc2;
constructor() {
bc = new BContract1();
bc2 = new BContract2();
}
function c() public auth {
_c();
}
function _c() internal view {
bc.a();
bc2.b();
bc2.b();
}
}

@ -8,6 +8,7 @@ from .functions.authorization import PrinterWrittenVariablesAndAuthorization
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

@ -0,0 +1,224 @@
"""
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.
"""
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
def compute_metrics(contracts):
"""
Compute CK metrics of a contract
Args:
contracts(list): list of contracts
Returns:
a tuple of (metrics1, metrics2, metrics3)
metrics1["contract name"] = {
"public": mut/view/pure,
"external":mut/view/pure,
"internal":mut/view/pure,
"private":mut/view/pure,
}
metrics2["contract name"] = {
"state_variables":int,
"constants":int,
"immutables":int,
}
metrics3["contract name"] = {
"external_mutating":int,
"no_auth":int,
"no_modifiers":int,
"rfc":int,
"external_calls":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 = {}
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] = {
"public": {"mutating": 0, "view": 0, "pure": 0},
"external": {"mutating": 0, "view": 0, "pure": 0},
"internal": {"mutating": 0, "view": 0, "pure": 0},
"private": {"mutating": 0, "view": 0, "pure": 0},
}
metrics2[c.name] = {
"state_variables": state_variables,
"constants": constants,
"immutables": immutables,
}
metrics3[c.name] = {
"external_mutating": 0,
"no_auth": 0,
"no_modifiers": 0,
"rfc": 0,
"external_calls": 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"
epm = external or public and mutating
external_public_mutating = epm
external_no_auth = epm and no_auth(func)
external_no_modifiers = epm 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))
metrics1[c.name]["public"][mutatability] += 1 if public else 0
metrics1[c.name]["external"][mutatability] += 1 if external else 0
metrics1[c.name]["internal"][mutatability] += 1 if internal else 0
metrics1[c.name]["private"][mutatability] += 1 if private else 0
metrics2[c.name] = {
"state_variables": state_variables,
"constants": constants,
"immutables": immutables,
}
metrics3[c.name] = {
"external_mutating": metrics3[c.name]["external_mutating"]
+ (1 if external_public_mutating else 0),
"no_auth": metrics3[c.name]["no_auth"] + (1 if external_no_auth else 0),
"no_modifiers": metrics3[c.name]["no_modifiers"]
+ (1 if external_no_modifiers else 0),
"rfc": rfc,
"external_calls": metrics3[c.name]["external_calls"] + len(external_calls),
}
metrics1_display = format_metrics1(metrics1)
return metrics1_display, metrics2, metrics3
def format_metrics1(metrics1):
metrics1_display = {}
totals = {
"public": {"mutating": 0, "view": 0, "pure": 0},
"external": {"mutating": 0, "view": 0, "pure": 0},
"internal": {"mutating": 0, "view": 0, "pure": 0},
"private": {"mutating": 0, "view": 0, "pure": 0},
}
for c in metrics1:
new_metrics = {}
for key in metrics1[c]:
values = metrics1[c][key]
new_metrics[key] = f"{values['mutating']} / {values['view']} / {values['pure']}"
# update totals
for k in values:
totals[key][k] += values[k]
metrics1_display[c] = new_metrics
# add totals
metrics1_display["TOTAL"] = {}
for key in totals:
values = totals[key]
metrics1_display["TOTAL"][
key
] = f"{values['mutating']} / {values['view']} / {values['pure']}"
return metrics1_display
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 = compute_metrics(self.contracts)
txt = ""
# metrics1: function visibility and mutability counts
txt += "\nCK complexity core metrics 1/3:\n"
keys = list(metrics1[self.contracts[0].name].keys())
table1 = make_pretty_table(
["Contract", "public", "external", "internal", "private"], metrics1, False
)
txt += str(table1) + "\n"
# metrics2: variable counts
txt += "\nCK complexity core metrics 2/3:\n"
keys = list(metrics2[self.contracts[0].name].keys())
table2 = make_pretty_table(["Contract", *keys], metrics2, True)
txt += str(table2) + "\n"
# metrics3: external mutability and rfc
txt += "\nCK complexity core metrics 3/3:\n"
keys = list(metrics3[self.contracts[0].name].keys())
table3 = make_pretty_table(["Contract", *keys], metrics3, True)
txt += str(table3) + "\n"
res = self.generate_output(txt)
res.add_pretty_table(table1, "CK complexity core metrics 1/3")
res.add_pretty_table(table2, "CK complexity core metrics 2/3")
res.add_pretty_table(table3, "CK complexity core metrics 3/3")
self.info(txt)
return res

@ -0,0 +1,168 @@
"""
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:
print(func.name)
import pdb; pdb.set_trace()
for node in func.nodes:
for operation in node.irs:
# use operation.expression.type to get the unique operator type
# TODO: This is broken. It considers every operation as a unique operator.
operators.append(operation)
all_operators.append(operation)
# 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, # TODO: Change order Total Unique Total Unique
"n2_unique_operands": n2,
"N1_total_operators": N1,
"N2_total_operands": N2,
}
extended_metrics = { # TODO: break into two tables extended1 extended2 or maybe different name
"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

@ -1,5 +1,6 @@
# Function computing the code complexity
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING, List, Tuple
from collections import OrderedDict
if TYPE_CHECKING:
from slither.core.declarations import Function
@ -80,3 +81,19 @@ def compute_cyclomatic_complexity(function: "Function") -> int:
N = len(function.nodes)
P = 1
return E - N + 2 * P
def count_abstracts(contracts) -> Tuple[int, int]:
"""
Count the number of abstract contracts and the total number of contracts
Args:
contracts(list): list of contracts
Returns:
Tuple of (abstract_contract_count, total_contract_count)
"""
total_contract_count = 0
abstract_contract_count = 0
for c in contracts:
total_contract_count += 1
if not c.is_fully_implemented:
abstract_contract_count += 1
return (abstract_contract_count, total_contract_count)

@ -4,17 +4,29 @@ from prettytable import PrettyTable
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:
@ -22,3 +34,66 @@ class MyPrettyTable:
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]
}

Loading…
Cancel
Save