slither.utils.halstead

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

  1"""
  2    Halstead complexity metrics
  3    https://en.wikipedia.org/wiki/Halstead_complexity_measures
  4
  5    12 metrics based on the number of unique operators and operands:
  6
  7    Core metrics:
  8    n1 = the number of distinct operators
  9    n2 = the number of distinct operands
 10    N1 = the total number of operators
 11    N2 = the total number of operands
 12
 13    Extended metrics1:
 14    n = n1 + n2  # Program vocabulary
 15    N = N1 + N2  # Program length
 16    S = n1 * log2(n1) + n2 * log2(n2) # Estimated program length
 17    V = N * log2(n) # Volume
 18
 19    Extended metrics2:
 20    D = (n1 / 2) * (N2 / n2) # Difficulty
 21    E = D * V # Effort
 22    T = E / 18 seconds # Time required to program
 23    B = (E^(2/3)) / 3000 # Number of delivered bugs
 24
 25
 26"""
 27import math
 28from collections import OrderedDict
 29from dataclasses import dataclass, field
 30from typing import Tuple, List, Dict
 31
 32from slither.core.declarations import Contract
 33from slither.slithir.variables.temporary import TemporaryVariable
 34from slither.utils.encoding import encode_ir_for_halstead
 35from slither.utils.myprettytable import make_pretty_table, MyPrettyTable
 36
 37
 38# pylint: disable=too-many-branches
 39
 40
 41@dataclass
 42# pylint: disable=too-many-instance-attributes
 43class HalsteadContractMetrics:
 44    """Class to hold the Halstead metrics for a single contract."""
 45
 46    contract: Contract
 47    all_operators: List[str] = field(default_factory=list)
 48    all_operands: List[str] = field(default_factory=list)
 49    n1: int = 0
 50    n2: int = 0
 51    N1: int = 0
 52    N2: int = 0
 53    n: int = 0
 54    N: int = 0
 55    S: float = 0
 56    V: float = 0
 57    D: float = 0
 58    E: float = 0
 59    T: float = 0
 60    B: float = 0
 61
 62    def __post_init__(self) -> None:
 63        """Operators and operands can be passed in as constructor args to avoid computing
 64        them based on the contract. Useful for computing metrics for ALL_CONTRACTS"""
 65
 66        if len(self.all_operators) == 0:
 67            if not hasattr(self.contract, "functions"):
 68                return
 69            self.populate_operators_and_operands()
 70        if len(self.all_operators) > 0:
 71            self.compute_metrics()
 72
 73    def to_dict(self) -> Dict[str, float]:
 74        """Return the metrics as a dictionary."""
 75        return OrderedDict(
 76            {
 77                "Total Operators": self.N1,
 78                "Unique Operators": self.n1,
 79                "Total Operands": self.N2,
 80                "Unique Operands": self.n2,
 81                "Vocabulary": str(self.n1 + self.n2),
 82                "Program Length": str(self.N1 + self.N2),
 83                "Estimated Length": f"{self.S:.0f}",
 84                "Volume": f"{self.V:.0f}",
 85                "Difficulty": f"{self.D:.0f}",
 86                "Effort": f"{self.E:.0f}",
 87                "Time": f"{self.T:.0f}",
 88                "Estimated Bugs": f"{self.B:.3f}",
 89            }
 90        )
 91
 92    def populate_operators_and_operands(self) -> None:
 93        """Populate the operators and operands lists."""
 94        operators = []
 95        operands = []
 96
 97        for func in self.contract.functions:
 98            for node in func.nodes:
 99                for operation in node.irs:
