From c15ef45d2969c6a53216c27126294b940e761583 Mon Sep 17 00:00:00 2001 From: chriseth Date: Mon, 6 Dec 2021 15:52:50 +0100 Subject: [PATCH 01/15] Explanation about operators. --- docs/types/operators.rst | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/types/operators.rst b/docs/types/operators.rst index 29962588f..450963e11 100644 --- a/docs/types/operators.rst +++ b/docs/types/operators.rst @@ -1,7 +1,35 @@ -.. index:: assignment, ! delete, lvalue +.. index:: ! operator -Operators Involving LValues -=========================== +Operators +========= + +Arithmetic and bit operators can be applied even if the two operands do not have the same type. +For example, you can compute ``y = x + z``, where ``x`` is a ``uint8`` and ``z`` has +the type ``int32``. In these cases, the following mechanism will be used to determine +the type in which the operation is computed (this is important in case of overflow) +and the type of the operator's result: + +1. If the type of the right operand can be implicitly converted to the type of the left + operand, use the type of the left operand, +2. if the type of the left operand can be implicitly converted to the type of the right + operand, use the type of the right operand, +3. otherwise, the operation is not allowed. + +In case one of the operands is a :ref:`literal number ` it is first converted to its +"mobile type", which is the smallest type that can hold the value +(unsigned types of the same bit-width are considered "smaller" than the signed types). +If both are literal numbers, the operation is computed with arbitrary precision. + +The operator's result type is the same as the type the operation is performed in, +except for comparison operators where the result is always ``bool``. + +The operators ``**`` (exponentiation), ``<<`` and ``>>`` use the type of the +left operand for the operation and the result. + +.. index:: assignment, lvalue, ! compound operators + +Compound and Increment/Decrement Operators +------------------------------------------ If ``a`` is an LValue (i.e. a variable or something that can be assigned to), the following operators are available as shorthands: @@ -12,6 +40,8 @@ to ``a += 1`` / ``a -= 1`` but the expression itself still has the previous valu of ``a``. In contrast, ``--a`` and ``++a`` have the same effect on ``a`` but return the value after the change. +.. index:: !delete + .. _delete: delete From 9b55d4788e5e008905af060b26f44cac7e45d98e Mon Sep 17 00:00:00 2001 From: nishant-sachdeva Date: Mon, 6 Dec 2021 18:01:27 +0530 Subject: [PATCH 02/15] Added sameType check for fromType and toType in YulUtilFunctions.cpp and relevant tests in semanticTests --- Changelog.md | 2 +- libsolidity/codegen/YulUtilFunctions.cpp | 11 ++++- ...on_external_storage_to_storage_dynamic.sol | 43 ++++++++++++++++++ ...o_storage_dynamic_different_mutability.sol | 45 +++++++++++++++++++ ... copy_function_internal_storage_array.sol} | 2 +- .../copy_storage_arrays_of_function_type.sol | 9 ++++ 6 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic.sol create mode 100644 test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic_different_mutability.sol rename test/libsolidity/semanticTests/array/copying/{copy_function_storage_array.sol => copy_function_internal_storage_array.sol} (94%) create mode 100644 test/libsolidity/syntaxTests/array/copy_storage_arrays_of_function_type.sol diff --git a/Changelog.md b/Changelog.md index f9d755274..c4b67e935 100644 --- a/Changelog.md +++ b/Changelog.md @@ -11,7 +11,7 @@ Bugfixes: * SMTChecker: Fix internal error when an unsafe target is solved more than once and the counterexample messages are different. * SMTChecker: Fix soundness of assigned storage/memory local pointers that were not erasing enough knowledge. * Fix internal error when a function has a calldata struct argument with an internal type inside. - + * IR Generator: Fix IR syntax error when copying storage arrays of functions. ### 0.8.10 (2021-11-09) diff --git a/libsolidity/codegen/YulUtilFunctions.cpp b/libsolidity/codegen/YulUtilFunctions.cpp index 1ad9aafa7..106ad260f 100644 --- a/libsolidity/codegen/YulUtilFunctions.cpp +++ b/libsolidity/codegen/YulUtilFunctions.cpp @@ -2057,7 +2057,7 @@ string YulUtilFunctions::copyValueArrayStorageToStorageFunction(ArrayType const& solAssert(!_fromType.isValueType(), ""); templ("functionName", functionName); templ("resizeArray", resizeArrayFunction(_toType)); - templ("arrayLength",arrayLengthFunction(_fromType)); + templ("arrayLength", arrayLengthFunction(_fromType)); templ("panic", panicFunction(PanicCode::ResourceError)); templ("srcDataLocation", arrayDataAreaFunction(_fromType)); templ("dstDataLocation", arrayDataAreaFunction(_toType)); @@ -2065,7 +2065,14 @@ string YulUtilFunctions::copyValueArrayStorageToStorageFunction(ArrayType const& unsigned itemsPerSlot = 32 / _toType.storageStride(); templ("itemsPerSlot", to_string(itemsPerSlot)); templ("multipleItemsPerSlotDst", itemsPerSlot > 1); - bool sameType = _fromType.baseType() == _toType.baseType(); + bool sameType = *_fromType.baseType() == *_toType.baseType(); + if (auto functionType = dynamic_cast(_fromType.baseType())) + { + solAssert(functionType->equalExcludingStateMutability( + dynamic_cast(*_toType.baseType()) + )); + sameType = true; + } templ("sameType", sameType); if (sameType) { diff --git a/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic.sol b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic.sol new file mode 100644 index 000000000..543342dfd --- /dev/null +++ b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic.sol @@ -0,0 +1,43 @@ +contract C { + function testFunction1() public {} + function testFunction2() public {} + function testFunction3() public {} + + + function() external [3] externalArray0; + function() external [3] externalArray1 = [ + this.testFunction1, + this.testFunction2, + this.testFunction3 + ]; + + function copyExternalStorageArrayOfFunctionType() external returns (bool) { + assert(keccak256(abi.encode(externalArray0)) != keccak256(abi.encode(externalArray1))); + externalArray0 = externalArray1; + return keccak256(abi.encode(externalArray0)) == keccak256(abi.encode(externalArray1)); + } + + function() internal [3] internalArray0; + function() internal [3] internalArray1 = [ + testFunction1, + testFunction2, + testFunction3 + ]; + + function copyInternalArrayOfFunctionType() external returns (bool) { + internalArray0 = internalArray1; + assert(internalArray0.length == 3); + + return + internalArray0.length == internalArray1.length && + internalArray0[0] == internalArray1[0] && + internalArray0[1] == internalArray1[1] && + internalArray0[2] == internalArray1[2]; + } +} +// ==== +// compileViaYul: also +// ---- +// copyExternalStorageArrayOfFunctionType() -> true +// gas legacy: 103412 +// copyInternalArrayOfFunctionType() -> true diff --git a/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic_different_mutability.sol b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic_different_mutability.sol new file mode 100644 index 000000000..0e356bce6 --- /dev/null +++ b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic_different_mutability.sol @@ -0,0 +1,45 @@ +contract C { + function testFunction1() public {} + function testFunction2() public view {} + function testFunction3() public pure {} + + function() external [3] externalArray0; + function() external [3] externalArray1 = [ + this.testFunction1, + this.testFunction2, + this.testFunction3 + ]; + + function copyExternalStorageArraysOfFunctionType() external returns (bool) + { + assert(keccak256(abi.encodePacked(externalArray0)) != keccak256(abi.encodePacked(externalArray1))); + externalArray0 = externalArray1; + return keccak256(abi.encodePacked(externalArray0)) == keccak256(abi.encodePacked(externalArray1)); + } + + function() internal [3] internalArray0; + function() internal [3] internalArray1 = [ + testFunction1, + testFunction2, + testFunction3 + ]; + + function copyInternalArrayOfFunctionType() external returns (bool) + { + internalArray0 = internalArray1; + assert(internalArray0.length == 3); + + return + internalArray0.length == internalArray1.length && + internalArray0[0] == internalArray1[0] && + internalArray0[1] == internalArray1[1] && + internalArray0[2] == internalArray1[2]; + } +} +// ==== +// compileViaYul: also +// ---- +// copyExternalStorageArraysOfFunctionType() -> true +// gas legacy: 103398 +// copyInternalArrayOfFunctionType() -> true +// gas legacy: 104178 diff --git a/test/libsolidity/semanticTests/array/copying/copy_function_storage_array.sol b/test/libsolidity/semanticTests/array/copying/copy_function_internal_storage_array.sol similarity index 94% rename from test/libsolidity/semanticTests/array/copying/copy_function_storage_array.sol rename to test/libsolidity/semanticTests/array/copying/copy_function_internal_storage_array.sol index 8448d2e69..6f019c77c 100644 --- a/test/libsolidity/semanticTests/array/copying/copy_function_storage_array.sol +++ b/test/libsolidity/semanticTests/array/copying/copy_function_internal_storage_array.sol @@ -18,6 +18,6 @@ contract C { // compileViaYul: also // ---- // test() -> 7 -// gas irOptimized: 126552 +// gas irOptimized: 124080 // gas legacy: 205196 // gas legacyOptimized: 204987 diff --git a/test/libsolidity/syntaxTests/array/copy_storage_arrays_of_function_type.sol b/test/libsolidity/syntaxTests/array/copy_storage_arrays_of_function_type.sol new file mode 100644 index 000000000..44011bea4 --- /dev/null +++ b/test/libsolidity/syntaxTests/array/copy_storage_arrays_of_function_type.sol @@ -0,0 +1,9 @@ +contract C { + function() external [] s0; + function() external view [] s1; + function copyStorageArrayOfFunctionType() public { + s1 = s0; + } +} +// ---- +// TypeError 7407: (148-150): Type function () external[] storage ref is not implicitly convertible to expected type function () view external[] storage ref. From fdeb9717084e7427100b7ec5778c8a1ab222f5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 15 Dec 2021 20:45:39 +0100 Subject: [PATCH 03/15] Add an appropriate label to each type of issue --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + .github/ISSUE_TEMPLATE/documentation_issue.md | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 571b97474..0edc9f79f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,7 @@ --- name: Bug Report about: Bug reports about the Solidity Compiler. +labels: ["bug :bug:"] --- ## Description diff --git a/.github/ISSUE_TEMPLATE/documentation_issue.md b/.github/ISSUE_TEMPLATE/documentation_issue.md index fc3dd77db..c5873f726 100644 --- a/.github/ISSUE_TEMPLATE/documentation_issue.md +++ b/.github/ISSUE_TEMPLATE/documentation_issue.md @@ -6,18 +6,12 @@ labels: ["documentation :book:"] ## Page - + ## Abstract - + ## Pull request - + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 0b86a08f4..7191414d4 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -9,40 +9,29 @@ labels: ["feature"] - First, many thanks for taking part in the community. We really appreciate that. - We realize there is a lot of data requested here. We ask only that you do your best to provide as much information as possible so we can better help you. - Support questions are better asked in one of the following locations: - - [Solidity chat](https://gitter.im/ethereum/solidity) - - [Stack Overflow](https://ethereum.stackexchange.com/) + - [Solidity chat](https://gitter.im/ethereum/solidity) + - [Stack Overflow](https://ethereum.stackexchange.com/) - Ensure the issue isn't already reported (check `feature` and `language design` labels). *Delete the above section and the instructions in the sections below before submitting* - --> ## Abstract - + ## Motivation - + ## Specification - + ## Backwards Compatibility \ No newline at end of file +--> From 6035e8c02a96653fa659b667a3dd2ba7705b4967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 15 Dec 2021 21:07:08 +0100 Subject: [PATCH 05/15] Configure issue type selection screen to always select the "Solidity" project --- .github/ISSUE_TEMPLATE/config.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..931bc8940 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Bug Report + url: https://github.com/ethereum/solidity/issues/new?template=bug_report.md&projects=ethereum/solidity/43 + - name: Documentation Issue + url: https://github.com/ethereum/solidity/issues/new?template=documentation_issue.md&projects=ethereum/solidity/43 + - name: Feature Request + url: https://github.com/ethereum/solidity/issues/new?template=feature_request.md&projects=ethereum/solidity/43 From b2a05a1bcfafa4fbe1344eba19e6e63ceecb6d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 15 Dec 2021 21:07:28 +0100 Subject: [PATCH 06/15] Remove unused "general" issue template --- .github/ISSUE_TEMPLATE/general.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/general.md diff --git a/.github/ISSUE_TEMPLATE/general.md b/.github/ISSUE_TEMPLATE/general.md deleted file mode 100644 index e69de29bb..000000000 From 9a0821f2c30f9c51bb5d7e6ac4cc85cf42bd6c9b Mon Sep 17 00:00:00 2001 From: chriseth Date: Thu, 16 Dec 2021 00:11:59 +0100 Subject: [PATCH 07/15] Fix warning about reference. --- libyul/backends/evm/StackLayoutGenerator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libyul/backends/evm/StackLayoutGenerator.cpp b/libyul/backends/evm/StackLayoutGenerator.cpp index 43a1d032c..50dcee5de 100644 --- a/libyul/backends/evm/StackLayoutGenerator.cpp +++ b/libyul/backends/evm/StackLayoutGenerator.cpp @@ -247,7 +247,7 @@ Stack createIdealLayout(Stack const& _operationOutput, Stack const& _post, Calla // before the operation, i.e. if PreviousSlot{2} is at a position at which _post contains VariableSlot{"tmp"}, // then we want the variable tmp in the slot at offset 2 in the layout before the operation. vector> idealLayout(_post.size(), nullopt); - for (auto const& [slot, idealPosition]: ranges::zip_view(_post, layout)) + for (auto&& [slot, idealPosition]: ranges::zip_view(_post, layout)) if (PreviousSlot* previousSlot = std::get_if(&idealPosition)) idealLayout.at(previousSlot->slot) = slot; From 19a74506e39af9448590ce4fefee65fdd7f9c8eb Mon Sep 17 00:00:00 2001 From: nishant-sachdeva Date: Wed, 17 Nov 2021 00:17:37 +0530 Subject: [PATCH 08/15] trial test cases extracted from SoliidityEndToEndTest.cpp into .sol files. removed libevmone.so files from the directory trial test cases extracted from SoliidityEndToEndTest.cpp into .sol files. Corresponding code in the .cpp file has been commented instead of begin removed pending preliminary reviews removed libevmone files Added testcase packed_storage_structs_delete added test case invalid_enum_logged added test case enum_referencing added test case memory_types_initialisation added test case return string added test case constant_string_literal.sol removed extractable keyword from solidityEndtoEnd.cpp, moved copying_bytes_multiassigned.sol to array/copying folder, added recv() function to copying_bytes_multiassigned.sol but this test case is failing now change typo error in the name of test case library_staticcall_delegatecal.sol to library_staticcall_delegatecall.sol Added compileToEwasm:false to call_forward_bytes.sol test case and moved it to semanticTests/fallback added compileToEwasm:false line to library_call_in_homestead added compileToEwasm: false line to copying_bytes_multiassign, copy_from_calldata_removes_bytes, enum_referencing, library_call_in_homestead, struct_referencing Added test case internal_types_in_library Added test case mapping_arguments_in_library Added test case mapping_returns_in_library Added test case mapping_returns_in_library_named Added test case using_library_mappings_public Added test case library_function_external Added test case library_stray_values added test case using_library_mappings_return added test case using_library_structs Added test case using_for_function_on_struct and corrections to using_library_structs, using_library_mpapings_return, library_stray_values Added test case using_for_overload added test case using_for_by_name added test case bound_function_in_function added test case bound_function_in_var added test case bound_function_to_string added test case payable_function_calls_library added function call corrections to copying_bytes_multiassign and call_forward_bytes Made changes to the test cases as per comments on PR #12289 mentioned in Changelog.md : Extraced some test cases from SolEndToEnd.cpp --- test/libsolidity/SolidityEndToEndTest.cpp | 519 ------------------ .../copying/copying_bytes_multiassign.sol | 33 ++ .../copy_from_calldata_removes_bytes_data.sol | 19 + .../semanticTests/enums/enum_referencing.sol | 41 ++ .../fallback/call_forward_bytes.sol | 27 + .../bound_function_in_function.sol | 16 + .../functionCall/bound_function_in_var.sol | 16 + .../functionCall/bound_function_to_string.sol | 20 + .../libraries/internal_types_in_library.sol | 30 + .../libraries/library_call_in_homestead.sol | 15 + .../libraries/library_stray_values.sol | 14 + .../mapping_arguments_in_library.sol | 41 ++ .../libraries/mapping_returns_in_library.sol | 75 +++ .../mapping_returns_in_library_named.sol | 31 ++ .../payable_function_calls_library.sol | 14 + .../libraries/using_for_by_name.sol | 16 + .../libraries/using_for_overload.sol | 20 + .../using_library_mappings_public.sol | 27 + .../using_library_mappings_return.sol | 25 + .../libraries/using_library_structs.sol | 27 + .../structs/struct_referencing.sol | 61 ++ .../structs/using_for_function_on_struct.sol | 16 + 22 files changed, 584 insertions(+), 519 deletions(-) create mode 100644 test/libsolidity/semanticTests/array/copying/copying_bytes_multiassign.sol create mode 100644 test/libsolidity/semanticTests/calldata/copy_from_calldata_removes_bytes_data.sol create mode 100644 test/libsolidity/semanticTests/enums/enum_referencing.sol create mode 100644 test/libsolidity/semanticTests/fallback/call_forward_bytes.sol create mode 100644 test/libsolidity/semanticTests/functionCall/bound_function_in_function.sol create mode 100644 test/libsolidity/semanticTests/functionCall/bound_function_in_var.sol create mode 100644 test/libsolidity/semanticTests/functionCall/bound_function_to_string.sol create mode 100644 test/libsolidity/semanticTests/libraries/internal_types_in_library.sol create mode 100644 test/libsolidity/semanticTests/libraries/library_call_in_homestead.sol create mode 100644 test/libsolidity/semanticTests/libraries/library_stray_values.sol create mode 100644 test/libsolidity/semanticTests/libraries/mapping_arguments_in_library.sol create mode 100644 test/libsolidity/semanticTests/libraries/mapping_returns_in_library.sol create mode 100644 test/libsolidity/semanticTests/libraries/mapping_returns_in_library_named.sol create mode 100644 test/libsolidity/semanticTests/libraries/payable_function_calls_library.sol create mode 100644 test/libsolidity/semanticTests/libraries/using_for_by_name.sol create mode 100644 test/libsolidity/semanticTests/libraries/using_for_overload.sol create mode 100644 test/libsolidity/semanticTests/libraries/using_library_mappings_public.sol create mode 100644 test/libsolidity/semanticTests/libraries/using_library_mappings_return.sol create mode 100644 test/libsolidity/semanticTests/libraries/using_library_structs.sol create mode 100644 test/libsolidity/semanticTests/structs/struct_referencing.sol create mode 100644 test/libsolidity/semanticTests/structs/using_for_function_on_struct.sol diff --git a/test/libsolidity/SolidityEndToEndTest.cpp b/test/libsolidity/SolidityEndToEndTest.cpp index 473114961..314d4283e 100644 --- a/test/libsolidity/SolidityEndToEndTest.cpp +++ b/test/libsolidity/SolidityEndToEndTest.cpp @@ -1551,26 +1551,6 @@ BOOST_AUTO_TEST_CASE(generic_staticcall) } } -BOOST_AUTO_TEST_CASE(library_call_in_homestead) -{ - char const* sourceCode = R"( - library Lib { function m() public returns (address) { return msg.sender; } } - contract Test { - address public sender; - function f() public { - sender = Lib.m(); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "Lib"); - compileAndRun(sourceCode, 0, "Test", bytes(), map{{":Lib", m_contractAddress}}); - ABI_CHECK(callContractFunction("f()"), encodeArgs()); - ABI_CHECK(callContractFunction("sender()"), encodeArgs(m_sender)); - ) -} - BOOST_AUTO_TEST_CASE(library_call_protection) { // This tests code that reverts a call if it is a direct call to a library @@ -1626,38 +1606,6 @@ BOOST_AUTO_TEST_CASE(bytes_from_calldata_to_memory) ); } -BOOST_AUTO_TEST_CASE(call_forward_bytes) -{ - char const* sourceCode = R"( - contract receiver { - uint public received; - function recv(uint x) public { received += x + 1; } - fallback() external { received = 0x80; } - } - contract sender { - constructor() { rec = new receiver(); } - fallback() external { savedData = msg.data; } - function forward() public returns (bool) { address(rec).call(savedData); return true; } - function clear() public returns (bool) { delete savedData; return true; } - function val() public returns (uint) { return rec.received(); } - receiver rec; - bytes savedData; - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN(); - compileAndRun(sourceCode, 0, "sender"); - ABI_CHECK(callContractFunction("recv(uint256)", 7), bytes()); - ABI_CHECK(callContractFunction("val()"), encodeArgs(0)); - ABI_CHECK(callContractFunction("forward()"), encodeArgs(true)); - ABI_CHECK(callContractFunction("val()"), encodeArgs(8)); - ABI_CHECK(callContractFunction("clear()"), encodeArgs(true)); - ABI_CHECK(callContractFunction("val()"), encodeArgs(8)); - ABI_CHECK(callContractFunction("forward()"), encodeArgs(true)); - ABI_CHECK(callContractFunction("val()"), encodeArgs(0x80)); - ); -} - BOOST_AUTO_TEST_CASE(call_forward_bytes_length) { char const* sourceCode = R"( @@ -2576,254 +2524,6 @@ BOOST_AUTO_TEST_CASE(library_function_external) ) } -BOOST_AUTO_TEST_CASE(library_stray_values) -{ - char const* sourceCode = R"( - library Lib { function m(uint x, uint y) public returns (uint) { return x * y; } } - contract Test { - function f(uint x) public returns (uint) { - Lib; - Lib.m; - return x + 9; - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "Lib"); - compileAndRun(sourceCode, 0, "Test", bytes(), map{{":Lib", m_contractAddress}}); - ABI_CHECK(callContractFunction("f(uint256)", u256(33)), encodeArgs(u256(42))); - ) -} - -BOOST_AUTO_TEST_CASE(internal_types_in_library) -{ - char const* sourceCode = R"( - library Lib { - function find(uint16[] storage _haystack, uint16 _needle) public view returns (uint) - { - for (uint i = 0; i < _haystack.length; ++i) - if (_haystack[i] == _needle) - return i; - return type(uint).max; - } - } - contract Test { - mapping(string => uint16[]) data; - function f() public returns (uint a, uint b) - { - while (data["abc"].length < 20) - data["abc"].push(); - data["abc"][4] = 9; - data["abc"][17] = 3; - a = Lib.find(data["abc"], 9); - b = Lib.find(data["abc"], 3); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "Lib"); - compileAndRun(sourceCode, 0, "Test", bytes(), map{{":Lib", m_contractAddress}}); - ABI_CHECK(callContractFunction("f()"), encodeArgs(u256(4), u256(17))); - ) -} - -BOOST_AUTO_TEST_CASE(mapping_arguments_in_library) -{ - char const* sourceCode = R"( - library Lib { - function set(mapping(uint => uint) storage m, uint key, uint value) internal - { - m[key] = value; - } - function get(mapping(uint => uint) storage m, uint key) internal view returns (uint) - { - return m[key]; - } - } - contract Test { - mapping(uint => uint) m; - function set(uint256 key, uint256 value) public returns (uint) - { - uint oldValue = Lib.get(m, key); - Lib.set(m, key, value); - return oldValue; - } - function get(uint256 key) public view returns (uint) { - return Lib.get(m, key); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "Lib"); - compileAndRun(sourceCode, 0, "Test", bytes(), map{{":Lib", m_contractAddress}}); - ABI_CHECK(callContractFunction("set(uint256,uint256)", u256(1), u256(42)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("set(uint256,uint256)", u256(2), u256(84)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("set(uint256,uint256)", u256(21), u256(7)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get(uint256)", u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get(uint256)", u256(1)), encodeArgs(u256(42))); - ABI_CHECK(callContractFunction("get(uint256)", u256(2)), encodeArgs(u256(84))); - ABI_CHECK(callContractFunction("get(uint256)", u256(21)), encodeArgs(u256(7))); - ABI_CHECK(callContractFunction("set(uint256,uint256)", u256(1), u256(21)), encodeArgs(u256(42))); - ABI_CHECK(callContractFunction("set(uint256,uint256)", u256(2), u256(42)), encodeArgs(u256(84))); - ABI_CHECK(callContractFunction("set(uint256,uint256)", u256(21), u256(14)), encodeArgs(u256(7))); - ABI_CHECK(callContractFunction("get(uint256)", u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get(uint256)", u256(1)), encodeArgs(u256(21))); - ABI_CHECK(callContractFunction("get(uint256)", u256(2)), encodeArgs(u256(42))); - ABI_CHECK(callContractFunction("get(uint256)", u256(21)), encodeArgs(u256(14))); - ) -} - -BOOST_AUTO_TEST_CASE(mapping_returns_in_library) -{ - char const* sourceCode = R"( - library Lib { - function choose_mapping(mapping(uint => uint) storage a, mapping(uint => uint) storage b, bool c) internal pure returns(mapping(uint=>uint) storage) - { - return c ? a : b; - } - } - contract Test { - mapping(uint => uint) a; - mapping(uint => uint) b; - function set(bool choice, uint256 key, uint256 value) public returns (uint) - { - mapping(uint => uint) storage m = Lib.choose_mapping(a, b, choice); - uint oldValue = m[key]; - m[key] = value; - return oldValue; - } - function get(bool choice, uint256 key) public view returns (uint) { - return Lib.choose_mapping(a, b, choice)[key]; - } - function get_a(uint256 key) public view returns (uint) { - return a[key]; - } - function get_b(uint256 key) public view returns (uint) { - return b[key]; - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "Lib"); - compileAndRun(sourceCode, 0, "Test", bytes(), map{{":Lib", m_contractAddress}}); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", true, u256(1), u256(42)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", true, u256(2), u256(84)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", true, u256(21), u256(7)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", false, u256(1), u256(10)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", false, u256(2), u256(11)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", false, u256(21), u256(12)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get(bool,uint256)", true, u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get(bool,uint256)", true, u256(1)), encodeArgs(u256(42))); - ABI_CHECK(callContractFunction("get(bool,uint256)", true, u256(2)), encodeArgs(u256(84))); - ABI_CHECK(callContractFunction("get(bool,uint256)", true, u256(21)), encodeArgs(u256(7))); - ABI_CHECK(callContractFunction("get_a(uint256)", u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get_a(uint256)", u256(1)), encodeArgs(u256(42))); - ABI_CHECK(callContractFunction("get_a(uint256)", u256(2)), encodeArgs(u256(84))); - ABI_CHECK(callContractFunction("get_a(uint256)", u256(21)), encodeArgs(u256(7))); - ABI_CHECK(callContractFunction("get(bool,uint256)", false, u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get(bool,uint256)", false, u256(1)), encodeArgs(u256(10))); - ABI_CHECK(callContractFunction("get(bool,uint256)", false, u256(2)), encodeArgs(u256(11))); - ABI_CHECK(callContractFunction("get(bool,uint256)", false, u256(21)), encodeArgs(u256(12))); - ABI_CHECK(callContractFunction("get_b(uint256)", u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get_b(uint256)", u256(1)), encodeArgs(u256(10))); - ABI_CHECK(callContractFunction("get_b(uint256)", u256(2)), encodeArgs(u256(11))); - ABI_CHECK(callContractFunction("get_b(uint256)", u256(21)), encodeArgs(u256(12))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", true, u256(1), u256(21)), encodeArgs(u256(42))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", true, u256(2), u256(42)), encodeArgs(u256(84))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", true, u256(21), u256(14)), encodeArgs(u256(7))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", false, u256(1), u256(30)), encodeArgs(u256(10))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", false, u256(2), u256(31)), encodeArgs(u256(11))); - ABI_CHECK(callContractFunction("set(bool,uint256,uint256)", false, u256(21), u256(32)), encodeArgs(u256(12))); - ABI_CHECK(callContractFunction("get_a(uint256)", u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get_a(uint256)", u256(1)), encodeArgs(u256(21))); - ABI_CHECK(callContractFunction("get_a(uint256)", u256(2)), encodeArgs(u256(42))); - ABI_CHECK(callContractFunction("get_a(uint256)", u256(21)), encodeArgs(u256(14))); - ABI_CHECK(callContractFunction("get(bool,uint256)", true, u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get(bool,uint256)", true, u256(1)), encodeArgs(u256(21))); - ABI_CHECK(callContractFunction("get(bool,uint256)", true, u256(2)), encodeArgs(u256(42))); - ABI_CHECK(callContractFunction("get(bool,uint256)", true, u256(21)), encodeArgs(u256(14))); - ABI_CHECK(callContractFunction("get_b(uint256)", u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get_b(uint256)", u256(1)), encodeArgs(u256(30))); - ABI_CHECK(callContractFunction("get_b(uint256)", u256(2)), encodeArgs(u256(31))); - ABI_CHECK(callContractFunction("get_b(uint256)", u256(21)), encodeArgs(u256(32))); - ABI_CHECK(callContractFunction("get(bool,uint256)", false, u256(0)), encodeArgs(u256(0))); - ABI_CHECK(callContractFunction("get(bool,uint256)", false, u256(1)), encodeArgs(u256(30))); - ABI_CHECK(callContractFunction("get(bool,uint256)", false, u256(2)), encodeArgs(u256(31))); - ABI_CHECK(callContractFunction("get(bool,uint256)", false, u256(21)), encodeArgs(u256(32))); - ) -} - -BOOST_AUTO_TEST_CASE(mapping_returns_in_library_named) -{ - char const* sourceCode = R"( - library Lib { - function f(mapping(uint => uint) storage a, mapping(uint => uint) storage b) internal returns(mapping(uint=>uint) storage r) - { - r = a; - r[1] = 42; - r = b; - r[1] = 21; - } - } - contract Test { - mapping(uint => uint) a; - mapping(uint => uint) b; - function f() public returns (uint, uint, uint, uint, uint, uint) - { - Lib.f(a, b)[2] = 84; - return (a[0], a[1], a[2], b[0], b[1], b[2]); - } - function g() public returns (uint, uint, uint, uint, uint, uint) - { - mapping(uint => uint) storage m = Lib.f(a, b); - m[2] = 17; - return (a[0], a[1], a[2], b[0], b[1], b[2]); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "Lib"); - compileAndRun(sourceCode, 0, "Test", bytes(), map{{":Lib", m_contractAddress}}); - ABI_CHECK(callContractFunction("f()"), encodeArgs(u256(0), u256(42), u256(0), u256(0), u256(21), u256(84))); - ABI_CHECK(callContractFunction("g()"), encodeArgs(u256(0), u256(42), u256(0), u256(0), u256(21), u256(17))); - ) -} - -BOOST_AUTO_TEST_CASE(using_library_mappings_public) -{ - char const* sourceCode = R"( - library Lib { - function set(mapping(uint => uint) storage m, uint key, uint value) public - { - m[key] = value; - } - } - contract Test { - mapping(uint => uint) m1; - mapping(uint => uint) m2; - function f() public returns (uint, uint, uint, uint, uint, uint) - { - Lib.set(m1, 0, 1); - Lib.set(m1, 2, 42); - Lib.set(m2, 0, 23); - Lib.set(m2, 2, 99); - return (m1[0], m1[1], m1[2], m2[0], m2[1], m2[2]); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "Lib"); - compileAndRun(sourceCode, 0, "Test", bytes(), map{{":Lib", m_contractAddress}}); - ABI_CHECK(callContractFunction("f()"), encodeArgs(u256(1), u256(0), u256(42), u256(23), u256(0), u256(99))); - ) -} - BOOST_AUTO_TEST_CASE(using_library_mappings_external) { char const* libSourceCode = R"( @@ -2860,65 +2560,6 @@ BOOST_AUTO_TEST_CASE(using_library_mappings_external) } } -BOOST_AUTO_TEST_CASE(using_library_mappings_return) -{ - char const* sourceCode = R"( - library Lib { - function choose(mapping(uint => mapping(uint => uint)) storage m, uint key) external returns (mapping(uint => uint) storage) { - return m[key]; - } - } - contract Test { - mapping(uint => mapping(uint => uint)) m; - function f() public returns (uint, uint, uint, uint, uint, uint) - { - Lib.choose(m, 0)[0] = 1; - Lib.choose(m, 0)[2] = 42; - Lib.choose(m, 1)[0] = 23; - Lib.choose(m, 1)[2] = 99; - return (m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2]); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "Lib"); - compileAndRun(sourceCode, 0, "Test", bytes(), map{{":Lib", m_contractAddress}}); - ABI_CHECK(callContractFunction("f()"), encodeArgs(u256(1), u256(0), u256(42), u256(23), u256(0), u256(99))); - ) -} - -BOOST_AUTO_TEST_CASE(using_library_structs) -{ - char const* sourceCode = R"( - library Lib { - struct Data { uint a; uint[] b; } - function set(Data storage _s) public - { - _s.a = 7; - while (_s.b.length < 20) - _s.b.push(); - _s.b[19] = 8; - } - } - contract Test { - mapping(string => Lib.Data) data; - function f() public returns (uint a, uint b) - { - Lib.set(data["abc"]); - a = data["abc"].a; - b = data["abc"].b[19]; - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "Lib"); - compileAndRun(sourceCode, 0, "Test", bytes(), map{{":Lib", m_contractAddress}}); - ABI_CHECK(callContractFunction("f()"), encodeArgs(u256(7), u256(8))); - ) -} - BOOST_AUTO_TEST_CASE(short_strings) { // This test verifies that the byte array encoding that combines length and data works @@ -3114,146 +2755,6 @@ BOOST_AUTO_TEST_CASE(create_memory_array_allocation_size) } } -BOOST_AUTO_TEST_CASE(using_for_function_on_struct) -{ - char const* sourceCode = R"( - library D { struct s { uint a; } function mul(s storage self, uint x) public returns (uint) { return self.a *= x; } } - contract C { - using D for D.s; - D.s public x; - function f(uint a) public returns (uint) { - x.a = 3; - return x.mul(a); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "D"); - compileAndRun(sourceCode, 0, "C", bytes(), map{{":D", m_contractAddress}}); - ABI_CHECK(callContractFunction("f(uint256)", u256(7)), encodeArgs(u256(3 * 7))); - ABI_CHECK(callContractFunction("x()"), encodeArgs(u256(3 * 7))); - ) -} - -BOOST_AUTO_TEST_CASE(using_for_overload) -{ - char const* sourceCode = R"( - library D { - struct s { uint a; } - function mul(s storage self, uint x) public returns (uint) { return self.a *= x; } - function mul(s storage self, bytes32 x) public returns (bytes32) { } - } - contract C { - using D for D.s; - D.s public x; - function f(uint a) public returns (uint) { - x.a = 6; - return x.mul(a); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "D"); - compileAndRun(sourceCode, 0, "C", bytes(), map{{":D", m_contractAddress}}); - ABI_CHECK(callContractFunction("f(uint256)", u256(7)), encodeArgs(u256(6 * 7))); - ABI_CHECK(callContractFunction("x()"), encodeArgs(u256(6 * 7))); - ) -} - -BOOST_AUTO_TEST_CASE(using_for_by_name) -{ - char const* sourceCode = R"( - library D { struct s { uint a; } function mul(s storage self, uint x) public returns (uint) { return self.a *= x; } } - contract C { - using D for D.s; - D.s public x; - function f(uint a) public returns (uint) { - x.a = 6; - return x.mul({x: a}); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "D"); - compileAndRun(sourceCode, 0, "C", bytes(), map{{":D", m_contractAddress}}); - ABI_CHECK(callContractFunction("f(uint256)", u256(7)), encodeArgs(u256(6 * 7))); - ABI_CHECK(callContractFunction("x()"), encodeArgs(u256(6 * 7))); - ) -} - -BOOST_AUTO_TEST_CASE(bound_function_in_function) -{ - char const* sourceCode = R"( - library L { - function g(function() internal returns (uint) _t) internal returns (uint) { return _t(); } - } - contract C { - using L for *; - function f() public returns (uint) { - return t.g(); - } - function t() public pure returns (uint) { return 7; } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "L"); - compileAndRun(sourceCode, 0, "C", bytes(), map{{":L", m_contractAddress}}); - ABI_CHECK(callContractFunction("f()"), encodeArgs(u256(7))); - ) -} - -BOOST_AUTO_TEST_CASE(bound_function_in_var) -{ - char const* sourceCode = R"( - library D { struct s { uint a; } function mul(s storage self, uint x) public returns (uint) { return self.a *= x; } } - contract C { - using D for D.s; - D.s public x; - function f(uint a) public returns (uint) { - x.a = 6; - return (x.mul)({x: a}); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "D"); - compileAndRun(sourceCode, 0, "C", bytes(), map{{":D", m_contractAddress}}); - ABI_CHECK(callContractFunction("f(uint256)", u256(7)), encodeArgs(u256(6 * 7))); - ABI_CHECK(callContractFunction("x()"), encodeArgs(u256(6 * 7))); - ) -} - -BOOST_AUTO_TEST_CASE(bound_function_to_string) -{ - char const* sourceCode = R"( - library D { function length(string memory self) public returns (uint) { return bytes(self).length; } } - contract C { - using D for string; - string x; - function f() public returns (uint) { - x = "abc"; - return x.length(); - } - function g() public returns (uint) { - string memory s = "abc"; - return s.length(); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "D"); - compileAndRun(sourceCode, 0, "C", bytes(), map{{":D", m_contractAddress}}); - ABI_CHECK(callContractFunction("f()"), encodeArgs(u256(3))); - ABI_CHECK(callContractFunction("g()"), encodeArgs(u256(3))); - ) -} - BOOST_AUTO_TEST_CASE(inline_long_string_return) { char const* sourceCode = R"( @@ -3406,26 +2907,6 @@ BOOST_AUTO_TEST_CASE(payable_function) BOOST_CHECK_EQUAL(balanceAt(m_contractAddress), 27 + 27); } -BOOST_AUTO_TEST_CASE(payable_function_calls_library) -{ - char const* sourceCode = R"( - library L { - function f() public returns (uint) { return 7; } - } - contract C { - function f() public payable returns (uint) { - return L.f(); - } - } - )"; - ALSO_VIA_YUL( - DISABLE_EWASM_TESTRUN() - compileAndRun(sourceCode, 0, "L"); - compileAndRun(sourceCode, 0, "C", bytes(), map{{":L", m_contractAddress}}); - ABI_CHECK(callContractFunctionWithValue("f()", 27), encodeArgs(u256(7))); - ) -} - BOOST_AUTO_TEST_CASE(non_payable_throw) { char const* sourceCode = R"( diff --git a/test/libsolidity/semanticTests/array/copying/copying_bytes_multiassign.sol b/test/libsolidity/semanticTests/array/copying/copying_bytes_multiassign.sol new file mode 100644 index 000000000..3583a752d --- /dev/null +++ b/test/libsolidity/semanticTests/array/copying/copying_bytes_multiassign.sol @@ -0,0 +1,33 @@ +contract receiver { + uint public received; + function recv(uint x) public { received += x + 1; } + fallback() external { received = 0x80; } +} +contract sender { + constructor() { rec = new receiver(); } + fallback() external { savedData1 = savedData2 = msg.data; } + function forward(bool selector) public returns (bool) { + if (selector) { address(rec).call(savedData1); delete savedData1; } + else { address(rec).call(savedData2); delete savedData2; } + return true; + } + function val() public returns (uint) { return rec.received(); } + receiver rec; + bytes savedData1; + bytes savedData2; +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// (): 7 -> +// gas irOptimized: 110941 +// gas legacy: 111082 +// gas legacyOptimized: 111027 +// val() -> 0 +// forward(bool): true -> true +// val() -> 0x80 +// forward(bool): false -> true +// val() -> 0x80 +// forward(bool): true -> true +// val() -> 0x80 diff --git a/test/libsolidity/semanticTests/calldata/copy_from_calldata_removes_bytes_data.sol b/test/libsolidity/semanticTests/calldata/copy_from_calldata_removes_bytes_data.sol new file mode 100644 index 000000000..87067b77b --- /dev/null +++ b/test/libsolidity/semanticTests/calldata/copy_from_calldata_removes_bytes_data.sol @@ -0,0 +1,19 @@ +contract c { + function set() public returns (bool) { data = msg.data; return true; } + function checkIfDataIsEmpty() public returns (bool) { return data.length == 0; } + function sendMessage() public returns (bool, bytes memory) { bytes memory emptyData; return address(this).call(emptyData);} + fallback() external { data = msg.data; } + bytes data; +} +// ==== +// EVMVersion: >=byzantium +// compileToEwasm: false +// compileViaYul: also +// ---- +// (): 1, 2, 3, 4, 5 -> +// gas irOptimized: 155178 +// gas legacy: 155254 +// gas legacyOptimized: 155217 +// checkIfDataIsEmpty() -> false +// sendMessage() -> true, 0x40, 0 +// checkIfDataIsEmpty() -> true diff --git a/test/libsolidity/semanticTests/enums/enum_referencing.sol b/test/libsolidity/semanticTests/enums/enum_referencing.sol new file mode 100644 index 000000000..537c05182 --- /dev/null +++ b/test/libsolidity/semanticTests/enums/enum_referencing.sol @@ -0,0 +1,41 @@ +interface I { + enum Direction { A, B, Left, Right } +} +library L { + enum Direction { Left, Right } + function f() public pure returns (Direction) { + return Direction.Right; + } + function g() public pure returns (I.Direction) { + return I.Direction.Right; + } +} +contract C is I { + function f() public pure returns (Direction) { + return Direction.Right; + } + function g() public pure returns (I.Direction) { + return I.Direction.Right; + } + function h() public pure returns (L.Direction) { + return L.Direction.Right; + } + function x() public pure returns (L.Direction) { + return L.f(); + } + function y() public pure returns (I.Direction) { + return L.g(); + } +} +// ==== +// compileViaYul: also +// compileToEwasm: false +// ---- +// library: L +// f() -> 3 +// g() -> 3 +// f() -> 3 +// g() -> 3 +// h() -> 1 +// x() -> 1 +// y() -> 3 diff --git a/test/libsolidity/semanticTests/fallback/call_forward_bytes.sol b/test/libsolidity/semanticTests/fallback/call_forward_bytes.sol new file mode 100644 index 000000000..39122e757 --- /dev/null +++ b/test/libsolidity/semanticTests/fallback/call_forward_bytes.sol @@ -0,0 +1,27 @@ +contract receiver { + uint256 public received; + function recv(uint256 x) public { received += x + 1; } + fallback() external { received = 0x80; } +} +contract sender { + constructor() { rec = new receiver();} + fallback() external { savedData = msg.data; } + function forward() public returns (bool) { address(rec).call(savedData); return true; } + function clear() public returns (bool) { delete savedData; return true; } + function val() public returns (uint) { return rec.received(); } + receiver rec; + bytes savedData; +} +// ==== +// allowNonExistingFunctions: true +// compileToEwasm: false +// compileViaYul: also +// ---- +// recv(uint256): 7 -> +// val() -> 0 +// forward() -> true +// val() -> 8 +// clear() -> true +// val() -> 8 +// forward() -> true +// val() -> 0x80 diff --git a/test/libsolidity/semanticTests/functionCall/bound_function_in_function.sol b/test/libsolidity/semanticTests/functionCall/bound_function_in_function.sol new file mode 100644 index 000000000..983de5e3c --- /dev/null +++ b/test/libsolidity/semanticTests/functionCall/bound_function_in_function.sol @@ -0,0 +1,16 @@ +library L { + function g(function() internal returns (uint) _t) internal returns (uint) { return _t(); } +} +contract C { + using L for *; + function f() public returns (uint) { + return t.g(); + } + function t() public pure returns (uint) { return 7; } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: L +// f() -> 7 diff --git a/test/libsolidity/semanticTests/functionCall/bound_function_in_var.sol b/test/libsolidity/semanticTests/functionCall/bound_function_in_var.sol new file mode 100644 index 000000000..ad8024127 --- /dev/null +++ b/test/libsolidity/semanticTests/functionCall/bound_function_in_var.sol @@ -0,0 +1,16 @@ +library D { struct s { uint a; } function mul(s storage self, uint x) public returns (uint) { return self.a *= x; } } +contract C { + using D for D.s; + D.s public x; + function f(uint a) public returns (uint) { + x.a = 6; + return (x.mul)({x: a}); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: D +// f(uint256): 7 -> 0x2a +// x() -> 0x2a diff --git a/test/libsolidity/semanticTests/functionCall/bound_function_to_string.sol b/test/libsolidity/semanticTests/functionCall/bound_function_to_string.sol new file mode 100644 index 000000000..376768b86 --- /dev/null +++ b/test/libsolidity/semanticTests/functionCall/bound_function_to_string.sol @@ -0,0 +1,20 @@ +library D { function length(string memory self) public returns (uint) { return bytes(self).length; } } +contract C { + using D for string; + string x; + function f() public returns (uint) { + x = "abc"; + return x.length(); + } + function g() public returns (uint) { + string memory s = "abc"; + return s.length(); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: D +// f() -> 3 +// g() -> 3 diff --git a/test/libsolidity/semanticTests/libraries/internal_types_in_library.sol b/test/libsolidity/semanticTests/libraries/internal_types_in_library.sol new file mode 100644 index 000000000..6b13ee09c --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/internal_types_in_library.sol @@ -0,0 +1,30 @@ +library Lib { + function find(uint16[] storage _haystack, uint16 _needle) public view returns (uint) + { + for (uint i = 0; i < _haystack.length; ++i) + if (_haystack[i] == _needle) + return i; + return type(uint).max; + } +} +contract Test { + mapping(string => uint16[]) data; + function f() public returns (uint a, uint b) + { + while (data["abc"].length < 20) + data["abc"].push(); + data["abc"][4] = 9; + data["abc"][17] = 3; + a = Lib.find(data["abc"], 9); + b = Lib.find(data["abc"], 3); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: Lib +// f() -> 4, 0x11 +// gas irOptimized: 115822 +// gas legacy: 135952 +// gas legacyOptimized: 119643 diff --git a/test/libsolidity/semanticTests/libraries/library_call_in_homestead.sol b/test/libsolidity/semanticTests/libraries/library_call_in_homestead.sol new file mode 100644 index 000000000..6841221f4 --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/library_call_in_homestead.sol @@ -0,0 +1,15 @@ +library Lib { function m() public returns (address) { return msg.sender; } } +contract Test { + address public sender; + function f() public { + sender = Lib.m(); + } +} +// ==== +// compileViaYul: also +// compileToEwasm: false +// EVMVersion: >=homestead +// ---- +// library: Lib +// f() -> +// sender() -> 0x1212121212121212121212121212120000000012 diff --git a/test/libsolidity/semanticTests/libraries/library_stray_values.sol b/test/libsolidity/semanticTests/libraries/library_stray_values.sol new file mode 100644 index 000000000..1692b018a --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/library_stray_values.sol @@ -0,0 +1,14 @@ +library Lib { function m(uint x, uint y) public returns (uint) { return x * y; } } +contract Test { + function f(uint x) public returns (uint) { + Lib; + Lib.m; + return x + 9; + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: Lib +// f(uint256): 33 -> 0x2a diff --git a/test/libsolidity/semanticTests/libraries/mapping_arguments_in_library.sol b/test/libsolidity/semanticTests/libraries/mapping_arguments_in_library.sol new file mode 100644 index 000000000..06cfd6e1a --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/mapping_arguments_in_library.sol @@ -0,0 +1,41 @@ +library Lib { + function set(mapping(uint => uint) storage m, uint key, uint value) internal + { + m[key] = value; + } + function get(mapping(uint => uint) storage m, uint key) internal view returns (uint) + { + return m[key]; + } +} +contract Test { + mapping(uint => uint) m; + function set(uint256 key, uint256 value) public returns (uint) + { + uint oldValue = Lib.get(m, key); + Lib.set(m, key, value); + return oldValue; + } + function get(uint256 key) public view returns (uint) { + return Lib.get(m, key); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: Lib +// set(uint256,uint256): 1, 42 -> 0 +// set(uint256,uint256): 2, 84 -> 0 +// set(uint256,uint256): 21, 7 -> 0 +// get(uint256): 0 -> 0 +// get(uint256): 1 -> 0x2a +// get(uint256): 2 -> 0x54 +// get(uint256): 21 -> 7 +// set(uint256,uint256): 1, 21 -> 0x2a +// set(uint256,uint256): 2, 42 -> 0x54 +// set(uint256,uint256): 21, 14 -> 7 +// get(uint256): 0 -> 0 +// get(uint256): 1 -> 0x15 +// get(uint256): 2 -> 0x2a +// get(uint256): 21 -> 14 diff --git a/test/libsolidity/semanticTests/libraries/mapping_returns_in_library.sol b/test/libsolidity/semanticTests/libraries/mapping_returns_in_library.sol new file mode 100644 index 000000000..dd2b2953f --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/mapping_returns_in_library.sol @@ -0,0 +1,75 @@ +library Lib { + function choose_mapping(mapping(uint => uint) storage a, mapping(uint => uint) storage b, bool c) internal pure returns(mapping(uint=>uint) storage) + { + return c ? a : b; + } +} +contract Test { + mapping(uint => uint) a; + mapping(uint => uint) b; + function set(bool choice, uint256 key, uint256 value) public returns (uint) + { + mapping(uint => uint) storage m = Lib.choose_mapping(a, b, choice); + uint oldValue = m[key]; + m[key] = value; + return oldValue; + } + function get(bool choice, uint256 key) public view returns (uint) { + return Lib.choose_mapping(a, b, choice)[key]; + } + function get_a(uint256 key) public view returns (uint) { + return a[key]; + } + function get_b(uint256 key) public view returns (uint) { + return b[key]; + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: Lib +// set(bool,uint256,uint256): true, 1, 42 -> 0 +// set(bool,uint256,uint256): true, 2, 84 -> 0 +// set(bool,uint256,uint256): true, 21, 7 -> 0 +// set(bool,uint256,uint256): false, 1, 10 -> 0 +// set(bool,uint256,uint256): false, 2, 11 -> 0 +// set(bool,uint256,uint256): false, 21, 12 -> 0 +// get(bool,uint256): true, 0 -> 0 +// get(bool,uint256): true, 1 -> 0x2a +// get(bool,uint256): true, 2 -> 0x54 +// get(bool,uint256): true, 21 -> 7 +// get_a(uint256): 0 -> 0 +// get_a(uint256): 1 -> 0x2a +// get_a(uint256): 2 -> 0x54 +// get_a(uint256): 21 -> 7 +// get(bool,uint256): false, 0 -> 0 +// get(bool,uint256): false, 1 -> 10 +// get(bool,uint256): false, 2 -> 11 +// get(bool,uint256): false, 21 -> 12 +// get_b(uint256): 0 -> 0 +// get_b(uint256): 1 -> 10 +// get_b(uint256): 2 -> 11 +// get_b(uint256): 21 -> 12 +// set(bool,uint256,uint256): true, 1, 21 -> 0x2a +// set(bool,uint256,uint256): true, 2, 42 -> 0x54 +// set(bool,uint256,uint256): true, 21, 14 -> 7 +// set(bool,uint256,uint256): false, 1, 30 -> 10 +// set(bool,uint256,uint256): false, 2, 31 -> 11 +// set(bool,uint256,uint256): false, 21, 32 -> 12 +// get_a(uint256): 0 -> 0 +// get_a(uint256): 1 -> 0x15 +// get_a(uint256): 2 -> 0x2a +// get_a(uint256): 21 -> 14 +// get(bool,uint256): true, 0 -> 0 +// get(bool,uint256): true, 1 -> 0x15 +// get(bool,uint256): true, 2 -> 0x2a +// get(bool,uint256): true, 21 -> 14 +// get_b(uint256): 0 -> 0 +// get_b(uint256): 1 -> 0x1e +// get_b(uint256): 2 -> 0x1f +// get_b(uint256): 21 -> 0x20 +// get(bool,uint256): false, 0 -> 0 +// get(bool,uint256): false, 1 -> 0x1e +// get(bool,uint256): false, 2 -> 0x1f +// get(bool,uint256): false, 21 -> 0x20 diff --git a/test/libsolidity/semanticTests/libraries/mapping_returns_in_library_named.sol b/test/libsolidity/semanticTests/libraries/mapping_returns_in_library_named.sol new file mode 100644 index 000000000..23f851279 --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/mapping_returns_in_library_named.sol @@ -0,0 +1,31 @@ +library Lib { + function f(mapping(uint => uint) storage a, mapping(uint => uint) storage b) internal returns(mapping(uint=>uint) storage r) + { + r = a; + r[1] = 42; + r = b; + r[1] = 21; + } +} +contract Test { + mapping(uint => uint) a; + mapping(uint => uint) b; + function f() public returns (uint, uint, uint, uint, uint, uint) + { + Lib.f(a, b)[2] = 84; + return (a[0], a[1], a[2], b[0], b[1], b[2]); + } + function g() public returns (uint, uint, uint, uint, uint, uint) + { + mapping(uint => uint) storage m = Lib.f(a, b); + m[2] = 17; + return (a[0], a[1], a[2], b[0], b[1], b[2]); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: Lib +// f() -> 0, 0x2a, 0, 0, 0x15, 0x54 +// g() -> 0, 0x2a, 0, 0, 0x15, 0x11 diff --git a/test/libsolidity/semanticTests/libraries/payable_function_calls_library.sol b/test/libsolidity/semanticTests/libraries/payable_function_calls_library.sol new file mode 100644 index 000000000..6b0488f06 --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/payable_function_calls_library.sol @@ -0,0 +1,14 @@ +library L { + function f() public returns (uint) { return 7; } +} +contract C { + function f() public payable returns (uint) { + return L.f(); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: L +// f(): 27 -> 7 diff --git a/test/libsolidity/semanticTests/libraries/using_for_by_name.sol b/test/libsolidity/semanticTests/libraries/using_for_by_name.sol new file mode 100644 index 000000000..8a84c8aaa --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/using_for_by_name.sol @@ -0,0 +1,16 @@ +library D { struct s { uint a; } function mul(s storage self, uint x) public returns (uint) { return self.a *= x; } } +contract C { + using D for D.s; + D.s public x; + function f(uint a) public returns (uint) { + x.a = 6; + return x.mul({x: a}); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: D +// f(uint256): 7 -> 0x2a +// x() -> 0x2a diff --git a/test/libsolidity/semanticTests/libraries/using_for_overload.sol b/test/libsolidity/semanticTests/libraries/using_for_overload.sol new file mode 100644 index 000000000..54cfe0ce1 --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/using_for_overload.sol @@ -0,0 +1,20 @@ +library D { + struct s { uint a; } + function mul(s storage self, uint x) public returns (uint) { return self.a *= x; } + function mul(s storage self, bytes32 x) public returns (bytes32) { } +} +contract C { + using D for D.s; + D.s public x; + function f(uint a) public returns (uint) { + x.a = 6; + return x.mul(a); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: D +// f(uint256): 7 -> 0x2a +// x() -> 0x2a diff --git a/test/libsolidity/semanticTests/libraries/using_library_mappings_public.sol b/test/libsolidity/semanticTests/libraries/using_library_mappings_public.sol new file mode 100644 index 000000000..d85f90694 --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/using_library_mappings_public.sol @@ -0,0 +1,27 @@ +library Lib { + function set(mapping(uint => uint) storage m, uint key, uint value) public + { + m[key] = value; + } +} +contract Test { + mapping(uint => uint) m1; + mapping(uint => uint) m2; + function f() public returns (uint, uint, uint, uint, uint, uint) + { + Lib.set(m1, 0, 1); + Lib.set(m1, 2, 42); + Lib.set(m2, 0, 23); + Lib.set(m2, 2, 99); + return (m1[0], m1[1], m1[2], m2[0], m2[1], m2[2]); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: Lib +// f() -> 1, 0, 0x2a, 0x17, 0, 0x63 +// gas irOptimized: 119757 +// gas legacy: 124793 +// gas legacyOptimized: 119694 diff --git a/test/libsolidity/semanticTests/libraries/using_library_mappings_return.sol b/test/libsolidity/semanticTests/libraries/using_library_mappings_return.sol new file mode 100644 index 000000000..9192fdfe3 --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/using_library_mappings_return.sol @@ -0,0 +1,25 @@ +library Lib { + function choose(mapping(uint => mapping(uint => uint)) storage m, uint key) external returns (mapping(uint => uint) storage) { + return m[key]; + } +} +contract Test { + mapping(uint => mapping(uint => uint)) m; + function f() public returns (uint, uint, uint, uint, uint, uint) + { + Lib.choose(m, 0)[0] = 1; + Lib.choose(m, 0)[2] = 42; + Lib.choose(m, 1)[0] = 23; + Lib.choose(m, 1)[2] = 99; + return (m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2]); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: Lib +// f() -> 1, 0, 0x2a, 0x17, 0, 0x63 +// gas irOptimized: 120471 +// gas legacy: 125245 +// gas legacyOptimized: 120153 diff --git a/test/libsolidity/semanticTests/libraries/using_library_structs.sol b/test/libsolidity/semanticTests/libraries/using_library_structs.sol new file mode 100644 index 000000000..4348c377c --- /dev/null +++ b/test/libsolidity/semanticTests/libraries/using_library_structs.sol @@ -0,0 +1,27 @@ +library Lib { + struct Data { uint a; uint[] b; } + function set(Data storage _s) public + { + _s.a = 7; + while (_s.b.length < 20) + _s.b.push(); + _s.b[19] = 8; + } +} +contract Test { + mapping(string => Lib.Data) data; + function f() public returns (uint a, uint b) + { + Lib.set(data["abc"]); + a = data["abc"].a; + b = data["abc"].b[19]; + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: Lib +// f() -> 7, 8 +// gas irOptimized: 101869 +// gas legacy: 101504 diff --git a/test/libsolidity/semanticTests/structs/struct_referencing.sol b/test/libsolidity/semanticTests/structs/struct_referencing.sol new file mode 100644 index 000000000..af2357a23 --- /dev/null +++ b/test/libsolidity/semanticTests/structs/struct_referencing.sol @@ -0,0 +1,61 @@ +pragma abicoder v2; +interface I { + struct S { uint a; } +} + +library L { + struct S { uint b; uint a; } + function f() public pure returns (S memory) { + S memory s; + s.a = 3; + return s; + } + function g() public pure returns (I.S memory) { + I.S memory s; + s.a = 4; + return s; + } + // argument-dependant lookup tests + function a(I.S memory) public pure returns (uint) { return 1; } + function a(S memory) public pure returns (uint) { return 2; } +} + +contract C is I { + function f() public pure returns (S memory) { + S memory s; + s.a = 1; + return s; + } + function g() public pure returns (I.S memory) { + I.S memory s; + s.a = 2; + return s; + } + function h() public pure returns (L.S memory) { + L.S memory s; + s.a = 5; + return s; + } + function x() public pure returns (L.S memory) { + return L.f(); + } + function y() public pure returns (I.S memory) { + return L.g(); + } + function a1() public pure returns (uint) { S memory s; return L.a(s); } + function a2() public pure returns (uint) { L.S memory s; return L.a(s); } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: L +// f() -> 1 +// g() -> 2 +// f() -> 1 +// g() -> 2 +// h() -> 0, 5 +// x() -> 0, 3 +// y() -> 4 +// a1() -> 1 +// a2() -> 2 diff --git a/test/libsolidity/semanticTests/structs/using_for_function_on_struct.sol b/test/libsolidity/semanticTests/structs/using_for_function_on_struct.sol new file mode 100644 index 000000000..347aeb595 --- /dev/null +++ b/test/libsolidity/semanticTests/structs/using_for_function_on_struct.sol @@ -0,0 +1,16 @@ +library D { struct s { uint a; } function mul(s storage self, uint x) public returns (uint) { return self.a *= x; } } +contract C { + using D for D.s; + D.s public x; + function f(uint a) public returns (uint) { + x.a = 3; + return x.mul(a); + } +} +// ==== +// compileToEwasm: false +// compileViaYul: also +// ---- +// library: D +// f(uint256): 7 -> 0x15 +// x() -> 0x15 From 7cc7a0f1839c95607c8e2eb4a80bb5a8d82229fd Mon Sep 17 00:00:00 2001 From: nishant-sachdeva Date: Mon, 6 Dec 2021 18:01:27 +0530 Subject: [PATCH 09/15] Added sameType check for fromType and toType in YulUtilFunctions.cpp and relevant tests in semanticTests --- ...on_external_storage_to_storage_dynamic.sol | 46 +++++++++++------- ...o_storage_dynamic_different_mutability.sol | 48 ++++++++++++------- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic.sol b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic.sol index 543342dfd..8bd9ee42c 100644 --- a/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic.sol +++ b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic.sol @@ -4,12 +4,27 @@ contract C { function testFunction3() public {} - function() external [3] externalArray0; - function() external [3] externalArray1 = [ - this.testFunction1, - this.testFunction2, - this.testFunction3 - ]; + function() external [] externalArray0; + function() external [] externalArray1; + + function() internal [] internalArray0; + function() internal [] internalArray1; + + constructor() { + externalArray0 = new function() external[] (3); + externalArray1 = [ + this.testFunction1, + this.testFunction2, + this.testFunction3 + ]; + + internalArray0 = new function() internal[] (3); + internalArray1 = [ + testFunction1, + testFunction2, + testFunction3 + ]; + } function copyExternalStorageArrayOfFunctionType() external returns (bool) { assert(keccak256(abi.encode(externalArray0)) != keccak256(abi.encode(externalArray1))); @@ -17,27 +32,22 @@ contract C { return keccak256(abi.encode(externalArray0)) == keccak256(abi.encode(externalArray1)); } - function() internal [3] internalArray0; - function() internal [3] internalArray1 = [ - testFunction1, - testFunction2, - testFunction3 - ]; - function copyInternalArrayOfFunctionType() external returns (bool) { internalArray0 = internalArray1; assert(internalArray0.length == 3); return - internalArray0.length == internalArray1.length && - internalArray0[0] == internalArray1[0] && - internalArray0[1] == internalArray1[1] && - internalArray0[2] == internalArray1[2]; + internalArray0.length == internalArray1.length && + internalArray0[0] == internalArray1[0] && + internalArray0[1] == internalArray1[1] && + internalArray0[2] == internalArray1[2]; } } // ==== // compileViaYul: also // ---- // copyExternalStorageArrayOfFunctionType() -> true -// gas legacy: 103412 +// gas irOptimized: 104701 +// gas legacy: 108725 +// gas legacyOptimized: 102441 // copyInternalArrayOfFunctionType() -> true diff --git a/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic_different_mutability.sol b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic_different_mutability.sol index 0e356bce6..63236ed77 100644 --- a/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic_different_mutability.sol +++ b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic_different_mutability.sol @@ -3,12 +3,29 @@ contract C { function testFunction2() public view {} function testFunction3() public pure {} - function() external [3] externalArray0; - function() external [3] externalArray1 = [ - this.testFunction1, - this.testFunction2, - this.testFunction3 - ]; + + function() external [] externalArray0; + function() external [] externalArray1; + + function() internal [] internalArray0; + function() internal [] internalArray1; + + constructor() { + externalArray0 = new function() external[] (3); + externalArray1 = [ + this.testFunction1, + this.testFunction2, + this.testFunction3 + ]; + + internalArray0 = new function() internal[] (3); + internalArray1 = [ + testFunction1, + testFunction2, + testFunction3 + ]; + } + function copyExternalStorageArraysOfFunctionType() external returns (bool) { @@ -17,29 +34,24 @@ contract C { return keccak256(abi.encodePacked(externalArray0)) == keccak256(abi.encodePacked(externalArray1)); } - function() internal [3] internalArray0; - function() internal [3] internalArray1 = [ - testFunction1, - testFunction2, - testFunction3 - ]; - function copyInternalArrayOfFunctionType() external returns (bool) { internalArray0 = internalArray1; assert(internalArray0.length == 3); return - internalArray0.length == internalArray1.length && - internalArray0[0] == internalArray1[0] && - internalArray0[1] == internalArray1[1] && - internalArray0[2] == internalArray1[2]; + internalArray0.length == internalArray1.length && + internalArray0[0] == internalArray1[0] && + internalArray0[1] == internalArray1[1] && + internalArray0[2] == internalArray1[2]; } } // ==== // compileViaYul: also // ---- // copyExternalStorageArraysOfFunctionType() -> true -// gas legacy: 103398 +// gas irOptimized: 104372 +// gas legacy: 108462 +// gas legacyOptimized: 102174 // copyInternalArrayOfFunctionType() -> true // gas legacy: 104178 From 7a96953e78cc2083db182db1628ad68da785d03c Mon Sep 17 00:00:00 2001 From: Marenz Date: Thu, 11 Nov 2021 16:21:23 +0100 Subject: [PATCH 10/15] Implement typechecked abi.encodeCall() --- Changelog.md | 1 + docs/cheatsheet.rst | 2 + docs/units-and-global-variables.rst | 1 + libsolidity/analysis/TypeChecker.cpp | 113 ++++++++++++++++++ libsolidity/analysis/TypeChecker.h | 3 + libsolidity/analysis/ViewPureChecker.cpp | 1 + libsolidity/ast/Types.cpp | 11 ++ libsolidity/ast/Types.h | 1 + libsolidity/codegen/ExpressionCompiler.cpp | 56 ++++++--- .../codegen/ir/IRGeneratorForStatements.cpp | 48 ++++++-- libsolidity/formal/SMTEncoder.cpp | 2 + libsolidity/formal/SymbolicState.cpp | 9 ++ .../abiencodedecode/abi_encode_call.sol | 51 ++++++++ .../abi_encode_call_is_consistent.sol | 63 ++++++++++ .../abi_encode_call_memory.sol | 28 +++++ .../abi_encode_call_special_args.sol | 48 ++++++++ .../abi_encode_with_signaturev2.sol | 1 - .../abi/abi_encode_call_simple.sol | 26 ++++ .../specialFunctions/encodeCall.sol | 82 +++++++++++++ .../encodeCall_nested_tuple.sol | 9 ++ .../encodeCall_tuple_incomplete.sol | 8 ++ 21 files changed, 538 insertions(+), 26 deletions(-) create mode 100644 test/libsolidity/semanticTests/abiencodedecode/abi_encode_call.sol create mode 100644 test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_is_consistent.sol create mode 100644 test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_memory.sol create mode 100644 test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_special_args.sol create mode 100644 test/libsolidity/smtCheckerTests/abi/abi_encode_call_simple.sol create mode 100644 test/libsolidity/syntaxTests/specialFunctions/encodeCall.sol create mode 100644 test/libsolidity/syntaxTests/specialFunctions/encodeCall_nested_tuple.sol create mode 100644 test/libsolidity/syntaxTests/specialFunctions/encodeCall_tuple_incomplete.sol diff --git a/Changelog.md b/Changelog.md index c4b67e935..577bc4064 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,7 @@ ### 0.8.11 (unreleased) Language Features: + * General: New builtin function ``abi.encodeCall(functionPointer, (arg1, arg2, ...))`` that type-checks the arguments and returns the ABI-encoded function call data. Compiler Features: diff --git a/docs/cheatsheet.rst b/docs/cheatsheet.rst index 17537914a..1aa8f24ef 100644 --- a/docs/cheatsheet.rst +++ b/docs/cheatsheet.rst @@ -80,6 +80,8 @@ Global Variables the given arguments. Note that this encoding can be ambiguous! - ``abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)``: :ref:`ABI `-encodes the given arguments starting from the second and prepends the given four-byte selector +- ``abi.encodeCall(function functionPointer, (...)) returns (bytes memory)``: ABI-encodes a call to ``functionPointer`` with the arguments found in the + tuple. Performs a full type-check, ensuring the types match the function signature. Result equals ``abi.encodeWithSelector(functionPointer.selector, (...))`` - ``abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)``: Equivalent to ``abi.encodeWithSelector(bytes4(keccak256(bytes(signature)), ...)`` - ``bytes.concat(...) returns (bytes memory)``: :ref:`Concatenates variable number of diff --git a/docs/units-and-global-variables.rst b/docs/units-and-global-variables.rst index ac11e0806..70125636c 100644 --- a/docs/units-and-global-variables.rst +++ b/docs/units-and-global-variables.rst @@ -136,6 +136,7 @@ ABI Encoding and Decoding Functions - ``abi.encodePacked(...) returns (bytes memory)``: Performs :ref:`packed encoding ` of the given arguments. Note that packed encoding can be ambiguous! - ``abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)``: ABI-encodes the given arguments starting from the second and prepends the given four-byte selector - ``abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)``: Equivalent to ``abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)`` +- ``abi.encodeCall(function functionPointer, (...)) returns (bytes memory)``: ABI-encodes a call to ``functionPointer`` with the arguments found in the tuple. Performs a full type-check, ensuring the types match the function signature. Result equals ``abi.encodeWithSelector(functionPointer.selector, (...))`` .. note:: These encoding functions can be used to craft data for external function calls without actually diff --git a/libsolidity/analysis/TypeChecker.cpp b/libsolidity/analysis/TypeChecker.cpp index 5cc208d69..aacd233a2 100644 --- a/libsolidity/analysis/TypeChecker.cpp +++ b/libsolidity/analysis/TypeChecker.cpp @@ -1996,6 +1996,7 @@ void TypeChecker::typeCheckABIEncodeFunctions( _functionType->kind() == FunctionType::Kind::ABIEncode || _functionType->kind() == FunctionType::Kind::ABIEncodePacked || _functionType->kind() == FunctionType::Kind::ABIEncodeWithSelector || + _functionType->kind() == FunctionType::Kind::ABIEncodeCall || _functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature, "ABI function has unexpected FunctionType::Kind." ); @@ -2020,6 +2021,13 @@ void TypeChecker::typeCheckABIEncodeFunctions( // Perform standard function call type checking typeCheckFunctionGeneralChecks(_functionCall, _functionType); + // No further generic checks needed as we do a precise check for ABIEncodeCall + if (_functionType->kind() == FunctionType::Kind::ABIEncodeCall) + { + typeCheckABIEncodeCallFunction(_functionCall); + return; + } + // Check additional arguments for variadic functions vector> const& arguments = _functionCall.arguments(); for (size_t i = 0; i < arguments.size(); ++i) @@ -2078,6 +2086,110 @@ void TypeChecker::typeCheckABIEncodeFunctions( } } +void TypeChecker::typeCheckABIEncodeCallFunction(FunctionCall const& _functionCall) +{ + vector> const& arguments = _functionCall.arguments(); + + // Expecting first argument to be the function pointer and second to be a tuple. + if (arguments.size() != 2) + { + m_errorReporter.typeError( + 6219_error, + _functionCall.location(), + "Expected two arguments: a function pointer followed by a tuple." + ); + return; + } + + auto const functionPointerType = dynamic_cast(type(*arguments.front())); + + if (!functionPointerType) + { + m_errorReporter.typeError( + 5511_error, + arguments.front()->location(), + "Expected first argument to be a function pointer, not \"" + + type(*arguments.front())->canonicalName() + + "\"." + ); + return; + } + + if (functionPointerType->kind() != FunctionType::Kind::External) + { + string msg = "Function must be \"public\" or \"external\"."; + SecondarySourceLocation ssl{}; + + if (functionPointerType->hasDeclaration()) + { + ssl.append("Function is declared here:", functionPointerType->declaration().location()); + if (functionPointerType->declaration().scope() == m_currentContract) + msg += " Did you forget to prefix \"this.\"?"; + } + + m_errorReporter.typeError(3509_error, arguments[0]->location(), ssl, msg); + return; + } + + solAssert(!functionPointerType->takesArbitraryParameters(), "Function must have fixed parameters."); + + // Tuples with only one component become that component + vector> callArguments; + + auto const* tupleType = dynamic_cast(type(*arguments[1])); + if (tupleType) + { + auto const& argumentTuple = dynamic_cast(*arguments[1].get()); + callArguments = decltype(callArguments){argumentTuple.components().begin(), argumentTuple.components().end()}; + } + else + callArguments.push_back(arguments[1]); + + if (functionPointerType->parameterTypes().size() != callArguments.size()) + { + if (tupleType) + m_errorReporter.typeError( + 7788_error, + _functionCall.location(), + "Expected " + + to_string(functionPointerType->parameterTypes().size()) + + " instead of " + + to_string(callArguments.size()) + + " components for the tuple parameter." + ); + else + m_errorReporter.typeError( + 7515_error, + _functionCall.location(), + "Expected a tuple with " + + to_string(functionPointerType->parameterTypes().size()) + + " components instead of a single non-tuple parameter." + ); + } + + // Use min() to check as much as we can before failing fatally + size_t const numParameters = min(callArguments.size(), functionPointerType->parameterTypes().size()); + + for (size_t i = 0; i < numParameters; i++) + { + Type const& argType = *type(*callArguments[i]); + BoolResult result = argType.isImplicitlyConvertibleTo(*functionPointerType->parameterTypes()[i]); + if (!result) + m_errorReporter.typeError( + 5407_error, + callArguments[i]->location(), + "Cannot implicitly convert component at position " + + to_string(i) + + " from \"" + + argType.canonicalName() + + "\" to \"" + + functionPointerType->parameterTypes()[i]->canonicalName() + + "\"" + + (result.message().empty() ? "." : ": " + result.message()) + ); + } +} + void TypeChecker::typeCheckBytesConcatFunction( FunctionCall const& _functionCall, FunctionType const* _functionType @@ -2507,6 +2619,7 @@ bool TypeChecker::visit(FunctionCall const& _functionCall) case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodeWithSelector: case FunctionType::Kind::ABIEncodeWithSignature: + case FunctionType::Kind::ABIEncodeCall: { typeCheckABIEncodeFunctions(_functionCall, functionType); returnTypes = functionType->returnParameterTypes(); diff --git a/libsolidity/analysis/TypeChecker.h b/libsolidity/analysis/TypeChecker.h index ba445ffbb..7586f9d15 100644 --- a/libsolidity/analysis/TypeChecker.h +++ b/libsolidity/analysis/TypeChecker.h @@ -110,6 +110,9 @@ private: FunctionTypePointer _functionType ); + /// Performs checks specific to the ABI encode functions of type ABIEncodeCall + void typeCheckABIEncodeCallFunction(FunctionCall const& _functionCall); + /// Performs general checks and checks specific to bytes concat function call void typeCheckBytesConcatFunction( FunctionCall const& _functionCall, diff --git a/libsolidity/analysis/ViewPureChecker.cpp b/libsolidity/analysis/ViewPureChecker.cpp index 81f755cec..252210a12 100644 --- a/libsolidity/analysis/ViewPureChecker.cpp +++ b/libsolidity/analysis/ViewPureChecker.cpp @@ -367,6 +367,7 @@ void ViewPureChecker::endVisit(MemberAccess const& _memberAccess) {MagicType::Kind::ABI, "encode"}, {MagicType::Kind::ABI, "encodePacked"}, {MagicType::Kind::ABI, "encodeWithSelector"}, + {MagicType::Kind::ABI, "encodeCall"}, {MagicType::Kind::ABI, "encodeWithSignature"}, {MagicType::Kind::Message, "data"}, {MagicType::Kind::Message, "sig"}, diff --git a/libsolidity/ast/Types.cpp b/libsolidity/ast/Types.cpp index fe0ac8ef8..aa1d5820f 100644 --- a/libsolidity/ast/Types.cpp +++ b/libsolidity/ast/Types.cpp @@ -2935,6 +2935,7 @@ string FunctionType::richIdentifier() const case Kind::ABIEncode: id += "abiencode"; break; case Kind::ABIEncodePacked: id += "abiencodepacked"; break; case Kind::ABIEncodeWithSelector: id += "abiencodewithselector"; break; + case Kind::ABIEncodeCall: id += "abiencodecall"; break; case Kind::ABIEncodeWithSignature: id += "abiencodewithsignature"; break; case Kind::ABIDecode: id += "abidecode"; break; case Kind::MetaType: id += "metatype"; break; @@ -3499,6 +3500,7 @@ bool FunctionType::isPure() const m_kind == Kind::ABIEncode || m_kind == Kind::ABIEncodePacked || m_kind == Kind::ABIEncodeWithSelector || + m_kind == Kind::ABIEncodeCall || m_kind == Kind::ABIEncodeWithSignature || m_kind == Kind::ABIDecode || m_kind == Kind::MetaType || @@ -4001,6 +4003,15 @@ MemberList::MemberMap MagicType::nativeMembers(ASTNode const*) const true, StateMutability::Pure )}, + {"encodeCall", TypeProvider::function( + TypePointers{}, + TypePointers{TypeProvider::array(DataLocation::Memory)}, + strings{}, + strings{1, ""}, + FunctionType::Kind::ABIEncodeCall, + true, + StateMutability::Pure + )}, {"encodeWithSignature", TypeProvider::function( TypePointers{TypeProvider::array(DataLocation::Memory, true)}, TypePointers{TypeProvider::array(DataLocation::Memory)}, diff --git a/libsolidity/ast/Types.h b/libsolidity/ast/Types.h index b08694820..8b3826ca0 100644 --- a/libsolidity/ast/Types.h +++ b/libsolidity/ast/Types.h @@ -1237,6 +1237,7 @@ public: ABIEncode, ABIEncodePacked, ABIEncodeWithSelector, + ABIEncodeCall, ABIEncodeWithSignature, ABIDecode, GasLeft, ///< gasleft() diff --git a/libsolidity/codegen/ExpressionCompiler.cpp b/libsolidity/codegen/ExpressionCompiler.cpp index 07d37c4c5..5a633d2e6 100644 --- a/libsolidity/codegen/ExpressionCompiler.cpp +++ b/libsolidity/codegen/ExpressionCompiler.cpp @@ -1236,28 +1236,47 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall) case FunctionType::Kind::ABIEncode: case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodeWithSelector: + case FunctionType::Kind::ABIEncodeCall: case FunctionType::Kind::ABIEncodeWithSignature: { bool const isPacked = function.kind() == FunctionType::Kind::ABIEncodePacked; bool const hasSelectorOrSignature = function.kind() == FunctionType::Kind::ABIEncodeWithSelector || + function.kind() == FunctionType::Kind::ABIEncodeCall || function.kind() == FunctionType::Kind::ABIEncodeWithSignature; TypePointers argumentTypes; - TypePointers targetTypes; - for (unsigned i = 0; i < arguments.size(); ++i) + + ASTNode::listAccept(arguments, *this); + + if (function.kind() == FunctionType::Kind::ABIEncodeCall) { - arguments[i]->accept(*this); - // Do not keep the selector as part of the ABI encoded args - if (!hasSelectorOrSignature || i > 0) - argumentTypes.push_back(arguments[i]->annotation().type); + solAssert(arguments.size() == 2); + + auto const functionPtr = dynamic_cast(arguments[0]->annotation().type); + solAssert(functionPtr); + solAssert(functionPtr->sizeOnStack() == 2); + + // Account for tuples with one component which become that component + if (auto const tupleType = dynamic_cast(arguments[1]->annotation().type)) + argumentTypes = tupleType->components(); + else + argumentTypes.emplace_back(arguments[1]->annotation().type); } + else + for (unsigned i = 0; i < arguments.size(); ++i) + { + // Do not keep the selector as part of the ABI encoded args + if (!hasSelectorOrSignature || i > 0) + argumentTypes.push_back(arguments[i]->annotation().type); + } + utils().fetchFreeMemoryPointer(); - // stack now: [] .. + // stack now: [] .. // adjust by 32(+4) bytes to accommodate the length(+selector) m_context << u256(32 + (hasSelectorOrSignature ? 4 : 0)) << Instruction::ADD; - // stack now: [] .. + // stack now: [] .. if (isPacked) { @@ -1270,7 +1289,7 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall) utils().abiEncode(argumentTypes, TypePointers()); } utils().fetchFreeMemoryPointer(); - // stack: [] + // stack: [] // size is end minus start minus length slot m_context.appendInlineAssembly(R"({ @@ -1278,16 +1297,17 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall) })", {"mem_end", "mem_ptr"}); m_context << Instruction::SWAP1; utils().storeFreeMemoryPointer(); - // stack: [] + // stack: [] if (hasSelectorOrSignature) { - // stack: + // stack: solAssert(arguments.size() >= 1, ""); Type const* selectorType = arguments[0]->annotation().type; utils().moveIntoStack(selectorType->sizeOnStack()); Type const* dataOnStack = selectorType; - // stack: + + // stack: if (function.kind() == FunctionType::Kind::ABIEncodeWithSignature) { // hash the signature @@ -1299,7 +1319,7 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall) else { utils().fetchFreeMemoryPointer(); - // stack: + // stack: utils().packedEncode(TypePointers{selectorType}, TypePointers()); utils().toSizeAfterFreeMemoryPointer(); m_context << Instruction::KECCAK256; @@ -1308,10 +1328,16 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall) dataOnStack = TypeProvider::fixedBytes(32); } } - else + else if (function.kind() == FunctionType::Kind::ABIEncodeCall) { - solAssert(function.kind() == FunctionType::Kind::ABIEncodeWithSelector, ""); + // stack: + // Extract selector from the stack + m_context << Instruction::SWAP1 << Instruction::POP; + // Conversion will be done below + dataOnStack = TypeProvider::uint(32); } + else + solAssert(function.kind() == FunctionType::Kind::ABIEncodeWithSelector, ""); utils().convertType(*dataOnStack, FixedBytesType(4), true); diff --git a/libsolidity/codegen/ir/IRGeneratorForStatements.cpp b/libsolidity/codegen/ir/IRGeneratorForStatements.cpp index 85b21bbdc..179b3e739 100644 --- a/libsolidity/codegen/ir/IRGeneratorForStatements.cpp +++ b/libsolidity/codegen/ir/IRGeneratorForStatements.cpp @@ -1104,29 +1104,57 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) case FunctionType::Kind::ABIEncode: case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodeWithSelector: + case FunctionType::Kind::ABIEncodeCall: case FunctionType::Kind::ABIEncodeWithSignature: { bool const isPacked = functionType->kind() == FunctionType::Kind::ABIEncodePacked; solAssert(functionType->padArguments() != isPacked, ""); bool const hasSelectorOrSignature = functionType->kind() == FunctionType::Kind::ABIEncodeWithSelector || + functionType->kind() == FunctionType::Kind::ABIEncodeCall || functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature; TypePointers argumentTypes; TypePointers targetTypes; vector argumentVars; - for (size_t i = 0; i < arguments.size(); ++i) + string selector; + vector> argumentsOfEncodeFunction; + + if (functionType->kind() == FunctionType::Kind::ABIEncodeCall) { - // ignore selector - if (hasSelectorOrSignature && i == 0) - continue; - argumentTypes.emplace_back(&type(*arguments[i])); - targetTypes.emplace_back(type(*arguments[i]).fullEncodingType(false, true, isPacked)); - argumentVars += IRVariable(*arguments[i]).stackSlots(); + solAssert(arguments.size() == 2, ""); + // Account for tuples with one component which become that component + if (type(*arguments[1]).category() == Type::Category::Tuple) + { + auto const& tupleExpression = dynamic_cast(*arguments[1]); + for (auto component: tupleExpression.components()) + argumentsOfEncodeFunction.push_back(component); + } + else + argumentsOfEncodeFunction.push_back(arguments[1]); + } + else + for (size_t i = 0; i < arguments.size(); ++i) + { + // ignore selector + if (hasSelectorOrSignature && i == 0) + continue; + argumentsOfEncodeFunction.push_back(arguments[i]); + } + + for (auto const& argument: argumentsOfEncodeFunction) + { + argumentTypes.emplace_back(&type(*argument)); + targetTypes.emplace_back(type(*argument).fullEncodingType(false, true, isPacked)); + argumentVars += IRVariable(*argument).stackSlots(); } - string selector; - if (functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature) + if (functionType->kind() == FunctionType::Kind::ABIEncodeCall) + selector = convert( + IRVariable(*arguments[0]).part("functionSelector"), + *TypeProvider::fixedBytes(4) + ).name(); + else if (functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature) { // hash the signature Type const& selectorType = type(*arguments.front()); @@ -1833,7 +1861,7 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess) define(_memberAccess) << requestedValue << "\n"; } - else if (set{"encode", "encodePacked", "encodeWithSelector", "encodeWithSignature", "decode"}.count(member)) + else if (set{"encode", "encodePacked", "encodeWithSelector", "encodeCall", "encodeWithSignature", "decode"}.count(member)) { // no-op } diff --git a/libsolidity/formal/SMTEncoder.cpp b/libsolidity/formal/SMTEncoder.cpp index 8326b6b2f..68545f4f6 100644 --- a/libsolidity/formal/SMTEncoder.cpp +++ b/libsolidity/formal/SMTEncoder.cpp @@ -637,6 +637,7 @@ void SMTEncoder::endVisit(FunctionCall const& _funCall) case FunctionType::Kind::ABIEncode: case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodeWithSelector: + case FunctionType::Kind::ABIEncodeCall: case FunctionType::Kind::ABIEncodeWithSignature: visitABIFunction(_funCall); break; @@ -3041,6 +3042,7 @@ set SMTEncoder::collectABICalls(ASTNode const* _node) case FunctionType::Kind::ABIEncode: case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodeWithSelector: + case FunctionType::Kind::ABIEncodeCall: case FunctionType::Kind::ABIEncodeWithSignature: case FunctionType::Kind::ABIDecode: abiCalls.insert(&_funCall); diff --git a/libsolidity/formal/SymbolicState.cpp b/libsolidity/formal/SymbolicState.cpp index e136f347d..6612723d0 100644 --- a/libsolidity/formal/SymbolicState.cpp +++ b/libsolidity/formal/SymbolicState.cpp @@ -236,6 +236,15 @@ void SymbolicState::buildABIFunctions(set const& _abiFuncti else solAssert(false, "Unexpected argument of abi.decode"); } + else if (t->kind() == FunctionType::Kind::ABIEncodeCall) + { + // abi.encodeCall : (functionPointer, tuple_of_args_or_one_non_tuple_arg(arguments)) -> bytes + solAssert(args.size() == 2, "Unexpected number of arguments for abi.encodeCall"); + + outTypes.emplace_back(TypeProvider::bytesMemory()); + inTypes.emplace_back(args.at(0)->annotation().type); + inTypes.emplace_back(args.at(1)->annotation().type); + } else { outTypes = returnTypes; diff --git a/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call.sol b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call.sol new file mode 100644 index 000000000..dc6ead60b --- /dev/null +++ b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call.sol @@ -0,0 +1,51 @@ +pragma abicoder v2; +contract C { + type UnsignedNumber is uint256; + enum Enum { First, Second, Third } + + struct Struct { + UnsignedNumber[] dynamicArray; + uint256 justAnInt; + string name; + bytes someBytes; + Enum theEnum; + } + + function callMeMaybe(Struct calldata _data, int256 _intVal, string memory _nameVal) external pure { + assert(_data.dynamicArray.length == 3); + assert(UnsignedNumber.unwrap(_data.dynamicArray[0]) == 0); + assert(UnsignedNumber.unwrap(_data.dynamicArray[1]) == 1); + assert(UnsignedNumber.unwrap(_data.dynamicArray[2]) == 2); + assert(_data.justAnInt == 6); + assert(keccak256(bytes(_data.name)) == keccak256("StructName")); + assert(keccak256(_data.someBytes) == keccak256(bytes("1234"))); + assert(_data.theEnum == Enum.Second); + assert(_intVal == 5); + assert(keccak256(bytes(_nameVal)) == keccak256("TestName")); + } + + function callExternal() public returns (bool) { + Struct memory structToSend; + structToSend.dynamicArray = new UnsignedNumber[](3); + structToSend.dynamicArray[0] = UnsignedNumber.wrap(0); + structToSend.dynamicArray[1] = UnsignedNumber.wrap(1); + structToSend.dynamicArray[2] = UnsignedNumber.wrap(2); + structToSend.justAnInt = 6; + structToSend.name = "StructName"; + structToSend.someBytes = bytes("1234"); + structToSend.theEnum = Enum.Second; + + (bool success,) = address(this).call(abi.encodeCall(this.callMeMaybe, ( + structToSend, + 5, + "TestName" + ))); + + return success; + } +} + +// ==== +// compileViaYul: also +// ---- +// callExternal() -> true diff --git a/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_is_consistent.sol b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_is_consistent.sol new file mode 100644 index 000000000..3c053e38a --- /dev/null +++ b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_is_consistent.sol @@ -0,0 +1,63 @@ +pragma abicoder v2; + +contract C { + bool sideEffectRan = false; + + function(uint256, string memory) external fPointer; + function fExternal(uint256 p, string memory t) external {} + string xstor; + function getExternalFunctionPointer() public returns (function(uint256, string memory) external) { + sideEffectRan = true; + return this.fExternal; + } + + function fSignatureFromLiteral() public pure returns (bytes memory) { + return abi.encodeWithSignature("fExternal(uint256,string)", 1, "123"); + } + function fSignatureFromLiteralCall() public view returns (bytes memory) { + return abi.encodeCall(this.fExternal, (1, "123")); + } + function fSignatureFromMemory() public pure returns (bytes memory) { + string memory x = "fExternal(uint256,string)"; + return abi.encodeWithSignature(x, 1, "123"); + } + function fSignatureFromMemoryCall() public view returns (bytes memory) { + return abi.encodeCall(this.fExternal, (1,"123")); + } + function fSignatureFromMemorys() public returns (bytes memory) { + xstor = "fExternal(uint256,string)"; + return abi.encodeWithSignature(xstor, 1, "123"); + } + function fPointerCall() public returns(bytes memory) { + fPointer = this.fExternal; + return abi.encodeCall(fPointer, (1, "123")); + } + function fLocalPointerCall() public returns(bytes memory) { + function(uint256, string memory) external localFunctionPointer = this.fExternal; + return abi.encodeCall(localFunctionPointer, (1, "123")); + } + function fReturnedFunctionPointer() public returns (bytes memory) { + return abi.encodeCall(getExternalFunctionPointer(), (1, "123")); + } + + function assertConsistentSelectors() public { + assert(keccak256(fSignatureFromLiteral()) == keccak256(fSignatureFromLiteralCall())); + assert(keccak256(fSignatureFromMemory()) == keccak256(fSignatureFromMemoryCall())); + assert(keccak256(fSignatureFromMemoryCall()) == keccak256(fSignatureFromMemorys())); + assert(keccak256(fPointerCall()) == keccak256(fSignatureFromLiteral())); + assert(keccak256(fLocalPointerCall()) == keccak256(fSignatureFromLiteral())); + assert(keccak256(fReturnedFunctionPointer()) == keccak256(fSignatureFromLiteral())); + } +} +// ==== +// compileViaYul: also +// ---- +// assertConsistentSelectors() -> +// fSignatureFromLiteral() -> 0x20, 0x84, 23450202028776381066253055403048136312616272755117076566855971503345107992576, 26959946667150639794667015087019630673637144422540572481103610249216, 1725436586697640946858688965569256363112777243042596638790631055949824, 86060793054017993816230018372407419485142305772921726565498526629888, 0 +// fSignatureFromLiteralCall() -> 0x20, 0x84, 23450202028776381066253055403048136312616272755117076566855971503345107992576, 26959946667150639794667015087019630673637144422540572481103610249216, 1725436586697640946858688965569256363112777243042596638790631055949824, 86060793054017993816230018372407419485142305772921726565498526629888, 0 +// fSignatureFromMemory() -> 0x20, 0x84, 23450202028776381066253055403048136312616272755117076566855971503345107992576, 26959946667150639794667015087019630673637144422540572481103610249216, 1725436586697640946858688965569256363112777243042596638790631055949824, 86060793054017993816230018372407419485142305772921726565498526629888, 0 +// fSignatureFromMemoryCall() -> 0x20, 0x84, 23450202028776381066253055403048136312616272755117076566855971503345107992576, 26959946667150639794667015087019630673637144422540572481103610249216, 1725436586697640946858688965569256363112777243042596638790631055949824, 86060793054017993816230018372407419485142305772921726565498526629888, 0 +// fSignatureFromMemorys() -> 0x20, 0x84, 23450202028776381066253055403048136312616272755117076566855971503345107992576, 26959946667150639794667015087019630673637144422540572481103610249216, 1725436586697640946858688965569256363112777243042596638790631055949824, 86060793054017993816230018372407419485142305772921726565498526629888, 0 +// fPointerCall() -> 0x20, 0x84, 23450202028776381066253055403048136312616272755117076566855971503345107992576, 26959946667150639794667015087019630673637144422540572481103610249216, 1725436586697640946858688965569256363112777243042596638790631055949824, 86060793054017993816230018372407419485142305772921726565498526629888, 0 +// fLocalPointerCall() -> 0x20, 0x84, 23450202028776381066253055403048136312616272755117076566855971503345107992576, 26959946667150639794667015087019630673637144422540572481103610249216, 1725436586697640946858688965569256363112777243042596638790631055949824, 86060793054017993816230018372407419485142305772921726565498526629888, 0 +// fReturnedFunctionPointer() -> 0x20, 0x84, 23450202028776381066253055403048136312616272755117076566855971503345107992576, 26959946667150639794667015087019630673637144422540572481103610249216, 1725436586697640946858688965569256363112777243042596638790631055949824, 86060793054017993816230018372407419485142305772921726565498526629888, 0 diff --git a/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_memory.sol b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_memory.sol new file mode 100644 index 000000000..781a6684f --- /dev/null +++ b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_memory.sol @@ -0,0 +1,28 @@ +pragma abicoder v2; + +contract D { + function something() external pure {} +} + +contract C { + function something() external pure {} + function test() external returns (bytes4) { + function() external[2] memory x; + x[0] = this.something; + x[1] = (new D()).something; + function() external f = x[1]; + bytes memory a = abi.encodeCall(x[0], ()); + bytes memory b = abi.encodeCall(x[1], ()); + bytes memory c = abi.encodeCall(f, ()); + assert(a.length == 4 && b.length == 4 && c.length == 4); + assert(bytes4(a) == bytes4(b)); + assert(bytes4(a) == bytes4(c)); + assert(bytes4(a) == f.selector); + return bytes4(a); + } +} + +// ==== +// compileViaYul: also +// ---- +// test() -> 0xa7a0d53700000000000000000000000000000000000000000000000000000000 diff --git a/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_special_args.sol b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_special_args.sol new file mode 100644 index 000000000..24e6d065f --- /dev/null +++ b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_call_special_args.sol @@ -0,0 +1,48 @@ +pragma abicoder v2; + +contract C { + bool sideEffectRan = false; + + function fNoArgs() external {} + function fArray(uint[] memory x) external {} + function fUint(uint x, uint y) external returns (uint a, uint b) {} + + function fSignatureFromLiteralNoArgs() public pure returns (bytes memory) { + return abi.encodeWithSignature("fNoArgs()"); + } + function fPointerNoArgs() public view returns (bytes memory) { + return abi.encodeCall(this.fNoArgs, ()); + } + + function fSignatureFromLiteralArray() public pure returns (bytes memory) { + uint[] memory x; + return abi.encodeWithSignature("fArray(uint256[])", x); + } + function fPointerArray() public view returns (bytes memory) { + uint[] memory x; + return abi.encodeCall(this.fArray, x); + } + + function fSignatureFromLiteralUint() public pure returns (bytes memory) { + return abi.encodeWithSignature("fUint(uint256,uint256)", 12, 13); + } + function fPointerUint() public view returns (bytes memory) { + return abi.encodeCall(this.fUint, (12,13)); + } + + function assertConsistentSelectors() public view { + assert(keccak256(fSignatureFromLiteralNoArgs()) == keccak256(fPointerNoArgs())); + assert(keccak256(fSignatureFromLiteralArray()) == keccak256(fPointerArray())); + assert(keccak256(fSignatureFromLiteralUint()) == keccak256(fPointerUint())); + } +} +// ==== +// compileViaYul: also +// ---- +// assertConsistentSelectors() -> +// fSignatureFromLiteralNoArgs() -> 0x20, 0x04, 12200448252684243758085936796735499259670113115893304444050964496075123064832 +// fPointerNoArgs() -> 0x20, 4, 12200448252684243758085936796735499259670113115893304444050964496075123064832 +// fSignatureFromLiteralArray() -> 0x20, 0x44, 4612216551196396486909126966576324289294165774260092952932219511233230929920, 862718293348820473429344482784628181556388621521298319395315527974912, 0 +// fPointerArray() -> 0x20, 0x44, 4612216551196396486909126966576324289294165774260092952932219511233230929920, 862718293348820473429344482784628181556388621521298319395315527974912, 0 +// fPointerUint() -> 0x20, 0x44, 30372892641494467502622535050667754357470287521126424526399600764424271429632, 323519360005807677536004181044235568083645733070486869773243322990592, 350479306672958317330671196131255198757282877493027442254346933239808 +// fSignatureFromLiteralUint() -> 0x20, 0x44, 30372892641494467502622535050667754357470287521126424526399600764424271429632, 323519360005807677536004181044235568083645733070486869773243322990592, 350479306672958317330671196131255198757282877493027442254346933239808 diff --git a/test/libsolidity/semanticTests/abiencodedecode/abi_encode_with_signaturev2.sol b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_with_signaturev2.sol index f6333a947..1968a76a8 100644 --- a/test/libsolidity/semanticTests/abiencodedecode/abi_encode_with_signaturev2.sol +++ b/test/libsolidity/semanticTests/abiencodedecode/abi_encode_with_signaturev2.sol @@ -26,7 +26,6 @@ contract C { } struct S { uint a; string b; uint16 c; } function f4() public pure returns (bytes memory) { - bytes4 x = 0x12345678; S memory s; s.a = 0x1234567; s.b = "Lorem ipsum dolor sit ethereum........"; diff --git a/test/libsolidity/smtCheckerTests/abi/abi_encode_call_simple.sol b/test/libsolidity/smtCheckerTests/abi/abi_encode_call_simple.sol new file mode 100644 index 000000000..72fd4290c --- /dev/null +++ b/test/libsolidity/smtCheckerTests/abi/abi_encode_call_simple.sol @@ -0,0 +1,26 @@ +contract C { + function callMeMaybe(uint a, uint b, uint[] memory c) external {} + + function abiEncodeSimple(uint x, uint y, uint z, uint[] memory a, uint[] memory b) public view { + require(x == y); + bytes memory b1 = abi.encodeCall(this.callMeMaybe, (x, z, a)); + bytes memory b2 = abi.encodeCall(this.callMeMaybe, (y, z, a)); + assert(b1.length == b2.length); + + bytes memory b3 = abi.encodeCall(this.callMeMaybe, (y, z, b)); + assert(b1.length == b3.length); // should fail + } +} +// ==== +// SMTEngine: all +// SMTIgnoreCex: yes +// ---- +// Warning 6031: (233-249): Internal error: Expression undefined for SMT solver. +// Warning 6031: (298-314): Internal error: Expression undefined for SMT solver. +// Warning 6031: (398-414): Internal error: Expression undefined for SMT solver. +// Warning 1218: (330-360): CHC: Error trying to invoke SMT solver. +// Warning 1218: (430-460): CHC: Error trying to invoke SMT solver. +// Warning 6328: (330-360): CHC: Assertion violation might happen here. +// Warning 6328: (430-460): CHC: Assertion violation might happen here. +// Warning 4661: (330-360): BMC: Assertion violation happens here. +// Warning 4661: (430-460): BMC: Assertion violation happens here. diff --git a/test/libsolidity/syntaxTests/specialFunctions/encodeCall.sol b/test/libsolidity/syntaxTests/specialFunctions/encodeCall.sol new file mode 100644 index 000000000..41b8f16cc --- /dev/null +++ b/test/libsolidity/syntaxTests/specialFunctions/encodeCall.sol @@ -0,0 +1,82 @@ +interface I { + function fExternal(uint256 p, string memory t) external; +} + +library L { + function fExternal(uint256 p, string memory t) external {} +} + +contract C { + using L for uint256; + + function f(int a) public {} + function f2(int a, string memory b) public {} + function f3(int a, int b) public {} + function f4() public {} + function fInternal(uint256 p, string memory t) internal {} + + function failFunctionArgsWrongType() public returns(bytes memory) { + return abi.encodeCall(this.f, ("test")); + } + function failFunctionArgsTooMany() public returns(bytes memory) { + return abi.encodeCall(this.f, (1, 2)); + } + function failFunctionArgsTooFew0() public returns(bytes memory) { + return abi.encodeCall(this.f, ()); + } + function failFunctionArgsTooFew1() public returns(bytes memory) { + return abi.encodeCall(this.f); + } + function failFunctionPtrMissing() public returns(bytes memory) { + return abi.encodeCall(1, this.f); + } + function failFunctionPtrWrongType() public returns(bytes memory) { + return abi.encodeCall(abi.encodeCall, (1, 2, 3, "test")); + } + function failFunctionInternal() public returns(bytes memory) { + return abi.encodeCall(fInternal, (1, "123")); + } + function failFunctionInternalFromVariable() public returns(bytes memory) { + function(uint256, string memory) internal localFunctionPointer = fInternal; + return abi.encodeCall(localFunctionPointer, (1, "123")); + } + function failFunctionArgsArrayLiteral() public returns(bytes memory) { + return abi.encodeCall(this.f3, [1, 2]); + } + function failLibraryPointerCall() public returns (bytes memory) { + return abi.encodeCall(L.fExternal, (1, "123")); + } + function failBoundLibraryPointerCall() public returns (bytes memory) { + uint256 x = 1; + return abi.encodeCall(x.fExternal, (1, "123")); + } + function failInterfacePointerCall() public returns (bytes memory) { + return abi.encodeCall(I.fExternal, (1, "123")); + } + function successFunctionArgsIntLiteralTuple() public returns(bytes memory) { + return abi.encodeCall(this.f, (1)); + } + function successFunctionArgsIntLiteral() public returns(bytes memory) { + return abi.encodeCall(this.f, 1); + } + function successFunctionArgsLiteralTuple() public returns(bytes memory) { + return abi.encodeCall(this.f2, (1, "test")); + } + function successFunctionArgsEmptyTuple() public returns(bytes memory) { + return abi.encodeCall(this.f4, ()); + } +} +// ---- +// TypeError 5407: (486-494): Cannot implicitly convert component at position 0 from "literal_string "test"" to "int256". +// TypeError 7788: (576-606): Expected 1 instead of 2 components for the tuple parameter. +// TypeError 7788: (687-713): Expected 1 instead of 0 components for the tuple parameter. +// TypeError 6219: (794-816): Expected two arguments: a function pointer followed by a tuple. +// TypeError 5511: (911-912): Expected first argument to be a function pointer, not "int_const 1". +// TypeError 3509: (1018-1032): Function must be "public" or "external". +// TypeError 3509: (1145-1154): Function must be "public" or "external". Did you forget to prefix "this."? +// TypeError 3509: (1350-1370): Function must be "public" or "external". +// TypeError 7515: (1469-1500): Expected a tuple with 2 components instead of a single non-tuple parameter. +// TypeError 5407: (1493-1499): Cannot implicitly convert component at position 0 from "uint8[2]" to "int256". +// TypeError 3509: (1596-1607): Function must be "public" or "external". +// TypeError 3509: (1738-1749): Function must be "public" or "external". +// TypeError 3509: (1860-1871): Function must be "public" or "external". diff --git a/test/libsolidity/syntaxTests/specialFunctions/encodeCall_nested_tuple.sol b/test/libsolidity/syntaxTests/specialFunctions/encodeCall_nested_tuple.sol new file mode 100644 index 000000000..c27f61d68 --- /dev/null +++ b/test/libsolidity/syntaxTests/specialFunctions/encodeCall_nested_tuple.sol @@ -0,0 +1,9 @@ +contract C { + function f(int a, int b) public {} + function failFunctionArgsIntLiteralNestedTuple() public returns(bytes memory) { + return abi.encodeCall(this.f, ((1,2))); + } +} +// ---- +// TypeError 7788: (139-170): Expected 2 instead of 1 components for the tuple parameter. +// TypeError 5407: (163-168): Cannot implicitly convert component at position 0 from "tuple(int_const 1,int_const 2)" to "int256". diff --git a/test/libsolidity/syntaxTests/specialFunctions/encodeCall_tuple_incomplete.sol b/test/libsolidity/syntaxTests/specialFunctions/encodeCall_tuple_incomplete.sol new file mode 100644 index 000000000..7f69995d5 --- /dev/null +++ b/test/libsolidity/syntaxTests/specialFunctions/encodeCall_tuple_incomplete.sol @@ -0,0 +1,8 @@ +contract C { + function f(int a) public {} + function failFunctionArgsIntLiteralTuple() public returns(bytes memory) { + return abi.encodeCall(this.f, (1,)); + } +} +// ---- +// TypeError 8381: (149-153): Tuple component cannot be empty. From fc88226376b97a53f39a1c70956fe138f13a0a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Thu, 16 Dec 2021 17:47:43 +0100 Subject: [PATCH 11/15] Move `about` field from issue templates to `config.yml` --- .github/ISSUE_TEMPLATE/bug_report.md | 1 - .github/ISSUE_TEMPLATE/config.yml | 3 +++ .github/ISSUE_TEMPLATE/documentation_issue.md | 1 - .github/ISSUE_TEMPLATE/feature_request.md | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 261f78fdc..7be77f422 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,5 @@ --- name: Bug Report -about: Bug reports about the Solidity Compiler. labels: ["bug :bug:"] --- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 931bc8940..139c11787 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,7 +2,10 @@ blank_issues_enabled: false contact_links: - name: Bug Report url: https://github.com/ethereum/solidity/issues/new?template=bug_report.md&projects=ethereum/solidity/43 + about: Bug reports about the Solidity Compiler. - name: Documentation Issue url: https://github.com/ethereum/solidity/issues/new?template=documentation_issue.md&projects=ethereum/solidity/43 + about: Solidity documentation. - name: Feature Request url: https://github.com/ethereum/solidity/issues/new?template=feature_request.md&projects=ethereum/solidity/43 + about: Solidity language or infrastructure feature requests. diff --git a/.github/ISSUE_TEMPLATE/documentation_issue.md b/.github/ISSUE_TEMPLATE/documentation_issue.md index c5873f726..9e58b8868 100644 --- a/.github/ISSUE_TEMPLATE/documentation_issue.md +++ b/.github/ISSUE_TEMPLATE/documentation_issue.md @@ -1,6 +1,5 @@ --- name: Documentation Issue -about: Solidity documentation. labels: ["documentation :book:"] --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7191414d4..0b5bfbd4a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,5 @@ --- name: Feature Request -about: Solidity language or infrastructure feature requests. labels: ["feature"] --- From 927b24df1f37cdffc794a74081cfb37d01d47c17 Mon Sep 17 00:00:00 2001 From: Christian Parpart Date: Mon, 13 Dec 2021 14:53:40 +0100 Subject: [PATCH 12/15] Initial implementation of Language Server --- .circleci/config.yml | 20 +- Changelog.md | 1 + libsolidity/CMakeLists.txt | 6 + libsolidity/lsp/FileRepository.cpp | 64 ++ libsolidity/lsp/FileRepository.h | 53 ++ libsolidity/lsp/LanguageServer.cpp | 402 ++++++++ libsolidity/lsp/LanguageServer.h | 110 +++ libsolidity/lsp/Transport.cpp | 141 +++ libsolidity/lsp/Transport.h | 101 ++ scripts/tests.sh | 3 + solc/CommandLineInterface.cpp | 19 +- solc/CommandLineInterface.h | 1 + solc/CommandLineParser.cpp | 14 + solc/CommandLineParser.h | 1 + test/libsolidity/lsp/didChange_template.sol | 6 + test/libsolidity/lsp/didOpen_with_import.sol | 12 + test/libsolidity/lsp/lib.sol | 15 + .../libsolidity/lsp/publish_diagnostics_1.sol | 18 + .../libsolidity/lsp/publish_diagnostics_2.sol | 21 + .../libsolidity/lsp/publish_diagnostics_3.sol | 10 + test/lsp.py | 874 ++++++++++++++++++ test/solc/CommandLineInterface.cpp | 2 +- 22 files changed, 1891 insertions(+), 3 deletions(-) create mode 100644 libsolidity/lsp/FileRepository.cpp create mode 100644 libsolidity/lsp/FileRepository.h create mode 100644 libsolidity/lsp/LanguageServer.cpp create mode 100644 libsolidity/lsp/LanguageServer.h create mode 100644 libsolidity/lsp/Transport.cpp create mode 100644 libsolidity/lsp/Transport.h create mode 100644 test/libsolidity/lsp/didChange_template.sol create mode 100644 test/libsolidity/lsp/didOpen_with_import.sol create mode 100644 test/libsolidity/lsp/lib.sol create mode 100644 test/libsolidity/lsp/publish_diagnostics_1.sol create mode 100644 test/libsolidity/lsp/publish_diagnostics_2.sol create mode 100644 test/libsolidity/lsp/publish_diagnostics_3.sol create mode 100755 test/lsp.py diff --git a/.circleci/config.yml b/.circleci/config.yml index cda6e5890..8123fefe8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -198,6 +198,19 @@ defaults: - store_artifacts: *artifacts_test_results - gitter_notify_failure_unless_pr + - steps_test_lsp: &steps_test_lsp + steps: + - checkout + - attach_workspace: + at: build + - run: + name: Install dependencies + command: pip install --user deepdiff colorama + - run: + name: Executing solc LSP test suite + command: ./test/lsp.py ./build/solc/solc + - gitter_notify_failure_unless_pr + - steps_soltest_all: &steps_soltest_all steps: - checkout @@ -519,7 +532,7 @@ jobs: command: apt -q update && apt install -y python3-pip - run: name: Install pylint - command: python3 -m pip install pylint z3-solver pygments-lexer-solidity parsec tabulate + command: python3 -m pip install pylint z3-solver pygments-lexer-solidity parsec tabulate deepdiff colorama # also z3-solver, parsec and tabulate to make sure pylint knows about this module, pygments-lexer-solidity for docs - run: name: Linting Python Scripts @@ -887,6 +900,10 @@ jobs: parallelism: 15 # 7 EVM versions, each with/without optimization + 1 ABIv1/@nooptions run <<: *steps_soltest_all + t_ubu_lsp: &t_ubu_lsp + <<: *base_ubuntu2004_small + <<: *steps_test_lsp + t_archlinux_soltest: &t_archlinux_soltest <<: *base_archlinux environment: @@ -1288,6 +1305,7 @@ workflows: - t_ubu_soltest_enforce_yul: *workflow_ubuntu2004 - b_ubu_clang: *workflow_trigger_on_tags - t_ubu_clang_soltest: *workflow_ubuntu2004_clang + - t_ubu_lsp: *workflow_ubuntu2004 # Ubuntu fake release build and tests - b_ubu_release: *workflow_trigger_on_tags diff --git a/Changelog.md b/Changelog.md index c4b67e935..9e3526fab 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ Language Features: Compiler Features: + * Commandline Interface: Add ``--lsp`` option to get ``solc`` to act as a Language Server (LSP) communicating over stdio. Bugfixes: diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index 7e3791eab..ff82cd9fb 100644 --- a/libsolidity/CMakeLists.txt +++ b/libsolidity/CMakeLists.txt @@ -155,6 +155,12 @@ set(sources interface/StorageLayout.h interface/Version.cpp interface/Version.h + lsp/LanguageServer.cpp + lsp/LanguageServer.h + lsp/FileRepository.cpp + lsp/FileRepository.h + lsp/Transport.cpp + lsp/Transport.h parsing/DocStringParser.cpp parsing/DocStringParser.h parsing/Parser.cpp diff --git a/libsolidity/lsp/FileRepository.cpp b/libsolidity/lsp/FileRepository.cpp new file mode 100644 index 000000000..9c7f72e0c --- /dev/null +++ b/libsolidity/lsp/FileRepository.cpp @@ -0,0 +1,64 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +using namespace std; +using namespace solidity; +using namespace solidity::lsp; + +namespace +{ + +string stripFilePrefix(string const& _path) +{ + if (_path.find("file://") == 0) + return _path.substr(7); + else + return _path; +} + +} + +string FileRepository::sourceUnitNameToClientPath(string const& _sourceUnitName) const +{ + if (m_sourceUnitNamesToClientPaths.count(_sourceUnitName)) + return m_sourceUnitNamesToClientPaths.at(_sourceUnitName); + else if (_sourceUnitName.find("file://") == 0) + return _sourceUnitName; + else + return "file://" + (m_fileReader.basePath() / _sourceUnitName).generic_string(); +} + +string FileRepository::clientPathToSourceUnitName(string const& _path) const +{ + return m_fileReader.cliPathToSourceUnitName(stripFilePrefix(_path)); +} + +map const& FileRepository::sourceUnits() const +{ + return m_fileReader.sourceUnits(); +} + +void FileRepository::setSourceByClientPath(string const& _uri, string _text) +{ + // This is needed for uris outside the base path. It can lead to collisions, + // but we need to mostly rewrite this in a future version anyway. + m_sourceUnitNamesToClientPaths.emplace(clientPathToSourceUnitName(_uri), _uri); + m_fileReader.addOrUpdateFile(stripFilePrefix(_uri), move(_text)); +} diff --git a/libsolidity/lsp/FileRepository.h b/libsolidity/lsp/FileRepository.h new file mode 100644 index 000000000..b6aa5ee08 --- /dev/null +++ b/libsolidity/lsp/FileRepository.h @@ -0,0 +1,53 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +#pragma once + +#include + +#include +#include + +namespace solidity::lsp +{ + +class FileRepository +{ +public: + explicit FileRepository(boost::filesystem::path const& _basePath): + m_fileReader(_basePath) {} + + boost::filesystem::path const& basePath() const { return m_fileReader.basePath(); } + + /// Translates a compiler-internal source unit name to an LSP client path. + std::string sourceUnitNameToClientPath(std::string const& _sourceUnitName) const; + /// Translates an LSP client path into a compiler-internal source unit name. + std::string clientPathToSourceUnitName(std::string const& _uri) const; + + /// @returns all sources by their compiler-internal source unit name. + std::map const& sourceUnits() const; + /// Changes the source identified by the LSP client path _uri to _text. + void setSourceByClientPath(std::string const& _uri, std::string _text); + + frontend::ReadCallback::Callback reader() { return m_fileReader.reader(); } + +private: + std::map m_sourceUnitNamesToClientPaths; + frontend::FileReader m_fileReader; +}; + +} diff --git a/libsolidity/lsp/LanguageServer.cpp b/libsolidity/lsp/LanguageServer.cpp new file mode 100644 index 000000000..586220df1 --- /dev/null +++ b/libsolidity/lsp/LanguageServer.cpp @@ -0,0 +1,402 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include + +#include + +#include +#include + +using namespace std; +using namespace std::placeholders; + +using namespace solidity::lsp; +using namespace solidity::langutil; +using namespace solidity::frontend; + +namespace +{ + +Json::Value toJson(LineColumn _pos) +{ + Json::Value json = Json::objectValue; + json["line"] = max(_pos.line, 0); + json["character"] = max(_pos.column, 0); + + return json; +} + +Json::Value toJsonRange(LineColumn const& _start, LineColumn const& _end) +{ + Json::Value json; + json["start"] = toJson(_start); + json["end"] = toJson(_end); + return json; +} + +optional parseLineColumn(Json::Value const& _lineColumn) +{ + if (_lineColumn.isObject() && _lineColumn["line"].isInt() && _lineColumn["character"].isInt()) + return LineColumn{_lineColumn["line"].asInt(), _lineColumn["character"].asInt()}; + else + return nullopt; +} + +constexpr int toDiagnosticSeverity(Error::Type _errorType) +{ + // 1=Error, 2=Warning, 3=Info, 4=Hint + switch (Error::errorSeverity(_errorType)) + { + case Error::Severity::Error: return 1; + case Error::Severity::Warning: return 2; + case Error::Severity::Info: return 3; + } + solAssert(false); + return -1; +} + +} + +LanguageServer::LanguageServer(Transport& _transport): + m_client{_transport}, + m_handlers{ + {"$/cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}}, + {"cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}}, + {"exit", [this](auto, auto) { m_state = (m_state == State::ShutdownRequested ? State::ExitRequested : State::ExitWithoutShutdown); }}, + {"initialize", bind(&LanguageServer::handleInitialize, this, _1, _2)}, + {"initialized", [](auto, auto) {}}, + {"shutdown", [this](auto, auto) { m_state = State::ShutdownRequested; }}, + {"textDocument/didOpen", bind(&LanguageServer::handleTextDocumentDidOpen, this, _1, _2)}, + {"textDocument/didChange", bind(&LanguageServer::handleTextDocumentDidChange, this, _1, _2)}, + {"textDocument/didClose", bind(&LanguageServer::handleTextDocumentDidClose, this, _1, _2)}, + {"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _1, _2)}, + }, + m_fileRepository("/" /* basePath */), + m_compilerStack{m_fileRepository.reader()} +{ +} + +optional LanguageServer::parsePosition( + string const& _sourceUnitName, + Json::Value const& _position +) const +{ + if (!m_fileRepository.sourceUnits().count(_sourceUnitName)) + return nullopt; + + if (optional lineColumn = parseLineColumn(_position)) + if (optional const offset = CharStream::translateLineColumnToPosition( + m_fileRepository.sourceUnits().at(_sourceUnitName), + *lineColumn + )) + return SourceLocation{*offset, *offset, make_shared(_sourceUnitName)}; + return nullopt; +} + +optional LanguageServer::parseRange(string const& _sourceUnitName, Json::Value const& _range) const +{ + if (!_range.isObject()) + return nullopt; + optional start = parsePosition(_sourceUnitName, _range["start"]); + optional end = parsePosition(_sourceUnitName, _range["end"]); + if (!start || !end) + return nullopt; + solAssert(*start->sourceName == *end->sourceName); + start->end = end->end; + return start; +} + +Json::Value LanguageServer::toRange(SourceLocation const& _location) const +{ + if (!_location.hasText()) + return toJsonRange({}, {}); + + solAssert(_location.sourceName, ""); + CharStream const& stream = m_compilerStack.charStream(*_location.sourceName); + LineColumn start = stream.translatePositionToLineColumn(_location.start); + LineColumn end = stream.translatePositionToLineColumn(_location.end); + return toJsonRange(start, end); +} + +Json::Value LanguageServer::toJson(SourceLocation const& _location) const +{ + solAssert(_location.sourceName); + Json::Value item = Json::objectValue; + item["uri"] = m_fileRepository.sourceUnitNameToClientPath(*_location.sourceName); + item["range"] = toRange(_location); + return item; +} + +void LanguageServer::changeConfiguration(Json::Value const& _settings) +{ + m_settingsObject = _settings; +} + +void LanguageServer::compile() +{ + // For files that are not open, we have to take changes on disk into account, + // so we just remove all non-open files. + + FileRepository oldRepository(m_fileRepository.basePath()); + swap(oldRepository, m_fileRepository); + + for (string const& fileName: m_openFiles) + m_fileRepository.setSourceByClientPath( + fileName, + oldRepository.sourceUnits().at(oldRepository.clientPathToSourceUnitName(fileName)) + ); + + // TODO: optimize! do not recompile if nothing has changed (file(s) not flagged dirty). + + m_compilerStack.reset(false); + m_compilerStack.setSources(m_fileRepository.sourceUnits()); + m_compilerStack.compile(CompilerStack::State::AnalysisPerformed); +} + +void LanguageServer::compileAndUpdateDiagnostics() +{ + compile(); + + // These are the source units we will sent diagnostics to the client for sure, + // even if it is just to clear previous diagnostics. + map diagnosticsBySourceUnit; + for (string const& sourceUnitName: m_fileRepository.sourceUnits() | ranges::views::keys) + diagnosticsBySourceUnit[sourceUnitName] = Json::arrayValue; + for (string const& sourceUnitName: m_nonemptyDiagnostics) + diagnosticsBySourceUnit[sourceUnitName] = Json::arrayValue; + + for (shared_ptr const& error: m_compilerStack.errors()) + { + SourceLocation const* location = error->sourceLocation(); + if (!location || !location->sourceName) + // LSP only has diagnostics applied to individual files. + continue; + + Json::Value jsonDiag; + jsonDiag["source"] = "solc"; + jsonDiag["severity"] = toDiagnosticSeverity(error->type()); + jsonDiag["code"] = Json::UInt64{error->errorId().error}; + string message = error->typeName() + ":"; + if (string const* comment = error->comment()) + message += " " + *comment; + jsonDiag["message"] = move(message); + jsonDiag["range"] = toRange(*location); + + if (auto const* secondary = error->secondarySourceLocation()) + for (auto&& [secondaryMessage, secondaryLocation]: secondary->infos) + { + Json::Value jsonRelated; + jsonRelated["message"] = secondaryMessage; + jsonRelated["location"] = toJson(secondaryLocation); + jsonDiag["relatedInformation"].append(jsonRelated); + } + + diagnosticsBySourceUnit[*location->sourceName].append(jsonDiag); + } + + m_nonemptyDiagnostics.clear(); + for (auto&& [sourceUnitName, diagnostics]: diagnosticsBySourceUnit) + { + Json::Value params; + params["uri"] = m_fileRepository.sourceUnitNameToClientPath(sourceUnitName); + if (!diagnostics.empty()) + m_nonemptyDiagnostics.insert(sourceUnitName); + params["diagnostics"] = move(diagnostics); + m_client.notify("textDocument/publishDiagnostics", move(params)); + } +} + +bool LanguageServer::run() +{ + while (m_state != State::ExitRequested && m_state != State::ExitWithoutShutdown && !m_client.closed()) + { + MessageID id; + try + { + optional const jsonMessage = m_client.receive(); + if (!jsonMessage) + continue; + + if ((*jsonMessage)["method"].isString()) + { + string const methodName = (*jsonMessage)["method"].asString(); + id = (*jsonMessage)["id"]; + + if (auto handler = valueOrDefault(m_handlers, methodName)) + handler(id, (*jsonMessage)["params"]); + else + m_client.error(id, ErrorCode::MethodNotFound, "Unknown method " + methodName); + } + else + m_client.error({}, ErrorCode::ParseError, "\"method\" has to be a string."); + } + catch (...) + { + m_client.error(id, ErrorCode::InternalError, "Unhandled exception: "s + boost::current_exception_diagnostic_information()); + } + } + return m_state == State::ExitRequested; +} + +bool LanguageServer::checkServerInitialized(MessageID _id) +{ + if (m_state != State::Initialized) + { + m_client.error(_id, ErrorCode::ServerNotInitialized, "Server is not properly initialized."); + return false; + } + else + return true; +} + +void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args) +{ + if (m_state != State::Started) + { + m_client.error(_id, ErrorCode::RequestFailed, "Initialize called at the wrong time."); + return; + } + m_state = State::Initialized; + + // The default of FileReader is to use `.`, but the path from where the LSP was started + // should not matter. + string rootPath("/"); + if (Json::Value uri = _args["rootUri"]) + { + rootPath = uri.asString(); + if (!boost::starts_with(rootPath, "file://")) + { + m_client.error(_id, ErrorCode::InvalidParams, "rootUri only supports file URI scheme."); + return; + } + rootPath = rootPath.substr(7); + } + else if (Json::Value rootPath = _args["rootPath"]) + rootPath = rootPath.asString(); + + m_fileRepository = FileRepository(boost::filesystem::path(rootPath)); + if (_args["initializationOptions"].isObject()) + changeConfiguration(_args["initializationOptions"]); + + Json::Value replyArgs; + replyArgs["serverInfo"]["name"] = "solc"; + replyArgs["serverInfo"]["version"] = string(VersionNumber); + replyArgs["capabilities"]["textDocumentSync"]["openClose"] = true; + replyArgs["capabilities"]["textDocumentSync"]["change"] = 2; // 0=none, 1=full, 2=incremental + + m_client.reply(_id, move(replyArgs)); +} + + +void LanguageServer::handleWorkspaceDidChangeConfiguration(MessageID _id, Json::Value const& _args) +{ + if (!checkServerInitialized(_id)) + return; + + if (_args["settings"].isObject()) + changeConfiguration(_args["settings"]); +} + +void LanguageServer::handleTextDocumentDidOpen(MessageID _id, Json::Value const& _args) +{ + if (!checkServerInitialized(_id)) + return; + + if (!_args["textDocument"]) + m_client.error(_id, ErrorCode::RequestFailed, "Text document parameter missing."); + + string text = _args["textDocument"]["text"].asString(); + string uri = _args["textDocument"]["uri"].asString(); + m_openFiles.insert(uri); + m_fileRepository.setSourceByClientPath(uri, move(text)); + compileAndUpdateDiagnostics(); +} + +void LanguageServer::handleTextDocumentDidChange(MessageID _id, Json::Value const& _args) +{ + if (!checkServerInitialized(_id)) + return; + + string const uri = _args["textDocument"]["uri"].asString(); + + for (Json::Value jsonContentChange: _args["contentChanges"]) + { + if (!jsonContentChange.isObject()) + { + m_client.error(_id, ErrorCode::RequestFailed, "Invalid content reference."); + return; + } + + string const sourceUnitName = m_fileRepository.clientPathToSourceUnitName(uri); + if (!m_fileRepository.sourceUnits().count(sourceUnitName)) + { + m_client.error(_id, ErrorCode::RequestFailed, "Unknown file: " + uri); + return; + } + + string text = jsonContentChange["text"].asString(); + if (jsonContentChange["range"].isObject()) // otherwise full content update + { + optional change = parseRange(sourceUnitName, jsonContentChange["range"]); + if (!change || !change->hasText()) + { + m_client.error( + _id, + ErrorCode::RequestFailed, + "Invalid source range: " + jsonCompactPrint(jsonContentChange["range"]) + ); + return; + } + string buffer = m_fileRepository.sourceUnits().at(sourceUnitName); + buffer.replace(static_cast(change->start), static_cast(change->end - change->start), move(text)); + text = move(buffer); + } + m_fileRepository.setSourceByClientPath(uri, move(text)); + } + + compileAndUpdateDiagnostics(); +} + +void LanguageServer::handleTextDocumentDidClose(MessageID _id, Json::Value const& _args) +{ + if (!checkServerInitialized(_id)) + return; + + if (!_args["textDocument"]) + m_client.error(_id, ErrorCode::RequestFailed, "Text document parameter missing."); + + string uri = _args["textDocument"]["uri"].asString(); + m_openFiles.erase(uri); + + compileAndUpdateDiagnostics(); +} diff --git a/libsolidity/lsp/LanguageServer.h b/libsolidity/lsp/LanguageServer.h new file mode 100644 index 000000000..802dd8198 --- /dev/null +++ b/libsolidity/lsp/LanguageServer.h @@ -0,0 +1,110 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace solidity::lsp +{ + +enum class ErrorCode; + +/** + * Solidity Language Server, managing one LSP client. + * This implements a subset of LSP version 3.16 that can be found at: + * https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/ + */ +class LanguageServer +{ +public: + /// @param _transport Customizable transport layer. + explicit LanguageServer(Transport& _transport); + + /// Re-compiles the project and updates the diagnostics pushed to the client. + void compileAndUpdateDiagnostics(); + + /// Loops over incoming messages via the transport layer until shutdown condition is met. + /// + /// The standard shutdown condition is when the maximum number of consecutive failures + /// has been exceeded. + /// + /// @return boolean indicating normal or abnormal termination. + bool run(); + +private: + /// Checks if the server is initialized (to be used by messages that need it to be initialized). + /// Reports an error and returns false if not. + bool checkServerInitialized(MessageID _id); + void handleInitialize(MessageID _id, Json::Value const& _args); + void handleWorkspaceDidChangeConfiguration(MessageID _id, Json::Value const& _args); + void handleTextDocumentDidOpen(MessageID _id, Json::Value const& _args); + void handleTextDocumentDidChange(MessageID _id, Json::Value const& _args); + void handleTextDocumentDidClose(MessageID _id, Json::Value const& _args); + + /// Invoked when the server user-supplied configuration changes (initiated by the client). + void changeConfiguration(Json::Value const&); + + /// Compile everything until after analysis phase. + void compile(); + + std::optional parsePosition( + std::string const& _sourceUnitName, + Json::Value const& _position + ) const; + /// @returns the source location given a source unit name and an LSP Range object, + /// or nullopt on failure. + std::optional parseRange( + std::string const& _sourceUnitName, + Json::Value const& _range + ) const; + Json::Value toRange(langutil::SourceLocation const& _location) const; + Json::Value toJson(langutil::SourceLocation const& _location) const; + + // LSP related member fields + using MessageHandler = std::function; + + enum class State { Started, Initialized, ShutdownRequested, ExitRequested, ExitWithoutShutdown }; + State m_state = State::Started; + + Transport& m_client; + std::map m_handlers; + + /// Set of files known to be open by the client. + std::set m_openFiles; + /// Set of source unit names for which we sent diagnostics to the client in the last iteration. + std::set m_nonemptyDiagnostics; + FileRepository m_fileRepository; + + frontend::CompilerStack m_compilerStack; + + /// User-supplied custom configuration settings (such as EVM version). + Json::Value m_settingsObject; +}; + +} diff --git a/libsolidity/lsp/Transport.cpp b/libsolidity/lsp/Transport.cpp new file mode 100644 index 000000000..fcd3c8249 --- /dev/null +++ b/libsolidity/lsp/Transport.cpp @@ -0,0 +1,141 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +#include + +#include +#include +#include +#include + +#include + +#include +#include + +using namespace std; +using namespace solidity::lsp; + +IOStreamTransport::IOStreamTransport(istream& _in, ostream& _out): + m_input{_in}, + m_output{_out} +{ +} + +IOStreamTransport::IOStreamTransport(): + IOStreamTransport(cin, cout) +{ +} + +bool IOStreamTransport::closed() const noexcept +{ + return m_input.eof(); +} + +optional IOStreamTransport::receive() +{ + auto const headers = parseHeaders(); + if (!headers) + { + error({}, ErrorCode::ParseError, "Could not parse RPC headers."); + return nullopt; + } + + if (!headers->count("content-length")) + { + error({}, ErrorCode::ParseError, "No content-length header found."); + return nullopt; + } + + string const data = util::readBytes(m_input, stoul(headers->at("content-length"))); + + Json::Value jsonMessage; + string jsonParsingErrors; + solidity::util::jsonParseStrict(data, jsonMessage, &jsonParsingErrors); + if (!jsonParsingErrors.empty() || !jsonMessage || !jsonMessage.isObject()) + { + error({}, ErrorCode::ParseError, "Could not parse RPC JSON payload. " + jsonParsingErrors); + return nullopt; + } + + return {move(jsonMessage)}; +} + +void IOStreamTransport::notify(string _method, Json::Value _message) +{ + Json::Value json; + json["method"] = move(_method); + json["params"] = move(_message); + send(move(json)); +} + +void IOStreamTransport::reply(MessageID _id, Json::Value _message) +{ + Json::Value json; + json["result"] = move(_message); + send(move(json), _id); +} + +void IOStreamTransport::error(MessageID _id, ErrorCode _code, string _message) +{ + Json::Value json; + json["error"]["code"] = static_cast(_code); + json["error"]["message"] = move(_message); + send(move(json), _id); +} + +void IOStreamTransport::send(Json::Value _json, MessageID _id) +{ + solAssert(_json.isObject()); + _json["jsonrpc"] = "2.0"; + if (_id != Json::nullValue) + _json["id"] = _id; + + string const jsonString = solidity::util::jsonCompactPrint(_json); + + m_output << "Content-Length: " << jsonString.size() << "\r\n"; + m_output << "\r\n"; + m_output << jsonString; + + m_output.flush(); +} + +optional> IOStreamTransport::parseHeaders() +{ + map headers; + + while (true) + { + string line; + getline(m_input, line); + if (boost::trim_copy(line).empty()) + break; + + auto const delimiterPos = line.find(':'); + if (delimiterPos == string::npos) + return nullopt; + + string name = boost::to_lower_copy(line.substr(0, delimiterPos)); + string value = line.substr(delimiterPos + 1); + if (!headers.emplace( + boost::trim_copy(name), + boost::trim_copy(value) + ).second) + return nullopt; + } + return {move(headers)}; +} diff --git a/libsolidity/lsp/Transport.h b/libsolidity/lsp/Transport.h new file mode 100644 index 000000000..c6ed8fa8a --- /dev/null +++ b/libsolidity/lsp/Transport.h @@ -0,0 +1,101 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace solidity::lsp +{ + +using MessageID = Json::Value; + +enum class ErrorCode +{ + // Defined by JSON RPC + ParseError = -32700, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + + // Defined by the protocol. + ServerNotInitialized = -32002, + RequestFailed = -32803 +}; + +/** + * Transport layer API + * + * The transport layer API is abstracted to make LSP more testable as well as + * this way it could be possible to support other transports (HTTP for example) easily. + */ +class Transport +{ +public: + virtual ~Transport() = default; + + virtual bool closed() const noexcept = 0; + virtual std::optional receive() = 0; + virtual void notify(std::string _method, Json::Value _params) = 0; + virtual void reply(MessageID _id, Json::Value _result) = 0; + virtual void error(MessageID _id, ErrorCode _code, std::string _message) = 0; +}; + +/** + * LSP Transport using JSON-RPC over iostreams. + */ +class IOStreamTransport: public Transport +{ +public: + /// Constructs a standard stream transport layer. + /// + /// @param _in for example std::cin (stdin) + /// @param _out for example std::cout (stdout) + IOStreamTransport(std::istream& _in, std::ostream& _out); + + // Constructs a JSON transport using standard I/O streams. + IOStreamTransport(); + + bool closed() const noexcept override; + std::optional receive() override; + void notify(std::string _method, Json::Value _params) override; + void reply(MessageID _id, Json::Value _result) override; + void error(MessageID _id, ErrorCode _code, std::string _message) override; + +protected: + /// Sends an arbitrary raw message to the client. + /// + /// Used by the notify/reply/error function family. + virtual void send(Json::Value _message, MessageID _id = Json::nullValue); + + /// Parses header section from the client including message-delimiting empty line. + std::optional> parseHeaders(); + +private: + std::istream& m_input; + std::ostream& m_output; +}; + +} diff --git a/scripts/tests.sh b/scripts/tests.sh index 029be1629..5515a6869 100755 --- a/scripts/tests.sh +++ b/scripts/tests.sh @@ -83,6 +83,9 @@ done printTask "Testing Python scripts..." "$REPO_ROOT/test/pyscriptTests.py" +printTask "Testing LSP..." +"$REPO_ROOT/scripts/test_solidity_lsp.py" "${SOLIDITY_BUILD_DIR}/solc/solc" + printTask "Running commandline tests..." # Only run in parallel if this is run on CI infrastructure if [[ -n "$CI" ]] diff --git a/solc/CommandLineInterface.cpp b/solc/CommandLineInterface.cpp index 94bfd8a68..235059a02 100644 --- a/solc/CommandLineInterface.cpp +++ b/solc/CommandLineInterface.cpp @@ -38,6 +38,8 @@ #include #include #include +#include +#include #include @@ -56,6 +58,7 @@ #include #include +#include #include #include @@ -499,7 +502,11 @@ void CommandLineInterface::readInputFiles() m_fileReader.setStdin(readUntilEnd(m_sin)); } - if (m_fileReader.sourceUnits().empty() && !m_standardJsonInput.has_value()) + if ( + m_options.input.mode != InputMode::LanguageServer && + m_fileReader.sourceUnits().empty() && + !m_standardJsonInput.has_value() + ) solThrow(CommandLineValidationError, "All specified input files either do not exist or are not regular files."); } @@ -624,6 +631,9 @@ void CommandLineInterface::processInput() m_standardJsonInput.reset(); break; } + case InputMode::LanguageServer: + serveLSP(); + break; case InputMode::Assembler: assemble(m_options.assembly.inputLanguage, m_options.assembly.targetMachine); break; @@ -884,6 +894,13 @@ void CommandLineInterface::handleAst() } } +void CommandLineInterface::serveLSP() +{ + lsp::IOStreamTransport transport; + if (!lsp::LanguageServer{transport}.run()) + solThrow(CommandLineExecutionError, "LSP terminated abnormally."); +} + void CommandLineInterface::link() { solAssert(m_options.input.mode == InputMode::Linker, ""); diff --git a/solc/CommandLineInterface.h b/solc/CommandLineInterface.h index 9d1646e52..951731825 100644 --- a/solc/CommandLineInterface.h +++ b/solc/CommandLineInterface.h @@ -82,6 +82,7 @@ private: void printVersion(); void printLicense(); void compile(); + void serveLSP(); void link(); void writeLinkedFiles(); /// @returns the ``// -> name`` hint for library placeholders. diff --git a/solc/CommandLineParser.cpp b/solc/CommandLineParser.cpp index 87348686c..65c7d7862 100644 --- a/solc/CommandLineParser.cpp +++ b/solc/CommandLineParser.cpp @@ -59,6 +59,7 @@ static string const g_strIPFS = "ipfs"; static string const g_strLicense = "license"; static string const g_strLibraries = "libraries"; static string const g_strLink = "link"; +static string const g_strLSP = "lsp"; static string const g_strMachine = "machine"; static string const g_strMetadataHash = "metadata-hash"; static string const g_strMetadataLiteral = "metadata-literal"; @@ -135,6 +136,7 @@ static map const g_inputModeName = { {InputMode::Assembler, "assembler"}, {InputMode::StandardJson, "standard JSON"}, {InputMode::Linker, "linker"}, + {InputMode::LanguageServer, "language server (LSP)"}, }; void CommandLineParser::checkMutuallyExclusive(vector const& _optionNames) @@ -455,6 +457,7 @@ void CommandLineParser::parseOutputSelection() case InputMode::Help: case InputMode::License: case InputMode::Version: + case InputMode::LanguageServer: solAssert(false); case InputMode::Compiler: case InputMode::CompilerWithASTImport: @@ -633,6 +636,11 @@ General Information)").c_str(), "Supported Inputs is the output of the --" + g_strStandardJSON + " or the one produced by " "--" + g_strCombinedJson + " " + CombinedJsonRequests::componentName(&CombinedJsonRequests::ast)).c_str() ) + ( + g_strLSP.c_str(), + "Switch to language server mode (\"LSP\"). Allows the compiler to be used as an analysis backend " + "for your favourite IDE." + ) ; desc.add(alternativeInputModes); @@ -865,6 +873,7 @@ void CommandLineParser::processArgs() g_strStrictAssembly, g_strYul, g_strImportAst, + g_strLSP }); if (m_args.count(g_strHelp) > 0) @@ -875,6 +884,8 @@ void CommandLineParser::processArgs() m_options.input.mode = InputMode::Version; else if (m_args.count(g_strStandardJSON) > 0) m_options.input.mode = InputMode::StandardJson; + else if (m_args.count(g_strLSP)) + m_options.input.mode = InputMode::LanguageServer; else if (m_args.count(g_strAssemble) > 0 || m_args.count(g_strStrictAssembly) > 0 || m_args.count(g_strYul) > 0) m_options.input.mode = InputMode::Assembler; else if (m_args.count(g_strLink) > 0) @@ -910,6 +921,9 @@ void CommandLineParser::processArgs() joinOptionNames(invalidOptionsForCurrentInputMode) ); + if (m_options.input.mode == InputMode::LanguageServer) + return; + checkMutuallyExclusive({g_strColor, g_strNoColor}); array const conflictingWithStopAfter{ diff --git a/solc/CommandLineParser.h b/solc/CommandLineParser.h index c1a95a11f..791e7f1c1 100644 --- a/solc/CommandLineParser.h +++ b/solc/CommandLineParser.h @@ -56,6 +56,7 @@ enum class InputMode StandardJson, Linker, Assembler, + LanguageServer }; struct CompilerOutputs diff --git a/test/libsolidity/lsp/didChange_template.sol b/test/libsolidity/lsp/didChange_template.sol new file mode 100644 index 000000000..d08ba140e --- /dev/null +++ b/test/libsolidity/lsp/didChange_template.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +contract C +{ +} diff --git a/test/libsolidity/lsp/didOpen_with_import.sol b/test/libsolidity/lsp/didOpen_with_import.sol new file mode 100644 index 000000000..f505ca6e5 --- /dev/null +++ b/test/libsolidity/lsp/didOpen_with_import.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +import './lib.sol'; + +contract C +{ + function f(uint a, uint b) public pure returns (uint) + { + return Lib.add(2 * a, b); + } +} diff --git a/test/libsolidity/lsp/lib.sol b/test/libsolidity/lsp/lib.sol new file mode 100644 index 000000000..f4fb51e77 --- /dev/null +++ b/test/libsolidity/lsp/lib.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +library Lib +{ + function add(uint a, uint b) public pure returns (uint result) + { + result = a + b; + } + + function warningWithUnused() public pure + { + uint unused; + } +} diff --git a/test/libsolidity/lsp/publish_diagnostics_1.sol b/test/libsolidity/lsp/publish_diagnostics_1.sol new file mode 100644 index 000000000..e66718512 --- /dev/null +++ b/test/libsolidity/lsp/publish_diagnostics_1.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +contract MyContract +{ + constructor() + { + uint unused; // [Warning 2072] Unused local variable. + } +} + +contract D +{ + function main() public payable returns (uint) + { + MyContract c = new MyContract(); + } +} diff --git a/test/libsolidity/lsp/publish_diagnostics_2.sol b/test/libsolidity/lsp/publish_diagnostics_2.sol new file mode 100644 index 000000000..968618955 --- /dev/null +++ b/test/libsolidity/lsp/publish_diagnostics_2.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +contract C +{ + function makeSomeError() public pure returns (uint res) + { + uint x = "hi"; + return; + res = 2; + } +} + +contract D +{ + function main() public payable returns (uint) + { + C c = new C(); + return c.makeSomeError(2, 3); + } +} diff --git a/test/libsolidity/lsp/publish_diagnostics_3.sol b/test/libsolidity/lsp/publish_diagnostics_3.sol new file mode 100644 index 000000000..bb8998a6a --- /dev/null +++ b/test/libsolidity/lsp/publish_diagnostics_3.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +abstract contract A { + function a() public virtual; +} + +contract B is A +{ +} diff --git a/test/lsp.py b/test/lsp.py new file mode 100755 index 000000000..d558c20d9 --- /dev/null +++ b/test/lsp.py @@ -0,0 +1,874 @@ +#!/usr/bin/env python3 + +import argparse +import fnmatch +import json +import os +import subprocess +import traceback + +from typing import Any, List, Optional, Tuple, Union + +import colorama # Enables the use of SGR & CUP terminal VT sequences on Windows. +from deepdiff import DeepDiff + +# {{{ JsonRpcProcess +class BadHeader(Exception): + def __init__(self, msg: str): + super().__init__("Bad header: " + msg) + +class JsonRpcProcess: + exe_path: str + exe_args: List[str] + process: subprocess.Popen + trace_io: bool + + def __init__(self, exe_path: str, exe_args: List[str], trace_io: bool = True): + self.exe_path = exe_path + self.exe_args = exe_args + self.trace_io = trace_io + + def __enter__(self): + self.process = subprocess.Popen( + [self.exe_path, *self.exe_args], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + return self + + def __exit__(self, exception_type, exception_value, traceback) -> None: + self.process.kill() + self.process.wait(timeout=2.0) + + def trace(self, topic: str, message: str) -> None: + if self.trace_io: + print(f"{SGR_TRACE}{topic}:{SGR_RESET} {message}") + + def receive_message(self) -> Union[None, dict]: + # Note, we should make use of timeout to avoid infinite blocking if nothing is received. + CONTENT_LENGTH_HEADER = "Content-Length: " + CONTENT_TYPE_HEADER = "Content-Type: " + if self.process.stdout == None: + return None + message_size = None + while True: + # read header + line = self.process.stdout.readline() + if line == '': + # server quit + return None + line = line.decode("utf-8") + if not line.endswith("\r\n"): + raise BadHeader("missing newline") + # remove the "\r\n" + line = line[:-2] + if line == '': + break # done with the headers + if line.startswith(CONTENT_LENGTH_HEADER): + line = line[len(CONTENT_LENGTH_HEADER):] + if not line.isdigit(): + raise BadHeader("size is not int") + message_size = int(line) + elif line.startswith(CONTENT_TYPE_HEADER): + # nothing todo with type for now. + pass + else: + raise BadHeader("unknown header") + if message_size is None: + raise BadHeader("missing size") + rpc_message = self.process.stdout.read(message_size).decode("utf-8") + json_object = json.loads(rpc_message) + self.trace('receive_message', json.dumps(json_object, indent=4, sort_keys=True)) + return json_object + + def send_message(self, method_name: str, params: Optional[dict]) -> None: + if self.process.stdin == None: + return + message = { + 'jsonrpc': '2.0', + 'method': method_name, + 'params': params + } + json_string = json.dumps(obj=message) + rpc_message = f"Content-Length: {len(json_string)}\r\n\r\n{json_string}" + self.trace(f'send_message ({method_name})', json.dumps(message, indent=4, sort_keys=True)) + self.process.stdin.write(rpc_message.encode("utf-8")) + self.process.stdin.flush() + + def call_method(self, method_name: str, params: Optional[dict]) -> Any: + self.send_message(method_name, params) + return self.receive_message() + + def send_notification(self, name: str, params: Optional[dict] = None) -> None: + self.send_message(name, params) + +# }}} + +SGR_RESET = '\033[m' +SGR_TRACE = '\033[1;36m' +SGR_NOTICE = '\033[1;35m' +SGR_TEST_BEGIN = '\033[1;33m' +SGR_ASSERT_BEGIN = '\033[1;34m' +SGR_STATUS_OKAY = '\033[1;32m' +SGR_STATUS_FAIL = '\033[1;31m' + +class ExpectationFailed(Exception): + def __init__(self, actual, expected): + self.actual = actual + self.expected = expected + diff = DeepDiff(actual, expected) + super().__init__( + f"Expectation failed.\n\tExpected {expected}\n\tbut got {actual}.\n\t{diff}" + ) + +def create_cli_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Solidity LSP Test suite") + parser.set_defaults(trace_io=False) + parser.add_argument( + "-T, --trace-io", + dest="trace_io", + action="store_true", + help="Be more verbose by also printing assertions." + ) + parser.set_defaults(print_assertions=False) + parser.add_argument( + "-v, --print-assertions", + dest="print_assertions", + action="store_true", + help="Be more verbose by also printing assertions." + ) + parser.add_argument( + "-t, --test-pattern", + dest="test_pattern", + type=str, + default="*", + help="Filters all available tests by matching against this test pattern (using globbing)", + nargs="?" + ) + parser.add_argument( + "solc_path", + type=str, + default="solc", + help="Path to solc binary to test against", + nargs="?" + ) + parser.add_argument( + "project_root_dir", + type=str, + default=f"{os.path.dirname(os.path.realpath(__file__))}/..", + help="Path to Solidity project's root directory (must be fully qualified).", + nargs="?" + ) + return parser + +class Counter: + total: int = 0 + passed: int = 0 + failed: int = 0 + +class SolidityLSPTestSuite: # {{{ + test_counter = Counter() + assertion_counter = Counter() + print_assertions: bool = False + trace_io: bool = False + test_pattern: str + + def __init__(self): + colorama.init() + args = create_cli_parser().parse_args() + self.solc_path = args.solc_path + self.project_root_dir = os.path.realpath(args.project_root_dir) + "/test/libsolidity/lsp" + self.project_root_uri = "file://" + self.project_root_dir + self.print_assertions = args.print_assertions + self.trace_io = args.trace_io + self.test_pattern = args.test_pattern + + print(f"{SGR_NOTICE}test pattern: {self.test_pattern}{SGR_RESET}") + + def main(self) -> int: + """ + Runs all test cases. + Returns 0 on success and the number of failing assertions (capped to 127) otherwise. + """ + all_tests = sorted([ + str(name)[5:] + for name in dir(SolidityLSPTestSuite) + if callable(getattr(SolidityLSPTestSuite, name)) and name.startswith("test_") + ]) + filtered_tests = fnmatch.filter(all_tests, self.test_pattern) + for method_name in filtered_tests: + test_fn = getattr(self, 'test_' + method_name) + title: str = test_fn.__name__[5:] + print(f"{SGR_TEST_BEGIN}Testing {title} ...{SGR_RESET}") + try: + with JsonRpcProcess(self.solc_path, ["--lsp"], trace_io=self.trace_io) as solc: + test_fn(solc) + self.test_counter.passed += 1 + except ExpectationFailed as e: + self.test_counter.failed += 1 + print(e) + print(traceback.format_exc()) + except Exception as e: # pragma pylint: disable=broad-except + self.test_counter.failed += 1 + print(f"Unhandled exception {e.__class__.__name__} caught: {e}") + print(traceback.format_exc()) + + print( + f"\n{SGR_NOTICE}Summary:{SGR_RESET}\n\n" + f" Test cases: {self.test_counter.passed} passed, {self.test_counter.failed} failed\n" + f" Assertions: {self.assertion_counter.passed} passed, {self.assertion_counter.failed} failed\n" + ) + + return min(max(self.test_counter.failed, self.assertion_counter.failed), 127) + + def setup_lsp(self, lsp: JsonRpcProcess, expose_project_root=True): + """ + Prepares the solc LSP server by calling `initialize`, + and `initialized` methods. + """ + params = { + 'processId': None, + 'rootUri': self.project_root_uri, + 'trace': 'off', + 'initializationOptions': {}, + 'capabilities': { + 'textDocument': { + 'publishDiagnostics': {'relatedInformation': True} + }, + 'workspace': { + 'applyEdit': True, + 'configuration': True, + 'didChangeConfiguration': {'dynamicRegistration': True}, + 'workspaceEdit': {'documentChanges': True}, + 'workspaceFolders': True + } + } + } + if expose_project_root == False: + params['rootUri'] = None + lsp.call_method('initialize', params) + lsp.send_notification('initialized') + + # {{{ helpers + def get_test_file_path(self, test_case_name): + return f"{self.project_root_dir}/{test_case_name}.sol" + + def get_test_file_uri(self, test_case_name): + return "file://" + self.get_test_file_path(test_case_name) + + def get_test_file_contents(self, test_case_name): + """ + Reads the file contents from disc for a given test case. + The `test_case_name` will be the basename of the file + in the test path (test/libsolidity/lsp). + """ + with open(self.get_test_file_path(test_case_name), mode="r", encoding="utf-8", newline='') as f: + return f.read() + + def require_params_for_method(self, method_name: str, message: dict) -> Any: + """ + Ensures the given RPC message does contain the + field 'method' with the given method name, + and then returns its passed params. + An exception is raised on expectation failures. + """ + assert message is not None + if 'error' in message.keys(): + code = message['error']["code"] + text = message['error']['message'] + raise RuntimeError(f"Error {code} received. {text}") + if 'method' not in message.keys(): + raise RuntimeError("No method received but something else.") + self.expect_equal(message['method'], method_name, "Ensure expected method name") + return message['params'] + + def wait_for_diagnostics(self, solc: JsonRpcProcess, count: int) -> List[dict]: + """ + Return `count` number of published diagnostic reports sorted by file URI. + """ + reports = [] + for _ in range(0, count): + message = solc.receive_message() + assert message is not None # This can happen if the server aborts early. + reports.append( + self.require_params_for_method( + 'textDocument/publishDiagnostics', + message, + ) + ) + return sorted(reports, key=lambda x: x['uri']) + + def open_file_and_wait_for_diagnostics( + self, + solc_process: JsonRpcProcess, + test_case_name: str, + max_diagnostic_reports: int = 1 + ) -> List[Any]: + """ + Opens file for given test case and waits for diagnostics to be published. + """ + assert max_diagnostic_reports > 0 + solc_process.send_message( + 'textDocument/didOpen', + { + 'textDocument': + { + 'uri': self.get_test_file_uri(test_case_name), + 'languageId': 'Solidity', + 'version': 1, + 'text': self.get_test_file_contents(test_case_name) + } + } + ) + return self.wait_for_diagnostics(solc_process, max_diagnostic_reports) + + def expect_equal(self, actual, expected, description="Equality") -> None: + self.assertion_counter.total += 1 + prefix = f"[{self.assertion_counter.total}] {SGR_ASSERT_BEGIN}{description}: " + diff = DeepDiff(actual, expected) + if len(diff) == 0: + self.assertion_counter.passed += 1 + if self.print_assertions: + print(prefix + SGR_STATUS_OKAY + 'OK' + SGR_RESET) + return + + # Failed assertions are always printed. + self.assertion_counter.failed += 1 + print(prefix + SGR_STATUS_FAIL + 'FAILED' + SGR_RESET) + raise ExpectationFailed(actual, expected) + + def expect_empty_diagnostics(self, published_diagnostics: List[dict]) -> None: + self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") + self.expect_equal(len(published_diagnostics[0]['diagnostics']), 0, "should not contain diagnostics") + + def expect_diagnostic( + self, + diagnostic, + code: int, + lineNo: int, + startEndColumns: Tuple[int, int] + ): + assert len(startEndColumns) == 2 + [startColumn, endColumn] = startEndColumns + self.expect_equal(diagnostic['code'], code, f'diagnostic: {code}') + self.expect_equal( + diagnostic['range'], + { + 'start': {'character': startColumn, 'line': lineNo}, + 'end': {'character': endColumn, 'line': lineNo} + }, + "diagnostic: check range" + ) + # }}} + + # {{{ actual tests + def test_publish_diagnostics_warnings(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc) + TEST_NAME = 'publish_diagnostics_1' + published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME) + + self.expect_equal(len(published_diagnostics), 1, "One published_diagnostics message") + report = published_diagnostics[0] + + self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") + diagnostics = report['diagnostics'] + + self.expect_equal(len(diagnostics), 3, "3 diagnostic messages") + self.expect_diagnostic(diagnostics[0], code=6321, lineNo=13, startEndColumns=(44, 48)) + self.expect_diagnostic(diagnostics[1], code=2072, lineNo= 7, startEndColumns=( 8, 19)) + self.expect_diagnostic(diagnostics[2], code=2072, lineNo=15, startEndColumns=( 8, 20)) + + def test_publish_diagnostics_errors(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc) + TEST_NAME = 'publish_diagnostics_2' + published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME) + + self.expect_equal(len(published_diagnostics), 1, "One published_diagnostics message") + report = published_diagnostics[0] + + self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") + diagnostics = report['diagnostics'] + + self.expect_equal(len(diagnostics), 3, "3 diagnostic messages") + self.expect_diagnostic(diagnostics[0], code=9574, lineNo= 7, startEndColumns=( 8, 21)) + self.expect_diagnostic(diagnostics[1], code=6777, lineNo= 8, startEndColumns=( 8, 15)) + self.expect_diagnostic(diagnostics[2], code=6160, lineNo=18, startEndColumns=(15, 36)) + + def test_publish_diagnostics_errors_multiline(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc) + TEST_NAME = 'publish_diagnostics_3' + published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME) + + self.expect_equal(len(published_diagnostics), 1, "One published_diagnostics message") + report = published_diagnostics[0] + + self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") + diagnostics = report['diagnostics'] + + self.expect_equal(len(diagnostics), 1, "3 diagnostic messages") + self.expect_equal(diagnostics[0]['code'], 3656, "diagnostic: check code") + self.expect_equal( + diagnostics[0]['range'], + { + 'end': {'character': 1, 'line': 9}, + 'start': {'character': 0, 'line': 7} + }, + "diagnostic: check range" + ) + + def test_textDocument_didOpen_with_relative_import(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc) + TEST_NAME = 'didOpen_with_import' + published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME, 2) + + self.expect_equal(len(published_diagnostics), 2, "Diagnostic reports for 2 files") + + # primary file: + report = published_diagnostics[0] + self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") + self.expect_equal(len(report['diagnostics']), 0, "no diagnostics") + + # imported file (./lib.sol): + report = published_diagnostics[1] + self.expect_equal(report['uri'], self.get_test_file_uri('lib'), "Correct file URI") + self.expect_equal(len(report['diagnostics']), 1, "one diagnostic") + self.expect_diagnostic(report['diagnostics'][0], code=2072, lineNo=12, startEndColumns=(8, 19)) + + def test_didChange_in_A_causing_error_in_B(self, solc: JsonRpcProcess) -> None: + # Reusing another test but now change some file that generates an error in the other. + self.test_textDocument_didOpen_with_relative_import(solc) + self.open_file_and_wait_for_diagnostics(solc, 'lib', 2) + solc.send_message( + 'textDocument/didChange', + { + 'textDocument': + { + 'uri': self.get_test_file_uri('lib') + }, + 'contentChanges': + [ + { + 'range': { + 'start': { 'line': 5, 'character': 0 }, + 'end': { 'line': 10, 'character': 0 } + }, + 'text': "" # deleting function `add` + } + ] + } + ) + published_diagnostics = self.wait_for_diagnostics(solc, 2) + self.expect_equal(len(published_diagnostics), 2, "Diagnostic reports for 2 files") + + # Main file now contains a new diagnostic + report = published_diagnostics[0] + self.expect_equal(report['uri'], self.get_test_file_uri('didOpen_with_import')) + diagnostics = report['diagnostics'] + self.expect_equal(len(diagnostics), 1, "now, no diagnostics") + self.expect_diagnostic(diagnostics[0], code=9582, lineNo=9, startEndColumns=(15, 22)) + + # The modified file retains the same diagnostics. + report = published_diagnostics[1] + self.expect_equal(report['uri'], self.get_test_file_uri('lib')) + self.expect_equal(len(report['diagnostics']), 0) + # The warning went away because the compiler aborts further processing after the error. + + def test_textDocument_didOpen_with_relative_import_without_project_url(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc, expose_project_root=False) + TEST_NAME = 'didOpen_with_import' + published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME, 2) + self.verify_didOpen_with_import_diagnostics(published_diagnostics) + + def verify_didOpen_with_import_diagnostics( + self, + published_diagnostics: List[Any], + main_file_name='didOpen_with_import' + ): + self.expect_equal(len(published_diagnostics), 2, "Diagnostic reports for 2 files") + + # primary file: + report = published_diagnostics[0] + self.expect_equal(report['uri'], self.get_test_file_uri(main_file_name), "Correct file URI") + self.expect_equal(len(report['diagnostics']), 0, "one diagnostic") + + # imported file (./lib.sol): + report = published_diagnostics[1] + self.expect_equal(report['uri'], self.get_test_file_uri('lib'), "Correct file URI") + self.expect_equal(len(report['diagnostics']), 1, "one diagnostic") + self.expect_diagnostic(report['diagnostics'][0], code=2072, lineNo=12, startEndColumns=(8, 19)) + + def test_textDocument_didChange_updates_diagnostics(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc) + TEST_NAME = 'publish_diagnostics_1' + published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME) + self.expect_equal(len(published_diagnostics), 1, "One published_diagnostics message") + report = published_diagnostics[0] + self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") + diagnostics = report['diagnostics'] + self.expect_equal(len(diagnostics), 3, "3 diagnostic messages") + self.expect_diagnostic(diagnostics[0], code=6321, lineNo=13, startEndColumns=(44, 48)) + self.expect_diagnostic(diagnostics[1], code=2072, lineNo= 7, startEndColumns=( 8, 19)) + self.expect_diagnostic(diagnostics[2], code=2072, lineNo=15, startEndColumns=( 8, 20)) + + solc.send_message( + 'textDocument/didChange', + { + 'textDocument': { + 'uri': self.get_test_file_uri(TEST_NAME) + }, + 'contentChanges': [ + { + 'range': { + 'start': { 'line': 7, 'character': 1 }, + 'end': { 'line': 8, 'character': 1 } + }, + 'text': "" + } + ] + } + ) + published_diagnostics = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(published_diagnostics), 1) + report = published_diagnostics[0] + self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") + diagnostics = report['diagnostics'] + self.expect_equal(len(diagnostics), 2) + self.expect_diagnostic(diagnostics[0], code=6321, lineNo=12, startEndColumns=(44, 48)) + self.expect_diagnostic(diagnostics[1], code=2072, lineNo=14, startEndColumns=( 8, 20)) + + def test_textDocument_didChange_delete_line_and_close(self, solc: JsonRpcProcess) -> None: + # Reuse this test to prepare and ensure it is as expected + self.test_textDocument_didOpen_with_relative_import(solc) + self.open_file_and_wait_for_diagnostics(solc, 'lib', 2) + # lib.sol: Fix the unused variable message by removing it. + solc.send_message( + 'textDocument/didChange', + { + 'textDocument': + { + 'uri': self.get_test_file_uri('lib') + }, + 'contentChanges': # delete the in-body statement: `uint unused;` + [ + { + 'range': + { + 'start': { 'line': 12, 'character': 1 }, + 'end': { 'line': 13, 'character': 1 } + }, + 'text': "" + } + ] + } + ) + published_diagnostics = self.wait_for_diagnostics(solc, 2) + self.expect_equal(len(published_diagnostics), 2, "published diagnostics count") + report1 = published_diagnostics[0] + self.expect_equal(report1['uri'], self.get_test_file_uri('didOpen_with_import'), "Correct file URI") + self.expect_equal(len(report1['diagnostics']), 0, "no diagnostics in didOpen_with_import.sol") + report2 = published_diagnostics[1] + self.expect_equal(report2['uri'], self.get_test_file_uri('lib'), "Correct file URI") + self.expect_equal(len(report2['diagnostics']), 0, "no diagnostics in lib.sol") + + # Now close the file and expect the warning to re-appear + solc.send_message( + 'textDocument/didClose', + { 'textDocument': { 'uri': self.get_test_file_uri('lib') }} + ) + + published_diagnostics = self.wait_for_diagnostics(solc, 2) + self.verify_didOpen_with_import_diagnostics(published_diagnostics) + + def test_textDocument_opening_two_new_files_edit_and_close(self, solc: JsonRpcProcess) -> None: + """ + Open two new files A and B, let A import B, expect no error, + then close B and now expect the error of file B not being found. + """ + + self.setup_lsp(solc) + FILE_A_URI = 'file:///a.sol' + solc.send_message('textDocument/didOpen', { + 'textDocument': { + 'uri': FILE_A_URI, + 'languageId': 'Solidity', + 'version': 1, + 'text': ''.join([ + '// SPDX-License-Identifier: UNLICENSED\n', + 'pragma solidity >=0.8.0;\n', + ]) + } + }) + reports = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(reports), 1, "one publish diagnostics notification") + self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") + + FILE_B_URI = 'file:///b.sol' + solc.send_message('textDocument/didOpen', { + 'textDocument': { + 'uri': FILE_B_URI, + 'languageId': 'Solidity', + 'version': 1, + 'text': ''.join([ + '// SPDX-License-Identifier: UNLICENSED\n', + 'pragma solidity >=0.8.0;\n', + ]) + } + }) + reports = self.wait_for_diagnostics(solc, 2) + self.expect_equal(len(reports), 2, "one publish diagnostics notification") + self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") + self.expect_equal(len(reports[1]['diagnostics']), 0, "should not contain diagnostics") + + solc.send_message('textDocument/didChange', { + 'textDocument': { + 'uri': FILE_A_URI + }, + 'contentChanges': [ + { + 'range': { + 'start': { 'line': 2, 'character': 0 }, + 'end': { 'line': 2, 'character': 0 } + }, + 'text': 'import "./b.sol";\n' + } + ] + }) + reports = self.wait_for_diagnostics(solc, 2) + self.expect_equal(len(reports), 2, "one publish diagnostics notification") + self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") + self.expect_equal(len(reports[1]['diagnostics']), 0, "should not contain diagnostics") + + solc.send_message( + 'textDocument/didClose', + { 'textDocument': { 'uri': FILE_B_URI }} + ) + # We only get one diagnostics message since the diagnostics for b.sol was empty. + reports = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(reports), 1, "one publish diagnostics notification") + self.expect_diagnostic(reports[0]['diagnostics'][0], 6275, 2, (0, 17)) # a.sol: File B not found + self.expect_equal(reports[0]['uri'], FILE_A_URI, "Correct uri") + + def test_textDocument_closing_virtual_file_removes_imported_real_file(self, solc: JsonRpcProcess) -> None: + """ + We open a virtual file that imports a real file with a warning. + Once we close the virtual file, the warning is removed from the diagnostics, + since the real file is not considered part of the project anymore. + """ + + self.setup_lsp(solc) + FILE_A_URI = f'file://{self.project_root_dir}/a.sol' + solc.send_message('textDocument/didOpen', { + 'textDocument': { + 'uri': FILE_A_URI, + 'languageId': 'Solidity', + 'version': 1, + 'text': + '// SPDX-License-Identifier: UNLICENSED\n' + 'pragma solidity >=0.8.0;\n' + 'import "./lib.sol";\n' + } + }) + reports = self.wait_for_diagnostics(solc, 2) + self.expect_equal(len(reports), 2, '') + self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") + self.expect_diagnostic(reports[1]['diagnostics'][0], 2072, 12, (8, 19)) # unused variable in lib.sol + + # Now close the file and expect the warning for lib.sol to be removed + solc.send_message( + 'textDocument/didClose', + { 'textDocument': { 'uri': FILE_A_URI }} + ) + reports = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(reports), 1, '') + self.expect_equal(reports[0]['uri'], f'file://{self.project_root_dir}/lib.sol', "") + self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") + + + def test_textDocument_didChange_at_eol(self, solc: JsonRpcProcess) -> None: + """ + Append at one line and insert a new one below. + """ + self.setup_lsp(solc) + FILE_NAME = 'didChange_template' + FILE_URI = self.get_test_file_uri(FILE_NAME) + solc.send_message('textDocument/didOpen', { + 'textDocument': { + 'uri': FILE_URI, + 'languageId': 'Solidity', + 'version': 1, + 'text': self.get_test_file_contents(FILE_NAME) + } + }) + published_diagnostics = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") + self.expect_equal(len(published_diagnostics[0]['diagnostics']), 0, "no diagnostics") + solc.send_message('textDocument/didChange', { + 'textDocument': { + 'uri': FILE_URI + }, + 'contentChanges': [ + { + 'range': { + 'start': { 'line': 6, 'character': 0 }, + 'end': { 'line': 6, 'character': 0 } + }, + 'text': " f" + } + ] + }) + published_diagnostics = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") + report2 = published_diagnostics[0] + self.expect_equal(report2['uri'], FILE_URI, "Correct file URI") + self.expect_equal(len(report2['diagnostics']), 1, "one diagnostic") + self.expect_diagnostic(report2['diagnostics'][0], 7858, 6, (1, 2)) + + solc.send_message('textDocument/didChange', { + 'textDocument': { 'uri': FILE_URI }, + 'contentChanges': [ + { + 'range': { + 'start': { 'line': 6, 'character': 2 }, + 'end': { 'line': 6, 'character': 2 } + }, + 'text': 'unction f() public {}' + } + ] + }) + published_diagnostics = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") + report3 = published_diagnostics[0] + self.expect_equal(report3['uri'], FILE_URI, "Correct file URI") + self.expect_equal(len(report3['diagnostics']), 1, "one diagnostic") + self.expect_diagnostic(report3['diagnostics'][0], 4126, 6, (1, 23)) + + def test_textDocument_didChange_empty_file(self, solc: JsonRpcProcess) -> None: + """ + Starts with an empty file and changes it to look like + the didOpen_with_import test case. Then we can use + the same verification calls to ensure it worked as expected. + """ + # This FILE_NAME must be alphabetically before lib.sol to not over-complify + # the test logic in verify_didOpen_with_import_diagnostics. + FILE_NAME = 'a_new_file' + FILE_URI = self.get_test_file_uri(FILE_NAME) + self.setup_lsp(solc) + solc.send_message('textDocument/didOpen', { + 'textDocument': { + 'uri': FILE_URI, + 'languageId': 'Solidity', + 'version': 1, + 'text': '' + } + }) + reports = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(reports), 1) + report = reports[0] + published_diagnostics = report['diagnostics'] + self.expect_equal(len(published_diagnostics), 2) + self.expect_diagnostic(published_diagnostics[0], code=1878, lineNo=0, startEndColumns=(0, 0)) + self.expect_diagnostic(published_diagnostics[1], code=3420, lineNo=0, startEndColumns=(0, 0)) + solc.send_message('textDocument/didChange', { + 'textDocument': { + 'uri': self.get_test_file_uri('a_new_file') + }, + 'contentChanges': [ + { + 'range': { + 'start': { 'line': 0, 'character': 0 }, + 'end': { 'line': 0, 'character': 0 } + }, + 'text': self.get_test_file_contents('didOpen_with_import') + } + ] + }) + published_diagnostics = self.wait_for_diagnostics(solc, 2) + self.verify_didOpen_with_import_diagnostics(published_diagnostics, 'a_new_file') + + def test_textDocument_didChange_multi_line(self, solc: JsonRpcProcess) -> None: + """ + Starts with an empty file and changes it to multiple times, changing + content across lines. + """ + self.setup_lsp(solc) + FILE_NAME = 'didChange_template' + FILE_URI = self.get_test_file_uri(FILE_NAME) + solc.send_message('textDocument/didOpen', { + 'textDocument': { + 'uri': FILE_URI, + 'languageId': 'Solidity', + 'version': 1, + 'text': self.get_test_file_contents(FILE_NAME) + } + }) + published_diagnostics = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") + self.expect_equal(len(published_diagnostics[0]['diagnostics']), 0, "no diagnostics") + solc.send_message('textDocument/didChange', { + 'textDocument': { 'uri': FILE_URI }, + 'contentChanges': [ + { + 'range': { + 'start': { 'line': 3, 'character': 3 }, + 'end': { 'line': 4, 'character': 1 } + }, + 'text': "tract D {\n\n uint x\n = -1; \n " + } + ] + }) + published_diagnostics = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") + report2 = published_diagnostics[0] + self.expect_equal(report2['uri'], FILE_URI, "Correct file URI") + self.expect_equal(len(report2['diagnostics']), 1, "one diagnostic") + self.expect_diagnostic(report2['diagnostics'][0], 7407, 6, (3, 5)) + + # Now we are changing the part "x\n = -" of "uint x\n = -1;" + solc.send_message('textDocument/didChange', { + 'textDocument': { 'uri': FILE_URI }, + 'contentChanges': [ + { + 'range': { + 'start': { 'line': 5, 'character': 7 }, + 'end': { 'line': 6, 'character': 4 } + }, + 'text': "y\n = [\nuint(1),\n3,4]+" + } + ] + }) + published_diagnostics = self.wait_for_diagnostics(solc, 1) + self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") + report3 = published_diagnostics[0] + self.expect_equal(report3['uri'], FILE_URI, "Correct file URI") + self.expect_equal(len(report3['diagnostics']), 2, "two diagnostics") + diagnostic = report3['diagnostics'][0] + self.expect_equal(diagnostic['code'], 2271, 'diagnostic: 2271') + # check multi-line error code + self.expect_equal( + diagnostic['range'], + { + 'end': {'character': 6, 'line': 8}, + 'start': {'character': 3, 'line': 6} + }, + "diagnostic: check range" + ) + diagnostic = report3['diagnostics'][1] + self.expect_equal(diagnostic['code'], 7407, 'diagnostic: 7407') + # check multi-line error code + self.expect_equal( + diagnostic['range'], + { + 'end': {'character': 6, 'line': 8}, + 'start': {'character': 3, 'line': 6} + }, + "diagnostic: check range" + ) + + # }}} + # }}} + +if __name__ == "__main__": + suite = SolidityLSPTestSuite() + exit_code = suite.main() + exit(exit_code) diff --git a/test/solc/CommandLineInterface.cpp b/test/solc/CommandLineInterface.cpp index 494dbbd94..73c999f3f 100644 --- a/test/solc/CommandLineInterface.cpp +++ b/test/solc/CommandLineInterface.cpp @@ -158,7 +158,7 @@ BOOST_AUTO_TEST_CASE(multiple_input_modes) }; string expectedMessage = "The following options are mutually exclusive: " - "--help, --license, --version, --standard-json, --link, --assemble, --strict-assembly, --yul, --import-ast. " + "--help, --license, --version, --standard-json, --link, --assemble, --strict-assembly, --yul, --import-ast, --lsp. " "Select at most one."; for (string const& mode1: inputModeOptions) From 84738eb9e6c630949cff0a21c1faedd8cbed9274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Thu, 16 Dec 2021 18:05:25 +0100 Subject: [PATCH 13/15] Add security policy to github's template chooser --- .github/ISSUE_TEMPLATE/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 139c11787..a5a945a67 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -9,3 +9,6 @@ contact_links: - name: Feature Request url: https://github.com/ethereum/solidity/issues/new?template=feature_request.md&projects=ethereum/solidity/43 about: Solidity language or infrastructure feature requests. + - name: Report a security vulnerability + url: https://github.com/ethereum/solidity/security/policy + about: Please review our security policy for more details. From b3d9c596cd8371728935cfaedb060ff439075315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Thu, 16 Dec 2021 18:12:04 +0100 Subject: [PATCH 14/15] Move issue template labels to config.yml --- .github/ISSUE_TEMPLATE/bug_report.md | 1 - .github/ISSUE_TEMPLATE/config.yml | 6 +++--- .github/ISSUE_TEMPLATE/documentation_issue.md | 1 - .github/ISSUE_TEMPLATE/feature_request.md | 1 - 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7be77f422..36e25b1f0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,5 @@ --- name: Bug Report -labels: ["bug :bug:"] ---