Source code for crytic_compile.platform.solc_standard_json

"""
Handle compilation through the standard solc json format
"""
import json
import logging
import os
import subprocess
from typing import Union, Dict, List, TYPE_CHECKING, Optional

from crytic_compile.compiler.compiler import CompilerVersion
from crytic_compile.platform.exceptions import InvalidCompilation
from crytic_compile.platform.solc import get_version, is_optimized, relative_to_short, Solc
from crytic_compile.platform.types import Type
from crytic_compile.utils.naming import convert_filename

# Cycle dependency
from crytic_compile.utils.natspec import Natspec

if TYPE_CHECKING:
    from crytic_compile import CryticCompile

LOGGER = logging.getLogger("CryticCompile")


# Inherits is_dependency/is_supported from Solc
[docs]class SolcStandardJson(Solc): """ Represent the Standard solc Json object """ NAME = "Solc-json" PROJECT_URL = "https://solidity.readthedocs.io/en/latest/using-the-compiler.html#compiler-input-and-output-json-description" TYPE = Type.SOLC_STANDARD_JSON def __init__(self, target: Union[str, dict] = None, **kwargs: str): """ Initializes an object which represents solc standard json :param target: A string path to a standard json """ super().__init__(str(target), **kwargs) if target is None: self._json: Dict = dict() elif isinstance(target, str): if os.path.isfile(target): with open(target, mode="r", encoding="utf-8") as target_file: self._json = json.load(target_file) else: self._json = json.loads(target) elif isinstance(target, dict): self._json = target # elif isinstance(target, SolcStandardJson): # self._json = target._json else: raise ValueError(f"Invalid target for solc standard json input.") # Set some default values if they are not provided if "language" not in self._json: self._json["language"] = "Solidity" if "sources" not in self._json: self._json["sources"] = {} if "settings" not in self._json: self._json["settings"] = {} if "remappings" not in self._json["settings"]: self._json["settings"]["remappings"] = [] if "optimizer" not in self._json["settings"]: self._json["settings"]["optimizer"] = {"enabled": False} if "outputSelection" not in self._json["settings"]: self._json["settings"]["outputSelection"] = { "*": { "*": [ "abi", "metadata", "devdoc", "userdoc", "evm.bytecode", "evm.deployedBytecode", ], "": ["ast"], } }
[docs] def add_source_file(self, file_path: str): """ Append file :param file_path: :return: """ self._json["sources"][file_path] = {"urls": [file_path]}
[docs] def add_remapping(self, remapping: str): """ Append our remappings :param remapping: :return: """ self._json["settings"]["remappings"].append(remapping)
[docs] def to_dict(self) -> Dict: """ Patch in our desired output types :return: """ return self._json
[docs] def compile(self, crytic_compile: "CryticCompile", **kwargs: str): """ Compile the target :param crytic_compile: :param target: :param kwargs: :return: """ solc = kwargs.get("solc", "solc") solc_disable_warnings = kwargs.get("solc_disable_warnings", False) solc_arguments = kwargs.get("solc_args", "") solc_remaps: Optional[Union[str, List[str]]] = kwargs.get("solc_remaps", None) solc_working_dir = kwargs.get("solc_working_dir", None) crytic_compile.compiler_version = CompilerVersion( compiler="solc", version=get_version(solc), optimized=is_optimized(solc_arguments) ) skip_filename = crytic_compile.compiler_version.version in [ f"0.4.{x}" for x in range(0, 10) ] # Add all remappings if solc_remaps: if isinstance(solc_remaps, str): solc_remaps = solc_remaps.split(" ") for solc_remap in solc_remaps: self.add_remapping(solc_remap) # Invoke solc targets_json = _run_solc_standard_json( self.to_dict(), solc, solc_disable_warnings=solc_disable_warnings ) if "contracts" in targets_json: for file_path, file_contracts in targets_json["contracts"].items(): for contract_name, info in file_contracts.items(): # for solc < 0.4.10 we cant retrieve the filename from the ast if skip_filename: contract_filename = convert_filename( self._target, relative_to_short, crytic_compile, working_dir=solc_working_dir, ) else: contract_filename = convert_filename( file_path, relative_to_short, crytic_compile, working_dir=solc_working_dir, ) crytic_compile.contracts_names.add(contract_name) crytic_compile.contracts_filenames[contract_name] = contract_filename crytic_compile.abis[contract_name] = info["abi"] userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) crytic_compile.natspec[contract_name] = natspec crytic_compile.bytecodes_init[contract_name] = info["evm"]["bytecode"]["object"] crytic_compile.bytecodes_runtime[contract_name] = info["evm"][ "deployedBytecode" ]["object"] crytic_compile.srcmaps_init[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") crytic_compile.srcmaps_runtime[contract_name] = info["evm"]["deployedBytecode"][ "sourceMap" ].split(";") if "sources" in targets_json: for path, info in targets_json["sources"].items(): if skip_filename: path = convert_filename( self._target, relative_to_short, crytic_compile, working_dir=solc_working_dir, ) else: path = convert_filename( path, relative_to_short, crytic_compile, working_dir=solc_working_dir ) crytic_compile.filenames.add(path) crytic_compile.asts[path.absolute] = info["ast"]
def _guessed_tests(self) -> List[str]: """ Guess the potential unit tests commands :return: """ return []
def _run_solc_standard_json( solc_input: Dict, solc: str, solc_disable_warnings=False, working_dir=None ): """ Note: Ensure that crytic_compile.compiler_version is set prior calling _run_solc :param solc_input: :param solc: :param solc_disable_warnings: :param working_dir: :return: """ cmd = [solc, "--standard-json", "--allow-paths", "."] additional_kwargs = {"cwd": working_dir} if working_dir else {} try: process = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **additional_kwargs, ) except OSError as error: raise InvalidCompilation(error) stdout_b, stderr_b = process.communicate(json.dumps(solc_input).encode("utf-8")) stdout, stderr = ( stdout_b.decode(), stderr_b.decode(), ) # convert bytestrings to unicode strings try: solc_json_output = json.loads(stdout) # Check for errors and raise them if any exist. solc_errors = solc_json_output.get("errors", []) if solc_errors: solc_error_occurred = False solc_exception_str = "" for solc_error in solc_errors: if solc_error["severity"] != "warning": solc_error_occurred = True elif solc_disable_warnings: continue solc_exception_str += ( f"{solc_error.get('type', 'UnknownExceptionType')}: " f"{solc_error.get('formattedMessage', 'N/A')}\n" ) if solc_error_occurred: raise InvalidCompilation(solc_exception_str) if solc_exception_str: LOGGER.warning(solc_exception_str) return solc_json_output except json.decoder.JSONDecodeError: raise InvalidCompilation(f"Invalid solc compilation {stderr}")