Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: martin complexity printer #1889

Merged
merged 30 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
201b4df
feat: halstead printer
devtooligan May 2, 2023
7eb270a
feat: halstead printer
devtooligan May 4, 2023
16b5726
feat: martin printer
devtooligan May 2, 2023
d682232
chore: add to scripts/ci_test_printers.sh
devtooligan May 11, 2023
6a0add0
Merge branch 'dev' into halstead
devtooligan Jun 14, 2023
134dd90
Merge branch 'dev' into martin-printer
devtooligan Jun 15, 2023
975da91
chore: add type
devtooligan Jun 15, 2023
6f280c1
chore: pylint
devtooligan Jun 15, 2023
45f353b
Merge branch 'dev' into martin-printer
devtooligan Jun 15, 2023
3791467
docs: fix docstring
devtooligan Jun 16, 2023
c3a674a
chore: black
devtooligan Jun 16, 2023
a826bc3
Merge branch 'dev' into halstead
devtooligan Jun 16, 2023
c175d52
Merge branch 'dev' into halstead
devtooligan Jul 5, 2023
06e218c
refactor: prefer custom classes to dicts
devtooligan Jul 6, 2023
db5ec71
chore: move halstead utilities to utils folder
devtooligan Jul 6, 2023
0fb6e42
chore: lint
devtooligan Jul 6, 2023
46c6177
Merge branch 'dev' into halstead
devtooligan Jul 6, 2023
8f831ad
fix: 'type' object is not subscriptable
devtooligan Jul 6, 2023
61e3076
chore: lint
devtooligan Jul 6, 2023
ce76d62
Merge branch 'halstead' into martin-printer
devtooligan Jul 6, 2023
8d2e7c4
refactor: prefer custom classes to dicts
devtooligan Jul 6, 2023
c787fb4
chore: move Martin logic to utils
devtooligan Jul 6, 2023
2e99e49
chore: lint
devtooligan Jul 6, 2023
8c51e47
Update scripts/ci_test_printers.sh
devtooligan Jul 7, 2023
5699349
fix: typo
devtooligan Jul 7, 2023
961a678
Merge branch 'halstead' into martin-printer
devtooligan Jul 7, 2023
4a3ab0a
fix: add martin printer to testing printer list
devtooligan Jul 7, 2023
42cd6e0
fix: account for case w no functions
devtooligan Jul 7, 2023
251dee0
Merge branch 'halstead' into martin-printer
devtooligan Jul 7, 2023
6843d03
fix: external calls
devtooligan Jul 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/ci_test_printers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
cd tests/e2e/solc_parsing/test_data/compile/ || exit

# Do not test the evm printer,as it needs a refactoring
ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,human-summary,inheritance,inheritance-graph,loc,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration"
ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc,martin,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration"

# Only test 0.5.17 to limit test time
for file in *0.5.17-compact.zip; do
Expand Down
2 changes: 2 additions & 0 deletions slither/printers/all_printers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .summary.slithir import PrinterSlithIR
from .summary.slithir_ssa import PrinterSlithIRSSA
from .summary.human_summary import PrinterHumanSummary
from .summary.halstead import Halstead
from .functions.cfg import CFG
from .summary.function_ids import FunctionIds
from .summary.variable_order import VariableOrder
Expand All @@ -21,3 +22,4 @@
from .summary.when_not_paused import PrinterWhenNotPaused
from .summary.declaration import Declaration
from .functions.dominator import Dominator
from .summary.martin import Martin
48 changes: 48 additions & 0 deletions slither/printers/summary/halstead.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
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 metrics1:
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

Extended metrics2:
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

"""
from slither.printers.abstract_printer import AbstractPrinter
from slither.utils.halstead import HalsteadMetrics


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")

halstead = HalsteadMetrics(self.contracts)

res = self.generate_output(halstead.full_text)
res.add_pretty_table(halstead.core.pretty_table, halstead.core.title)
res.add_pretty_table(halstead.extended1.pretty_table, halstead.extended1.title)
res.add_pretty_table(halstead.extended2.pretty_table, halstead.extended2.title)
self.info(halstead.full_text)

return res
31 changes: 31 additions & 0 deletions slither/printers/summary/martin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
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 slither.printers.abstract_printer import AbstractPrinter
from slither.utils.martin import MartinMetrics


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):
if len(self.contracts) == 0:
return self.generate_output("No contract found")

martin = MartinMetrics(self.contracts)

res = self.generate_output(martin.full_text)
res.add_pretty_table(martin.core.pretty_table, martin.core.title)
self.info(martin.full_text)
return res
227 changes: 227 additions & 0 deletions slither/utils/halstead.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
"""
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 metrics1:
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

Extended metrics2:
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 dataclasses import dataclass, field
from typing import Tuple, List, Dict
from collections import OrderedDict
from slither.core.declarations import Contract
from slither.slithir.variables.temporary import TemporaryVariable
from slither.utils.myprettytable import make_pretty_table, MyPrettyTable
from slither.utils.upgradeability import encode_ir_for_halstead


@dataclass
# pylint: disable=too-many-instance-attributes
class HalsteadContractMetrics:
"""Class to hold the Halstead metrics for a single contract."""

