From bcf570ebb6a333a5f6bc1b59a53211788264e77d Mon Sep 17 00:00:00 2001 From: devtooligan Date: Tue, 2 May 2023 15:51:04 -0700 Subject: [PATCH] feat: loc printer --- slither/printers/all_printers.py | 1 + slither/printers/summary/human_summary.py | 79 ++++++-------- slither/printers/summary/loc.py | 127 ++++++++++++++++++++++ slither/utils/myprettytable.py | 39 +++++++ 4 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 slither/printers/summary/loc.py diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 6dc8dddbdd..3dd64da3e8 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 Loc 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..157b8228a9 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,27 +163,6 @@ 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): total_asm_lines = 0 for contract in self.contracts: @@ -226,7 +203,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 +267,36 @@ def _get_features(self, contract): # pylint: disable=too-many-branches "Proxy": contract.is_upgradeable_proxy, } + def _get_contracts(self, txt): + ( + 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, results): + lines_dict = compute_loc_metrics(self.slither) + txt += "Source lines of code (SLOC) in source files: " + txt += f"{lines_dict['src']['sloc']}\n" + if lines_dict["dep"]["sloc"] > 0: + txt += "Source lines of code (SLOC) in dependencies: " + txt += f"{lines_dict['dep']['sloc']}\n" + if lines_dict["test"]["sloc"] > 0: + txt += "Source lines of code (SLOC) in tests : " + txt += f"{lines_dict['test']['sloc']}\n" + results["number_lines"] = lines_dict["src"]["sloc"] + results["number_lines__dependencies"] = lines_dict["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 +317,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 +342,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 +353,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..4ed9aa6ab0 --- /dev/null +++ b/slither/printers/summary/loc.py @@ -0,0 +1,127 @@ +""" + 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 pathlib import Path +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.myprettytable import transpose, make_pretty_table +from slither.utils.tests_pattern import is_test_file + + +def count_lines(contract_lines: list) -> tuple: + """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_dict(file_type: str, lines: list, lines_dict: dict) -> dict: + """An internal function used to update (mutate in place) the lines_dict. + Args: + file_type: str indicating "src" (source files), "dep" (dependency files), or "test" tests. + lines: list(str) representing the lines of a contract. + lines_dict: dict to be updated with this shape: + { + "src" : {"loc": 30, "sloc": 20, "cloc": 5}, # code in source files + "dep" : {"loc": 50, "sloc": 30, "cloc": 10}, # code in dependencies + "test": {"loc": 80, "sloc": 60, "cloc": 10}, # code in tests + } + Returns: + an updated lines_dict + """ + cloc, sloc, loc = count_lines(lines) + lines_dict[file_type]["loc"] += loc + lines_dict[file_type]["cloc"] += cloc + lines_dict[file_type]["sloc"] += sloc + return lines_dict + + +def compute_loc_metrics(slither) -> dict: + """Used to compute the lines of code metrics for a Slither object. + Args: + slither: A Slither object + Returns: + A new dict with the following shape: + { + "src" : {"loc": 30, "sloc": 20, "cloc": 5}, # code in source files + "dep" : {"loc": 50, "sloc": 30, "cloc": 10}, # code in dependencies + "test": {"loc": 80, "sloc": 60, "cloc": 10}, # code in tests + } + """ + + lines_dict = { + "src": {"loc": 0, "sloc": 0, "cloc": 0}, + "dep": {"loc": 0, "sloc": 0, "cloc": 0}, + "test": {"loc": 0, "sloc": 0, "cloc": 0}, + } + + if not slither.source_code: + return lines_dict + + 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) + file_type = "dep" if is_dep else "test" if is_test_file(Path(filename)) else "src" + lines_dict = _update_lines_dict(file_type, current_lines, lines_dict) + return lines_dict + + +class Loc(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): + # compute loc metrics + lines_dict = compute_loc_metrics(self.slither) + + # prepare the table + headers = [""] + list(lines_dict.keys()) + report_dict = transpose(lines_dict) + table = make_pretty_table(headers, report_dict) + 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/myprettytable.py b/slither/utils/myprettytable.py index af10a6ff25..efdb965048 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -22,3 +22,42 @@ def to_json(self) -> Dict: def __str__(self) -> str: return str(self.to_pretty_table()) + + +# **Dict to MyPrettyTable utility functions** + + +# Converts a dict to a MyPrettyTable. Dict keys are the row headers. +# @param headers str[] of column names +# @param body dict of row headers with a dict of the values +# @param totals bool optional add Totals row +def make_pretty_table(headers: list, body: dict, totals: bool = False) -> MyPrettyTable: + table = MyPrettyTable(headers) + for row in body: + table_row = [row] + [body[row][key] for key in headers[1:]] + table.add_row(table_row) + if totals: + table.add_row(["Total"] + [sum([body[row][key] for row in body]) for key in headers[1:]]) + return table + + +# takes a dict of dicts and returns a dict of dicts with the keys transposed +# example: +# in: +# { +# "dep": {"loc": 0, "sloc": 0, "cloc": 0}, +# "test": {"loc": 0, "sloc": 0, "cloc": 0}, +# "src": {"loc": 0, "sloc": 0, "cloc": 0}, +# } +# out: +# { +# 'loc': {'dep': 0, 'test': 0, 'src': 0}, +# 'sloc': {'dep': 0, 'test': 0, 'src': 0}, +# 'cloc': {'dep': 0, 'test': 0, 'src': 0}, +# } +def transpose(table): + any_key = list(table.keys())[0] + return { + inner_key: {outer_key: table[outer_key][inner_key] for outer_key in table} + for inner_key in table[any_key] + }