Skip to content

Commit

Permalink
feat: loc printer
Browse files Browse the repository at this point in the history
  • Loading branch information
devtooligan committed May 2, 2023
1 parent 176c85c commit bcf570e
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 45 deletions.
1 change: 1 addition & 0 deletions slither/printers/all_printers.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
79 changes: 34 additions & 45 deletions slither/printers/summary/human_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
Expand All @@ -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]

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -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

Expand Down
127 changes: 127 additions & 0 deletions slither/printers/summary/loc.py
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions slither/utils/myprettytable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

0 comments on commit bcf570e

Please sign in to comment.