diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 3dd64da3e..5555fe708 100644 --- a/slither/printers/all_printers.py +++ b/slither/printers/all_printers.py @@ -1,7 +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 .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 157b8228a..314335ebf 100644 --- a/slither/printers/summary/human_summary.py +++ b/slither/printers/summary/human_summary.py @@ -163,7 +163,7 @@ class PrinterHumanSummary(AbstractPrinter): def _number_functions(contract): return len(contract.functions) - 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: @@ -179,9 +179,7 @@ class PrinterHumanSummary(AbstractPrinter): 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] @@ -267,7 +265,7 @@ class PrinterHumanSummary(AbstractPrinter): "Proxy": contract.is_upgradeable_proxy, } - def _get_contracts(self, txt): + def _get_contracts(self, txt: str) -> str: ( number_contracts, number_contracts_deps, @@ -280,18 +278,18 @@ class PrinterHumanSummary(AbstractPrinter): 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) + 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"{lines_dict['src']['sloc']}\n" - if lines_dict["dep"]["sloc"] > 0: + txt += f"{loc.src.sloc}\n" + if loc.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 += f"{loc.dep.sloc}\n" + if loc.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"] + 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 diff --git a/slither/printers/summary/loc.py b/slither/printers/summary/loc.py index 4ed9aa6ab..35bb20fc4 100644 --- a/slither/printers/summary/loc.py +++ b/slither/printers/summary/loc.py @@ -9,102 +9,13 @@ 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 +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.loc import compute_loc_metrics +from slither.utils.output import Output -class Loc(AbstractPrinter): +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), \ @@ -112,14 +23,11 @@ class Loc(AbstractPrinter): WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#loc" - def output(self, _filename): + def output(self, _filename: str) -> Output: # compute loc metrics - lines_dict = compute_loc_metrics(self.slither) + loc = 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) + table = loc.to_pretty_table() txt = "Lines of Code \n" + str(table) self.info(txt) res = self.generate_output(txt) diff --git a/slither/utils/loc.py b/slither/utils/loc.py new file mode 100644 index 000000000..0e51dfa46 --- /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 diff --git a/slither/utils/myprettytable.py b/slither/utils/myprettytable.py index 57e130884..2f2be7e72 100644 --- a/slither/utils/myprettytable.py +++ b/slither/utils/myprettytable.py @@ -22,42 +22,3 @@ class MyPrettyTable: 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] - }