mirror of https://github.com/crytic/slither
commit
0fb65976d6
@ -0,0 +1,115 @@ |
||||
""" |
||||
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 typing import Tuple |
||||
from slither.slithir.operations.high_level_call import HighLevelCall |
||||
from slither.utils.myprettytable import make_pretty_table |
||||
from slither.printers.abstract_printer import AbstractPrinter |
||||
|
||||
|
||||
def count_abstracts(contracts) -> Tuple[int, int]: |
||||
""" |
||||
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 |
@ -0,0 +1,2 @@ |
||||
import {ERC20 as ERC20_1} from "./import.sol"; |
||||
contract ERC20 is ERC20_1 {} |
@ -0,0 +1 @@ |
||||
contract ERC20 {} |
@ -0,0 +1,18 @@ |
||||
from pathlib import Path |
||||
import pytest |
||||
|
||||
|
||||
from slither import Slither |
||||
from slither.solc_parsing.slither_compilation_unit_solc import InheritanceResolutionError |
||||
|
||||
TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" |
||||
INHERITANCE_ERROR_ROOT = Path(TEST_DATA_DIR, "inheritance_resolution_error") |
||||
|
||||
|
||||
def test_inheritance_resolution_error(solc_binary_path) -> None: |
||||
with pytest.raises(InheritanceResolutionError): |
||||
solc_path = solc_binary_path("0.8.0") |
||||
Slither( |
||||
Path(INHERITANCE_ERROR_ROOT, "contract_with_duplicate_names.sol").as_posix(), |
||||
solc=solc_path, |
||||
) |
Loading…
Reference in new issue