Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: loc printer #1882

Merged
merged 6 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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