Merge pull request #12423 from ethereum/develop

Merge develop into breaking.
This commit is contained in:
chriseth 2021-12-20 11:40:40 +01:00 committed by GitHub
commit 923d1cf2d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 3206 additions and 592 deletions

View File

@ -198,6 +198,19 @@ defaults:
- store_artifacts: *artifacts_test_results - store_artifacts: *artifacts_test_results
- gitter_notify_failure_unless_pr - 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_soltest_all: &steps_soltest_all
steps: steps:
- checkout - checkout
@ -519,7 +532,7 @@ jobs:
command: apt -q update && apt install -y python3-pip command: apt -q update && apt install -y python3-pip
- run: - run:
name: Install pylint 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 # also z3-solver, parsec and tabulate to make sure pylint knows about this module, pygments-lexer-solidity for docs
- run: - run:
name: Linting Python Scripts name: Linting Python Scripts
@ -887,6 +900,10 @@ jobs:
parallelism: 15 # 7 EVM versions, each with/without optimization + 1 ABIv1/@nooptions run parallelism: 15 # 7 EVM versions, each with/without optimization + 1 ABIv1/@nooptions run
<<: *steps_soltest_all <<: *steps_soltest_all
t_ubu_lsp: &t_ubu_lsp
<<: *base_ubuntu2004_small
<<: *steps_test_lsp
t_archlinux_soltest: &t_archlinux_soltest t_archlinux_soltest: &t_archlinux_soltest
<<: *base_archlinux <<: *base_archlinux
environment: environment:
@ -1288,6 +1305,7 @@ workflows:
- t_ubu_soltest_enforce_yul: *workflow_ubuntu2004 - t_ubu_soltest_enforce_yul: *workflow_ubuntu2004
- b_ubu_clang: *workflow_trigger_on_tags - b_ubu_clang: *workflow_trigger_on_tags
- t_ubu_clang_soltest: *workflow_ubuntu2004_clang - t_ubu_clang_soltest: *workflow_ubuntu2004_clang
- t_ubu_lsp: *workflow_ubuntu2004
# Ubuntu fake release build and tests # Ubuntu fake release build and tests
- b_ubu_release: *workflow_trigger_on_tags - b_ubu_release: *workflow_trigger_on_tags

View File

