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: ck printer #1895

Merged
merged 47 commits into from
Sep 15, 2023
Merged
Changes from 1 commit
Commits
Show all changes
47 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
34a343e
feat: ck printer
devtooligan May 5, 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
1fa616b
Merge branch 'dev' into ck-printer
devtooligan Jun 14, 2023
314364e
chore: remove comments
devtooligan Jun 15, 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
14c9761
feat: add CBO
devtooligan Jun 15, 2023
45f353b
Merge branch 'dev' into martin-printer
devtooligan Jun 15, 2023
fa22c34
chore: update label
devtooligan Jun 15, 2023
0fb6597
Merge branch 'martin-printer' into ck-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
fb25cbb
refactor: initial
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
524b755
Merge branch 'martin-printer' into ck-printer
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
6552ba0
Merge branch 'martin-printer' into ck-printer
devtooligan Jul 7, 2023
8d483ed
fix: external calls
devtooligan Jul 7, 2023
5c6cfb8
Merge branch 'martin-printer' into ck-printer
devtooligan Jul 7, 2023
6843d03
fix: external calls
devtooligan Jul 7, 2023
8a34cd4
Merge branch 'martin-printer' into ck-printer
devtooligan Jul 7, 2023
a35dff3
refactor: ck
devtooligan Jul 7, 2023
628f723
Merge branch 'dev' into ck-printer
montyly Sep 15, 2023
4053c9b
minor improvements
montyly Sep 15, 2023
c8bd72e
Fix circular dep
montyly Sep 15, 2023
bcbe4ff
Update ci_test_printers.sh
montyly Sep 15, 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
Prev Previous commit
Next Next commit
refactor: ck
devtooligan committed Jul 7, 2023

Unverified

This user has not yet uploaded their public signing key.
commit a35dff3a15b47ca75391029838aeddf5a6175ce2
269 changes: 23 additions & 246 deletions slither/printers/summary/ck.py
Original file line number Diff line number Diff line change
@@ -14,214 +14,24 @@

During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated.
These are also included in the output:
TODO!!!
- State variables: total number of state variables
- Constants: total number of constants
- Immutables: total number of immutables
- Public: total number of public functions
- External: total number of external functions
- Internal: total number of internal functions
- Private: total number of private functions
- Mutating: total number of state mutating functions
- View: total number of view functions
- Pure: total number of pure functions
- External mutating: total number of external mutating functions
- No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers
- No modifiers: total number of functions without modifiers
- Ext calls: total number of external calls

