diff --git a/scripts/ci_test_printers.sh b/scripts/ci_test_printers.sh index 61994b337d..d13932b816 100755 --- a/scripts/ci_test_printers.sh +++ b/scripts/ci_test_printers.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -### Test printer +### Test printer 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,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,human-summary,inheritance,inheritance-graph,loc,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 diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 6dc8dddbdd..5555fe7082 100644 --- a/slither/printers/all_printers.py +++ b/slither/printers/all_printers.py @@ -1,6 +1,7 @@ # pylint: disable=unused-import,relative-beyond-top-level from .summary.function import FunctionSummary from .summary.contract import ContractSummary +from .summary.loc import LocPrinter from .inheritance.inheritance import PrinterInheritance from .inheritance.inheritance_graph import PrinterInheritanceGraph from .call.call_graph import PrinterCallGraph diff --git a/slither/printers/summary/human_summary.py b/slither/printers/summary/human_summary.py index 9eacb97c65..314335ebf4 100644 --- a/slither/printers/summary/human_summary.py +++ b/slither/printers/summary/human_summary.py @@ -2,12 +2,12 @@ Module printing summary of the contract """ import logging -from pathlib import Path from typing import Tuple, List, Dict from slither.core.declarations import SolidityFunction, Function from slither.core.variables.state_variable import StateVariable from slither.printers.abstract_printer import AbstractPrinter +from slither.printers.summary.loc import compute_loc_metrics from slither.slithir.operations import ( LowLevelCall, HighLevelCall, @@ -21,7 +21,6 @@ from slither.utils.myprettytable import MyPrettyTable from slither.utils.standard_libraries import is_standard_library from slither.core.cfg.node import NodeType -from slither.utils.tests_pattern import is_test_file class PrinterHumanSummary(AbstractPrinter): @@ -32,7 +31,6 @@ class PrinterHumanSummary(AbstractPrinter): @staticmethod def _get_summary_erc20(contract): - functions_name = [f.name for f in contract.functions] state_variables = [v.name for v in contract.state_variables] @@ -165,28 +163,7 @@ def is_complex_code(self, contract): def _number_functions(contract): return len(contract.functions) - def _lines_number(self): - if not self.slither.source_code: - return None - total_dep_lines = 0 - total_lines = 0 - total_tests_lines = 0 - - for filename, source_code in self.slither.source_code.items(): - lines = len(source_code.splitlines()) - is_dep = False - if self.slither.crytic_compile: - is_dep = self.slither.crytic_compile.is_dependency(filename) - if is_dep: - total_dep_lines += lines - else: - if is_test_file(Path(filename)): - total_tests_lines += lines - else: - total_lines += lines - return total_lines, total_dep_lines, total_tests_lines - - def _get_number_of_assembly_lines(self): + def _get_number_of_assembly_lines(self) -> int: total_asm_lines = 0 for contract in self.contracts: for function in contract.functions_declared: @@ -202,9 +179,7 @@ def _compilation_type(self): return "Compilation non standard\n" return f"Compiled with {str(self.slither.crytic_compile.type)}\n" - def _number_contracts(self): - if self.slither.crytic_compile is None: - return len(self.slither.contracts), 0, 0 + def _number_contracts(self) -> Tuple[int, int, int]: contracts = self.slither.contracts deps = [c for c in contracts if c.is_from_dependency()] tests = [c for c in contracts if c.is_test] @@ -226,7 +201,6 @@ def _ercs(self): return list(set(ercs)) def _get_features(self, contract): # pylint: disable=too-many-branches - has_payable = False can_send_eth = False can_selfdestruct = False @@ -291,6 +265,36 @@ def _get_features(self, contract): # pylint: disable=too-many-branches "Proxy": contract.is_upgradeable_proxy, } + def _get_contracts(self, txt: str) -> str: + ( + number_contracts, + number_contracts_deps, + number_contracts_tests, + ) = self._number_contracts() + txt += f"Total number of contracts in source files: {number_contracts}\n" + if number_contracts_deps > 0: + txt += f"Number of contracts in dependencies: {number_contracts_deps}\n" + if number_contracts_tests > 0: + txt += f"Number of contracts in tests : {number_contracts_tests}\n" + return txt + + def _get_number_lines(self, txt: str, results: Dict) -> Tuple[str, Dict]: + loc = compute_loc_metrics(self.slither) + txt += "Source lines of code (SLOC) in source files: " + txt += f"{loc.src.sloc}\n" + if loc.dep.sloc > 0: + txt += "Source lines of code (SLOC) in dependencies: " + txt += f"{loc.dep.sloc}\n" + if loc.test.sloc > 0: + txt += "Source lines of code (SLOC) in tests : " + txt += f"{loc.test.sloc}\n" + results["number_lines"] = loc.src.sloc + results["number_lines__dependencies"] = loc.dep.sloc + total_asm_lines = self._get_number_of_assembly_lines() + txt += f"Number of assembly lines: {total_asm_lines}\n" + results["number_lines_assembly"] = total_asm_lines + return txt, results + def output(self, _filename): # pylint: disable=too-many-locals,too-many-statements """ _filename is not used @@ -311,24 +315,8 @@ def output(self, _filename): # pylint: disable=too-many-locals,too-many-stateme "number_findings": {}, "detectors": [], } - - lines_number = self._lines_number() - if lines_number: - total_lines, total_dep_lines, total_tests_lines = lines_number - txt += f"Number of lines: {total_lines} (+ {total_dep_lines} in dependencies, + {total_tests_lines} in tests)\n" - results["number_lines"] = total_lines - results["number_lines__dependencies"] = total_dep_lines - total_asm_lines = self._get_number_of_assembly_lines() - txt += f"Number of assembly lines: {total_asm_lines}\n" - results["number_lines_assembly"] = total_asm_lines - - ( - number_contracts, - number_contracts_deps, - number_contracts_tests, - ) = self._number_contracts() - txt += f"Number of contracts: {number_contracts} (+ {number_contracts_deps} in dependencies, + {number_contracts_tests} tests) \n\n" - + txt = self._get_contracts(txt) + txt, results = self._get_number_lines(txt, results) ( txt_detectors, detectors_results, @@ -352,7 +340,7 @@ def output(self, _filename): # pylint: disable=too-many-locals,too-many-stateme libs = self._standard_libraries() if libs: txt += f'\nUse: {", ".join(libs)}\n' - results["standard_libraries"] = [str(l) for l in libs] + results["standard_libraries"] = [str(lib) for lib in libs] ercs = self._ercs() if ercs: @@ -363,7 +351,6 @@ def output(self, _filename): # pylint: disable=too-many-locals,too-many-stateme ["Name", "# functions", "ERCS", "ERC20 info", "Complex code", "Features"] ) for contract in self.slither.contracts_derived: - if contract.is_from_dependency() or contract.is_test: continue diff --git a/slither/printers/summary/loc.py b/slither/printers/summary/loc.py new file mode 100644 index 0000000000..35bb20fc4b --- /dev/null +++ b/slither/printers/summary/loc.py @@ -0,0 +1,35 @@ +""" + Lines of Code (LOC) printer + + Definitions: + cloc: comment lines of code containing only comments + sloc: source lines of code with no whitespace or comments + loc: all lines of code including whitespace and comments + src: source files (excluding tests and dependencies) + dep: dependency files + test: test files +""" + +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.loc import compute_loc_metrics +from slither.utils.output import Output + + +class LocPrinter(AbstractPrinter): + ARGUMENT = "loc" + HELP = """Count the total number lines of code (LOC), source lines of code (SLOC), \ + and comment lines of code (CLOC) found in source files (SRC), dependencies (DEP), \ + and test files (TEST).""" + + WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#loc" + + def output(self, _filename: str) -> Output: + # compute loc metrics + loc = compute_loc_metrics(self.slither) + + table = loc.to_pretty_table() + txt = "Lines of Code \n" + str(table) + self.info(txt) + res = self.generate_output(txt) + res.add_pretty_table(table, "Code Lines") + return res diff --git a/slither/utils/loc.py b/slither/utils/loc.py new file mode 100644 index 0000000000..0e51dfa460 --- /dev/null +++ b/slither/utils/loc.py @@ -0,0 +1,105 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import List, Tuple + +from slither import Slither +from slither.utils.myprettytable import MyPrettyTable +from slither.utils.tests_pattern import is_test_file + + +@dataclass +class LoCInfo: + loc: int = 0 + sloc: int = 0 + cloc: int = 0 + + def total(self) -> int: + return self.loc + self.sloc + self.cloc + + +@dataclass +class LoC: + src: LoCInfo = LoCInfo() + dep: LoCInfo = LoCInfo() + test: LoCInfo = LoCInfo() + + def to_pretty_table(self) -> MyPrettyTable: + table = MyPrettyTable(["", "src", "dep", "test"]) + + table.add_row(["loc", str(self.src.loc), str(self.dep.loc), str(self.test.loc)]) + table.add_row(["sloc", str(self.src.sloc), str(self.dep.sloc), str(self.test.sloc)]) + table.add_row(["cloc", str(self.src.cloc), str(self.dep.cloc), str(self.test.cloc)]) + table.add_row( + ["Total", str(self.src.total()), str(self.dep.total()), str(self.test.total())] + ) + return table + + +def count_lines(contract_lines: List[str]) -> Tuple[int, int, int]: + """Function to count and classify the lines of code in a contract. + Args: + contract_lines: list(str) representing the lines of a contract. + Returns: + tuple(int, int, int) representing (cloc, sloc, loc) + """ + multiline_comment = False + cloc = 0 + sloc = 0 + loc = 0 + + for line in contract_lines: + loc += 1 + stripped_line = line.strip() + if not multiline_comment: + if stripped_line.startswith("//"): + cloc += 1 + elif "/*" in stripped_line: + # Account for case where /* is followed by */ on the same line. + # If it is, then multiline_comment does not need to be set to True + start_idx = stripped_line.find("/*") + end_idx = stripped_line.find("*/", start_idx + 2) + if end_idx == -1: + multiline_comment = True + cloc += 1 + elif stripped_line: + sloc += 1 + else: + cloc += 1 + if "*/" in stripped_line: + multiline_comment = False + + return cloc, sloc, loc + + +def _update_lines(loc_info: LoCInfo, lines: list) -> None: + """An internal function used to update (mutate in place) the loc_info. + + Args: + loc_info: LoCInfo to be updated + lines: list(str) representing the lines of a contract. + """ + cloc, sloc, loc = count_lines(lines) + loc_info.loc += loc + loc_info.cloc += cloc + loc_info.sloc += sloc + + +def compute_loc_metrics(slither: Slither) -> LoC: + """Used to compute the lines of code metrics for a Slither object. + + Args: + slither: A Slither object + Returns: + A LoC object + """ + + loc = LoC() + + for filename, source_code in slither.source_code.items(): + current_lines = source_code.splitlines() + is_dep = False + if slither.crytic_compile: + is_dep = slither.crytic_compile.is_dependency(filename) + loc_type = loc.dep if is_dep else loc.test if is_test_file(Path(filename)) else loc.src + _update_lines(loc_type, current_lines) + return loc