Skip to content

Commit

Permalink
- Move LoC features into utils.loc as they are now used by two printers
Browse files Browse the repository at this point in the history
- 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)
  • Loading branch information
montyly committed May 25, 2023
1 parent e1cd39f commit e855728
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 153 deletions.
2 changes: 1 addition & 1 deletion slither/printers/all_printers.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
26 changes: 12 additions & 14 deletions slither/printers/summary/human_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def is_complex_code(self, contract):
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:
Expand All @@ -179,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]
Expand Down Expand Up @@ -267,7 +265,7 @@ def _get_features(self, contract): # pylint: disable=too-many-branches
"Proxy": contract.is_upgradeable_proxy,
}

def _get_contracts(self, txt):
def _get_contracts(self, txt: str) -> str:
(
number_contracts,
number_contracts_deps,
Expand All @@ -280,18 +278,18 @@ def _get_contracts(self, txt):
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
Expand Down
106 changes: 7 additions & 99 deletions slither/printers/summary/loc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,117 +9,25 @@
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), \
and test files (TEST)."""

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)
Expand Down
105 changes: 105 additions & 0 deletions slither/utils/loc.py
Original file line number Diff line number Diff line change
@@ -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
39 changes: 0 additions & 39 deletions slither/utils/myprettytable.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,42 +22,3 @@ 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]
}

0 comments on commit e855728

Please sign in to comment.