mirror of https://github.com/crytic/slither
parent
176c85c092
commit
34a343e36c
@ -0,0 +1,266 @@ |
||||
""" |
||||
|
||||
# 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 |
||||
|
||||
- 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. |
||||
|
||||
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.myprettytable import make_pretty_table |
||||
from slither.slithir.operations.high_level_call import HighLevelCall |
||||
from slither.utils.colors import bold |
||||
|
||||
|
||||
def compute_dit(contract, depth=0): |
||||
""" |
||||
Recursively compute the depth of inheritance tree (DIT) of a contract |
||||
Args: |
||||
contract: Contract - the contract to compute the DIT for |
||||
depth: int - the depth of the contract in the inheritance tree |
||||
Returns: |
||||
the depth of the contract in the inheritance tree |
||||
""" |
||||
if not contract.inheritance: |
||||
return depth |
||||
max_dit = depth |
||||
for inherited_contract in contract.inheritance: |
||||
dit = compute_dit(inherited_contract, depth + 1) |
||||
max_dit = max(max_dit, dit) |
||||
return max_dit |
||||
|
||||
|
||||
# pylint: disable=too-many-locals |
||||
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"] = { |
||||
"Ext calls":int, |
||||
"Response For a Class":int, |
||||
"NOC":int, |
||||
"DIT":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 = {} |
||||
dependents = { |
||||
inherited.name: { |
||||
contract.name for contract in contracts if inherited.name in contract.inheritance |
||||
} |
||||
for inherited in contracts |
||||
} |
||||
|
||||
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] = { |
||||
"Ext calls": 0, |
||||
"RFC": 0, |
||||
"NOC": len(dependents[c.name]), |
||||
"DIT": compute_dit(c), |
||||
} |
||||
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" |
||||
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 = [] |
||||
for h in high_level_calls: |
||||
if hasattr(h.destination, "name"): |
||||
external_calls.append(f"{h.function_name}{h.destination.name}") |
||||
else: |
||||
external_calls.append(f"{h.function_name}{h.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 |
||||
|
||||
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]["Ext 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: ext calls and rfc |
||||
txt += bold("\nExt calls and RFC\n") |
||||
keys = list(metrics5[self.contracts[0].name].keys()) |
||||
table4 = make_pretty_table(["Contract", *keys], metrics5, False) |
||||
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 |
Loading…
Reference in new issue