crytic_compile.platform.vyper
Vyper platform
1""" 2Vyper platform 3""" 4import json 5import logging 6import os 7import shutil 8import subprocess 9from pathlib import Path 10from typing import TYPE_CHECKING, Dict, List, Optional 11 12from crytic_compile.compilation_unit import CompilationUnit 13from crytic_compile.compiler.compiler import CompilerVersion 14from crytic_compile.platform.abstract_platform import AbstractPlatform 15from crytic_compile.platform.exceptions import InvalidCompilation 16from crytic_compile.platform.types import Type 17from crytic_compile.utils.naming import convert_filename 18 19# Handle cycle 20from crytic_compile.utils.natspec import Natspec 21 22if TYPE_CHECKING: 23 from crytic_compile import CryticCompile 24 25LOGGER = logging.getLogger("CryticCompile") 26 27 28class VyperStandardJson(AbstractPlatform): 29 """ 30 Vyper platform 31 """ 32 33 NAME = "vyper" 34 PROJECT_URL = "https://github.com/vyperlang/vyper" 35 TYPE = Type.VYPER 36 37 def __init__(self, target: Optional[Path] = None, **_kwargs: str): 38 super().__init__(str(target), **_kwargs) 39 self.standard_json_input = { 40 "language": "Vyper", 41 "sources": {}, 42 "settings": { 43 "outputSelection": { 44 "*": { 45 "*": [ 46 "abi", 47 "devdoc", 48 "userdoc", 49 "evm.bytecode", 50 "evm.deployedBytecode", 51 "evm.deployedBytecode.sourceMap", 52 ], 53 "": ["ast"], 54 } 55 } 56 }, 57 } 58 59 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 60 """Compile the target 61 62 Args: 63 crytic_compile (CryticCompile): CryticCompile object to populate 64 **kwargs: optional arguments. Used "vyper" 65 66 67 """ 68 target = self._target 69 # If the target was a directory `add_source_file` should have been called 70 # by `compile_all`. Otherwise, we should have a single file target. 71 if self._target is not None and os.path.isfile(self._target): 72 self.add_source_files([target]) 73 74 vyper_bin = kwargs.get("vyper", "vyper") 75 76 compilation_artifacts = _run_vyper_standard_json(self.standard_json_input, vyper_bin) 77 compilation_unit = CompilationUnit(crytic_compile, str(target)) 78 79 compiler_version = compilation_artifacts["compiler"].split("-")[1] 80 if compiler_version != "0.3.7": 81 LOGGER.info("Vyper != 0.3.7 support is a best effort and might fail") 82 compilation_unit.compiler_version = CompilerVersion( 83 compiler="vyper", version=compiler_version, optimized=False 84 ) 85 86 for source_file, contract_info in compilation_artifacts["contracts"].items(): 87 filename = convert_filename(source_file, _relative_to_short, crytic_compile) 88 source_unit = compilation_unit.create_source_unit(filename) 89 for contract_name, contract_metadata in contract_info.items(): 90 source_unit.add_contract_name(contract_name) 91 compilation_unit.filename_to_contracts[filename].add(contract_name) 92 93 source_unit.abis[contract_name] = contract_metadata["abi"] 94 source_unit.bytecodes_init[contract_name] = contract_metadata["evm"]["bytecode"][ 95 "object" 96 ].replace("0x", "") 97 # Vyper does not provide the source mapping for the init bytecode 98 source_unit.srcmaps_init[contract_name] = [] 99 source_unit.srcmaps_runtime[contract_name] = contract_metadata["evm"][ 100 "deployedBytecode" 101 ]["sourceMap"].split(";") 102 source_unit.bytecodes_runtime[contract_name] = contract_metadata["evm"][ 103 "deployedBytecode" 104 ]["object"].replace("0x", "") 105 source_unit.natspec[contract_name] = Natspec( 106 contract_metadata["userdoc"], contract_metadata["devdoc"] 107 ) 108 109 for source_file, ast in compilation_artifacts["sources"].items(): 110 filename = convert_filename(source_file, _relative_to_short, crytic_compile) 111 source_unit = compilation_unit.create_source_unit(filename) 112 source_unit.ast = ast 113 114 def add_source_files(self, file_paths: List[str]) -> None: 115 """ 116 Append files 117 118 Args: 119 file_paths (List[str]): files to append 120 121 Returns: 122 123 """ 124 125 for file_path in file_paths: 126 with open(file_path, "r", encoding="utf8") as f: 127 self.standard_json_input["sources"][file_path] = { # type: ignore 128 "content": f.read(), 129 } 130 131 def clean(self, **_kwargs: str) -> None: 132 """Clean compilation artifacts 133 134 Args: 135 **_kwargs: unused. 136 """ 137 return 138 139 def is_dependency(self, _path: str) -> bool: 140 """Check if the path is a dependency (not supported for vyper) 141 142 Args: 143 _path (str): path to the target 144 145 Returns: 146 bool: True if the target is a dependency 147 """ 148 return False 149 150 @staticmethod 151 def is_supported(target: str, **kwargs: str) -> bool: 152 """Check if the target is a vyper project 153 154 Args: 155 target (str): path to the target 156 **kwargs: optional arguments. Used "vyper_ignore" 157 158 Returns: 159 bool: True if the target is a vyper project 160 """ 161 vyper_ignore = kwargs.get("vyper_ignore", False) 162 if vyper_ignore: 163 return False 164 return os.path.isfile(target) and target.endswith(".vy") 165 166 def _guessed_tests(self) -> List[str]: 167 """Guess the potential unit tests commands 168 169 Returns: 170 List[str]: The guessed unit tests commands 171 """ 172 return [] 173 174 175def _run_vyper_standard_json( 176 standard_json_input: Dict, vyper: str, env: Optional[Dict] = None 177) -> Dict: 178 """Run vyper and write compilation output to a file 179 180 Args: 181 standard_json_input (Dict): Dict containing the vyper standard json input 182 vyper (str): vyper binary 183 env (Optional[Dict], optional): Environment variables. Defaults to None. 184 185 Raises: 186 InvalidCompilation: If vyper failed to run 187 188 Returns: 189 Dict: Vyper json compilation artifact 190 """ 191 cmd = [vyper, "--standard-json"] 192 193 with subprocess.Popen( 194 cmd, 195 stdin=subprocess.PIPE, 196 stdout=subprocess.PIPE, 197 stderr=subprocess.PIPE, 198 env=env, 199 executable=shutil.which(cmd[0]), 200 ) as process: 201 202 stdout_b, stderr_b = process.communicate(json.dumps(standard_json_input).encode("utf-8")) 203 stdout, _stderr = ( 204 stdout_b.decode(), 205 stderr_b.decode(errors="backslashreplace"), 206 ) # convert bytestrings to unicode strings 207 208 vyper_standard_output = json.loads(stdout) 209 210 if "errors" in vyper_standard_output: 211 212 has_errors = False 213 for diagnostic in vyper_standard_output["errors"]: 214 215 if diagnostic["severity"] == "warning": 216 continue 217 218 msg = diagnostic.get("formattedMessage", diagnostic["message"]) 219 LOGGER.error(msg) 220 has_errors = True 221 222 if has_errors: 223 raise InvalidCompilation("Vyper compilation errored") 224 225 return vyper_standard_output 226 227 228def _relative_to_short(relative: Path) -> Path: 229 """Translate relative path to short (do nothing for vyper) 230 231 Args: 232 relative (Path): path to the target 233 234 Returns: 235 Path: Translated path 236 """ 237 return relative
29class VyperStandardJson(AbstractPlatform): 30 """ 31 Vyper platform 32 """ 33 34 NAME = "vyper" 35 PROJECT_URL = "https://github.com/vyperlang/vyper" 36 TYPE = Type.VYPER 37 38 def __init__(self, target: Optional[Path] = None, **_kwargs: str): 39 super().__init__(str(target), **_kwargs) 40 self.standard_json_input = { 41 "language": "Vyper", 42 "sources": {}, 43 "settings": { 44 "outputSelection": { 45 "*": { 46 "*": [ 47 "abi", 48 "devdoc", 49 "userdoc", 50 "evm.bytecode", 51 "evm.deployedBytecode", 52 "evm.deployedBytecode.sourceMap", 53 ], 54 "": ["ast"], 55 } 56 } 57 }, 58 } 59 60 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 61 """Compile the target 62 63 Args: 64 crytic_compile (CryticCompile): CryticCompile object to populate 65 **kwargs: optional arguments. Used "vyper" 66 67 68 """ 69 target = self._target 70 # If the target was a directory `add_source_file` should have been called 71 # by `compile_all`. Otherwise, we should have a single file target. 72 if self._target is not None and os.path.isfile(self._target): 73 self.add_source_files([target]) 74 75 vyper_bin = kwargs.get("vyper", "vyper") 76 77 compilation_artifacts = _run_vyper_standard_json(self.standard_json_input, vyper_bin) 78 compilation_unit = CompilationUnit(crytic_compile, str(target)) 79 80 compiler_version = compilation_artifacts["compiler"].split("-")[1] 81 if compiler_version != "0.3.7": 82 LOGGER.info("Vyper != 0.3.7 support is a best effort and might fail") 83 compilation_unit.compiler_version = CompilerVersion( 84 compiler="vyper", version=compiler_version, optimized=False 85 ) 86 87 for source_file, contract_info in compilation_artifacts["contracts"].items(): 88 filename = convert_filename(source_file, _relative_to_short, crytic_compile) 89 source_unit = compilation_unit.create_source_unit(filename) 90 for contract_name, contract_metadata in contract_info.items(): 91 source_unit.add_contract_name(contract_name) 92 compilation_unit.filename_to_contracts[filename].add(contract_name) 93 94 source_unit.abis[contract_name] = contract_metadata["abi"] 95 source_unit.bytecodes_init[contract_name] = contract_metadata["evm"]["bytecode"][ 96 "object" 97 ].replace("0x", "") 98 # Vyper does not provide the source mapping for the init bytecode 99 source_unit.srcmaps_init[contract_name] = [] 100 source_unit.srcmaps_runtime[contract_name] = contract_metadata["evm"][ 101 "deployedBytecode" 102 ]["sourceMap"].split(";") 103 source_unit.bytecodes_runtime[contract_name] = contract_metadata["evm"][ 104 "deployedBytecode" 105 ]["object"].replace("0x", "") 106 source_unit.natspec[contract_name] = Natspec( 107 contract_metadata["userdoc"], contract_metadata["devdoc"] 108 ) 109 110 for source_file, ast in compilation_artifacts["sources"].items(): 111 filename = convert_filename(source_file, _relative_to_short, crytic_compile) 112 source_unit = compilation_unit.create_source_unit(filename) 113 source_unit.ast = ast 114 115 def add_source_files(self, file_paths: List[str]) -> None: 116 """ 117 Append files 118 119 Args: 120 file_paths (List[str]): files to append 121 122 Returns: 123 124 """ 125 126 for file_path in file_paths: 127 with open(file_path, "r", encoding="utf8") as f: 128 self.standard_json_input["sources"][file_path] = { # type: ignore 129 "content": f.read(), 130 } 131 132 def clean(self, **_kwargs: str) -> None: 133 """Clean compilation artifacts 134 135 Args: 136 **_kwargs: unused. 137 """ 138 return 139 140 def is_dependency(self, _path: str) -> bool: 141 """Check if the path is a dependency (not supported for vyper) 142 143 Args: 144 _path (str): path to the target 145 146 Returns: 147 bool: True if the target is a dependency 148 """ 149 return False 150 151 @staticmethod 152 def is_supported(target: str, **kwargs: str) -> bool: 153 """Check if the target is a vyper project 154 155 Args: 156 target (str): path to the target 157 **kwargs: optional arguments. Used "vyper_ignore" 158 159 Returns: 160 bool: True if the target is a vyper project 161 """ 162 vyper_ignore = kwargs.get("vyper_ignore", False) 163 if vyper_ignore: 164 return False 165 return os.path.isfile(target) and target.endswith(".vy") 166 167 def _guessed_tests(self) -> List[str]: 168 """Guess the potential unit tests commands 169 170 Returns: 171 List[str]: The guessed unit tests commands 172 """ 173 return []
Vyper platform
38 def __init__(self, target: Optional[Path] = None, **_kwargs: str): 39 super().__init__(str(target), **_kwargs) 40 self.standard_json_input = { 41 "language": "Vyper", 42 "sources": {}, 43 "settings": { 44 "outputSelection": { 45 "*": { 46 "*": [ 47 "abi", 48 "devdoc", 49 "userdoc", 50 "evm.bytecode", 51 "evm.deployedBytecode", 52 "evm.deployedBytecode.sourceMap", 53 ], 54 "": ["ast"], 55 } 56 } 57 }, 58 }
Init the object
Args: target (str): path to the target **_kwargs: optional arguments.
Raises: IncorrectPlatformInitialization: If the Platform was not correctly designed
60 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 61 """Compile the target 62 63 Args: 64 crytic_compile (CryticCompile): CryticCompile object to populate 65 **kwargs: optional arguments. Used "vyper" 66 67 68 """ 69 target = self._target 70 # If the target was a directory `add_source_file` should have been called 71 # by `compile_all`. Otherwise, we should have a single file target. 72 if self._target is not None and os.path.isfile(self._target): 73 self.add_source_files([target]) 74 75 vyper_bin = kwargs.get("vyper", "vyper") 76 77 compilation_artifacts = _run_vyper_standard_json(self.standard_json_input, vyper_bin) 78 compilation_unit = CompilationUnit(crytic_compile, str(target)) 79 80 compiler_version = compilation_artifacts["compiler"].split("-")[1] 81 if compiler_version != "0.3.7": 82 LOGGER.info("Vyper != 0.3.7 support is a best effort and might fail") 83 compilation_unit.compiler_version = CompilerVersion( 84 compiler="vyper", version=compiler_version, optimized=False 85 ) 86 87 for source_file, contract_info in compilation_artifacts["contracts"].items(): 88 filename = convert_filename(source_file, _relative_to_short, crytic_compile) 89 source_unit = compilation_unit.create_source_unit(filename) 90 for contract_name, contract_metadata in contract_info.items(): 91 source_unit.add_contract_name(contract_name) 92 compilation_unit.filename_to_contracts[filename].add(contract_name) 93 94 source_unit.abis[contract_name] = contract_metadata["abi"] 95 source_unit.bytecodes_init[contract_name] = contract_metadata["evm"]["bytecode"][ 96 "object" 97 ].replace("0x", "") 98 # Vyper does not provide the source mapping for the init bytecode 99 source_unit.srcmaps_init[contract_name] = [] 100 source_unit.srcmaps_runtime[contract_name] = contract_metadata["evm"][ 101 "deployedBytecode" 102 ]["sourceMap"].split(";") 103 source_unit.bytecodes_runtime[contract_name] = contract_metadata["evm"][ 104 "deployedBytecode" 105 ]["object"].replace("0x", "") 106 source_unit.natspec[contract_name] = Natspec( 107 contract_metadata["userdoc"], contract_metadata["devdoc"] 108 ) 109 110 for source_file, ast in compilation_artifacts["sources"].items(): 111 filename = convert_filename(source_file, _relative_to_short, crytic_compile) 112 source_unit = compilation_unit.create_source_unit(filename) 113 source_unit.ast = ast
Compile the target
Args: crytic_compile (CryticCompile): CryticCompile object to populate **kwargs: optional arguments. Used "vyper"
115 def add_source_files(self, file_paths: List[str]) -> None: 116 """ 117 Append files 118 119 Args: 120 file_paths (List[str]): files to append 121 122 Returns: 123 124 """ 125 126 for file_path in file_paths: 127 with open(file_path, "r", encoding="utf8") as f: 128 self.standard_json_input["sources"][file_path] = { # type: ignore 129 "content": f.read(), 130 }
Append files
Args: file_paths (List[str]): files to append
Returns:
132 def clean(self, **_kwargs: str) -> None: 133 """Clean compilation artifacts 134 135 Args: 136 **_kwargs: unused. 137 """ 138 return
Clean compilation artifacts
Args: **_kwargs: unused.
140 def is_dependency(self, _path: str) -> bool: 141 """Check if the path is a dependency (not supported for vyper) 142 143 Args: 144 _path (str): path to the target 145 146 Returns: 147 bool: True if the target is a dependency 148 """ 149 return False
Check if the path is a dependency (not supported for vyper)
Args: _path (str): path to the target
Returns: bool: True if the target is a dependency
151 @staticmethod 152 def is_supported(target: str, **kwargs: str) -> bool: 153 """Check if the target is a vyper project 154 155 Args: 156 target (str): path to the target 157 **kwargs: optional arguments. Used "vyper_ignore" 158 159 Returns: 160 bool: True if the target is a vyper project 161 """ 162 vyper_ignore = kwargs.get("vyper_ignore", False) 163 if vyper_ignore: 164 return False 165 return os.path.isfile(target) and target.endswith(".vy")
Check if the target is a vyper project
Args: target (str): path to the target **kwargs: optional arguments. Used "vyper_ignore"
Returns: bool: True if the target is a vyper project