- Move LoC features into utils.loc as they are now used by two printers

- Remove the usage of dict and create LoC/LoCInfo dataclass to reduce complexity
- Add to_pretty_table in LoC to reduce complexity
- Use proper type (remove PEP 585 for python 3.8 support)
pull/1882/head
Feist Josselin 1 year ago
parent e1cd39fb41
commit e85572824d
  1. 2
      slither/printers/all_printers.py
  2. 26
      slither/printers/summary/human_summary.py
  3. 106
      slither/printers/summary/loc.py
  4. 105
      slither/utils/loc.py
  5. 39
      slither/utils/myprettytable.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

@ -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

@ -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)

@ -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

@ -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]
}

Loading…
Cancel
Save