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 )
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.
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.
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.
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.
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.
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:
- Core metrics (n1, n2, N1, N2)
- Extended metrics 1 (n, N, S, V)
- Extended metrics 2 (D, E, T, B)
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 )
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 )