contract: Contract
all_operators: List[str] = field(default_factory=list)
all_operands: List[str] = field(default_factory=list)
n1: int = 0
n2: int = 0
N1: int = 0
N2: int = 0
n: int = 0
N: int = 0
S: float = 0
V: float = 0
D: float = 0
E: float = 0
T: float = 0
B: float = 0

def __post_init__(self):
"""Operators and operands can be passed in as constructor args to avoid computing
them based on the contract. Useful for computing metrics for ALL_CONTRACTS"""
if len(self.all_operators) == 0:
self.populate_operators_and_operands()
if len(self.all_operators) > 0:
self.compute_metrics()

def to_dict(self) -> Dict[str, float]:
"""Return the metrics as a dictionary."""
return OrderedDict(
{
"Total Operators": self.N1,
"Unique Operators": self.n1,
"Total Operands": self.N2,
"Unique Operands": self.n2,
"Vocabulary": str(self.n1 + self.n2),
"Program Length": str(self.N1 + self.N2),
"Estimated Length": f"{self.S:.0f}",
"Volume": f"{self.V:.0f}",
"Difficulty": f"{self.D:.0f}",
"Effort": f"{self.E:.0f}",
"Time": f"{self.T:.0f}",
"Estimated Bugs": f"{self.B:.3f}",
}
)

def populate_operators_and_operands(self):
"""Populate the operators and operands lists."""
operators = []
operands = []
if not hasattr(self.contract, "functions"):
return
for func in self.contract.functions:
for node in func.nodes:
for operation in node.irs:
# use operation.expression.type to get the unique operator type
encoded_operator = encode_ir_for_halstead(operation)
operators.append(encoded_operator)

# use operation.used to get the operands of the operation ignoring the temporary variables
operands.extend(
[op for op in operation.used if not isinstance(op, TemporaryVariable)]
)
self.all_operators.extend(operators)
self.all_operands.extend(operands)

def compute_metrics(self, all_operators=None, all_operands=None):
"""Compute the Halstead metrics."""
if all_operators is None:
all_operators = self.all_operators
all_operands = self.all_operands

# core metrics
self.n1 = len(set(all_operators))
self.n2 = len(set(all_operands))
self.N1 = len(all_operators)
self.N2 = len(all_operands)
if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]):
raise ValueError("n1 and n2 must be greater than 0")

# extended metrics 1
self.n = self.n1 + self.n2
self.N = self.N1 + self.N2
self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2)
self.V = self.N * math.log2(self.n)

# extended metrics 2
self.D = (self.n1 / 2) * (self.N2 / self.n2)
self.E = self.D * self.V
self.T = self.E / 18
self.B = (self.E ** (2 / 3)) / 3000


@dataclass
class SectionInfo:
"""Class to hold the information for a section of the report."""

title: str
pretty_table: MyPrettyTable
txt: str


@dataclass
# pylint: disable=too-many-instance-attributes
class HalsteadMetrics:
"""Class to hold the Halstead metrics for all contracts. Contains methods useful for reporting.

There are 3 sections in the report:
1. Core metrics (n1, n2, N1, N2)
2. Extended metrics 1 (n, N, S, V)
3. Extended metrics 2 (D, E, T, B)

"""

contracts: List[Contract] = field(default_factory=list)
contract_metrics: OrderedDict = field(default_factory=OrderedDict)
title: str = "Halstead complexity metrics"
full_text: str = ""
core: SectionInfo = field(default=SectionInfo)
extended1: SectionInfo = field(default=SectionInfo)
extended2: SectionInfo = field(default=SectionInfo)
CORE_KEYS = (
"Total Operators",
"Unique Operators",
"Total Operands",
"Unique Operands",
)
EXTENDED1_KEYS = (
"Vocabulary",
"Program Length",
"Estimated Length",
"Volume",
)
EXTENDED2_KEYS = (
"Difficulty",
"Effort",
"Time",
"Estimated Bugs",
)
SECTIONS: Tuple[Tuple[str, Tuple[str]]] = (
("Core", CORE_KEYS),
("Extended1", EXTENDED1_KEYS),
("Extended2", EXTENDED2_KEYS),
)

def __post_init__(self):
# Compute the metrics for each contract and for all contracts.
self.update_contract_metrics()
self.add_all_contracts_metrics()
self.update_reporting_sections()

def update_contract_metrics(self):
for contract in self.contracts:
self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract)

def add_all_contracts_metrics(self):
# If there are more than 1 contract, compute the metrics for all contracts.
if len(self.contracts) <= 1:
return
all_operators = [
operator
for contract in self.contracts
for operator in self.contract_metrics[contract.name].all_operators
]
all_operands = [
operand
for contract in self.contracts
for operand in self.contract_metrics[contract.name].all_operands
]
self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics(
None, all_operators=all_operators, all_operands=all_operands
)

def update_reporting_sections(self):
# Create the table and text for each section.
data = {
contract.name: self.contract_metrics[contract.name].to_dict()
for contract in self.contracts
}
for (title, keys) in self.SECTIONS:
pretty_table = make_pretty_table(["Contract", *keys], data, False)
section_title = f"{self.title} ({title})"
txt = f"\n\n{section_title}:\n{pretty_table}\n"
self.full_text += txt
setattr(
self,
title.lower(),
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
)
Loading