Skip to content

Commit

Permalink
Merge pull request #1882 from crytic/loc-printer
Browse files Browse the repository at this point in the history
feat: loc printer
  • Loading branch information
montyly authored Jun 22, 2023
2 parents 7f7ff77 + 1a1fc0e commit baf5948
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 51 deletions.
4 changes: 2 additions & 2 deletions scripts/ci_test_printers.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#!/usr/bin/env bash

### Test printer
### Test printer

cd tests/e2e/solc_parsing/test_data/compile/ || exit

# Do not test the evm printer,as it needs a refactoring
ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,human-summary,inheritance,inheritance-graph,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration"
ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,human-summary,inheritance,inheritance-graph,loc,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration"

# Only test 0.5.17 to limit test time
for file in *0.5.17-compact.zip; do
Expand Down
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 LocPrinter
from .inheritance.inheritance import PrinterInheritance
from .inheritance.inheritance_graph import PrinterInheritanceGraph
from .call.call_graph import PrinterCallGraph
Expand Down
85 changes: 36 additions & 49 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,28 +163,7 @@ 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):
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 @@ -202,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 All @@ -226,7 +201,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 +265,36 @@ def _get_features(self, contract): # pylint: disable=too-many-branches
"Proxy": contract.is_upgradeable_proxy,
}

def _get_contracts(self, txt: str) -> str:
(
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: str, results: Dict) -> Tuple[str, Dict]:
loc = compute_loc_metrics(self.slither)
txt += "Source lines of code (SLOC) in source files: "
txt += f"{loc.src.sloc}\n"
if loc.dep.sloc > 0:
txt += "Source lines of code (SLOC) in dependencies: "
txt += f"{loc.dep.sloc}\n"
if loc.test.sloc > 0:
txt += "Source lines of code (SLOC) in tests : "
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
return txt, results

def output(self, _filename): # pylint: disable=too-many-locals,too-many-statements
"""
_filename is not used
Expand All @@ -311,24 +315,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 +340,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 +351,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
35 changes: 35 additions & 0 deletions slither/printers/summary/loc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
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 slither.printers.abstract_printer import AbstractPrinter
from slither.utils.loc import compute_loc_metrics
from slither.utils.output import Output


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: str) -> Output:
# compute loc metrics
loc = compute_loc_metrics(self.slither)

table = loc.to_pretty_table()
txt = "Lines of Code \n" + str(table)
self.info(txt)
res = self.generate_output(txt)
res.add_pretty_table(table, "Code Lines")
return res
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

0 comments on commit baf5948

Please sign in to comment.