/* This file is part of solidity. solidity is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. solidity is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with solidity. If not, see . */ // SPDX-License-Identifier: GPL-3.0 #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace solidity; using namespace solidity::util; using namespace solidity::frontend; using namespace solidity::frontend::test; using namespace std; 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; } vector TestFileParser::parseFunctionCalls(size_t _lineOffset) { vector 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.")); string runType = m_scanner.currentLiteral(); if (set{"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); 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 " + to_string(_lineOffset + m_lineNumber) + ": " + _e.what()) ); } } } } return calls; } vector TestFileParser::parseFunctionCallSideEffects() { vector result; while (accept(Token::Tilde, false)) { 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; } pair TestFileParser::parseFunctionSignature() { 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); 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}; 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}; 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.")); 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.")); 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}; string parsed = parseDecimalNumber(); parameter.rawString += parsed; if (isSigned) parsed = "-" + parsed; if (parsed.find('.') == 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; } string TestFileParser::parseIdentifierOrTuple() { 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; } string TestFileParser::parseBoolean() { string literal = m_scanner.currentLiteral(); expect(Token::Boolean); return literal; } string TestFileParser::parseComment() { string comment = m_scanner.currentLiteral(); if (accept(Token::Comment, true)) return comment; return string{}; } string TestFileParser::parseDecimalNumber() { string literal = m_scanner.currentLiteral(); expect(Token::Number); return literal; } string TestFileParser::parseHexNumber() { string literal = m_scanner.currentLiteral(); expect(Token::HexNumber); return literal; } string TestFileParser::parseString() { string literal = m_scanner.currentLiteral(); expect(Token::String); return literal; } void TestFileParser::Scanner::readStream(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 { 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: '" + string{current()} + "'")); break; } } while (m_currentToken == Token::Whitespace); } string TestFileParser::Scanner::readLine() { 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; } string TestFileParser::Scanner::scanComment() { string comment; advance(); while (current() != '#') { comment += current(); advance(); } return comment; } string TestFileParser::Scanner::scanIdentifierOrKeyword() { string identifier; identifier += current(); while (langutil::isIdentifierPart(peek())) { advance(); identifier += current(); } return identifier; } string TestFileParser::Scanner::scanDecimalNumber() { string number; number += current(); while (langutil::isDecimalDigit(peek()) || '.' == peek()) { advance(); number += current(); } return number; } string TestFileParser::Scanner::scanHexNumber() { string number; number += current(); while (langutil::isHexDigit(peek())) { advance(); number += current(); } return number; } string TestFileParser::Scanner::scanString() { 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, 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(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(value); } bool TestFileParser::isBuiltinFunction(std::string const& _signature) { return m_builtins.count(_signature) > 0; }