"""
from typing import Tuple
from slither.utils.colors import bold
from slither.utils.myprettytable import make_pretty_table
from slither.slithir.operations.high_level_call import HighLevelCall
from slither.printers.abstract_printer import AbstractPrinter
from slither.utils.martin import MartinMetrics


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
}

# Use MartinMetrics to compute Ca and Ce
martin = MartinMetrics(contracts)

for contract in contracts:
(state_variables, constants, immutables, public_getters) = count_variables(contract)
rfc = public_getters # add 1 for each public getter
metrics1[contract.name] = {
"State variables": state_variables,
"Constants": constants,
"Immutables": immutables,
}
metrics2[contract.name] = {
"Public": 0,
"External": 0,
"Internal": 0,
"Private": 0,
}
metrics3[contract.name] = {
"Mutating": 0,
"View": 0,
"Pure": 0,
}
metrics4[contract.name] = {
"External mutating": 0,
"No auth or onlyOwner": 0,
"No modifiers": 0,
}
metrics5[contract.name] = {
"Ext calls": 0,
"RFC": 0,
"NOC": len(dependents[contract.name]),
"DIT": compute_dit(contract),
"CBO": coupling[contract.name]["Dependents"] + coupling[contract.name]["Dependencies"],
}
for func in contract.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 high_level_call in high_level_calls:
if hasattr(high_level_call.destination, "name"):
external_calls.append(
f"{high_level_call.function_name}{high_level_call.destination.name}"
)
else:
external_calls.append(
f"{high_level_call.function_name}{high_level_call.destination.type.type.name}"
)

rfc += len(set(external_calls))

metrics2[contract.name]["Public"] += 1 if public else 0
metrics2[contract.name]["External"] += 1 if external else 0
metrics2[contract.name]["Internal"] += 1 if internal else 0
metrics2[contract.name]["Private"] += 1 if private else 0

metrics3[contract.name]["Mutating"] += 1 if mutating else 0
metrics3[contract.name]["View"] += 1 if view else 0
metrics3[contract.name]["Pure"] += 1 if pure else 0

metrics4[contract.name]["External mutating"] += 1 if external_public_mutating else 0
metrics4[contract.name]["No auth or onlyOwner"] += 1 if external_no_auth else 0
metrics4[contract.name]["No modifiers"] += 1 if external_no_modifiers else 0

metrics5[contract.name]["Ext calls"] += len(external_calls)
metrics5[contract.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
from slither.utils.ck import CKMetrics


class CK(AbstractPrinter):
@@ -233,48 +43,15 @@ class CK(AbstractPrinter):
def output(self, _filename):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add the return types for all the functions? (use -> None if the function returns nothing)

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("\nState mutability\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 ck metrics
txt += bold("\nExternal calls and CK Metrics:\n")
txt += bold("Response For a Class (RFC)\n")
txt += bold("Number of Children (NOC)\n")
txt += bold("Depth of Inheritance Tree (DIT)\n")
txt += bold("Coupling Between Object Classes (CBO)\n")
keys = list(metrics5[self.contracts[0].name].keys())
table4 = make_pretty_table(["Contract", *keys], metrics5, False)
txt += str(table4) + "\n"
ck = CKMetrics(self.contracts)

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)
res = self.generate_output(ck.full_text)
res.add_pretty_table(ck.auxiliary1.pretty_table, ck.auxiliary1.title)
res.add_pretty_table(ck.auxiliary2.pretty_table, ck.auxiliary2.title)
res.add_pretty_table(ck.auxiliary3.pretty_table, ck.auxiliary3.title)
res.add_pretty_table(ck.auxiliary4.pretty_table, ck.auxiliary4.title)
res.add_pretty_table(ck.core.pretty_table, ck.core.title)
self.info(ck.full_text)

return res
413 changes: 297 additions & 116 deletions slither/utils/ck.py
Original file line number Diff line number Diff line change
@@ -1,79 +1,239 @@
"""
Description
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.
- Coupling Between Object Classes (CBO) is a metric that measures the number of classes a class is coupled to.
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.
During the calculation of the metrics above, there are a number of other intermediate metrics that are calculated.
These are also included in the output:
- State variables: total number of state variables
- Constants: total number of constants
- Immutables: total number of immutables
- Public: total number of public functions
- External: total number of external functions
- Internal: total number of internal functions
- Private: total number of private functions
- Mutating: total number of state mutating functions
- View: total number of view functions
- Pure: total number of pure functions
- External mutating: total number of external mutating functions
- No auth or onlyOwner: total number of functions without auth or onlyOwner modifiers
- No modifiers: total number of functions without modifiers
- Ext calls: total number of external calls
"""
import math
from dataclasses import dataclass, field
from typing import Tuple, List, Dict
from collections import OrderedDict
from typing import Tuple, List, Dict
from dataclasses import dataclass, field
from slither.utils.colors import bold
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
from slither.utils.martin import MartinMetrics
from slither.slithir.operations.high_level_call import HighLevelCall


# Utility functions


def compute_dit(contract: Contract, depth: int = 0) -> int:
"""
Recursively compute the depth of inheritance tree (DIT) of a contract
Args:
contract(core.declarations.contract.Contract): contract to compute DIT for
depth(int): current depth of the contract
Returns:
int: depth of the contract
"""
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


def has_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 True if it does have auth or only_owner modifiers
"""
for modifier in func.modifiers:
if "auth" in modifier.name or "only_owner" in modifier.name:
return True
return False


# Utility classes for calculating CK metrics


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

contract: Contract

# Used to calculate CBO - should be passed in as a constructor arg
martin_metrics: Dict

# Used to calculate NOC
dependents: Dict

state_variables: int = 0
constants: int = 0
immutables: int = 0
public: int = 0
external: int = 0
internal: int = 0
private: int = 0
mutating: int = 0
view: int = 0
pure: int = 0
external_mutating: int = 0
no_auth_or_only_owner: int = 0
no_modifiers: int = 0
ext_calls: int = 0
rfc: int = 0
noc: int = 0
dit: int = 0
cbo: int = 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()
pass
if not hasattr(self.contract, "functions"):
return
self.count_variables()
self.noc = len(self.dependents[self.contract.name])
self.dit = compute_dit(self.contract)
self.cbo = (
self.martin_metrics[self.contract.name].ca + self.martin_metrics[self.contract.name].ce
)
self.calculate_metrics()

# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
def calculate_metrics(self):
"""Calculate the metrics for a contract"""
rfc = self.public # initialize with public getter count
for func in self.contract.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 not has_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 high_level_call in high_level_calls:
if isinstance(high_level_call.destination, Contract):
destination_contract = high_level_call.destination.name
elif isinstance(high_level_call.destination, str):
destination_contract = high_level_call.destination
elif not hasattr(high_level_call.destination, "type"):
continue
elif isinstance(high_level_call.destination.type, Contract):
destination_contract = high_level_call.destination.type.name
elif isinstance(high_level_call.destination.type, str):
destination_contract = high_level_call.destination.type
elif not hasattr(high_level_call.destination.type, "type"):
continue
elif isinstance(high_level_call.destination.type.type, Contract):
destination_contract = high_level_call.destination.type.type.name
elif isinstance(high_level_call.destination.type.type, str):
destination_contract = high_level_call.destination.type.type
else:
continue
external_calls.append(f"{high_level_call.function_name}{destination_contract}")
rfc += len(set(external_calls))

self.public += public
self.external += external
self.internal += internal
self.private += private

self.mutating += mutating
self.view += view
self.pure += pure

self.external_mutating += external_public_mutating
self.no_auth_or_only_owner += external_no_auth
self.no_modifiers += external_no_modifiers

self.ext_calls += len(external_calls)
self.rfc = rfc

def count_variables(self):
"""Count the number of variables in a contract"""
state_variable_count = 0
constant_count = 0
immutable_count = 0
public_getter_count = 0
for variable in self.contract.variables:
if variable.is_constant:
constant_count += 1
elif variable.is_immutable:
immutable_count += 1
else:
state_variable_count += 1
if variable.visibility == "Public":
public_getter_count += 1
self.state_variables = state_variable_count
self.constants = constant_count
self.immutables = immutable_count

# initialize RFC with public getter count
# self.public is used count public functions not public variables
self.rfc = public_getter_count

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}",
# }
{
"State variables": self.state_variables,
"Constants": self.constants,
"Immutables": self.immutables,
"Public": self.public,
"External": self.external,
"Internal": self.internal,
"Private": self.private,
"Mutating": self.mutating,
"View": self.view,
"Pure": self.pure,
"External mutating": self.external_mutating,
"No auth or onlyOwner": self.no_auth_or_only_owner,
"No modifiers": self.no_modifiers,
"Ext calls": self.ext_calls,
"RFC": self.rfc,
"NOC": self.noc,
"DIT": self.dit,
"CBO": self.cbo,
}
)

def compute_metrics(self):
# """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
pass


@dataclass
class SectionInfo:
@@ -86,81 +246,102 @@ class SectionInfo:

