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/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 571b97474..36e25b1f0 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. --- ## Description diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..b4e9b3d50 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +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&labels=bug+%3Abug%3A + 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&labels=documentation+%3Abook%3A + about: Solidity documentation. + - name: Feature Request + url: https://github.com/ethereum/solidity/issues/new?template=feature_request.md&projects=ethereum/solidity/43&labels=feature + 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. diff --git a/.github/ISSUE_TEMPLATE/documentation_issue.md b/.github/ISSUE_TEMPLATE/documentation_issue.md index c0706b5f3..7509ec395 100644 --- a/.github/ISSUE_TEMPLATE/documentation_issue.md +++ b/.github/ISSUE_TEMPLATE/documentation_issue.md @@ -1,22 +1,15 @@ --- name: Documentation Issue -about: Solidity documentation. --- ## Page - + ## Abstract - + ## Pull request - + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 6702b62cb..900e6977c 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. --- ## Abstract - + ## Motivation - + ## Specification - + ## Backwards Compatibility \ No newline at end of file +--> diff --git a/.github/ISSUE_TEMPLATE/general.md b/.github/ISSUE_TEMPLATE/general.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/Changelog.md b/Changelog.md index 1fa3ee987..0546c1699 100644 --- a/Changelog.md +++ b/Changelog.md @@ -11,9 +11,11 @@ Breaking changes: ### 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: + * Commandline Interface: Add ``--lsp`` option to get ``solc`` to act as a Language Server (LSP) communicating over stdio. Bugfixes: @@ -21,7 +23,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/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/control-structures.rst b/docs/control-structures.rst index 84b65918d..7881a22ec 100644 --- a/docs/control-structures.rst +++ b/docs/control-structures.rst @@ -648,7 +648,7 @@ in the following situations: #. If your contract receives Ether via a public getter function. For the following cases, the error data from the external call -(if provided) is forwarded. This mean that it can either cause +(if provided) is forwarded. This means that it can either cause an `Error` or a `Panic` (or whatever else was given): #. If a ``.transfer()`` fails. @@ -718,7 +718,7 @@ The ``revert`` statement takes a custom error as direct argument without parenth revert CustomError(arg1, arg2); -For backards-compatibility reasons, there is also the ``revert()`` function, which uses parentheses +For backwards-compatibility reasons, there is also the ``revert()`` function, which uses parentheses and accepts a string: revert(); @@ -863,7 +863,7 @@ type of error: It is planned to support other types of error data in the future. -The strings ``Error`` and ``Panic`` are currently parsed as is and are not treated as an identifiers. +The strings ``Error`` and ``Panic`` are currently parsed as is and are not treated as identifiers. In order to catch all error cases, you have to have at least the clause ``catch { ...}`` or the clause ``catch (bytes memory lowLevelData) { ... }``. 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 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/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/analysis/TypeChecker.cpp b/libsolidity/analysis/TypeChecker.cpp index b45245627..2a8d1c98e 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 a6e57a9ac..3bad6d24e 100644 --- a/libsolidity/ast/Types.cpp +++ b/libsolidity/ast/Types.cpp @@ -2939,6 +2939,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; @@ -3503,6 +3504,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 || @@ -4005,6 +4007,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/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/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/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/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; 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 aecc49d81..40f1c2361 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 53d293d59..beab340b4 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) @@ -443,6 +445,7 @@ void CommandLineParser::parseOutputSelection() case InputMode::Help: case InputMode::License: case InputMode::Version: + case InputMode::LanguageServer: solAssert(false); case InputMode::Compiler: case InputMode::CompilerWithASTImport: @@ -610,6 +613,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); @@ -842,6 +850,7 @@ void CommandLineParser::processArgs() g_strStrictAssembly, g_strYul, g_strImportAst, + g_strLSP }); if (m_args.count(g_strHelp) > 0) @@ -852,6 +861,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) @@ -887,6 +898,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/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/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/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/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..8bd9ee42c --- /dev/null +++ b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic.sol @@ -0,0 +1,53 @@ +contract C { + function testFunction1() public {} + function testFunction2() public {} + function testFunction3() public {} + + + 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))); + externalArray0 = externalArray1; + return keccak256(abi.encode(externalArray0)) == keccak256(abi.encode(externalArray1)); + } + + 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 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 new file mode 100644 index 000000000..63236ed77 --- /dev/null +++ b/test/libsolidity/semanticTests/array/copying/array_of_function_external_storage_to_storage_dynamic_different_mutability.sol @@ -0,0 +1,57 @@ +contract C { + function testFunction1() public {} + function testFunction2() public view {} + function testFunction3() public pure {} + + + 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) + { + assert(keccak256(abi.encodePacked(externalArray0)) != keccak256(abi.encodePacked(externalArray1))); + externalArray0 = externalArray1; + return keccak256(abi.encodePacked(externalArray0)) == keccak256(abi.encodePacked(externalArray1)); + } + + 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 irOptimized: 104372 +// gas legacy: 108462 +// gas legacyOptimized: 102174 +// 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/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 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/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. 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. 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 b5fcbee57..cd6b9955b 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)