@ -1,6 +1,5 @@
--- ---
name: Bug Report name: Bug Report
about: Bug reports about the Solidity Compiler.
--- ---
<!--## Prerequisites <!--## Prerequisites
@ -8,13 +7,12 @@ about: Bug reports about the Solidity Compiler.
- First, many thanks for taking part in the community. We really appreciate that. - First, many thanks for taking part in the community. We really appreciate that.
- We realize there is a lot of information requested here. We ask only that you do your best to provide as much information as possible so we can better help you. - We realize there is a lot of information requested here. We ask only that you do your best to provide as much information as possible so we can better help you.
- Support questions are better asked in one of the following locations: - Support questions are better asked in one of the following locations:
- [Solidity chat](https://gitter.im/ethereum/solidity) - [Solidity chat](https://gitter.im/ethereum/solidity)
- [Stack Overflow](https://ethereum.stackexchange.com/) - [Stack Overflow](https://ethereum.stackexchange.com/)
- Ensure the issue isn't already reported. - Ensure the issue isn't already reported.
- The issue should be reproducible with the latest solidity version; however, this isn't a hard requirement and being reproducible with an older version is sufficient. - The issue should be reproducible with the latest solidity version; however, this isn't a hard requirement and being reproducible with an older version is sufficient.
*Delete the above section and the instructions in the sections below before submitting* *Delete the above section and the instructions in the sections below before submitting*
--> -->
## Description ## Description

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -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.

View File

@ -1,22 +1,15 @@
--- ---
name: Documentation Issue name: Documentation Issue
about: Solidity documentation.
--- ---
## Page ## Page
<!-- <!--Please link directly to the page which you think has a problem.-->
Please link directly to the page which you think has a problem
-->
## Abstract ## Abstract
<!-- <!--Please describe in detail what is wrong.-->
Please describe in detail what is wrong.
-->
## Pull request ## Pull request
<!-- <!--Please link to your pull request which resolves this issue.-->
Please link to your pull request which resolves this issue
-->

View File

@ -1,6 +1,5 @@
--- ---
name: Feature Request name: Feature Request
about: Solidity language or infrastructure feature requests.
--- ---
<!--## Prerequisites <!--## Prerequisites
@ -8,40 +7,29 @@ about: Solidity language or infrastructure feature requests.
- First, many thanks for taking part in the community. We really appreciate that. - First, many thanks for taking part in the community. We really appreciate that.
- We realize there is a lot of data requested here. We ask only that you do your best to provide as much information as possible so we can better help you. - We realize there is a lot of data requested here. We ask only that you do your best to provide as much information as possible so we can better help you.
- Support questions are better asked in one of the following locations: - Support questions are better asked in one of the following locations:
- [Solidity chat](https://gitter.im/ethereum/solidity) - [Solidity chat](https://gitter.im/ethereum/solidity)
- [Stack Overflow](https://ethereum.stackexchange.com/) - [Stack Overflow](https://ethereum.stackexchange.com/)
- Ensure the issue isn't already reported (check `feature` and `language design` labels). - Ensure the issue isn't already reported (check `feature` and `language design` labels).
*Delete the above section and the instructions in the sections below before submitting* *Delete the above section and the instructions in the sections below before submitting*
--> -->
## Abstract ## Abstract
<!-- <!--Please describe by example what problem you see in the current Solidity language and reason about it.-->
Please describe by example what problem you see in the current Solidity language
and reason about it.
-->
## Motivation ## Motivation
<!-- <!--In this section you describe how you propose to address the problem you described earlier, including by giving one or more exemplary source code snippets for demonstration.-->
In this section you describe how you propose to address the problem you described earlier,
including by giving one or more exemplary source code snippets for demonstration.
-->
## Specification ## Specification
<!-- <!--The technical specification should describe the syntax and semantics of any new feature. The specification should be detailed enough to allow any developer to implement the functionality.-->
The technical specification should describe the syntax and semantics of any new feature. The
specification should be detailed enough to allow any developer to implement the functionality.
-->
## Backwards Compatibility ## Backwards Compatibility
<!-- <!--
All language changes that introduce backwards incompatibilities must include a section describing All language changes that introduce backwards incompatibilities must include a section describing these incompatibilities and their severity.
these incompatibilities and their severity.
Please describe how you propose to deal with these incompatibilities. Please describe how you propose to deal with these incompatibilities.
--> -->

View File

View File

@ -11,9 +11,11 @@ Breaking changes:
### 0.8.11 (unreleased) ### 0.8.11 (unreleased)
Language Features: 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: Compiler Features:
* Commandline Interface: Add ``--lsp`` option to get ``solc`` to act as a Language Server (LSP) communicating over stdio.
Bugfixes: 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 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. * 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. * 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) ### 0.8.10 (2021-11-09)

View File

@ -80,6 +80,8 @@ Global Variables
the given arguments. Note that this encoding can be ambiguous! the given arguments. Note that this encoding can be ambiguous!
- ``abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)``: :ref:`ABI <ABI>`-encodes - ``abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)``: :ref:`ABI <ABI>`-encodes
the given arguments starting from the second and prepends the given four-byte selector 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 - ``abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)``: Equivalent
to ``abi.encodeWithSelector(bytes4(keccak256(bytes(signature)), ...)`` to ``abi.encodeWithSelector(bytes4(keccak256(bytes(signature)), ...)``
- ``bytes.concat(...) returns (bytes memory)``: :ref:`Concatenates variable number of - ``bytes.concat(...) returns (bytes memory)``: :ref:`Concatenates variable number of

View File

@ -648,7 +648,7 @@ in the following situations:
#. If your contract receives Ether via a public getter function. #. If your contract receives Ether via a public getter function.
For the following cases, the error data from the external call 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): an `Error` or a `Panic` (or whatever else was given):
#. If a ``.transfer()`` fails. #. If a ``.transfer()`` fails.
@ -718,7 +718,7 @@ The ``revert`` statement takes a custom error as direct argument without parenth
revert CustomError(arg1, arg2); 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: and accepts a string:
revert(); revert();
@ -863,7 +863,7 @@ type of error:
It is planned to support other types of error data in the future. 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 In order to catch all error cases, you have to have at least the clause
``catch { ...}`` or the clause ``catch (bytes memory lowLevelData) { ... }``. ``catch { ...}`` or the clause ``catch (bytes memory lowLevelData) { ... }``.

View File

@ -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 <rational_literals>` 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 If ``a`` is an LValue (i.e. a variable or something that can be assigned to), the
following operators are available as shorthands: 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 of ``a``. In contrast, ``--a`` and ``++a`` have the same effect on ``a`` but
return the value after the change. return the value after the change.
.. index:: !delete
.. _delete: .. _delete:
delete delete

View File

@ -136,6 +136,7 @@ ABI Encoding and Decoding Functions
- ``abi.encodePacked(...) returns (bytes memory)``: Performs :ref:`packed encoding <abi_packed_mode>` of the given arguments. Note that packed encoding can be ambiguous! - ``abi.encodePacked(...) returns (bytes memory)``: Performs :ref:`packed encoding <abi_packed_mode>` 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.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.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:: .. note::
These encoding functions can be used to craft data for external function calls without actually These encoding functions can be used to craft data for external function calls without actually

View File

@ -155,6 +155,12 @@ set(sources
interface/StorageLayout.h interface/StorageLayout.h
interface/Version.cpp interface/Version.cpp
interface/Version.h 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.cpp
parsing/DocStringParser.h parsing/DocStringParser.h
parsing/Parser.cpp parsing/Parser.cpp

View File

@ -1996,6 +1996,7 @@ void TypeChecker::typeCheckABIEncodeFunctions(
_functionType->kind() == FunctionType::Kind::ABIEncode || _functionType->kind() == FunctionType::Kind::ABIEncode ||
_functionType->kind() == FunctionType::Kind::ABIEncodePacked || _functionType->kind() == FunctionType::Kind::ABIEncodePacked ||
_functionType->kind() == FunctionType::Kind::ABIEncodeWithSelector || _functionType->kind() == FunctionType::Kind::ABIEncodeWithSelector ||
_functionType->kind() == FunctionType::Kind::ABIEncodeCall ||
_functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature, _functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature,
"ABI function has unexpected FunctionType::Kind." "ABI function has unexpected FunctionType::Kind."
); );
@ -2020,6 +2021,13 @@ void TypeChecker::typeCheckABIEncodeFunctions(
// Perform standard function call type checking // Perform standard function call type checking
typeCheckFunctionGeneralChecks(_functionCall, _functionType); 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 // Check additional arguments for variadic functions
vector<ASTPointer<Expression const>> const& arguments = _functionCall.arguments(); vector<ASTPointer<Expression const>> const& arguments = _functionCall.arguments();
for (size_t i = 0; i < arguments.size(); ++i) for (size_t i = 0; i < arguments.size(); ++i)
@ -2078,6 +2086,110 @@ void TypeChecker::typeCheckABIEncodeFunctions(
} }
} }
void TypeChecker::typeCheckABIEncodeCallFunction(FunctionCall const& _functionCall)
{
vector<ASTPointer<Expression const>> 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<FunctionTypePointer>(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<ASTPointer<Expression const>> callArguments;
auto const* tupleType = dynamic_cast<TupleType const*>(type(*arguments[1]));
if (tupleType)
{
auto const& argumentTuple = dynamic_cast<TupleExpression const&>(*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( void TypeChecker::typeCheckBytesConcatFunction(
FunctionCall const& _functionCall, FunctionCall const& _functionCall,
FunctionType const* _functionType FunctionType const* _functionType
@ -2507,6 +2619,7 @@ bool TypeChecker::visit(FunctionCall const& _functionCall)
case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodePacked:
case FunctionType::Kind::ABIEncodeWithSelector: case FunctionType::Kind::ABIEncodeWithSelector:
case FunctionType::Kind::ABIEncodeWithSignature: case FunctionType::Kind::ABIEncodeWithSignature:
case FunctionType::Kind::ABIEncodeCall:
{ {
typeCheckABIEncodeFunctions(_functionCall, functionType); typeCheckABIEncodeFunctions(_functionCall, functionType);
returnTypes = functionType->returnParameterTypes(); returnTypes = functionType->returnParameterTypes();

View File

@ -110,6 +110,9 @@ private:
FunctionTypePointer _functionType 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 /// Performs general checks and checks specific to bytes concat function call
void typeCheckBytesConcatFunction( void typeCheckBytesConcatFunction(
FunctionCall const& _functionCall, FunctionCall const& _functionCall,

View File

@ -367,6 +367,7 @@ void ViewPureChecker::endVisit(MemberAccess const& _memberAccess)
{MagicType::Kind::ABI, "encode"}, {MagicType::Kind::ABI, "encode"},
{MagicType::Kind::ABI, "encodePacked"}, {MagicType::Kind::ABI, "encodePacked"},
{MagicType::Kind::ABI, "encodeWithSelector"}, {MagicType::Kind::ABI, "encodeWithSelector"},
{MagicType::Kind::ABI, "encodeCall"},
{MagicType::Kind::ABI, "encodeWithSignature"}, {MagicType::Kind::ABI, "encodeWithSignature"},
{MagicType::Kind::Message, "data"}, {MagicType::Kind::Message, "data"},
{MagicType::Kind::Message, "sig"}, {MagicType::Kind::Message, "sig"},

View File

@ -2939,6 +2939,7 @@ string FunctionType::richIdentifier() const
case Kind::ABIEncode: id += "abiencode"; break; case Kind::ABIEncode: id += "abiencode"; break;
case Kind::ABIEncodePacked: id += "abiencodepacked"; break; case Kind::ABIEncodePacked: id += "abiencodepacked"; break;
case Kind::ABIEncodeWithSelector: id += "abiencodewithselector"; break; case Kind::ABIEncodeWithSelector: id += "abiencodewithselector"; break;
case Kind::ABIEncodeCall: id += "abiencodecall"; break;
case Kind::ABIEncodeWithSignature: id += "abiencodewithsignature"; break; case Kind::ABIEncodeWithSignature: id += "abiencodewithsignature"; break;
case Kind::ABIDecode: id += "abidecode"; break; case Kind::ABIDecode: id += "abidecode"; break;
case Kind::MetaType: id += "metatype"; break; case Kind::MetaType: id += "metatype"; break;
@ -3503,6 +3504,7 @@ bool FunctionType::isPure() const
m_kind == Kind::ABIEncode || m_kind == Kind::ABIEncode ||
m_kind == Kind::ABIEncodePacked || m_kind == Kind::ABIEncodePacked ||
m_kind == Kind::ABIEncodeWithSelector || m_kind == Kind::ABIEncodeWithSelector ||
m_kind == Kind::ABIEncodeCall ||
m_kind == Kind::ABIEncodeWithSignature || m_kind == Kind::ABIEncodeWithSignature ||
m_kind == Kind::ABIDecode || m_kind == Kind::ABIDecode ||
m_kind == Kind::MetaType || m_kind == Kind::MetaType ||
@ -4005,6 +4007,15 @@ MemberList::MemberMap MagicType::nativeMembers(ASTNode const*) const
true, true,
StateMutability::Pure StateMutability::Pure
)}, )},
{"encodeCall", TypeProvider::function(
TypePointers{},
TypePointers{TypeProvider::array(DataLocation::Memory)},
strings{},
strings{1, ""},
FunctionType::Kind::ABIEncodeCall,
true,
StateMutability::Pure
)},
{"encodeWithSignature", TypeProvider::function( {"encodeWithSignature", TypeProvider::function(
TypePointers{TypeProvider::array(DataLocation::Memory, true)}, TypePointers{TypeProvider::array(DataLocation::Memory, true)},
TypePointers{TypeProvider::array(DataLocation::Memory)}, TypePointers{TypeProvider::array(DataLocation::Memory)},

View File

@ -1237,6 +1237,7 @@ public:
ABIEncode, ABIEncode,
ABIEncodePacked, ABIEncodePacked,
ABIEncodeWithSelector, ABIEncodeWithSelector,
ABIEncodeCall,
ABIEncodeWithSignature, ABIEncodeWithSignature,
ABIDecode, ABIDecode,
GasLeft, ///< gasleft() GasLeft, ///< gasleft()

View File

@ -1236,28 +1236,47 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall)
case FunctionType::Kind::ABIEncode: case FunctionType::Kind::ABIEncode:
case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodePacked:
case FunctionType::Kind::ABIEncodeWithSelector: case FunctionType::Kind::ABIEncodeWithSelector:
case FunctionType::Kind::ABIEncodeCall:
case FunctionType::Kind::ABIEncodeWithSignature: case FunctionType::Kind::ABIEncodeWithSignature:
{ {
bool const isPacked = function.kind() == FunctionType::Kind::ABIEncodePacked; bool const isPacked = function.kind() == FunctionType::Kind::ABIEncodePacked;
bool const hasSelectorOrSignature = bool const hasSelectorOrSignature =
function.kind() == FunctionType::Kind::ABIEncodeWithSelector || function.kind() == FunctionType::Kind::ABIEncodeWithSelector ||
function.kind() == FunctionType::Kind::ABIEncodeCall ||
function.kind() == FunctionType::Kind::ABIEncodeWithSignature; function.kind() == FunctionType::Kind::ABIEncodeWithSignature;
TypePointers argumentTypes; 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); solAssert(arguments.size() == 2);
// Do not keep the selector as part of the ABI encoded args
if (!hasSelectorOrSignature || i > 0) auto const functionPtr = dynamic_cast<FunctionTypePointer>(arguments[0]->annotation().type);
argumentTypes.push_back(arguments[i]->annotation().type); solAssert(functionPtr);
solAssert(functionPtr->sizeOnStack() == 2);
// Account for tuples with one component which become that component
if (auto const tupleType = dynamic_cast<TupleType const*>(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(); utils().fetchFreeMemoryPointer();
// stack now: [<selector>] <arg1> .. <argN> <free_mem> // stack now: [<selector/functionPointer/signature>] <arg1> .. <argN> <free_mem>
// adjust by 32(+4) bytes to accommodate the length(+selector) // adjust by 32(+4) bytes to accommodate the length(+selector)
m_context << u256(32 + (hasSelectorOrSignature ? 4 : 0)) << Instruction::ADD; m_context << u256(32 + (hasSelectorOrSignature ? 4 : 0)) << Instruction::ADD;
// stack now: [<selector>] <arg1> .. <argN> <data_encoding_area_start> // stack now: [<selector/functionPointer/signature>] <arg1> .. <argN> <data_encoding_area_start>
if (isPacked) if (isPacked)
{ {
@ -1270,7 +1289,7 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall)
utils().abiEncode(argumentTypes, TypePointers()); utils().abiEncode(argumentTypes, TypePointers());
} }
utils().fetchFreeMemoryPointer(); utils().fetchFreeMemoryPointer();
// stack: [<selector>] <data_encoding_area_end> <bytes_memory_ptr> // stack: [<selector/functionPointer/signature>] <data_encoding_area_end> <bytes_memory_ptr>
// size is end minus start minus length slot // size is end minus start minus length slot
m_context.appendInlineAssembly(R"({ m_context.appendInlineAssembly(R"({
@ -1278,16 +1297,17 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall)
})", {"mem_end", "mem_ptr"}); })", {"mem_end", "mem_ptr"});
m_context << Instruction::SWAP1; m_context << Instruction::SWAP1;
utils().storeFreeMemoryPointer(); utils().storeFreeMemoryPointer();
// stack: [<selector>] <memory ptr> // stack: [<selector/functionPointer/signature>] <memory ptr>
if (hasSelectorOrSignature) if (hasSelectorOrSignature)
{ {
// stack: <selector> <memory pointer> // stack: <selector/functionPointer/signature> <memory pointer>
solAssert(arguments.size() >= 1, ""); solAssert(arguments.size() >= 1, "");
Type const* selectorType = arguments[0]->annotation().type; Type const* selectorType = arguments[0]->annotation().type;
utils().moveIntoStack(selectorType->sizeOnStack()); utils().moveIntoStack(selectorType->sizeOnStack());
Type const* dataOnStack = selectorType; Type const* dataOnStack = selectorType;
// stack: <memory pointer> <selector>
// stack: <memory pointer> <selector/functionPointer/signature>
if (function.kind() == FunctionType::Kind::ABIEncodeWithSignature) if (function.kind() == FunctionType::Kind::ABIEncodeWithSignature)
{ {
// hash the signature // hash the signature
@ -1299,7 +1319,7 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall)
else else
{ {
utils().fetchFreeMemoryPointer(); utils().fetchFreeMemoryPointer();
// stack: <memory pointer> <selector> <free mem ptr> // stack: <memory pointer> <signature> <free mem ptr>
utils().packedEncode(TypePointers{selectorType}, TypePointers()); utils().packedEncode(TypePointers{selectorType}, TypePointers());
utils().toSizeAfterFreeMemoryPointer(); utils().toSizeAfterFreeMemoryPointer();
m_context << Instruction::KECCAK256; m_context << Instruction::KECCAK256;
@ -1308,10 +1328,16 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall)
dataOnStack = TypeProvider::fixedBytes(32); dataOnStack = TypeProvider::fixedBytes(32);
} }
} }
else else if (function.kind() == FunctionType::Kind::ABIEncodeCall)
{ {
solAssert(function.kind() == FunctionType::Kind::ABIEncodeWithSelector, ""); // stack: <memory pointer> <functionPointer>
// 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); utils().convertType(*dataOnStack, FixedBytesType(4), true);

View File

@ -2057,7 +2057,7 @@ string YulUtilFunctions::copyValueArrayStorageToStorageFunction(ArrayType const&
solAssert(!_fromType.isValueType(), ""); solAssert(!_fromType.isValueType(), "");
templ("functionName", functionName); templ("functionName", functionName);
templ("resizeArray", resizeArrayFunction(_toType)); templ("resizeArray", resizeArrayFunction(_toType));
templ("arrayLength",arrayLengthFunction(_fromType)); templ("arrayLength", arrayLengthFunction(_fromType));
templ("panic", panicFunction(PanicCode::ResourceError)); templ("panic", panicFunction(PanicCode::ResourceError));
templ("srcDataLocation", arrayDataAreaFunction(_fromType)); templ("srcDataLocation", arrayDataAreaFunction(_fromType));
templ("dstDataLocation", arrayDataAreaFunction(_toType)); templ("dstDataLocation", arrayDataAreaFunction(_toType));
@ -2065,7 +2065,14 @@ string YulUtilFunctions::copyValueArrayStorageToStorageFunction(ArrayType const&
unsigned itemsPerSlot = 32 / _toType.storageStride(); unsigned itemsPerSlot = 32 / _toType.storageStride();
templ("itemsPerSlot", to_string(itemsPerSlot)); templ("itemsPerSlot", to_string(itemsPerSlot));
templ("multipleItemsPerSlotDst", itemsPerSlot > 1); templ("multipleItemsPerSlotDst", itemsPerSlot > 1);
bool sameType = _fromType.baseType() == _toType.baseType(); bool sameType = *_fromType.baseType() == *_toType.baseType();
if (auto functionType = dynamic_cast<FunctionType const*>(_fromType.baseType()))
{
solAssert(functionType->equalExcludingStateMutability(
dynamic_cast<FunctionType const&>(*_toType.baseType())
));
sameType = true;
}
templ("sameType", sameType); templ("sameType", sameType);
if (sameType) if (sameType)
{ {

View File

@ -1104,29 +1104,57 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall)
case FunctionType::Kind::ABIEncode: case FunctionType::Kind::ABIEncode:
case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodePacked:
case FunctionType::Kind::ABIEncodeWithSelector: case FunctionType::Kind::ABIEncodeWithSelector:
case FunctionType::Kind::ABIEncodeCall:
case FunctionType::Kind::ABIEncodeWithSignature: case FunctionType::Kind::ABIEncodeWithSignature:
{ {
bool const isPacked = functionType->kind() == FunctionType::Kind::ABIEncodePacked; bool const isPacked = functionType->kind() == FunctionType::Kind::ABIEncodePacked;
solAssert(functionType->padArguments() != isPacked, ""); solAssert(functionType->padArguments() != isPacked, "");
bool const hasSelectorOrSignature = bool const hasSelectorOrSignature =
functionType->kind() == FunctionType::Kind::ABIEncodeWithSelector || functionType->kind() == FunctionType::Kind::ABIEncodeWithSelector ||
functionType->kind() == FunctionType::Kind::ABIEncodeCall ||
functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature; functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature;
TypePointers argumentTypes; TypePointers argumentTypes;
TypePointers targetTypes; TypePointers targetTypes;
vector<string> argumentVars; vector<string> argumentVars;
for (size_t i = 0; i < arguments.size(); ++i) string selector;
vector<ASTPointer<Expression const>> argumentsOfEncodeFunction;
if (functionType->kind() == FunctionType::Kind::ABIEncodeCall)
{ {
// ignore selector solAssert(arguments.size() == 2, "");
if (hasSelectorOrSignature && i == 0) // Account for tuples with one component which become that component
continue; if (type(*arguments[1]).category() == Type::Category::Tuple)
argumentTypes.emplace_back(&type(*arguments[i])); {
targetTypes.emplace_back(type(*arguments[i]).fullEncodingType(false, true, isPacked)); auto const& tupleExpression = dynamic_cast<TupleExpression const&>(*arguments[1]);
argumentVars += IRVariable(*arguments[i]).stackSlots(); 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::ABIEncodeCall)
if (functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature) selector = convert(
IRVariable(*arguments[0]).part("functionSelector"),
*TypeProvider::fixedBytes(4)
).name();
else if (functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature)
{ {
// hash the signature // hash the signature
Type const& selectorType = type(*arguments.front()); Type const& selectorType = type(*arguments.front());
@ -1833,7 +1861,7 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess)
define(_memberAccess) << requestedValue << "\n"; define(_memberAccess) << requestedValue << "\n";
} }
else if (set<string>{"encode", "encodePacked", "encodeWithSelector", "encodeWithSignature", "decode"}.count(member)) else if (set<string>{"encode", "encodePacked", "encodeWithSelector", "encodeCall", "encodeWithSignature", "decode"}.count(member))
{ {
// no-op // no-op
} }

View File

@ -637,6 +637,7 @@ void SMTEncoder::endVisit(FunctionCall const& _funCall)
case FunctionType::Kind::ABIEncode: case FunctionType::Kind::ABIEncode:
case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodePacked:
case FunctionType::Kind::ABIEncodeWithSelector: case FunctionType::Kind::ABIEncodeWithSelector:
case FunctionType::Kind::ABIEncodeCall:
case FunctionType::Kind::ABIEncodeWithSignature: case FunctionType::Kind::ABIEncodeWithSignature:
visitABIFunction(_funCall); visitABIFunction(_funCall);
break; break;
@ -3041,6 +3042,7 @@ set<FunctionCall const*> SMTEncoder::collectABICalls(ASTNode const* _node)
case FunctionType::Kind::ABIEncode: case FunctionType::Kind::ABIEncode:
case FunctionType::Kind::ABIEncodePacked: case FunctionType::Kind::ABIEncodePacked:
case FunctionType::Kind::ABIEncodeWithSelector: case FunctionType::Kind::ABIEncodeWithSelector:
case FunctionType::Kind::ABIEncodeCall:
case FunctionType::Kind::ABIEncodeWithSignature: case FunctionType::Kind::ABIEncodeWithSignature:
case FunctionType::Kind::ABIDecode: case FunctionType::Kind::ABIDecode:
abiCalls.insert(&_funCall); abiCalls.insert(&_funCall);

View File

@ -236,6 +236,15 @@ void SymbolicState::buildABIFunctions(set<FunctionCall const*> const& _abiFuncti
else else
solAssert(false, "Unexpected argument of abi.decode"); 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 else
{ {
outTypes = returnTypes; outTypes = returnTypes;

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/FileRepository.h>
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<string, string> 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));
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <libsolidity/interface/FileReader.h>
#include <string>
#include <map>
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<std::string, std::string> 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<std::string, std::string> m_sourceUnitNamesToClientPaths;
frontend::FileReader m_fileReader;
};
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/ASTUtils.h>
#include <libsolidity/ast/ASTVisitor.h>
#include <libsolidity/interface/ReadFile.h>
#include <libsolidity/interface/StandardCompiler.h>
#include <libsolidity/lsp/LanguageServer.h>
#include <liblangutil/SourceReferenceExtractor.h>
#include <liblangutil/CharStream.h>
#include <libsolutil/Visitor.h>
#include <libsolutil/JSON.h>
#include <boost/exception/diagnostic_information.hpp>
#include <boost/filesystem.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <fmt/format.h>
#include <ostream>
#include <string>
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<LineColumn> 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<SourceLocation> LanguageServer::parsePosition(
string const& _sourceUnitName,
Json::Value const& _position
) const
{
if (!m_fileRepository.sourceUnits().count(_sourceUnitName))
return nullopt;
if (optional<LineColumn> lineColumn = parseLineColumn(_position))
if (optional<int> const offset = CharStream::translateLineColumnToPosition(
m_fileRepository.sourceUnits().at(_sourceUnitName),
*lineColumn
))
return SourceLocation{*offset, *offset, make_shared<string>(_sourceUnitName)};
return nullopt;
}
optional<SourceLocation> LanguageServer::parseRange(string const& _sourceUnitName, Json::Value const& _range) const
{
if (!_range.isObject())
return nullopt;
optional<SourceLocation> start = parsePosition(_sourceUnitName, _range["start"]);
optional<SourceLocation> 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<string, Json::Value> 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<Error const> 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<Json::Value> 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<SourceLocation> 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<size_t>(change->start), static_cast<size_t>(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();
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <libsolidity/lsp/Transport.h>
#include <libsolidity/lsp/FileRepository.h>
#include <libsolidity/interface/CompilerStack.h>
#include <libsolidity/interface/FileReader.h>
#include <json/value.h>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <vector>
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<langutil::SourceLocation> 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<langutil::SourceLocation> 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<void(MessageID, Json::Value const&)>;
enum class State { Started, Initialized, ShutdownRequested, ExitRequested, ExitWithoutShutdown };
State m_state = State::Started;
Transport& m_client;
std::map<std::string, MessageHandler> m_handlers;
/// Set of files known to be open by the client.
std::set<std::string> m_openFiles;
/// Set of source unit names for which we sent diagnostics to the client in the last iteration.
std::set<std::string> m_nonemptyDiagnostics;
FileRepository m_fileRepository;
frontend::CompilerStack m_compilerStack;
/// User-supplied custom configuration settings (such as EVM version).
Json::Value m_settingsObject;
};
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/Transport.h>
#include <libsolutil/JSON.h>
#include <libsolutil/Visitor.h>
#include <libsolutil/CommonIO.h>
#include <liblangutil/Exceptions.h>
#include <boost/algorithm/string.hpp>
#include <iostream>
#include <sstream>
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<Json::Value> 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<int>(_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<map<string, string>> IOStreamTransport::parseHeaders()
{
map<string, string> 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)};
}

101
libsolidity/lsp/Transport.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <json/value.h>
#include <functional>
#include <iosfwd>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
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<Json::Value> 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<Json::Value> 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<std::map<std::string, std::string>> parseHeaders();
private:
std::istream& m_input;
std::ostream& m_output;
};
}

View File

@ -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"}, // 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. // then we want the variable tmp in the slot at offset 2 in the layout before the operation.
vector<optional<StackSlot>> idealLayout(_post.size(), nullopt); vector<optional<StackSlot>> 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<PreviousSlot>(&idealPosition)) if (PreviousSlot* previousSlot = std::get_if<PreviousSlot>(&idealPosition))
idealLayout.at(previousSlot->slot) = slot; idealLayout.at(previousSlot->slot) = slot;

View File

@ -83,6 +83,9 @@ done
printTask "Testing Python scripts..." printTask "Testing Python scripts..."
"$REPO_ROOT/test/pyscriptTests.py" "$REPO_ROOT/test/pyscriptTests.py"
printTask "Testing LSP..."
"$REPO_ROOT/scripts/test_solidity_lsp.py" "${SOLIDITY_BUILD_DIR}/solc/solc"
printTask "Running commandline tests..." printTask "Running commandline tests..."
# Only run in parallel if this is run on CI infrastructure # Only run in parallel if this is run on CI infrastructure
if [[ -n "$CI" ]] if [[ -n "$CI" ]]

View File

@ -38,6 +38,8 @@
#include <libsolidity/interface/DebugSettings.h> #include <libsolidity/interface/DebugSettings.h>
#include <libsolidity/interface/ImportRemapper.h> #include <libsolidity/interface/ImportRemapper.h>
#include <libsolidity/interface/StorageLayout.h> #include <libsolidity/interface/StorageLayout.h>
#include <libsolidity/lsp/LanguageServer.h>
#include <libsolidity/lsp/Transport.h>
#include <libyul/AssemblyStack.h> #include <libyul/AssemblyStack.h>
@ -56,6 +58,7 @@
#include <libsolutil/JSON.h> #include <libsolutil/JSON.h>
#include <algorithm> #include <algorithm>
#include <fstream>
#include <memory> #include <memory>
#include <range/v3/view/map.hpp> #include <range/v3/view/map.hpp>
@ -499,7 +502,11 @@ void CommandLineInterface::readInputFiles()
m_fileReader.setStdin(readUntilEnd(m_sin)); 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."); 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(); m_standardJsonInput.reset();
break; break;
} }
case InputMode::LanguageServer:
serveLSP();
break;
case InputMode::Assembler: case InputMode::Assembler:
assemble(m_options.assembly.inputLanguage, m_options.assembly.targetMachine); assemble(m_options.assembly.inputLanguage, m_options.assembly.targetMachine);
break; 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() void CommandLineInterface::link()
{ {
solAssert(m_options.input.mode == InputMode::Linker, ""); solAssert(m_options.input.mode == InputMode::Linker, "");

View File

@ -82,6 +82,7 @@ private:
void printVersion(); void printVersion();
void printLicense(); void printLicense();
void compile(); void compile();
void serveLSP();
void link(); void link();
void writeLinkedFiles(); void writeLinkedFiles();
/// @returns the ``// <identifier> -> name`` hint for library placeholders. /// @returns the ``// <identifier> -> name`` hint for library placeholders.

View File

@ -59,6 +59,7 @@ static string const g_strIPFS = "ipfs";
static string const g_strLicense = "license"; static string const g_strLicense = "license";
static string const g_strLibraries = "libraries"; static string const g_strLibraries = "libraries";
static string const g_strLink = "link"; static string const g_strLink = "link";
static string const g_strLSP = "lsp";
static string const g_strMachine = "machine"; static string const g_strMachine = "machine";
static string const g_strMetadataHash = "metadata-hash"; static string const g_strMetadataHash = "metadata-hash";
static string const g_strMetadataLiteral = "metadata-literal"; static string const g_strMetadataLiteral = "metadata-literal";
@ -135,6 +136,7 @@ static map<InputMode, string> const g_inputModeName = {
{InputMode::Assembler, "assembler"}, {InputMode::Assembler, "assembler"},
{InputMode::StandardJson, "standard JSON"}, {InputMode::StandardJson, "standard JSON"},
{InputMode::Linker, "linker"}, {InputMode::Linker, "linker"},
{InputMode::LanguageServer, "language server (LSP)"},
}; };
void CommandLineParser::checkMutuallyExclusive(vector<string> const& _optionNames) void CommandLineParser::checkMutuallyExclusive(vector<string> const& _optionNames)
@ -443,6 +445,7 @@ void CommandLineParser::parseOutputSelection()
case InputMode::Help: case InputMode::Help:
case InputMode::License: case InputMode::License:
case InputMode::Version: case InputMode::Version:
case InputMode::LanguageServer:
solAssert(false); solAssert(false);
case InputMode::Compiler: case InputMode::Compiler:
case InputMode::CompilerWithASTImport: 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 " "Supported Inputs is the output of the --" + g_strStandardJSON + " or the one produced by "
"--" + g_strCombinedJson + " " + CombinedJsonRequests::componentName(&CombinedJsonRequests::ast)).c_str() "--" + 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); desc.add(alternativeInputModes);
@ -842,6 +850,7 @@ void CommandLineParser::processArgs()
g_strStrictAssembly, g_strStrictAssembly,
g_strYul, g_strYul,
g_strImportAst, g_strImportAst,
g_strLSP
}); });
if (m_args.count(g_strHelp) > 0) if (m_args.count(g_strHelp) > 0)
@ -852,6 +861,8 @@ void CommandLineParser::processArgs()
m_options.input.mode = InputMode::Version; m_options.input.mode = InputMode::Version;
else if (m_args.count(g_strStandardJSON) > 0) else if (m_args.count(g_strStandardJSON) > 0)
m_options.input.mode = InputMode::StandardJson; 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) 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; m_options.input.mode = InputMode::Assembler;
else if (m_args.count(g_strLink) > 0) else if (m_args.count(g_strLink) > 0)
@ -887,6 +898,9 @@ void CommandLineParser::processArgs()
joinOptionNames(invalidOptionsForCurrentInputMode) joinOptionNames(invalidOptionsForCurrentInputMode)
); );
if (m_options.input.mode == InputMode::LanguageServer)
return;
checkMutuallyExclusive({g_strColor, g_strNoColor}); checkMutuallyExclusive({g_strColor, g_strNoColor});
array<string, 9> const conflictingWithStopAfter{ array<string, 9> const conflictingWithStopAfter{

View File

@ -56,6 +56,7 @@ enum class InputMode
StandardJson, StandardJson,
Linker, Linker,
Assembler, Assembler,
LanguageServer
}; };
struct CompilerOutputs struct CompilerOutputs

View File

@ -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<string, h160>{{":Lib", m_contractAddress}});
ABI_CHECK(callContractFunction("f()"), encodeArgs());
ABI_CHECK(callContractFunction("sender()"), encodeArgs(m_sender));
)
}
BOOST_AUTO_TEST_CASE(library_call_protection) BOOST_AUTO_TEST_CASE(library_call_protection)
{ {
// This tests code that reverts a call if it is a direct call to a library // 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) BOOST_AUTO_TEST_CASE(call_forward_bytes_length)
{ {
char const* sourceCode = R"( 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<string, h160>{{":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<string, h160>{{":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<string, h160>{{":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<string, h160>{{":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<string, h160>{{":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<string, h160>{{":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) BOOST_AUTO_TEST_CASE(using_library_mappings_external)
{ {
char const* libSourceCode = R"( 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<string, h160>{{":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<string, h160>{{":Lib", m_contractAddress}});
ABI_CHECK(callContractFunction("f()"), encodeArgs(u256(7), u256(8)));
)
}
BOOST_AUTO_TEST_CASE(short_strings) BOOST_AUTO_TEST_CASE(short_strings)
{ {
// This test verifies that the byte array encoding that combines length and data works // 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<string, h160>{{":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<string, h160>{{":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<string, h160>{{":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<string, h160>{{":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<string, h160>{{":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<string, h160>{{":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) BOOST_AUTO_TEST_CASE(inline_long_string_return)
{ {
char const* sourceCode = R"( char const* sourceCode = R"(
@ -3406,26 +2907,6 @@ BOOST_AUTO_TEST_CASE(payable_function)
BOOST_CHECK_EQUAL(balanceAt(m_contractAddress), 27 + 27); 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<string, h160>{{":L", m_contractAddress}});
ABI_CHECK(callContractFunctionWithValue("f()", 27), encodeArgs(u256(7)));
)
}
BOOST_AUTO_TEST_CASE(non_payable_throw) BOOST_AUTO_TEST_CASE(non_payable_throw)
{ {
char const* sourceCode = R"( char const* sourceCode = R"(

View File

@ -0,0 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
contract C
{
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,10 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
abstract contract A {
function a() public virtual;
}
contract B is A
{
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -26,7 +26,6 @@ contract C {
} }
struct S { uint a; string b; uint16 c; } struct S { uint a; string b; uint16 c; }
function f4() public pure returns (bytes memory) { function f4() public pure returns (bytes memory) {
bytes4 x = 0x12345678;
S memory s; S memory s;
s.a = 0x1234567; s.a = 0x1234567;
s.b = "Lorem ipsum dolor sit ethereum........"; s.b = "Lorem ipsum dolor sit ethereum........";

View File

@ -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

View File

@ -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

View File

@ -18,6 +18,6 @@ contract C {
// compileViaYul: also // compileViaYul: also
// ---- // ----
// test() -> 7 // test() -> 7
// gas irOptimized: 126552 // gas irOptimized: 124080
// gas legacy: 205196 // gas legacy: 205196
// gas legacyOptimized: 204987 // gas legacyOptimized: 204987

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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".

View File

@ -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".

View File

@ -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.

874
test/lsp.py Executable file
View File

@ -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)

View File

@ -158,7 +158,7 @@ BOOST_AUTO_TEST_CASE(multiple_input_modes)
}; };
string expectedMessage = string expectedMessage =
"The following options are mutually exclusive: " "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."; "Select at most one.";
for (string const& mode1: inputModeOptions) for (string const& mode1: inputModeOptions)