solidity/test/libsolidity/util/TestFileParser.cpp

799 lines
20 KiB
C++

/*
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 <libsolutil/StringUtils.h>
#include <test/libsolidity/util/TestFileParser.h>
#include <test/libsolidity/util/BytesUtils.h>
#include <test/libsolidity/util/SoltestErrors.h>
#include <test/Common.h>
#include <liblangutil/Common.h>
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/throw_exception.hpp>
#include <fstream>
#include <memory>
#include <optional>
#include <stdexcept>
using namespace solidity;
using namespace solidity::util;
using namespace solidity::frontend;
using namespace solidity::frontend::test;
using Token = soltest::Token;
char TestFileParser::Scanner::peek() const noexcept
{
if (std::distance(m_char, m_source.end()) < 2)
return '\0';
auto next = m_char;
std::advance(next, 1);
return *next;
}
std::vector<solidity::frontend::test::FunctionCall> TestFileParser::parseFunctionCalls(size_t _lineOffset)
{
std::vector<FunctionCall> calls;
if (!accept(Token::EOS))
{
soltestAssert(m_scanner.currentToken() == Token::Unknown, "");
m_scanner.scanNextToken();
while (!accept(Token::EOS))
{
if (!accept(Token::Whitespace))
{
/// If this is not the first call in the test,
/// the last call to parseParameter could have eaten the
/// new line already. This could only be fixed with a one
/// token lookahead that checks parseParameter
/// if the next token is an identifier.
if (calls.empty())
expect(Token::Newline);
else
if (accept(Token::Newline, true))
m_lineNumber++;
try
{
if (accept(Token::Gas, true))
{
if (calls.empty())
BOOST_THROW_EXCEPTION(TestParserError("Expected function call before gas usage filter."));
std::string runType = m_scanner.currentLiteral();
if (std::set<std::string>{"ir", "irOptimized", "legacy", "legacyOptimized"}.count(runType) > 0)
{
m_scanner.scanNextToken();
expect(Token::Colon);
if (calls.back().expectations.gasUsed.count(runType) > 0)
throw TestParserError("Gas usage expectation set multiple times.");
calls.back().expectations.gasUsed[runType] = u256(parseDecimalNumber());
}
else
BOOST_THROW_EXCEPTION(TestParserError(
"Expected \"ir\", \"irOptimized\", \"legacy\", or \"legacyOptimized\"."
));
}
else
{
FunctionCall call;
if (accept(Token::Library, true))
{
expect(Token::Colon);
std::string libraryName;
if (accept(Token::String))
{
call.libraryFile = m_scanner.currentLiteral();
expect(Token::String);
expect(Token::Colon);
libraryName += m_scanner.currentLiteral();
expect(Token::Identifier);
}
else if (accept(Token::Colon, true))
{
libraryName = m_scanner.currentLiteral();
expect(Token::Identifier);
}
else
{
libraryName = m_scanner.currentLiteral();
expect(Token::Identifier);
}
call.signature = libraryName;
call.kind = FunctionCall::Kind::Library;
call.expectations.failure = false;
}
else
{
bool lowLevelCall = false;
tie(call.signature, lowLevelCall) = parseFunctionSignature();
if (lowLevelCall)
call.kind = FunctionCall::Kind::LowLevel;
else if (isBuiltinFunction(call.signature))
call.kind = FunctionCall::Kind::Builtin;
if (accept(Token::Comma, true))
call.value = parseFunctionCallValue();
if (accept(Token::Colon, true))
call.arguments = parseFunctionCallArguments();
if (accept(Token::Newline, true))
{
call.displayMode = FunctionCall::DisplayMode::MultiLine;
m_lineNumber++;
}
call.arguments.comment = parseComment();
if (accept(Token::Newline, true))
{
call.displayMode = FunctionCall::DisplayMode::MultiLine;
m_lineNumber++;
}
if (accept(Token::Arrow, true))
{
call.omitsArrow = false;
call.expectations = parseFunctionCallExpectations();
if (accept(Token::Newline, true))
m_lineNumber++;
}
else
{
call.expectations.failure = false;
call.displayMode = FunctionCall::DisplayMode::SingleLine;
}
call.expectations.comment = parseComment();
if (call.signature == "constructor()")
call.kind = FunctionCall::Kind::Constructor;
}
accept(Token::Newline, true);
call.expectedSideEffects = parseFunctionCallSideEffects();
calls.emplace_back(std::move(call));
}
}
catch (TestParserError const& _e)
{
BOOST_THROW_EXCEPTION(
TestParserError("Line " + std::to_string(_lineOffset + m_lineNumber) + ": " + _e.what())
);
}
}
}
}
return calls;
}
std::vector<std::string> TestFileParser::parseFunctionCallSideEffects()
{
std::vector<std::string> result;
while (accept(Token::Tilde, false))
{
std::string effect = m_scanner.currentLiteral();
result.emplace_back(effect);
soltestAssert(m_scanner.currentToken() == Token::Tilde, "");
m_scanner.scanNextToken();
if (m_scanner.currentToken() == Token::Newline)
m_scanner.scanNextToken();
}
return result;
}
bool TestFileParser::accept(Token _token, bool const _expect)
{
if (m_scanner.currentToken() != _token)
return false;
if (_expect)
return expect(_token);
return true;
}
bool TestFileParser::expect(Token _token, bool const _advance)
{
if (m_scanner.currentToken() != _token || m_scanner.currentToken() == Token::Invalid)
BOOST_THROW_EXCEPTION(TestParserError(
"Unexpected " + formatToken(m_scanner.currentToken()) + ": \"" +
m_scanner.currentLiteral() + "\". " +
"Expected \"" + formatToken(_token) + "\"."
)
);
if (_advance)
m_scanner.scanNextToken();
return true;
}
std::pair<std::string, bool> TestFileParser::parseFunctionSignature()
{
std::string signature;
bool hasName = false;
if (accept(Token::Identifier, false))
{
hasName = true;
signature = m_scanner.currentLiteral();
expect(Token::Identifier);
}
if (isBuiltinFunction(signature) && m_scanner.currentToken() != Token::LParen)
return {signature, false};
signature += formatToken(Token::LParen);
expect(Token::LParen);
std::string parameters;
if (!accept(Token::RParen, false))
parameters = parseIdentifierOrTuple();
while (accept(Token::Comma))
{
parameters += formatToken(Token::Comma);
expect(Token::Comma);
parameters += parseIdentifierOrTuple();
}
if (accept(Token::Arrow, true))
BOOST_THROW_EXCEPTION(TestParserError("Invalid signature detected: " + signature));
if (!hasName && !parameters.empty())
BOOST_THROW_EXCEPTION(TestParserError("Signatures without a name cannot have parameters: " + signature));
else
signature += parameters;
expect(Token::RParen);
signature += formatToken(Token::RParen);
return {signature, !hasName};
}
FunctionValue TestFileParser::parseFunctionCallValue()
{
try
{
u256 value{ parseDecimalNumber() };
Token token = m_scanner.currentToken();
if (token != Token::Ether && token != Token::Wei)
BOOST_THROW_EXCEPTION(TestParserError("Invalid value unit provided. Coins can be wei or ether."));
m_scanner.scanNextToken();
FunctionValueUnit unit = token == Token::Wei ? FunctionValueUnit::Wei : FunctionValueUnit::Ether;
return { (unit == FunctionValueUnit::Wei ? u256(1) : exp256(u256(10), u256(18))) * value, unit };
}
catch (std::exception const&)
{
BOOST_THROW_EXCEPTION(TestParserError("Ether value encoding invalid."));
}
}
FunctionCallArgs TestFileParser::parseFunctionCallArguments()
{
FunctionCallArgs arguments;
auto param = parseParameter();
if (param.abiType.type == ABIType::None)
BOOST_THROW_EXCEPTION(TestParserError("No argument provided."));
arguments.parameters.emplace_back(param);
while (accept(Token::Comma, true))
arguments.parameters.emplace_back(parseParameter());
return arguments;
}
FunctionCallExpectations TestFileParser::parseFunctionCallExpectations()
{
FunctionCallExpectations expectations;
auto param = parseParameter();
if (param.abiType.type == ABIType::None)
{
expectations.failure = false;
return expectations;
}
expectations.result.emplace_back(param);
while (accept(Token::Comma, true))
expectations.result.emplace_back(parseParameter());
/// We have always one virtual parameter in the parameter list.
/// If its type is FAILURE, the expected result is also a REVERT etc.
if (expectations.result.at(0).abiType.type != ABIType::Failure)
expectations.failure = false;
return expectations;
}
Parameter TestFileParser::parseParameter()
{
Parameter parameter;
if (accept(Token::Newline, true))
{
parameter.format.newline = true;
m_lineNumber++;
}
parameter.abiType = ABIType{ABIType::None, ABIType::AlignNone, 0};
bool isSigned = false;
if (accept(Token::Left, true))
{
parameter.rawString += formatToken(Token::Left);
expect(Token::LParen);
parameter.rawString += formatToken(Token::LParen);
parameter.alignment = Parameter::Alignment::Left;
}
if (accept(Token::Right, true))
{
parameter.rawString += formatToken(Token::Right);
expect(Token::LParen);
parameter.rawString += formatToken(Token::LParen);
parameter.alignment = Parameter::Alignment::Right;
}
if (accept(Token::Sub, true))
{
parameter.rawString += formatToken(Token::Sub);
isSigned = true;
}
if (accept(Token::Boolean))
{
if (isSigned)
BOOST_THROW_EXCEPTION(TestParserError("Invalid boolean literal."));
parameter.abiType = ABIType{ABIType::Boolean, ABIType::AlignRight, 32};
std::string parsed = parseBoolean();
parameter.rawString += parsed;
parameter.rawBytes = BytesUtils::applyAlign(
parameter.alignment,
parameter.abiType,
BytesUtils::convertBoolean(parsed)
);
}
else if (accept(Token::HexNumber))
{
if (isSigned)
BOOST_THROW_EXCEPTION(TestParserError("Invalid hex number literal."));
parameter.abiType = ABIType{ABIType::Hex, ABIType::AlignRight, 32};
std::string parsed = parseHexNumber();
parameter.rawString += parsed;
parameter.rawBytes = BytesUtils::applyAlign(
parameter.alignment,
parameter.abiType,
BytesUtils::convertHexNumber(parsed)
);
}
else if (accept(Token::Hex, true))
{
if (isSigned)
BOOST_THROW_EXCEPTION(TestParserError("Invalid hex string literal."));
if (parameter.alignment != Parameter::Alignment::None)
BOOST_THROW_EXCEPTION(TestParserError("Hex string literals cannot be aligned or padded."));
std::string parsed = parseString();
parameter.rawString += "hex\"" + parsed + "\"";
parameter.rawBytes = BytesUtils::convertHexNumber(parsed);
parameter.abiType = ABIType{
ABIType::HexString, ABIType::AlignNone, parameter.rawBytes.size()
};
}
else if (accept(Token::String))
{
if (isSigned)
BOOST_THROW_EXCEPTION(TestParserError("Invalid string literal."));
if (parameter.alignment != Parameter::Alignment::None)
BOOST_THROW_EXCEPTION(TestParserError("String literals cannot be aligned or padded."));
std::string parsed = parseString();
parameter.abiType = ABIType{ABIType::String, ABIType::AlignLeft, parsed.size()};
parameter.rawString += "\"" + parsed + "\"";
parameter.rawBytes = BytesUtils::applyAlign(
Parameter::Alignment::Left,
parameter.abiType,
BytesUtils::convertString(parsed)
);
}
else if (accept(Token::Number))
{
auto type = isSigned ? ABIType::SignedDec : ABIType::UnsignedDec;
parameter.abiType = ABIType{type, ABIType::AlignRight, 32};
std::string parsed = parseDecimalNumber();
parameter.rawString += parsed;
if (isSigned)
parsed = "-" + parsed;
if (parsed.find('.') == std::string::npos)
parameter.rawBytes = BytesUtils::applyAlign(
parameter.alignment,
parameter.abiType,
BytesUtils::convertNumber(parsed)
);
else
{
parameter.abiType.type = isSigned ? ABIType::SignedFixedPoint : ABIType::UnsignedFixedPoint;
parameter.rawBytes = BytesUtils::convertFixedPoint(parsed, parameter.abiType.fractionalDigits);
}
}
else if (accept(Token::Failure, true))
{
if (isSigned)
BOOST_THROW_EXCEPTION(TestParserError("Invalid failure literal."));
parameter.abiType = ABIType{ABIType::Failure, ABIType::AlignRight, 0};
parameter.rawBytes = bytes{};
}
if (parameter.alignment != Parameter::Alignment::None)
{
expect(Token::RParen);
parameter.rawString += formatToken(Token::RParen);
}
return parameter;
}
std::string TestFileParser::parseIdentifierOrTuple()
{
std::string identOrTuple;
auto parseArrayDimensions = [&]()
{
while (accept(Token::LBrack))
{
identOrTuple += formatToken(Token::LBrack);
expect(Token::LBrack);
if (accept(Token::Number))
identOrTuple += parseDecimalNumber();
identOrTuple += formatToken(Token::RBrack);
expect(Token::RBrack);
}
};
if (accept(Token::Identifier))
{
identOrTuple = m_scanner.currentLiteral();
expect(Token::Identifier);
parseArrayDimensions();
return identOrTuple;
}
expect(Token::LParen);
identOrTuple += formatToken(Token::LParen);
identOrTuple += parseIdentifierOrTuple();
while (accept(Token::Comma))
{
identOrTuple += formatToken(Token::Comma);
expect(Token::Comma);
identOrTuple += parseIdentifierOrTuple();
}
expect(Token::RParen);
identOrTuple += formatToken(Token::RParen);
parseArrayDimensions();
return identOrTuple;
}
std::string TestFileParser::parseBoolean()
{
std::string literal = m_scanner.currentLiteral();
expect(Token::Boolean);
return literal;
}
std::string TestFileParser::parseComment()
{
std::string comment = m_scanner.currentLiteral();
if (accept(Token::Comment, true))
return comment;
return std::string{};
}
std::string TestFileParser::parseDecimalNumber()
{
std::string literal = m_scanner.currentLiteral();
expect(Token::Number);
return literal;
}
std::string TestFileParser::parseHexNumber()
{
std::string literal = m_scanner.currentLiteral();
expect(Token::HexNumber);
return literal;
}
std::string TestFileParser::parseString()
{
std::string literal = m_scanner.currentLiteral();
expect(Token::String);
return literal;
}
void TestFileParser::Scanner::readStream(std::istream& _stream)
{
std::string line;
// TODO: std::getline(..) removes newlines '\n', if present. This could be improved.
while (std::getline(_stream, line))
m_source += line;
m_char = m_source.begin();
}
void TestFileParser::Scanner::scanNextToken()
{
// Make code coverage happy.
soltestAssert(formatToken(Token::NUM_TOKENS).empty(), "");
auto detectKeyword = [](std::string const& _literal = "") -> std::pair<Token, std::string> {
if (_literal == "true") return {Token::Boolean, "true"};
if (_literal == "false") return {Token::Boolean, "false"};
if (_literal == "ether") return {Token::Ether, ""};
if (_literal == "wei") return {Token::Wei, ""};
if (_literal == "left") return {Token::Left, ""};
if (_literal == "library") return {Token::Library, ""};
if (_literal == "right") return {Token::Right, ""};
if (_literal == "hex") return {Token::Hex, ""};
if (_literal == "FAILURE") return {Token::Failure, ""};
if (_literal == "gas") return {Token::Gas, ""};
return {Token::Identifier, _literal};
};
auto selectToken = [this](Token _token, std::string const& _literal = "") {
advance();
m_currentToken = _token;
m_currentLiteral = _literal;
};
m_currentToken = Token::Unknown;
m_currentLiteral = "";
do
{
switch(current())
{
case '/':
advance();
if (current() == '/')
selectToken(Token::Newline);
else
selectToken(Token::Invalid);
break;
case '-':
if (peek() == '>')
{
advance();
selectToken(Token::Arrow);
}
else
selectToken(Token::Sub);
break;
case '~':
advance();
selectToken(Token::Tilde, readLine());
break;
case ':':
selectToken(Token::Colon);
break;
case '#':
selectToken(Token::Comment, scanComment());
break;
case ',':
selectToken(Token::Comma);
break;
case '(':
selectToken(Token::LParen);
break;
case ')':
selectToken(Token::RParen);
break;
case '[':
selectToken(Token::LBrack);
break;
case ']':
selectToken(Token::RBrack);
break;
case '\"':
selectToken(Token::String, scanString());
break;
default:
if (langutil::isIdentifierStart(current()))
{
std::tie(m_currentToken, m_currentLiteral) = detectKeyword(scanIdentifierOrKeyword());
advance();
}
else if (langutil::isDecimalDigit(current()))
{
if (current() == '0' && peek() == 'x')
{
advance();
advance();
selectToken(Token::HexNumber, "0x" + scanHexNumber());
}
else
selectToken(Token::Number, scanDecimalNumber());
}
else if (langutil::isWhiteSpace(current()))
selectToken(Token::Whitespace);
else if (isEndOfFile())
{
m_currentToken = Token::EOS;
m_currentLiteral = "";
}
else
BOOST_THROW_EXCEPTION(TestParserError("Unexpected character: '" + std::string{current()} + "'"));
break;
}
}
while (m_currentToken == Token::Whitespace);
}
std::string TestFileParser::Scanner::readLine()
{
std::string line;
// Right now the scanner discards all (real) new-lines '\n' in TestFileParser::Scanner::readStream(..).
// Token::NewLine is defined as `//`, and NOT '\n'. We are just searching here for the next `/`.
// Note that `/` anywhere else than at the beginning of a line is currently forbidden (TODO: until we fix newline handling).
// Once the end of the file would be reached (or beyond), peek() will return '\0'.
while (peek() != '\0' && peek() != '/')
{
advance();
line += current();
}
return line;
}
std::string TestFileParser::Scanner::scanComment()
{
std::string comment;
advance();
while (current() != '#')
{
comment += current();
advance();
}
return comment;
}
std::string TestFileParser::Scanner::scanIdentifierOrKeyword()
{
std::string identifier;
identifier += current();
while (langutil::isIdentifierPart(peek()))
{
advance();
identifier += current();
}
return identifier;
}
std::string TestFileParser::Scanner::scanDecimalNumber()
{
std::string number;
number += current();
while (langutil::isDecimalDigit(peek()) || '.' == peek())
{
advance();
number += current();
}
return number;
}
std::string TestFileParser::Scanner::scanHexNumber()
{
std::string number;
number += current();
while (langutil::isHexDigit(peek()))
{
advance();
number += current();
}
return number;
}
std::string TestFileParser::Scanner::scanString()
{
std::string str;
advance();
while (current() != '\"')
{
if (current() == '\\')
{
advance();
switch (current())
{
case '\\':
str += current();
advance();
break;
case 'n':
str += '\n';
advance();
break;
case 'r':
str += '\r';
advance();
break;
case 't':
str += '\t';
advance();
break;
case '0':
str += '\0';
advance();
break;
case 'x':
str += scanHexPart();
break;
default:
BOOST_THROW_EXCEPTION(TestParserError("Invalid or escape sequence found in string literal."));
}
}
else
{
str += current();
advance();
}
}
return str;
}
// TODO: use fromHex() from CommonData
char TestFileParser::Scanner::scanHexPart()
{
auto toLower = [](char _c) -> char { return tolower(_c, std::locale::classic()); };
advance(); // skip 'x'
int value{};
if (isDigit(current()))
value = current() - '0';
else if (toLower(current()) >= 'a' && toLower(current()) <= 'f')
value = toLower(current()) - 'a' + 10;
else
BOOST_THROW_EXCEPTION(TestParserError("\\x used with no following hex digits."));
advance();
if (current() == '"')
return static_cast<char>(value);
value <<= 4;
if (isDigit(current()))
value |= current() - '0';
else if (toLower(current()) >= 'a' && toLower(current()) <= 'f')
value |= toLower(current()) - 'a' + 10;
advance();
return static_cast<char>(value);
}
bool TestFileParser::isBuiltinFunction(std::string const& _signature)
{
return m_builtins.count(_signature) > 0;
}