diff --git a/.github/workflows/IR.yml b/.github/workflows/IR.yml index 434cef75b..891de2bfb 100644 --- a/.github/workflows/IR.yml +++ b/.github/workflows/IR.yml @@ -34,9 +34,14 @@ jobs: - name: Install dependencies run: | pip install ".[dev]" - solc-select install all - solc-select use 0.8.11 + solc-select install 0.5.0 + solc-select use 0.8.11 --always-install + + - name: Install old solc + if: matrix.os == 'ubuntu-latest' + run: solc-select install 0.4.0 + - name: Test with pytest run: | - pytest tests/test_ssa_generation.py + pytest tests/test_ssa_generation.py \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6503d2b62..9913e487d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,8 +55,9 @@ jobs: - name: Install dependencies run: | pip install ".[dev]" - solc-select install all - solc-select use 0.5.1 + solc-select use 0.4.25 --always-install + solc-select use 0.8.0 --always-install + solc-select use 0.5.1 --always-install pip install typing_extensions==4.1.1 pip install importlib_metadata==4.8.3 diff --git a/.github/workflows/detectors.yml b/.github/workflows/detectors.yml index 8f3b45d15..15aa8a5dd 100644 --- a/.github/workflows/detectors.yml +++ b/.github/workflows/detectors.yml @@ -35,8 +35,7 @@ jobs: run: | pip install ".[dev]" - solc-select install all - solc-select use 0.7.3 + solc-select use 0.7.3 --always-install - name: Test with pytest run: | pytest tests/test_detectors.py diff --git a/.github/workflows/features.yml b/.github/workflows/features.yml index 49db14793..5007fd7bf 100644 --- a/.github/workflows/features.yml +++ b/.github/workflows/features.yml @@ -35,8 +35,7 @@ jobs: run: | pip install ".[dev]" - solc-select install all - solc-select use 0.8.0 + solc-select use 0.8.0 --always-install cd tests/test_node_modules/ npm install hardhat diff --git a/Dockerfile b/Dockerfile index 71bb9f57f..d0a7d67be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM ubuntu:jammy AS python-wheels RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ gcc \ + git \ python3-dev \ python3-pip \ && rm -rf /var/lib/apt/lists/* @@ -9,7 +10,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins COPY . /slither RUN cd /slither && \ - echo pip3 install --no-cache-dir --upgrade pip && \ + pip3 install --no-cache-dir --upgrade pip && \ pip3 wheel -w /wheels . solc-select pip setuptools wheel @@ -44,7 +45,7 @@ ENV PATH="/home/slither/.local/bin:${PATH}" # no-index ensures we install the freshly-built wheels RUN --mount=type=bind,target=/mnt,source=/wheels,from=python-wheels \ - pip3 install --user --no-cache-dir --upgrade --no-index --find-links /mnt pip slither-analyzer solc-select + pip3 install --user --no-cache-dir --upgrade --no-index --find-links /mnt --no-deps /mnt/*.whl RUN solc-select install 0.4.25 && solc-select use 0.4.25 diff --git a/slither/solc_parsing/slither_compilation_unit_solc.py b/slither/solc_parsing/slither_compilation_unit_solc.py index c8b69d4b2..296ac8238 100644 --- a/slither/solc_parsing/slither_compilation_unit_solc.py +++ b/slither/solc_parsing/slither_compilation_unit_solc.py @@ -48,16 +48,22 @@ def _handle_import_aliases( """ for symbol_alias in symbol_aliases: - if ( - "foreign" in symbol_alias - and "name" in symbol_alias["foreign"] - and "local" in symbol_alias - ): - original_name = symbol_alias["foreign"]["name"] - local_name = symbol_alias["local"] - import_directive.renaming[local_name] = original_name - # Assuming that two imports cannot collide in renaming - scope.renaming[local_name] = original_name + if "foreign" in symbol_alias and "local" in symbol_alias: + if isinstance(symbol_alias["foreign"], dict) and "name" in symbol_alias["foreign"]: + + original_name = symbol_alias["foreign"]["name"] + local_name = symbol_alias["local"] + import_directive.renaming[local_name] = original_name + # Assuming that two imports cannot collide in renaming + scope.renaming[local_name] = original_name + + # This path should only be hit for the malformed AST of solc 0.5.12 where + # the foreign identifier cannot be found but is required to resolve the alias. + # see https://github.com/crytic/slither/issues/1319 + elif symbol_alias["local"]: + raise SlitherException( + "Cannot resolve local alias for import directive due to malformed AST. Please upgrade to solc 0.6.0 or higher." + ) class SlitherCompilationUnitSolc(CallerContextExpression): diff --git a/slither/tools/documentation/README.md b/slither/tools/documentation/README.md index 2ed90692c..b4b3e6a76 100644 --- a/slither/tools/documentation/README.md +++ b/slither/tools/documentation/README.md @@ -1,6 +1,5 @@ -# Demo +# slither-documentation -This directory contains an example of Slither utility. - -See the [utility documentation](https://github.com/crytic/slither/wiki/Adding-a-new-utility) +`slither-documentation` uses [codex](https://beta.openai.com) to generate natspec documenation. +This tool is experimental. See [solmate documentation](https://github.com/montyly/solmate/pull/1) for an example of usage. diff --git a/slither/tools/documentation/__main__.py b/slither/tools/documentation/__main__.py index ea45ed8e1..8e545fb09 100644 --- a/slither/tools/documentation/__main__.py +++ b/slither/tools/documentation/__main__.py @@ -36,7 +36,12 @@ def parse_args() -> argparse.Namespace: default=False, ) - parser.add_argument("--retry", help="Retry failed query (default 1). Each retry increases the temperature by 0.1", action="store", default=1) + parser.add_argument( + "--retry", + help="Retry failed query (default 1). Each retry increases the temperature by 0.1", + action="store", + default=1, + ) # Add default arguments from crytic-compile cryticparser.init(parser) @@ -122,7 +127,75 @@ def _handle_codex( return None -# pylint: disable=too-many-locals +# pylint: disable=too-many-locals,too-many-arguments +def _handle_function( + function: Function, + overwrite: bool, + all_patches: Dict, + logging_file: Optional[str], + slither: Slither, + retry: int, + force: bool, +) -> bool: + if ( + function.source_mapping.is_dependency + or function.has_documentation + or function.is_constructor_variables + ): + return overwrite + prompt = "Create a natpsec documentation for this solidity code with only notice and dev.\n" + src_mapping = function.source_mapping + content = function.compilation_unit.core.source_code[src_mapping.filename.absolute] + start = src_mapping.start + end = src_mapping.start + src_mapping.length + prompt += content[start:end] + + use_tab = _use_tab(content[start - 1]) + if use_tab is None and src_mapping.starting_column > 1: + logger.info(f"Non standard space indentation found {content[start - 1:end]}") + if overwrite: + logger.info("Disable overwrite to avoid mistakes") + overwrite = False + + openai = codex.openai_module() # type: ignore + if openai is None: + raise ImportError + + if logging_file: + codex.log_codex(logging_file, "Q: " + prompt) + + tentative = 0 + answer_processed: Optional[str] = None + while tentative < retry: + tentative += 1 + + answer = openai.Completion.create( # type: ignore + prompt=prompt, + model=slither.codex_model, + temperature=min(slither.codex_temperature + tentative * 0.1, 1), + max_tokens=slither.codex_max_tokens, + ) + + if logging_file: + codex.log_codex(logging_file, "A: " + str(answer)) + + answer_processed = _handle_codex(answer, src_mapping.starting_column, use_tab, force) + if answer_processed: + break + + logger.info( + f"Codex could not generate a well formatted answer for {function.canonical_name}" + ) + logger.info(answer) + + if not answer_processed: + return overwrite + + create_patch(all_patches, src_mapping.filename.absolute, start, start, "", answer_processed) + + return overwrite + + def _handle_compilation_unit( slither: Slither, compilation_unit: SlitherCompilationUnit, @@ -130,12 +203,15 @@ def _handle_compilation_unit( force: bool, retry: int, ) -> None: - - logging_file = str(uuid.uuid4()) + logging_file: Optional[str] + if slither.codex_log: + logging_file = str(uuid.uuid4()) + else: + logging_file = None for scope in compilation_unit.scopes.values(): - # TODO remove hardcoded filtering + # Dont send tests file if ( ".t.sol" in scope.filename.absolute or "mock" in scope.filename.absolute.lower() @@ -153,63 +229,8 @@ def _handle_compilation_unit( all_patches: Dict = {} for function in functions_target: - - if function.source_mapping.is_dependency or function.has_documentation or function.is_constructor_variables: - continue - prompt = ( - "Create a natpsec documentation for this solidity code with only notice and dev.\n" - ) - src_mapping = function.source_mapping - content = compilation_unit.core.source_code[src_mapping.filename.absolute] - start = src_mapping.start - end = src_mapping.start + src_mapping.length - prompt += content[start:end] - - use_tab = _use_tab(content[start - 1]) - if use_tab is None and src_mapping.starting_column > 1: - logger.info(f"Non standard space indentation found {content[start-1:end]}") - if overwrite: - logger.info("Disable overwrite to avoid mistakes") - overwrite = False - - openai = codex.openai_module() # type: ignore - if openai is None: - return - - if slither.codex_log: - codex.log_codex(logging_file, "Q: " + prompt) - - tentative = 0 - answer_processed: Optional[str] = None - while tentative < retry: - tentative += 1 - - answer = openai.Completion.create( # type: ignore - prompt=prompt, - model=slither.codex_model, - temperature=min(slither.codex_temperature + tentative*0.1, 1), - max_tokens=slither.codex_max_tokens, - ) - - if slither.codex_log: - codex.log_codex(logging_file, "A: " + str(answer)) - - answer_processed = _handle_codex( - answer, src_mapping.starting_column, use_tab, force - ) - if answer_processed: - break - - logger.info( - f"Codex could not generate a well formatted answer for {function.canonical_name}" - ) - logger.info(answer) - - if not answer_processed: - continue - - create_patch( - all_patches, src_mapping.filename.absolute, start, start, "", answer_processed + overwrite = _handle_function( + function, overwrite, all_patches, logging_file, slither, retry, force ) # all_patches["patches"] should have only 1 file @@ -242,10 +263,17 @@ def main() -> None: logger.info("Be aware of OpenAI ToS: https://openai.com/api/policies/terms/") slither = Slither(args.project, **vars(args)) - for compilation_unit in slither.compilation_units: - _handle_compilation_unit( - slither, compilation_unit, args.overwrite, args.force_answer_parsing, int(args.retry) - ) + try: + for compilation_unit in slither.compilation_units: + _handle_compilation_unit( + slither, + compilation_unit, + args.overwrite, + args.force_answer_parsing, + int(args.retry), + ) + except ImportError: + pass if __name__ == "__main__": diff --git a/tests/ast-parsing/compile/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-compact.zip b/tests/ast-parsing/compile/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-compact.zip new file mode 100644 index 000000000..f1b133056 Binary files /dev/null and b/tests/ast-parsing/compile/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-compact.zip differ diff --git a/tests/ast-parsing/compile/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-legacy.zip b/tests/ast-parsing/compile/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-legacy.zip new file mode 100644 index 000000000..3f457a34d Binary files /dev/null and b/tests/ast-parsing/compile/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-legacy.zip differ diff --git a/tests/ast-parsing/complex_imports/import_aliases_issue_1319/import.sol b/tests/ast-parsing/complex_imports/import_aliases_issue_1319/import.sol new file mode 100644 index 000000000..7cfff4bfa --- /dev/null +++ b/tests/ast-parsing/complex_imports/import_aliases_issue_1319/import.sol @@ -0,0 +1 @@ +contract A {} \ No newline at end of file diff --git a/tests/ast-parsing/complex_imports/import_aliases_issue_1319/test.sol b/tests/ast-parsing/complex_imports/import_aliases_issue_1319/test.sol new file mode 100644 index 000000000..7c5bf1eee --- /dev/null +++ b/tests/ast-parsing/complex_imports/import_aliases_issue_1319/test.sol @@ -0,0 +1,9 @@ +pragma solidity 0.5.12; + +import {A} from "./import.sol"; + +contract Z is A { + function test() public pure returns (uint) { + return 1; + } +} \ No newline at end of file diff --git a/tests/ast-parsing/complex_imports/import_aliases_issue_1319/test_fail.sol b/tests/ast-parsing/complex_imports/import_aliases_issue_1319/test_fail.sol new file mode 100644 index 000000000..eb2ab8af0 --- /dev/null +++ b/tests/ast-parsing/complex_imports/import_aliases_issue_1319/test_fail.sol @@ -0,0 +1,9 @@ +pragma solidity 0.5.12; + +import {A as X, A as Y} from "./import.sol"; + +contract Z is X { + function test() public pure returns (uint) { + return 1; + } +} \ No newline at end of file diff --git a/tests/ast-parsing/expected/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-compact.json b/tests/ast-parsing/expected/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-compact.json new file mode 100644 index 000000000..4132f73d9 --- /dev/null +++ b/tests/ast-parsing/expected/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-compact.json @@ -0,0 +1,6 @@ +{ + "A": {}, + "Z": { + "test()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n" + } +} \ No newline at end of file diff --git a/tests/ast-parsing/expected/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-legacy.json b/tests/ast-parsing/expected/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-legacy.json new file mode 100644 index 000000000..4132f73d9 --- /dev/null +++ b/tests/ast-parsing/expected/complex_imports/import_aliases_issue_1319/test.sol-0.5.12-legacy.json @@ -0,0 +1,6 @@ +{ + "A": {}, + "Z": { + "test()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: RETURN 1\n\"];\n}\n" + } +} \ No newline at end of file diff --git a/tests/source_unit/README.md b/tests/source_unit/README.md new file mode 100644 index 000000000..9cf3657e0 --- /dev/null +++ b/tests/source_unit/README.md @@ -0,0 +1,3 @@ +# README + +Before using this project, run `forge init --no-git --no-commit --force` to initialize submodules diff --git a/tests/source_unit/lib/forge-std b/tests/source_unit/lib/forge-std deleted file mode 160000 index eb980e1d4..000000000 --- a/tests/source_unit/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit eb980e1d4f0e8173ec27da77297ae411840c8ccb diff --git a/tests/test_ast_parsing.py b/tests/test_ast_parsing.py index 7e22ea186..4e452a0fc 100644 --- a/tests/test_ast_parsing.py +++ b/tests/test_ast_parsing.py @@ -438,6 +438,7 @@ ALL_TESTS = [ Test("using-for-global-0.8.0.sol", ["0.8.15"]), Test("library_event-0.8.16.sol", ["0.8.16"]), Test("top-level-struct-0.8.0.sol", ["0.8.0"]), + Test("complex_imports/import_aliases_issue_1319/test.sol", ["0.5.12"]), ] # create the output folder if needed try: diff --git a/tests/test_source_unit.py b/tests/test_source_unit.py index 7b653599e..73c165016 100644 --- a/tests/test_source_unit.py +++ b/tests/test_source_unit.py @@ -1,6 +1,18 @@ +from pathlib import Path +import shutil + +import pytest from slither import Slither +# NB: read tests/source_unit/README.md for setup before using this test + +foundry_available = shutil.which("forge") is not None +project_ready = Path("./tests/source_unit/lib/forge-std").exists() + +@pytest.mark.skipif( + not foundry_available or not project_ready, reason="requires Foundry and project setup" +) def test_contract_info() -> None: slither = Slither("./tests/source_unit")