Skip to content

Commit

Permalink
Merge pull request crytic#1384 from crytic/dev-slither-doctor
Browse files Browse the repository at this point in the history
Add tool to troubleshoot slither errors
  • Loading branch information
montyly authored Oct 26, 2022
2 parents bb5d477 be9199e commit a16aa7e
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 0 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 44,7 @@
"slither-prop = slither.tools.properties.__main__:main",
"slither-mutate = slither.tools.mutator.__main__:main",
"slither-read-storage = slither.tools.read_storage.__main__:main",
"slither-doctor = slither.tools.doctor.__main__:main",
]
},
)
3 changes: 3 additions & 0 deletions slither/tools/doctor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 1,3 @@
# Slither doctor

Slither doctor is a tool designed to troubleshoot running Slither on a project.
Empty file.
37 changes: 37 additions & 0 deletions slither/tools/doctor/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 1,37 @@
import argparse

from crytic_compile import cryticparser

from slither.tools.doctor.utils import report_section
from slither.tools.doctor.checks import ALL_CHECKS


def parse_args() -> argparse.Namespace:
"""
Parse the underlying arguments for the program.
:return: Returns the arguments for the program.
"""
parser = argparse.ArgumentParser(
description="Troubleshoot running Slither on your project",
usage="slither-doctor project",
)

parser.add_argument("project", help="The codebase to be tested.")

# Add default arguments from crytic-compile
cryticparser.init(parser)

return parser.parse_args()


def main():
args = parse_args()
kwargs = vars(args)

for check in ALL_CHECKS:
with report_section(check.title):
check.function(**kwargs)


if __name__ == "__main__":
main()
18 changes: 18 additions & 0 deletions slither/tools/doctor/checks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 1,18 @@
from typing import Callable, List
from dataclasses import dataclass

from slither.tools.doctor.checks.platform import compile_project, detect_platform
from slither.tools.doctor.checks.versions import show_versions


@dataclass
class Check:
title: str
function: Callable[..., None]


ALL_CHECKS: List[Check] = [
Check("Software versions", show_versions),
Check("Project platform", detect_platform),
Check("Project compilation", compile_project),
]
59 changes: 59 additions & 0 deletions slither/tools/doctor/checks/platform.py
Original file line number Diff line number Diff line change
@@ -0,0 1,59 @@
import logging
from pathlib import Path

from crytic_compile import crytic_compile

from slither.tools.doctor.utils import snip_section
from slither.utils.colors import red, yellow, green


def detect_platform(project: str, **kwargs) -> None:
path = Path(project)
if path.is_file():
print(
yellow(
f"{project!r} is a file. Using it as target will manually compile your code with solc and _not_ use a compilation framework. Is that what you meant to do?"
)
)
return

print(f"Trying to detect project type for {project!r}")

supported_platforms = crytic_compile.get_platforms()
skip_platforms = {"solc", "solc-json", "archive", "standard", "etherscan"}
detected_platforms = {
platform.NAME: platform.is_supported(project, **kwargs)
for platform in supported_platforms
if platform.NAME.lower() not in skip_platforms
}
platform_qty = len([platform for platform, state in detected_platforms.items() if state])

print("Is this project using...")
for platform, state in detected_platforms.items():
print(f" => {platform '?':<15}{state and green('Yes') or red('No')}")
print()

if platform_qty == 0:
print(red("No platform was detected! This doesn't sound right."))
print(
yellow(
"Are you trying to analyze a folder with standalone solidity files, without using a compilation framework? If that's the case, then this is okay."
)
)
elif platform_qty > 1:
print(red("More than one platform was detected! This doesn't sound right."))
print(
red("Please use `--compile-force-framework` in Slither to force the correct framework.")
)
else:
print(green("A single platform was detected."), yellow("Is it the one you expected?"))


def compile_project(project: str, **kwargs):
print("Invoking crytic-compile on the project, please wait...")

try:
crytic_compile.CryticCompile(project, **kwargs)
except Exception as e: # pylint: disable=broad-except
with snip_section("Project compilation failed :( The following error was generated:"):
logging.exception(e)
59 changes: 59 additions & 0 deletions slither/tools/doctor/checks/versions.py
Original file line number Diff line number Diff line change
@@ -0,0 1,59 @@
from importlib import metadata
import json
from typing import Optional
import urllib

from packaging.version import parse, LegacyVersion, Version

from slither.utils.colors import yellow, green


def get_installed_version(name: str) -> Optional[LegacyVersion | Version]:
try:
return parse(metadata.version(name))
except metadata.PackageNotFoundError:
return None


def get_github_version(name: str) -> Optional[LegacyVersion | Version]:
try:
with urllib.request.urlopen(
f"https://api.github.com/repos/crytic/{name}/releases/latest"
) as response:
text = response.read()
data = json.loads(text)
return parse(data["tag_name"])
except: # pylint: disable=bare-except
return None


def show_versions(**_kwargs) -> None:
versions = {
"Slither": (get_installed_version("slither-analyzer"), get_github_version("slither")),
"crytic-compile": (
get_installed_version("crytic-compile"),
get_github_version("crytic-compile"),
),
"solc-select": (get_installed_version("solc-select"), get_github_version("solc-select")),
}

outdated = {
name
for name, (installed, latest) in versions.items()
if not installed or not latest or latest > installed
}

for name, (installed, latest) in versions.items():
color = yellow if name in outdated else green
print(f"{name ':':<16}{color(installed or 'N/A'):<16} (latest is {latest or 'Unknown'})")

if len(outdated) > 0:
print()
print(
yellow(
f"Please update {', '.join(outdated)} to the latest release before creating a bug report."
)
)
else:
print()
print(green("Your tools are up to date."))
28 changes: 28 additions & 0 deletions slither/tools/doctor/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 1,28 @@
from contextlib import contextmanager
import logging
from typing import Optional
from slither.utils.colors import bold, yellow, red


@contextmanager
def snip_section(message: Optional[str]) -> None:
if message:
print(red(message), end="\n\n")

print(yellow("---- snip 8< ----"))
yield
print(yellow("---- >8 snip ----"))


@contextmanager
def report_section(title: str) -> None:
print(bold(f"## {title}"), end="\n\n")
try:
yield
except Exception as e: # pylint: disable=broad-except
with snip_section(
"slither-doctor failed unexpectedly! Please report this on the Slither GitHub issue tracker, and include the output below:"
):
logging.exception(e)
finally:
print(end="\n\n")
2 changes: 2 additions & 0 deletions slither/utils/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 10,7 @@ class Colors: # pylint: disable=too-few-public-methods
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
BOLD = "\033[1m"
END = "\033[0m"


Expand Down Expand Up @@ -83,6 84,7 @@ def set_colorization_enabled(enabled: bool):
red = partial(colorize, Colors.RED)
blue = partial(colorize, Colors.BLUE)
magenta = partial(colorize, Colors.MAGENTA)
bold = partial(colorize, Colors.BOLD)

# We enable colorization by default if the output is a tty
set_colorization_enabled(sys.stdout.isatty())

0 comments on commit a16aa7e

Please sign in to comment.