100                    # use operation.expression.type to get the unique operator type
101                    encoded_operator = encode_ir_for_halstead(operation)
102                    operators.append(encoded_operator)
103
104                    # use operation.used to get the operands of the operation ignoring the temporary variables
105                    operands.extend(
106                        [op for op in operation.used if not isinstance(op, TemporaryVariable)]
107                    )
108        self.all_operators.extend(operators)
109        self.all_operands.extend(operands)
110
111    def compute_metrics(self, all_operators=None, all_operands=None) -> None:
112        """Compute the Halstead metrics."""
113        if all_operators is None:
114            all_operators = self.all_operators
115            all_operands = self.all_operands
116
117        # core metrics
118        self.n1 = len(set(all_operators))
119        self.n2 = len(set(all_operands))
120        self.N1 = len(all_operators)
121        self.N2 = len(all_operands)
122        if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]):
123            raise ValueError("n1 and n2 must be greater than 0")
124
125        # extended metrics 1
126        self.n = self.n1 + self.n2
127        self.N = self.N1 + self.N2
128        self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2)
129        self.V = self.N * math.log2(self.n)
130
131        # extended metrics 2
132        self.D = (self.n1 / 2) * (self.N2 / self.n2)
133        self.E = self.D * self.V
134        self.T = self.E / 18
135        self.B = (self.E ** (2 / 3)) / 3000
136
137
138@dataclass
139class SectionInfo:
140    """Class to hold the information for a section of the report."""
141
142    title: str
143    pretty_table: MyPrettyTable
144    txt: str
145
146
147@dataclass
148# pylint: disable=too-many-instance-attributes
149class HalsteadMetrics:
150    """Class to hold the Halstead metrics for all contracts. Contains methods useful for reporting.
151
152    There are 3 sections in the report:
153    1. Core metrics (n1, n2, N1, N2)
154    2. Extended metrics 1 (n, N, S, V)
155    3. Extended metrics 2 (D, E, T, B)
156
157    """
158
159    contracts: List[Contract] = field(default_factory=list)
160    contract_metrics: OrderedDict = field(default_factory=OrderedDict)
161    title: str = "Halstead complexity metrics"
162    full_text: str = ""
163    core: SectionInfo = field(default=SectionInfo)
164    extended1: SectionInfo = field(default=SectionInfo)
165    extended2: SectionInfo = field(default=SectionInfo)
166    CORE_KEYS = (
167        "Total Operators",
168        "Unique Operators",
169        "Total Operands",
170        "Unique Operands",
171    )
172    EXTENDED1_KEYS = (
173        "Vocabulary",
174        "Program Length",
175        "Estimated Length",
176        "Volume",
177    )
178    EXTENDED2_KEYS = (
179        "Difficulty",
180        "Effort",
181        "Time",
182        "Estimated Bugs",
183    )
184    SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (
185        ("Core", "core", CORE_KEYS),
186        ("Extended 1/2", "extended1", EXTENDED1_KEYS),
187        ("Extended 2/2", "extended2", EXTENDED2_KEYS),
188    )
189
190    def __post_init__(self) -> None:
191        # Compute the metrics for each contract and for all contracts.
192        self.update_contract_metrics()
193        self.add_all_contracts_metrics()
194        self.update_reporting_sections()
195
196    def update_contract_metrics(self) -> None:
197        for contract in self.contracts:
198            self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract)
199
200    def add_all_contracts_metrics(self) -> None:
201        # If there are more than 1 contract, compute the metrics for all contracts.
202        if len(self.contracts) <= 1:
203            return
204        all_operators = [
205            operator
206            for contract in self.contracts
207            for operator in self.contract_metrics[contract.name].all_operators
208        ]
209        all_operands = [
210            operand
211            for contract in self.contracts
212            for operand in self.contract_metrics[contract.name].all_operands
213        ]
214        self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics(
215            None, all_operators=all_operators, all_operands=all_operands
216        )
217
218    def update_reporting_sections(self) -> None:
219        # Create the table and text for each section.
220        data = {
221            contract.name: self.contract_metrics[contract.name].to_dict()
222            for contract in self.contracts
223        }
224        for (title, attr, keys) in self.SECTIONS:
225            pretty_table = make_pretty_table(["Contract", *keys], data, False)
226            section_title = f"{self.title} ({title})"
227            txt = f"\n\n{section_title}:\n{pretty_table}\n"
228            self.full_text += txt
229            setattr(
230                self,
231                attr,
232                SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
233            )
class HalsteadContractMetrics:
 44class HalsteadContractMetrics:
 45    """Class to hold the Halstead metrics for a single contract."""
 46
 47    contract: Contract
 48    all_operators: List[str] = field(default_factory=list)
 49    all_operands: List[str] = field(default_factory=list)
 50    n1: int = 0
 51    n2: int = 0
 52    N1: int = 0
 53    N2: int = 0
 54    n: int = 0
 55    N: int = 0
 56    S: float = 0
 57    V: float = 0
 58    D: float = 0
 59    E: float = 0
 60    T: float = 0
 61    B: float = 0
 62
 63    def __post_init__(self) -> None:
 64        """Operators and operands can be passed in as constructor args to avoid computing
 65        them based on the contract. Useful for computing metrics for ALL_CONTRACTS"""
 66
 67        if len(self.all_operators) == 0:
 68            if not hasattr(self.contract, "functions"):
 69                return
 70            self.populate_operators_and_operands()
 71        if len(self.all_operators) > 0:
 72            self.compute_metrics()
 73
 74    def to_dict(self) -> Dict[str, float]:
 75        """Return the metrics as a dictionary."""
 76        return OrderedDict(
 77            {
 78                "Total Operators": self.N1,
 79                "Unique Operators": self.n1,
 80                "Total Operands": self.N2,
 81                "Unique Operands": self.n2,
 82                "Vocabulary": str(self.n1 + self.n2),
 83                "Program Length": str(self.N1 + self.N2),
 84                "Estimated Length": f"{self.S:.0f}",
 85                "Volume": f"{self.V:.0f}",
 86                "Difficulty": f"{self.D:.0f}",
 87                "Effort": f"{self.E:.0f}",
 88                "Time": f"{self.T:.0f}",
 89                "Estimated Bugs": f"{self.B:.3f}",
 90            }
 91        )
 92
 93    def populate_operators_and_operands(self) -> None:
 94        """Populate the operators and operands lists."""
 95        operators = []
 96        operands = []
 97
 98        for func in self.contract.functions:
 99            for node in func.nodes:
100                for operation in node.irs:
101                    # use operation.expression.type to get the unique operator type
102                    encoded_operator = encode_ir_for_halstead(operation)
103                    operators.append(encoded_operator)
104
105                    # use operation.used to get the operands of the operation ignoring the temporary variables
106                    operands.extend(
107                        [op for op in operation.used if not isinstance(op, TemporaryVariable)]
108                    )
109        self.all_operators.extend(operators)
110        self.all_operands.extend(operands)
111
112    def compute_metrics(self, all_operators=None, all_operands=None) -> None:
113        """Compute the Halstead metrics."""
114        if all_operators is None:
115            all_operators = self.all_operators
116            all_operands = self.all_operands
117
118        # core metrics
119        self.n1 = len(set(all_operators))
120        self.n2 = len(set(all_operands))
121        self.N1 = len(all_operators)
122        self.N2 = len(all_operands)
123        if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]):
124            raise ValueError("n1 and n2 must be greater than 0")
125
126        # extended metrics 1
127        self.n = self.n1 + self.n2
128        self.N = self.N1 + self.N2
129        self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2)
130        self.V = self.N * math.log2(self.n)
131
132        # extended metrics 2
133        self.D = (self.n1 / 2) * (self.N2 / self.n2)
134        self.E = self.D * self.V
135        self.T = self.E / 18
136        self.B = (self.E ** (2 / 3)) / 3000

Class to hold the Halstead metrics for a single contract.

HalsteadContractMetrics( contract: slither.core.declarations.contract.Contract, all_operators: List[str] = <factory>, all_operands: List[str] = <factory>, 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)
all_operators: List[str]
all_operands: List[str]
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 to_dict(self) -> Dict[str, float]:
74    def to_dict(self) -> Dict[str, float]:
75        """Return the metrics as a dictionary."""
76        return OrderedDict(
77            {
78                "Total Operators": self.N1,
79                "Unique Operators": self.n1,
80                "Total Operands": self.N2,
81                "Unique Operands": self.n2,
82                "Vocabulary": str(self.n1 + self.n2),
83                "Program Length": str(self.N1 + self.N2),
84                "Estimated Length": f"{self.S:.0f}",
85                "Volume": f"{self.V:.0f}",
86                "Difficulty": f"{self.D:.0f}",
87                "Effort": f"{self.E:.0f}",
88                "Time": f"{self.T:.0f}",
89                "Estimated Bugs": f"{self.B:.3f}",
90            }
91        )