@dataclass
# pylint: disable=too-many-instance-attributes
class TEMPLATEMetrics:
"""Class to hold the TEMPLATE 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)
class CKMetrics:
"""Class to hold the CK metrics for all contracts. Contains methods useful for reporting.
There are 5 sections in the report:
1. Variable count by type (state, constant, immutable)
2. Function count by visibility (public, external, internal, private)
3. Function count by mutability (mutating, view, pure)
4. External mutating function count by modifier (external mutating, no auth or onlyOwner, no modifiers)
5. CK metrics (RFC, NOC, DIT, CBO)
"""

contracts: List[Contract] = field(default_factory=list)
contract_metrics: OrderedDict = field(default_factory=OrderedDict)
title: str = "Halstead complexity metrics"
full_txt: 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),
# )
title: str = "CK complexity metrics"
full_text: str = ""
auxiliary1: SectionInfo = field(default=SectionInfo)
auxiliary2: SectionInfo = field(default=SectionInfo)
auxiliary3: SectionInfo = field(default=SectionInfo)
auxiliary4: SectionInfo = field(default=SectionInfo)
core: SectionInfo = field(default=SectionInfo)
AUXILIARY1_KEYS = (
"State variables",
"Constants",
"Immutables",
)
AUXILIARY2_KEYS = (
"Public",
"External",
"Internal",
"Private",
)
AUXILIARY3_KEYS = (
"Mutating",
"View",
"Pure",
)
AUXILIARY4_KEYS = (
"External mutating",
"No auth or onlyOwner",
"No modifiers",
)
CORE_KEYS = (
"Ext calls",
"RFC",
"NOC",
"DIT",
"CBO",
)
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (
("Variables", "auxiliary1", AUXILIARY1_KEYS),
("Function visibility", "auxiliary2", AUXILIARY2_KEYS),
("State mutability", "auxiliary3", AUXILIARY3_KEYS),
("External mutating functions", "auxiliary4", AUXILIARY4_KEYS),
("Core", "core", CORE_KEYS),
)

def __post_init__(self):
# # Compute the metrics for each contract and for all contracts.
# for contract in self.contracts:
# self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract)

# # If there are more than 1 contract, compute the metrics for all contracts.
# if len(self.contracts) > 1:
# 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
# )
pass
martin_metrics = MartinMetrics(self.contracts).contract_metrics
dependents = {
inherited.name: {
contract.name
for contract in self.contracts
if inherited.name in contract.inheritance
}
for inherited in self.contracts
}
for contract in self.contracts:
self.contract_metrics[contract.name] = CKContractMetrics(
contract=contract, martin_metrics=martin_metrics, dependents=dependents
)

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

# Update each section
for (title, attr, keys) in self.SECTIONS:
if attr == "core":
# Special handling for core section
totals_enabled = False
subtitle += bold("RFC: Response For a Class\n")
subtitle += bold("NOC: Number of Children\n")
subtitle += bold("DIT: Depth of Inheritance Tree\n")
subtitle += bold("CBO: Coupling Between Object Classes\n")
else:
totals_enabled = True
subtitle = ""

pretty_table = make_pretty_table(["Contract", *keys], data, totals=totals_enabled)
section_title = f"{self.title} ({title})"
txt = f"\n\n{section_title}:\n{pretty_table}\n"
self.full_txt += txt
txt = f"\n\n{section_title}:\n{subtitle}{pretty_table}\n"
self.full_text += txt
setattr(
self,
title.lower(),
attr,
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
)
18 changes: 10 additions & 8 deletions slither/utils/halstead.py
Original file line number Diff line number Diff line change
@@ -58,7 +58,10 @@ class HalsteadContractMetrics:
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:
if not hasattr(self.contract, "functions"):
return
self.populate_operators_and_operands()
if len(self.all_operators) > 0:
self.compute_metrics()
@@ -86,8 +89,7 @@ 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:
@@ -175,10 +177,10 @@ class HalsteadMetrics:
"Time",
"Estimated Bugs",
)
SECTIONS: Tuple[Tuple[str, Tuple[str]]] = (
("Core", CORE_KEYS),
("Extended1", EXTENDED1_KEYS),
("Extended2", EXTENDED2_KEYS),
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (
("Core", "core", CORE_KEYS),
("Extended 1/2", "extended1", EXTENDED1_KEYS),
("Extended 2/2", "extended2", EXTENDED2_KEYS),
)

def __post_init__(self):
@@ -215,13 +217,13 @@ def update_reporting_sections(self):
contract.name: self.contract_metrics[contract.name].to_dict()
for contract in self.contracts
}
for (title, keys) in self.SECTIONS:
for (title, attr, 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(),
attr,
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
)
6 changes: 3 additions & 3 deletions slither/utils/martin.py
Original file line number Diff line number Diff line change
@@ -63,7 +63,7 @@ class MartinMetrics:
"Instability",
"Distance from main sequence",
)
SECTIONS: Tuple[Tuple[str, Tuple[str]]] = (("Core", CORE_KEYS),)
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (("Core", "core", CORE_KEYS),)

def __post_init__(self):
self.update_abstractness()
@@ -76,7 +76,7 @@ def update_reporting_sections(self):
contract.name: self.contract_metrics[contract.name].to_dict()
for contract in self.contracts
}
for (title, keys) in self.SECTIONS:
for (title, attr, 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"
@@ -94,7 +94,7 @@ def update_reporting_sections(self):
self.full_text += txt
setattr(
self,
title.lower(),
attr,
SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
)

1 change: 1 addition & 0 deletions slither/utils/myprettytable.py
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ def __str__(self) -> str:

# UTILITY FUNCTIONS


def make_pretty_table(
headers: list, body: dict, totals: bool = False, total_header="TOTAL"
) -> MyPrettyTable: