import json import os import re import subprocess import sys from collections import namedtuple from distutils.version import StrictVersion from typing import List, Dict import pytest from deepdiff import DeepDiff from slither import Slither # these solc versions only support legacy ast format LEGACY_SOLC_VERS = [f"0.4.{v}" for v in range(12)] SLITHER_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TEST_ROOT = os.path.join(SLITHER_ROOT, "tests", "ast-parsing") # these are tests that are currently failing right now XFAIL = [ "emit_0.4.0_legacy", "emit_0.4.1_legacy", "emit_0.4.2_legacy", "emit_0.4.3_legacy", "emit_0.4.4_legacy", "emit_0.4.5_legacy", "emit_0.4.6_legacy", "emit_0.4.7_legacy", "emit_0.4.8_legacy", "emit_0.4.9_legacy", "emit_0.4.10_legacy", "emit_0.4.11_legacy", "emit_0.4.12_legacy", "emit_0.4.12_compact", "emit_0.4.13_legacy", "emit_0.4.13_compact", "emit_0.4.14_legacy", "emit_0.4.14_compact", "emit_0.4.15_legacy", "emit_0.4.15_compact", "emit_0.4.16_legacy", "emit_0.4.16_compact", "emit_0.4.17_legacy", "emit_0.4.17_compact", "emit_0.4.18_legacy", "emit_0.4.18_compact", "emit_0.4.19_legacy", "emit_0.4.19_compact", "emit_0.4.20_legacy", "emit_0.4.20_compact", "emit_0.4.21_legacy", "emit_0.4.21_compact", "emit_0.4.22_legacy", "emit_0.4.22_compact", "emit_0.4.23_legacy", "emit_0.4.23_compact", "emit_0.4.24_legacy", "emit_0.4.24_compact", "emit_0.4.25_legacy", "emit_0.4.25_compact", "emit_0.4.26_legacy", "emit_0.4.26_compact", "function_0.6.0_legacy", "function_0.6.1_legacy", "function_0.6.2_legacy", "function_0.6.3_legacy", "function_0.6.4_legacy", "function_0.6.5_legacy", "function_0.6.6_legacy", "function_0.6.7_legacy", "function_0.6.8_legacy", "function_0.6.9_legacy", "function_0.6.10_legacy", "function_0.6.11_legacy", "function_0.6.12_legacy", "function_0.7.0_legacy", "function_0.7.1_legacy", "function_0.7.1_compact", "function_0.7.2_legacy", "function_0.7.2_compact", "function_0.7.3_legacy", "function_0.7.3_compact", "function_0.7.4_legacy", "function_0.7.4_compact", "function_0.7.5_legacy", "function_0.7.5_compact", "import_0.4.0_legacy", "import_0.4.1_legacy", "import_0.4.2_legacy", "import_0.4.3_legacy", "import_0.4.4_legacy", "import_0.4.5_legacy", "import_0.4.6_legacy", "import_0.4.7_legacy", "import_0.4.8_legacy", "import_0.4.9_legacy", "import_0.4.10_legacy", "import_0.4.11_legacy", "import_0.4.12_legacy", "import_0.4.12_compact", "import_0.4.13_legacy", "import_0.4.13_compact", "import_0.4.14_legacy", "import_0.4.14_compact", "import_0.4.15_legacy", "import_0.4.15_compact", "import_0.4.16_legacy", "import_0.4.16_compact", "import_0.4.17_legacy", "import_0.4.17_compact", "import_0.4.18_legacy", "import_0.4.18_compact", "import_0.4.19_legacy", "import_0.4.19_compact", "import_0.4.20_legacy", "import_0.4.20_compact", "import_0.4.21_legacy", "import_0.4.21_compact", "import_0.4.22_legacy", "import_0.4.22_compact", "import_0.4.23_legacy", "import_0.4.23_compact", "import_0.4.24_legacy", "import_0.4.24_compact", "import_0.4.25_legacy", "import_0.4.25_compact", "import_0.4.26_legacy", "import_0.4.26_compact", "import_0.5.0_legacy", "import_0.5.0_compact", "import_0.5.1_legacy", "import_0.5.1_compact", "import_0.5.2_legacy", "import_0.5.2_compact", "import_0.5.3_legacy", "import_0.5.3_compact", "import_0.5.4_legacy", "import_0.5.4_compact", "import_0.5.5_legacy", "import_0.5.5_compact", "import_0.5.6_legacy", "import_0.5.6_compact", "import_0.5.7_legacy", "import_0.5.7_compact", "import_0.5.8_legacy", "import_0.5.8_compact", "import_0.5.9_legacy", "import_0.5.9_compact", "import_0.5.10_legacy", "import_0.5.10_compact", "import_0.5.11_legacy", "import_0.5.11_compact", "import_0.5.12_legacy", "import_0.5.12_compact", "import_0.5.13_legacy", "import_0.5.13_compact", "import_0.5.14_legacy", "import_0.5.14_compact", "import_0.5.15_legacy", "import_0.5.15_compact", "import_0.5.16_legacy", "import_0.5.16_compact", "import_0.5.17_legacy", "import_0.5.17_compact", "import_0.6.0_legacy", "import_0.6.0_compact", "import_0.6.1_legacy", "import_0.6.1_compact", "import_0.6.2_legacy", "import_0.6.2_compact", "import_0.6.3_legacy", "import_0.6.3_compact", "import_0.6.4_legacy", "import_0.6.4_compact", "import_0.6.5_legacy", "import_0.6.5_compact", "import_0.6.6_legacy", "import_0.6.6_compact", "import_0.6.7_legacy", "import_0.6.7_compact", "import_0.6.8_legacy", "import_0.6.8_compact", "import_0.6.9_legacy", "import_0.6.9_compact", "import_0.6.10_legacy", "import_0.6.10_compact", "import_0.6.11_legacy", "import_0.6.11_compact", "import_0.6.12_legacy", "import_0.6.12_compact", "import_0.7.0_legacy", "import_0.7.0_compact", "import_0.7.1_legacy", "import_0.7.1_compact", "import_0.7.2_legacy", "import_0.7.2_compact", "import_0.7.3_legacy", "import_0.7.3_compact", "import_0.7.4_legacy", "import_0.7.4_compact", "import_0.7.5_legacy", "import_0.7.5_compact", "indexrangeaccess_0.6.1_legacy", "indexrangeaccess_0.6.2_legacy", "indexrangeaccess_0.6.3_legacy", "indexrangeaccess_0.6.4_legacy", "indexrangeaccess_0.6.5_legacy", "indexrangeaccess_0.6.6_legacy", "indexrangeaccess_0.6.7_legacy", "indexrangeaccess_0.6.8_legacy", "indexrangeaccess_0.6.9_legacy", "indexrangeaccess_0.6.10_legacy", "indexrangeaccess_0.6.11_legacy", "indexrangeaccess_0.6.12_legacy", "indexrangeaccess_0.7.0_legacy", "indexrangeaccess_0.7.1_legacy", "indexrangeaccess_0.7.2_legacy", "indexrangeaccess_0.7.3_legacy", "indexrangeaccess_0.7.4_legacy", "indexrangeaccess_0.7.5_legacy", "literal_0.7.0_legacy", "literal_0.7.0_compact", "literal_0.7.1_legacy", "literal_0.7.1_compact", "literal_0.7.2_legacy", "literal_0.7.2_compact", "literal_0.7.3_legacy", "literal_0.7.3_compact", "literal_0.7.4_legacy", "literal_0.7.4_compact", "literal_0.7.5_legacy", "literal_0.7.5_compact", "memberaccess_0.6.8_legacy", "memberaccess_0.6.9_legacy", "memberaccess_0.6.10_legacy", "memberaccess_0.6.11_legacy", "memberaccess_0.6.12_legacy", "memberaccess_0.7.0_legacy", "memberaccess_0.7.1_legacy", "memberaccess_0.7.2_legacy", "struct_0.6.0_legacy", "struct_0.6.1_legacy", "struct_0.6.2_legacy", "struct_0.6.3_legacy", "struct_0.6.4_legacy", "struct_0.6.5_legacy", "struct_0.6.6_legacy", "struct_0.6.7_legacy", "struct_0.6.8_legacy", "struct_0.6.9_legacy", "struct_0.6.10_legacy", "struct_0.6.11_legacy", "struct_0.6.12_legacy", "struct_0.7.0_legacy", "struct_0.7.1_legacy", "struct_0.7.2_legacy", "struct_0.7.3_legacy", "struct_0.7.4_legacy", "struct_0.7.5_legacy", "trycatch_0.6.0_legacy", "trycatch_0.6.1_legacy", "trycatch_0.6.2_legacy", "trycatch_0.6.3_legacy", "trycatch_0.6.4_legacy", "trycatch_0.6.5_legacy", "trycatch_0.6.6_legacy", "trycatch_0.6.7_legacy", "trycatch_0.6.8_legacy", "trycatch_0.6.9_legacy", "trycatch_0.6.10_legacy", "trycatch_0.6.11_legacy", "trycatch_0.6.12_legacy", "trycatch_0.7.0_legacy", "trycatch_0.7.1_legacy", "trycatch_0.7.2_legacy", "trycatch_0.7.3_legacy", "trycatch_0.7.4_legacy", "trycatch_0.7.5_legacy", "variable_0.6.5_legacy", "variable_0.6.5_compact", "variable_0.6.6_legacy", "variable_0.6.6_compact", "variable_0.6.7_legacy", "variable_0.6.7_compact", "variable_0.6.8_legacy", "variable_0.6.8_compact", "variable_0.6.9_legacy", "variable_0.6.9_compact", "variable_0.6.10_legacy", "variable_0.6.10_compact", "variable_0.6.11_legacy", "variable_0.6.11_compact", "variable_0.6.12_legacy", "variable_0.6.12_compact", "variable_0.7.0_legacy", "variable_0.7.0_compact", "variable_0.7.1_legacy", "variable_0.7.1_compact", "variabledeclaration_0.4.0_legacy", "variabledeclaration_0.4.1_legacy", "variabledeclaration_0.4.2_legacy", "variabledeclaration_0.4.3_legacy", "variabledeclaration_0.4.4_legacy", "variabledeclaration_0.4.5_legacy", "variabledeclaration_0.4.6_legacy", "variabledeclaration_0.4.7_legacy", "variabledeclaration_0.4.8_legacy", "variabledeclaration_0.4.9_legacy", "variabledeclaration_0.4.10_legacy", "variabledeclaration_0.4.11_legacy", "variabledeclaration_0.4.12_legacy", "variabledeclaration_0.4.12_compact", "variabledeclaration_0.4.13_legacy", "variabledeclaration_0.4.13_compact", "variabledeclaration_0.4.14_legacy", "variabledeclaration_0.4.14_compact", "variabledeclaration_0.4.15_legacy", "variabledeclaration_0.4.15_compact", "variabledeclaration_0.4.16_legacy", "variabledeclaration_0.4.16_compact", "variabledeclaration_0.4.17_legacy", "variabledeclaration_0.4.17_compact", "variabledeclaration_0.4.18_legacy", "variabledeclaration_0.4.18_compact", "variabledeclaration_0.4.19_legacy", "variabledeclaration_0.4.19_compact", "variabledeclaration_0.4.20_legacy", "variabledeclaration_0.4.20_compact", "variabledeclaration_0.4.21_legacy", "variabledeclaration_0.4.21_compact", "variabledeclaration_0.4.22_legacy", "variabledeclaration_0.4.22_compact", "variabledeclaration_0.4.23_legacy", "variabledeclaration_0.4.23_compact", "variabledeclaration_0.4.24_legacy", "variabledeclaration_0.4.24_compact", "variabledeclaration_0.4.25_legacy", "variabledeclaration_0.4.25_compact", "variabledeclaration_0.4.26_legacy", "variabledeclaration_0.4.26_compact", "variabledeclaration_0.5.0_legacy", "variabledeclaration_0.5.1_legacy", "variabledeclaration_0.5.2_legacy", "variabledeclaration_0.5.3_legacy", "variabledeclaration_0.5.4_legacy", "variabledeclaration_0.5.5_legacy", "variabledeclaration_0.5.6_legacy", "variabledeclaration_0.5.7_legacy", "variabledeclaration_0.5.8_legacy", "variabledeclaration_0.5.9_legacy", "variabledeclaration_0.5.10_legacy", "variabledeclaration_0.5.11_legacy", "variabledeclaration_0.5.12_legacy", "variabledeclaration_0.5.13_legacy", "variabledeclaration_0.5.14_legacy", "variabledeclaration_0.5.15_legacy", "variabledeclaration_0.5.16_legacy", "variabledeclaration_0.5.17_legacy", "variabledeclaration_0.6.0_legacy", "variabledeclaration_0.6.1_legacy", "variabledeclaration_0.6.2_legacy", "variabledeclaration_0.6.3_legacy", "variabledeclaration_0.6.4_legacy", "variabledeclaration_0.6.5_legacy", "variabledeclaration_0.6.6_legacy", "variabledeclaration_0.6.7_legacy", "variabledeclaration_0.6.8_legacy", "variabledeclaration_0.6.9_legacy", "variabledeclaration_0.6.10_legacy", "variabledeclaration_0.6.11_legacy", "variabledeclaration_0.6.12_legacy", "variabledeclaration_0.7.0_legacy", "variabledeclaration_0.7.1_legacy", "variabledeclaration_0.7.2_legacy", "variabledeclaration_0.7.3_legacy", "variabledeclaration_0.7.4_legacy", "variabledeclaration_0.7.5_legacy", "yul_0.6.0_compact", "yul_0.6.1_compact", "yul_0.6.2_compact", "yul_0.6.3_compact", "yul_0.6.4_compact", "yul_0.6.5_compact", "yul_0.6.6_compact", "yul_0.6.7_compact", "yul_0.6.8_compact", "yul_0.6.9_compact", "yul_0.6.10_compact", "yul_0.6.11_compact", "yul_0.6.12_compact", "yul_0.7.0_compact", "yul_0.7.1_compact", "yul_0.7.2_compact", "yul_0.7.3_compact", "yul_0.7.4_compact", "yul_0.7.5_compact", ] def get_solc_versions() -> List[str]: """ get a list of all the supported versions of solidity, sorted from earliest to latest :return: ascending list of versions, for example ["0.4.0", "0.4.1", ...] """ result = subprocess.run(["solc", "--versions"], stdout=subprocess.PIPE, check=True) solc_versions = result.stdout.decode("utf-8").split("\n") # there's an extra newline so just remove all empty strings solc_versions = [version for version in solc_versions if version != ""] solc_versions.reverse() return solc_versions def get_tests(solc_versions) -> Dict[str, List[str]]: """ parse the list of testcases on disk :param solc_versions: the list of valid solidity versions :return: a dictionary of test id to list of base solidity versions supported """ slither_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) test_dir = os.path.join(slither_root, "tests", "ast-parsing") tests: Dict[str, List[str]] = {} for name in os.listdir(test_dir): if not name.endswith(".sol"): continue test_name, test_ver = name[:-4].rsplit("-", 1) if test_name not in tests: tests[test_name] = [] tests[test_name].append(test_ver) for key in tests: if len(tests[key]) > 1: tests[key] = sorted(tests[key], key=StrictVersion) # validate tests for test, vers in tests.items(): if len(vers) == 1: if vers[0] != "all": raise Exception("only one test found but not called all", test) else: for ver in vers: if ver not in solc_versions: raise Exception("base version not found", test, ver) return tests Item = namedtuple("TestItem", ["test_id", "base_ver", "solc_ver", "is_legacy",],) def get_all_test() -> List[Item]: """ generate a list of testcases by testing each test id with every solidity version for both legacy and compact ast :return: the testcases """ solc_versions = get_solc_versions() tests = get_tests(solc_versions) ret = [] for test, base_vers in tests.items(): print(f"generating testcases id={test} vers={base_vers}") base_ver_idx = 0 for solc_ver in solc_versions: # if it's time to move to the next base version, do it now if base_ver_idx + 1 < len(base_vers) and base_vers[base_ver_idx + 1] == solc_ver: base_ver_idx += 1 for legacy_json in [True, False]: if not legacy_json and solc_ver in LEGACY_SOLC_VERS: continue ret.append( Item( test_id=test, base_ver=base_vers[base_ver_idx], solc_ver=solc_ver, is_legacy=legacy_json, ) ) return ret def id_test(test_item: Item): flavor = "legacy" if test_item.is_legacy else "compact" return f"{test_item.test_id}_{test_item.solc_ver}_{flavor}" def generate_output(sl: Slither) -> Dict[str, Dict[str, str]]: output = {} for contract in sl.contracts: output[contract.name] = {} for func_or_modifier in contract.functions + contract.modifiers: output[contract.name][ func_or_modifier.full_name ] = func_or_modifier.slithir_cfg_to_dot_str(skip_expressions=True) return output ALL_TESTS = get_all_test() # create the output folder if needed try: os.mkdir("test_artifacts") except OSError: pass def set_solc(test_item: Item): # hacky hack hack to pick the solc version we want env = dict(os.environ) env["SOLC_VERSION"] = test_item.solc_ver os.environ.clear() os.environ.update(env) @pytest.mark.parametrize("test_item", ALL_TESTS, ids=id_test) def test_parsing(test_item: Item): flavor = "legacy" if test_item.is_legacy else "compact" test_file = os.path.join(TEST_ROOT, f"{test_item.test_id}-{test_item.base_ver}.sol") expected_file = os.path.join( TEST_ROOT, "expected", f"{test_item.test_id}-{test_item.solc_ver}-{flavor}.json" ) if id_test(test_item) in XFAIL: pytest.xfail("this test needs to be fixed") set_solc(test_item) sl = Slither( test_file, solc_force_legacy_json=test_item.is_legacy, disallow_partial=True, skip_analyze=True, ) actual = generate_output(sl) try: with open(expected_file, "r") as f: expected = json.load(f) except OSError: pytest.xfail("the file for this test was not generated") raise diff = DeepDiff(expected, actual, ignore_order=True, verbose_level=2, view="tree") if diff: for change in diff["values_changed"]: path_list = re.findall(r"\['(.*?)'\]", change.path()) path = "_".join(path_list) with open(f"test_artifacts/{id_test(test_item)}_{path}_expected.dot", "w") as f: f.write(change.t1) with open(f"test_artifacts/{id_test(test_item)}_{path}_actual.dot", "w") as f: f.write(change.t2) assert not diff, diff.pretty() def _generate_test(test_item: Item, skip_existing=False): flavor = "legacy" if test_item.is_legacy else "compact" test_file = os.path.join(TEST_ROOT, f"{test_item.test_id}-{test_item.base_ver}.sol") expected_file = os.path.join( TEST_ROOT, "expected", f"{test_item.test_id}-{test_item.solc_ver}-{flavor}.json" ) if skip_existing: if os.path.isfile(expected_file): return if id_test(test_item) in XFAIL: return set_solc(test_item) sl = Slither( test_file, solc_force_legacy_json=test_item.is_legacy, disallow_partial=True, skip_analyze=True, ) actual = generate_output(sl) print(f"Generate {expected_file}") with open(expected_file, "w") as f: json.dump(actual, f, indent=" ") if __name__ == "__main__": if len(sys.argv) != 2 or sys.argv[1] not in ["--generate", "--overwrite"]: print( "To generate the missing json artifacts run\n\tpython tests/test_ast_parsing.py --generate" ) print( "To re-generate all the json artifacts run\n\tpython tests/test_ast_parsing.py --overwrite" ) print("\tThis will overwrite the previous json files") elif sys.argv[1] == "--generate": for next_test in ALL_TESTS: _generate_test(next_test, skip_existing=True) elif sys.argv[1] == "--overwrite": for next_test in ALL_TESTS: _generate_test(next_test)