Return the metrics as a dictionary.

def populate_operators_and_operands(self) -> None:
 93    def populate_operators_and_operands(self) -> None:
 94        """Populate the operators and operands lists."""
 95        operators = []
 96        operands = []
 97
 98        for func in self.contract.functions:
 99            for node in func.nodes:
100                for operation in node.irs:
101                    # use operation.expression.type to get the unique operator type
102                    encoded_operator = encode_ir_for_halstead(operation)
103                    operators.append(encoded_operator)
104
105                    # use operation.used to get the operands of the operation ignoring the temporary variables
106                    operands.extend(
107                        [op for op in operation.used if not isinstance(op, TemporaryVariable)]
108                    )
109        self.all_operators.extend(operators)
110        self.all_operands.extend(operands)

Populate the operators and operands lists.

def compute_metrics(self, all_operators=None, all_operands=None) -> None:
112    def compute_metrics(self, all_operators=None, all_operands=None) -> None:
113        """Compute the Halstead metrics."""
114        if all_operators is None:
115            all_operators = self.all_operators
116            all_operands = self.all_operands
117
118        # core metrics
119        self.n1 = len(set(all_operators))
120        self.n2 = len(set(all_operands))
121        self.N1 = len(all_operators)
122        self.N2 = len(all_operands)
123        if any(number <= 0 for number in [self.n1, self.n2, self.N1, self.N2]):
124            raise ValueError("n1 and n2 must be greater than 0")
125
126        # extended metrics 1
127        self.n = self.n1 + self.n2
128        self.N = self.N1 + self.N2
129        self.S = self.n1 * math.log2(self.n1) + self.n2 * math.log2(self.n2)
130        self.V = self.N * math.log2(self.n)
131
132        # extended metrics 2
133        self.D = (self.n1 / 2) * (self.N2 / self.n2)
134        self.E = self.D * self.V
135        self.T = self.E / 18
136        self.B = (self.E ** (2 / 3)) / 3000

Compute the Halstead metrics.

class SectionInfo:
140class SectionInfo:
141    """Class to hold the information for a section of the report."""
142
143    title: str
144    pretty_table: MyPrettyTable
145    txt: str

Class to hold the information for a section of the report.

SectionInfo( title: str, pretty_table: slither.utils.myprettytable.MyPrettyTable, txt: str)
title: str
txt: str
class HalsteadMetrics:
150class HalsteadMetrics:
151    """Class to hold the Halstead metrics for all contracts. Contains methods useful for reporting.
152
153    There are 3 sections in the report:
154    1. Core metrics (n1, n2, N1, N2)
155    2. Extended metrics 1 (n, N, S, V)
156    3. Extended metrics 2 (D, E, T, B)
157
158    """
159
160    contracts: List[Contract] = field(default_factory=list)
161    contract_metrics: OrderedDict = field(default_factory=OrderedDict)
162    title: str = "Halstead complexity metrics"
163    full_text: str = ""
164    core: SectionInfo = field(default=SectionInfo)
165    extended1: SectionInfo = field(default=SectionInfo)
166    extended2: SectionInfo = field(default=SectionInfo)
167    CORE_KEYS = (
168        "Total Operators",
169        "Unique Operators",
170        "Total Operands",
171        "Unique Operands",
172    )
173    EXTENDED1_KEYS = (
174        "Vocabulary",
175        "Program Length",
176        "Estimated Length",
177        "Volume",
178    )
179    EXTENDED2_KEYS = (
180        "Difficulty",
181        "Effort",
182        "Time",
183        "Estimated Bugs",
184    )
185    SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (
186        ("Core", "core", CORE_KEYS),
187        ("Extended 1/2", "extended1", EXTENDED1_KEYS),
188        ("Extended 2/2", "extended2", EXTENDED2_KEYS),
189    )
190
191    def __post_init__(self) -> None:
192        # Compute the metrics for each contract and for all contracts.
193        self.update_contract_metrics()
194        self.add_all_contracts_metrics()
195        self.update_reporting_sections()
196
197    def update_contract_metrics(self) -> None:
198        for contract in self.contracts:
199            self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract)
200
201    def add_all_contracts_metrics(self) -> None:
202        # If there are more than 1 contract, compute the metrics for all contracts.
203        if len(self.contracts) <= 1:
204            return
205        all_operators = [
206            operator
207            for contract in self.contracts
208            for operator in self.contract_metrics[contract.name].all_operators
209        ]
210        all_operands = [
211            operand
212            for contract in self.contracts
213            for operand in self.contract_metrics[contract.name].all_operands
214        ]
215        self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics(
216            None, all_operators=all_operators, all_operands=all_operands
217        )
218
219    def update_reporting_sections(self) -> None:
220        # Create the table and text for each section.
221        data = {
222            contract.name: self.contract_metrics[contract.name].to_dict()
223            for contract in self.contracts
224        }
225        for (title, attr, keys) in self.SECTIONS:
226            pretty_table = make_pretty_table(["Contract", *keys], data, False)
227            section_title = f"{self.title} ({title})"
228            txt = f"\n\n{section_title}:\n{pretty_table}\n"
229            self.full_text += txt
230            setattr(
231                self,
232                attr,
233                SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
234            )

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)
HalsteadMetrics( contracts: List[slither.core.declarations.contract.Contract] = <factory>, contract_metrics: collections.OrderedDict = <factory>, title: str = 'Halstead complexity metrics', full_text: str = '', core: SectionInfo = <class 'SectionInfo'>, extended1: SectionInfo = <class 'SectionInfo'>, extended2: SectionInfo = <class 'SectionInfo'>, SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (('Core', 'core', ('Total Operators', 'Unique Operators', 'Total Operands', 'Unique Operands')), ('Extended 1/2', 'extended1', ('Vocabulary', 'Program Length', 'Estimated Length', 'Volume')), ('Extended 2/2', 'extended2', ('Difficulty', 'Effort', 'Time', 'Estimated Bugs'))))
contract_metrics: collections.OrderedDict
title: str = 'Halstead complexity metrics'
full_text: str = ''
core: SectionInfo = <class 'SectionInfo'>
extended1: SectionInfo = <class 'SectionInfo'>
extended2: SectionInfo = <class '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, str, Tuple[str]]] = (('Core', 'core', ('Total Operators', 'Unique Operators', 'Total Operands', 'Unique Operands')), ('Extended 1/2', 'extended1', ('Vocabulary', 'Program Length', 'Estimated Length', 'Volume')), ('Extended 2/2', 'extended2', ('Difficulty', 'Effort', 'Time', 'Estimated Bugs')))
def update_contract_metrics(self) -> None:
197    def update_contract_metrics(self) -> None:
198        for contract in self.contracts:
199            self.contract_metrics[contract.name] = HalsteadContractMetrics(contract=contract)
def add_all_contracts_metrics(self) -> None:
201    def add_all_contracts_metrics(self) -> None:
202        # If there are more than 1 contract, compute the metrics for all contracts.
203        if len(self.contracts) <= 1:
204            return
205        all_operators = [
206            operator
207            for contract in self.contracts
208            for operator in self.contract_metrics[contract.name].all_operators
209        ]
210        all_operands = [
211            operand
212            for contract in self.contracts
213            for operand in self.contract_metrics[contract.name].all_operands
214        ]
215        self.contract_metrics["ALL CONTRACTS"] = HalsteadContractMetrics(
216            None, all_operators=all_operators, all_operands=all_operands
217        )
def update_reporting_sections(self) -> None:
219    def update_reporting_sections(self) -> None:
220        # Create the table and text for each section.
221        data = {
222            contract.name: self.contract_metrics[contract.name].to_dict()
223            for contract in self.contracts
224        }
225        for (title, attr, keys) in self.SECTIONS:
226            pretty_table = make_pretty_table(["Contract", *keys], data, False)
227            section_title = f"{self.title} ({title})"
228            txt = f"\n\n{section_title}:\n{pretty_table}\n"
229            self.full_text += txt
230            setattr(
231                self,
232                attr,
233                SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt),
234            )