Merge branch 'martin-printer' into ck-printer

pull/1895/head
devtooligan 1 year ago
commit 0fb65976d6
No known key found for this signature in database
GPG Key ID: A5606B287899E7FB
  1. 115
      slither/printers/summary/martin.py
  2. 18
      slither/solc_parsing/slither_compilation_unit_solc.py
  3. 2
      tests/unit/core/test_data/inheritance_resolution_error/contract_with_duplicate_names.sol
  4. 1
      tests/unit/core/test_data/inheritance_resolution_error/import.sol
  5. 18
      tests/unit/core/test_error_messages.py

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

@ -33,6 +33,10 @@ logger = logging.getLogger("SlitherSolcParsing")
logger.setLevel(logging.INFO)
class InheritanceResolutionError(SlitherException):
pass
def _handle_import_aliases(
symbol_aliases: Dict, import_directive: Import, scope: FileScope
) -> None:
@ -194,6 +198,13 @@ class SlitherCompilationUnitSolc(CallerContextExpression):
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
def parse_top_level_from_loaded_json(self, data_loaded: Dict, filename: str) -> None:
if not data_loaded or data_loaded is None:
logger.error(
"crytic-compile returned an empty AST. "
"If you are trying to analyze a contract from etherscan or similar make sure it has source code available."
)
return
if "nodeType" in data_loaded:
self._is_compact_ast = True
@ -432,7 +443,12 @@ Please rename it, this name is reserved for Slither's internals"""
target = contract_parser.underlying_contract.file_scope.get_contract_from_name(
contract_name
)
assert target
if target == contract_parser.underlying_contract:
raise InheritanceResolutionError(
"Could not resolve contract inheritance. This is likely caused by an import renaming that collides with existing names (see https://github.com/crytic/slither/issues/1758)."
f"\n Try changing `contract {target}` ({target.source_mapping}) to a unique name."
)
assert target, f"Contract {contract_name} not found"
ancestors.append(target)
elif i in self._contracts_by_id:
ancestors.append(self._contracts_by_id[i])

@ -0,0 +1,2 @@
import {ERC20 as ERC20_1} from "./import.sol";
contract ERC20 is ERC20_1 {}

@ -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…
Cancel
Save