slither.utils.martin
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)
1""" 2 Robert "Uncle Bob" Martin - Agile software metrics 3 https://en.wikipedia.org/wiki/Software_package_metrics 4 5 Efferent Coupling (Ce): Number of contracts that the contract depends on 6 Afferent Coupling (Ca): Number of contracts that depend on a contract 7 Instability (I): Ratio of efferent coupling to total coupling (Ce / (Ce + Ca)) 8 Abstractness (A): Number of abstract contracts / total number of contracts 9 Distance from the Main Sequence (D): abs(A + I - 1) 10 11""" 12from typing import Tuple, List, Dict 13from dataclasses import dataclass, field 14from collections import OrderedDict 15from slither.slithir.operations.high_level_call import HighLevelCall 16from slither.core.declarations import Contract 17from slither.utils.myprettytable import make_pretty_table, MyPrettyTable 18 19 20@dataclass 21class MartinContractMetrics: 22 contract: Contract 23 ca: int 24 ce: int 25 abstractness: float 26 i: float = 0.0 27 d: float = 0.0 28 29 def __post_init__(self) -> None: 30 if self.ce + self.ca > 0: 31 self.i = float(self.ce / (self.ce + self.ca)) 32 self.d = float(abs(self.i - self.abstractness)) 33 34 def to_dict(self) -> Dict: 35 return { 36 "Dependents": self.ca, 37 "Dependencies": self.ce, 38 "Instability": f"{self.i:.2f}", 39 "Distance from main sequence": f"{self.d:.2f}", 40 } 41 42 43@dataclass 44class SectionInfo: 45 """Class to hold the information for a section of the report.""" 46 47 title: str 48 pretty_table: MyPrettyTable 49 txt: str 50 51 52@dataclass 53class MartinMetrics: 54 contracts: List[Contract] = field(default_factory=list) 55 abstractness: float = 0.0 56 contract_metrics: OrderedDict = field(default_factory=OrderedDict) 57 title: str = "Martin complexity metrics" 58 full_text: str = "" 59 core: SectionInfo = field(default=SectionInfo) 60 CORE_KEYS = ( 61 "Dependents", 62 "Dependencies", 63 "Instability", 64 "Distance from main sequence", 65 ) 66 SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (("Core", "core", CORE_KEYS),) 67 68 def __post_init__(self) -> None: 69 self.update_abstractness() 70 self.update_coupling() 71 self.update_reporting_sections() 72 73 def update_reporting_sections(self) -> None: 74 # Create the table and text for each section. 75 data = { 76 contract.name: self.contract_metrics[contract.name].to_dict() 77 for contract in self.contracts 78 } 79 for (title, attr, keys) in self.SECTIONS: 80 pretty_table = make_pretty_table(["Contract", *keys], data, False) 81 section_title = f"{self.title} ({title})" 82 txt = f"\n\n{section_title}:\n" 83 txt = "Martin agile software metrics\n" 84 txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" 85 txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" 86 txt += ( 87 "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" 88 ) 89 txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" 90 txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" 91 txt += "\n" 92 txt += f"Abstractness (overall): {round(self.abstractness, 2)}\n" 93 txt += f"{pretty_table}\n" 94 self.full_text += txt 95 setattr( 96 self, 97 attr, 98 SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), 99 ) 100 101 def update_abstractness(self) -> None: 102 abstract_contract_count = 0 103 for c in self.contracts: 104 if not c.is_fully_implemented: 105 abstract_contract_count += 1 106 self.abstractness = float(abstract_contract_count / len(self.contracts)) 107 108 # pylint: disable=too-many-branches 109 def update_coupling(self) -> None: 110 dependencies = {} 111 for contract in self.contracts: 112 external_calls = [] 113 for func in contract.functions: 114 high_level_calls = [ 115 ir 116 for node in func.nodes 117 for ir in node.irs_ssa 118 if isinstance(ir, HighLevelCall) 119 ] 120 # convert irs to string with target function and contract name 121 # Get the target contract name for each high level call 122 new_external_calls = [] 123 for high_level_call in high_level_calls: 124 if isinstance(high_level_call.destination, Contract): 125 new_external_call = high_level_call.destination.name 126 elif isinstance(high_level_call.destination, str): 127 new_external_call = high_level_call.destination 128 elif not hasattr(high_level_call.destination, "type"): 129 continue 130 elif isinstance(high_level_call.destination.type, Contract): 131 new_external_call = high_level_call.destination.type.name 132 elif isinstance(high_level_call.destination.type, str): 133 new_external_call = high_level_call.destination.type 134 elif not hasattr(high_level_call.destination.type, "type"): 135 continue 136 elif isinstance(high_level_call.destination.type.type, Contract): 137 new_external_call = high_level_call.destination.type.type.name 138 elif isinstance(high_level_call.destination.type.type, str): 139 new_external_call = high_level_call.destination.type.type 140 else: 141 continue 142 new_external_calls.append(new_external_call) 143 external_calls.extend(new_external_calls) 144 dependencies[contract.name] = set(external_calls) 145 dependents = {} 146 for contract, deps in dependencies.items(): 147 for dep in deps: 148 if dep not in dependents: 149 dependents[dep] = set() 150 dependents[dep].add(contract) 151 152 for contract in self.contracts: 153 ce = len(dependencies.get(contract.name, [])) 154 ca = len(dependents.get(contract.name, [])) 155 self.contract_metrics[contract.name] = MartinContractMetrics( 156 contract, ca, ce, self.abstractness 157 )
class
MartinContractMetrics:
22class MartinContractMetrics: 23 contract: Contract 24 ca: int 25 ce: int 26 abstractness: float 27 i: float = 0.0 28 d: float = 0.0 29 30 def __post_init__(self) -> None: 31 if self.ce + self.ca > 0: 32 self.i = float(self.ce / (self.ce + self.ca)) 33 self.d = float(abs(self.i - self.abstractness)) 34 35 def to_dict(self) -> Dict: 36 return { 37 "Dependents": self.ca, 38 "Dependencies": self.ce, 39 "Instability": f"{self.i:.2f}", 40 "Distance from main sequence": f"{self.d:.2f}", 41 }
MartinContractMetrics( contract: slither.core.declarations.contract.Contract, ca: int, ce: int, abstractness: float, i: float = 0.0, d: float = 0.0)
class
SectionInfo:
45class SectionInfo: 46 """Class to hold the information for a section of the report.""" 47 48 title: str 49 pretty_table: MyPrettyTable 50 txt: str
Class to hold the information for a section of the report.
SectionInfo( title: str, pretty_table: slither.utils.myprettytable.MyPrettyTable, txt: str)
pretty_table: slither.utils.myprettytable.MyPrettyTable
class
MartinMetrics:
54class MartinMetrics: 55 contracts: List[Contract] = field(default_factory=list) 56 abstractness: float = 0.0 57 contract_metrics: OrderedDict = field(default_factory=OrderedDict) 58 title: str = "Martin complexity metrics" 59 full_text: str = "" 60 core: SectionInfo = field(default=SectionInfo) 61 CORE_KEYS = ( 62 "Dependents", 63 "Dependencies", 64 "Instability", 65 "Distance from main sequence", 66 ) 67 SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (("Core", "core", CORE_KEYS),) 68 69 def __post_init__(self) -> None: 70 self.update_abstractness() 71 self.update_coupling() 72 self.update_reporting_sections() 73 74 def update_reporting_sections(self) -> None: 75 # Create the table and text for each section. 76 data = { 77 contract.name: self.contract_metrics[contract.name].to_dict() 78 for contract in self.contracts 79 } 80 for (title, attr, keys) in self.SECTIONS: 81 pretty_table = make_pretty_table(["Contract", *keys], data, False) 82 section_title = f"{self.title} ({title})" 83 txt = f"\n\n{section_title}:\n" 84 txt = "Martin agile software metrics\n" 85 txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" 86 txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" 87 txt += ( 88 "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" 89 ) 90 txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" 91 txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" 92 txt += "\n" 93 txt += f"Abstractness (overall): {round(self.abstractness, 2)}\n" 94 txt += f"{pretty_table}\n" 95 self.full_text += txt 96 setattr( 97 self, 98 attr, 99 SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), 100 ) 101 102 def update_abstractness(self) -> None: 103 abstract_contract_count = 0 104 for c in self.contracts: 105 if not c.is_fully_implemented: 106 abstract_contract_count += 1 107 self.abstractness = float(abstract_contract_count / len(self.contracts)) 108 109 # pylint: disable=too-many-branches 110 def update_coupling(self) -> None: 111 dependencies = {} 112 for contract in self.contracts: 113 external_calls = [] 114 for func in contract.functions: 115 high_level_calls = [ 116 ir 117 for node in func.nodes 118 for ir in node.irs_ssa 119 if isinstance(ir, HighLevelCall) 120 ] 121 # convert irs to string with target function and contract name 122 # Get the target contract name for each high level call 123 new_external_calls = [] 124 for high_level_call in high_level_calls: 125 if isinstance(high_level_call.destination, Contract): 126 new_external_call = high_level_call.destination.name 127 elif isinstance(high_level_call.destination, str): 128 new_external_call = high_level_call.destination 129 elif not hasattr(high_level_call.destination, "type"): 130 continue 131 elif isinstance(high_level_call.destination.type, Contract): 132 new_external_call = high_level_call.destination.type.name 133 elif isinstance(high_level_call.destination.type, str): 134 new_external_call = high_level_call.destination.type 135 elif not hasattr(high_level_call.destination.type, "type"): 136 continue 137 elif isinstance(high_level_call.destination.type.type, Contract): 138 new_external_call = high_level_call.destination.type.type.name 139 elif isinstance(high_level_call.destination.type.type, str): 140 new_external_call = high_level_call.destination.type.type 141 else: 142 continue 143 new_external_calls.append(new_external_call) 144 external_calls.extend(new_external_calls) 145 dependencies[contract.name] = set(external_calls) 146 dependents = {} 147 for contract, deps in dependencies.items(): 148 for dep in deps: 149 if dep not in dependents: 150 dependents[dep] = set() 151 dependents[dep].add(contract) 152 153 for contract in self.contracts: 154 ce = len(dependencies.get(contract.name, [])) 155 ca = len(dependents.get(contract.name, [])) 156 self.contract_metrics[contract.name] = MartinContractMetrics( 157 contract, ca, ce, self.abstractness 158 )
MartinMetrics( contracts: List[slither.core.declarations.contract.Contract] = <factory>, abstractness: float = 0.0, contract_metrics: collections.OrderedDict = <factory>, title: str = 'Martin complexity metrics', full_text: str = '', core: SectionInfo = <class 'SectionInfo'>, SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] = (('Core', 'core', ('Dependents', 'Dependencies', 'Instability', 'Distance from main sequence')),))
contracts: List[slither.core.declarations.contract.Contract]
SECTIONS: Tuple[Tuple[str, str, Tuple[str]]] =
(('Core', 'core', ('Dependents', 'Dependencies', 'Instability', 'Distance from main sequence')),)
def
update_reporting_sections(self) -> None:
74 def update_reporting_sections(self) -> None: 75 # Create the table and text for each section. 76 data = { 77 contract.name: self.contract_metrics[contract.name].to_dict() 78 for contract in self.contracts 79 } 80 for (title, attr, keys) in self.SECTIONS: 81 pretty_table = make_pretty_table(["Contract", *keys], data, False) 82 section_title = f"{self.title} ({title})" 83 txt = f"\n\n{section_title}:\n" 84 txt = "Martin agile software metrics\n" 85 txt += "Efferent Coupling (Ce) - Number of contracts that a contract depends on\n" 86 txt += "Afferent Coupling (Ca) - Number of contracts that depend on the contract\n" 87 txt += ( 88 "Instability (I) - Ratio of efferent coupling to total coupling (Ce / (Ce + Ca))\n" 89 ) 90 txt += "Abstractness (A) - Number of abstract contracts / total number of contracts\n" 91 txt += "Distance from the Main Sequence (D) - abs(A + I - 1)\n" 92 txt += "\n" 93 txt += f"Abstractness (overall): {round(self.abstractness, 2)}\n" 94 txt += f"{pretty_table}\n" 95 self.full_text += txt 96 setattr( 97 self, 98 attr, 99 SectionInfo(title=section_title, pretty_table=pretty_table, txt=txt), 100 )
def
update_coupling(self) -> None:
110 def update_coupling(self) -> None: 111 dependencies = {} 112 for contract in self.contracts: 113 external_calls = [] 114 for func in contract.functions: 115 high_level_calls = [ 116 ir 117 for node in func.nodes 118 for ir in node.irs_ssa 119 if isinstance(ir, HighLevelCall) 120 ] 121 # convert irs to string with target function and contract name 122 # Get the target contract name for each high level call 123 new_external_calls = [] 124 for high_level_call in high_level_calls: 125 if isinstance(high_level_call.destination, Contract): 126 new_external_call = high_level_call.destination.name 127 elif isinstance(high_level_call.destination, str): 128 new_external_call = high_level_call.destination 129 elif not hasattr(high_level_call.destination, "type"): 130 continue 131 elif isinstance(high_level_call.destination.type, Contract): 132 new_external_call = high_level_call.destination.type.name 133 elif isinstance(high_level_call.destination.type, str): 134 new_external_call = high_level_call.destination.type 135 elif not hasattr(high_level_call.destination.type, "type"): 136 continue 137 elif isinstance(high_level_call.destination.type.type, Contract): 138 new_external_call = high_level_call.destination.type.type.name 139 elif isinstance(high_level_call.destination.type.type, str): 140 new_external_call = high_level_call.destination.type.type 141 else: 142 continue 143 new_external_calls.append(new_external_call) 144 external_calls.extend(new_external_calls) 145 dependencies[contract.name] = set(external_calls) 146 dependents = {} 147 for contract, deps in dependencies.items(): 148 for dep in deps: 149 if dep not in dependents: 150 dependents[dep] = set() 151 dependents[dep].add(contract) 152 153 for contract in self.contracts: 154 ce = len(dependencies.get(contract.name, [])) 155 ca = len(dependents.get(contract.name, [])) 156 self.contract_metrics[contract.name] = MartinContractMetrics( 157 contract, ca, ce, self.abstractness 158 )