diff --git a/libsolutil/JSON.cpp b/libsolutil/JSON.cpp index 27dcf677f..ea75ada0f 100644 --- a/libsolutil/JSON.cpp +++ b/libsolutil/JSON.cpp @@ -144,4 +144,19 @@ bool jsonParseStrict(std::string const& _input, Json::Value& _json, std::string* return parse(readerBuilder, _input, _json, _errs); } +std::optional jsonValueByPath(Json::Value const& _node, std::string_view _jsonPath) +{ + if (!_node.isObject() || _jsonPath.empty()) + return {}; + + std::string memberName = std::string(_jsonPath.substr(0, _jsonPath.find_first_of('.'))); + if (!_node.isMember(memberName)) + return {}; + + if (memberName == _jsonPath) + return _node[memberName]; + + return jsonValueByPath(_node[memberName], _jsonPath.substr(memberName.size() + 1)); +} + } // namespace solidity::util diff --git a/libsolutil/JSON.h b/libsolutil/JSON.h index 905859f6d..f368739e1 100644 --- a/libsolutil/JSON.h +++ b/libsolutil/JSON.h @@ -26,6 +26,8 @@ #include #include +#include +#include namespace solidity::util { @@ -67,6 +69,13 @@ std::string jsonPrint(Json::Value const& _input, JsonFormat const& _format); /// \return \c true if the document was successfully parsed, \c false if an error occurred. bool jsonParseStrict(std::string const& _input, Json::Value& _json, std::string* _errs = nullptr); +/// Retrieves the value specified by @p _jsonPath by from a series of nested JSON dictionaries. +/// @param _jsonPath A dot-separated series of dictionary keys. +/// @param _node The node representing the start of the path. +/// @returns The value of the last key on the path. @a nullptr if any node on the path descends +/// into something that is not a dictionary or the key is not present. +std::optional jsonValueByPath(Json::Value const& _node, std::string_view _jsonPath); + namespace detail { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1ea70fe7a..119104aca 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -105,6 +105,8 @@ set(libsolidity_sources libsolidity/ViewPureChecker.cpp libsolidity/analysis/FunctionCallGraph.cpp libsolidity/interface/FileReader.cpp + libsolidity/ASTPropertyTest.h + libsolidity/ASTPropertyTest.cpp ) detect_stray_source_files("${libsolidity_sources}" "libsolidity/") diff --git a/test/InteractiveTests.h b/test/InteractiveTests.h index 9e054c109..52877af48 100644 --- a/test/InteractiveTests.h +++ b/test/InteractiveTests.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -76,7 +77,8 @@ Testsuite const g_interactiveTestsuites[] = { {"JSON ABI", "libsolidity", "ABIJson", false, false, &ABIJsonTest::create}, {"SMT Checker", "libsolidity", "smtCheckerTests", true, false, &SMTCheckerTest::create}, {"Gas Estimates", "libsolidity", "gasTests", false, false, &GasTest::create}, - {"Memory Guard Tests", "libsolidity", "memoryGuardTests", false, false, &MemoryGuardTest::create}, + {"Memory Guard", "libsolidity", "memoryGuardTests", false, false, &MemoryGuardTest::create}, + {"AST Properties", "libsolidity", "astPropertyTests", false, false, &ASTPropertyTest::create}, }; } diff --git a/test/libsolidity/ASTPropertyTest.cpp b/test/libsolidity/ASTPropertyTest.cpp new file mode 100644 index 000000000..194b2c92b --- /dev/null +++ b/test/libsolidity/ASTPropertyTest.cpp @@ -0,0 +1,204 @@ +/* + 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::util; +using namespace solidity::langutil; +using namespace solidity::frontend; +using namespace solidity::frontend::test; +using namespace solidity; +using namespace std; + +ASTPropertyTest::ASTPropertyTest(string const& _filename): + TestCase(_filename) +{ + if (!boost::algorithm::ends_with(_filename, ".sol")) + BOOST_THROW_EXCEPTION(runtime_error("Not a Solidity file: \"" + _filename + "\".")); + + m_source = m_reader.source(); + readExpectations(); + soltestAssert(m_tests.size() > 0, "No tests specified in " + _filename); +} + +string ASTPropertyTest::formatExpectations(bool _obtainedResult) +{ + string expectations; + for (string const& testId: m_testOrder) + { + soltestAssert(m_tests.count(testId) > 0); + expectations += + testId + + ": " + + (_obtainedResult ? m_tests[testId].obtainedValue : m_tests[testId].expectedValue) + + "\n"; + } + return expectations; +} + +vector ASTPropertyTest::readKeyValuePairs(string const& _input) +{ + vector result; + for (string line: _input | ranges::views::split('\n') | ranges::to>) + { + boost::trim(line); + if (line.empty()) + continue; + + soltestAssert( + ranges::all_of(line, [](char c) { return isprint(c); }), + "Non-printable character(s) found in property test: " + line + ); + + auto colonPosition = line.find_first_of(':'); + soltestAssert(colonPosition != string::npos, "Property test is missing a colon: " + line); + + StringPair pair{ + boost::trim_copy(line.substr(0, colonPosition)), + boost::trim_copy(line.substr(colonPosition + 1)) + }; + soltestAssert(!get<0>(pair).empty() != false, "Empty key in property test: " + line); + soltestAssert(!get<1>(pair).empty() != false, "Empty value in property test: " + line); + + result.push_back(pair); + } + return result; +} + +void ASTPropertyTest::readExpectations() +{ + for (auto const& [testId, testExpectation]: readKeyValuePairs(m_reader.simpleExpectations())) + { + soltestAssert(m_tests.count(testId) == 0, "More than one expectation for test \"" + testId + "\""); + m_tests.emplace(testId, Test{"", testExpectation, ""}); + m_testOrder.push_back(testId); + } + m_expectation = formatExpectations(false /* _obtainedResult */); +} + +void ASTPropertyTest::extractTestsFromAST(Json::Value const& _astJson) +{ + queue nodesToVisit; + nodesToVisit.push(_astJson); + + while (!nodesToVisit.empty()) + { + Json::Value& node = nodesToVisit.front(); + + if (node.isArray()) + for (auto&& member: node) + nodesToVisit.push(member); + else if (node.isObject()) + for (string const& memberName: node.getMemberNames()) + { + if (memberName != "documentation") + { + nodesToVisit.push(node[memberName]); + continue; + } + + string nodeDocstring = node["documentation"].isObject() ? + node["documentation"]["text"].asString() : + node["documentation"].asString(); + soltestAssert(!nodeDocstring.empty()); + + vector pairs = readKeyValuePairs(nodeDocstring); + if (pairs.empty()) + continue; + + for (auto const& [testId, testedProperty]: pairs) + { + soltestAssert( + m_tests.count(testId) > 0, + "Test \"" + testId + "\" does not have a corresponding expected value." + ); + soltestAssert( + m_tests[testId].property.empty(), + "Test \"" + testId + "\" was already defined before." + ); + m_tests[testId].property = testedProperty; + + soltestAssert(node.isMember("nodeType")); + optional propertyNode = jsonValueByPath(node, testedProperty); + soltestAssert( + propertyNode.has_value(), + node["nodeType"].asString() + " node does not have a property named \""s + testedProperty + "\"" + ); + soltestAssert( + !propertyNode->isObject() && !propertyNode->isArray(), + "Property \"" + testedProperty + "\" is an object or an array." + ); + m_tests[testId].obtainedValue = propertyNode->asString(); + } + } + + nodesToVisit.pop(); + } + + auto firstTestWithoutProperty = ranges::find_if( + m_tests, + [&](auto const& _testCase) { return _testCase.second.property.empty(); } + ); + soltestAssert( + firstTestWithoutProperty == ranges::end(m_tests), + "AST property not defined for test \"" + firstTestWithoutProperty->first + "\"" + ); + + m_obtainedResult = formatExpectations(true /* _obtainedResult */); +} + +TestCase::TestResult ASTPropertyTest::run(ostream& _stream, string const& _linePrefix, bool const _formatted) +{ + CompilerStack compiler; + + compiler.setSources({{ + "A", + "pragma solidity >=0.0;\n// SPDX-License-Identifier: GPL-3.0\n" + m_source + }}); + compiler.setEVMVersion(solidity::test::CommonOptions::get().evmVersion()); + compiler.setOptimiserSettings(solidity::test::CommonOptions::get().optimize); + if (!compiler.parseAndAnalyze()) + BOOST_THROW_EXCEPTION(runtime_error( + "Parsing contract failed" + + SourceReferenceFormatter::formatErrorInformation(compiler.errors(), compiler, _formatted) + )); + + Json::Value astJson = ASTJsonExporter(compiler.state()).toJson(compiler.ast("A")); + soltestAssert(astJson); + + extractTestsFromAST(astJson); + + return checkResult(_stream, _linePrefix, _formatted); +} diff --git a/test/libsolidity/ASTPropertyTest.h b/test/libsolidity/ASTPropertyTest.h new file mode 100644 index 000000000..e4cec0528 --- /dev/null +++ b/test/libsolidity/ASTPropertyTest.h @@ -0,0 +1,66 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include + +#include + +#include +#include + +namespace solidity::frontend +{ +class CompilerStack; +} + +namespace solidity::frontend::test +{ + +using StringPair = std::pair; + +class ASTPropertyTest: public TestCase +{ +public: + static std::unique_ptr create(Config const& _config) + { + return std::make_unique(_config.filename); + } + ASTPropertyTest(std::string const& _filename); + + TestResult run(std::ostream& _stream, std::string const& _linePrefix = "", bool const _formatted = false) override; + +private: + struct Test + { + std::string property; + std::string expectedValue; + std::string obtainedValue; + }; + + void readExpectations(); + std::vector readKeyValuePairs(std::string const& _input); + void extractTestsFromAST(Json::Value const& _astJson); + std::string formatExpectations(bool _obtainedResult = true); + + std::vector m_testOrder; + std::map m_tests; +}; + +} diff --git a/test/libsolidity/astPropertyTests/blank_test_case.sol b/test/libsolidity/astPropertyTests/blank_test_case.sol new file mode 100644 index 000000000..fc2fa5809 --- /dev/null +++ b/test/libsolidity/astPropertyTests/blank_test_case.sol @@ -0,0 +1,9 @@ +/// TestCase1: name +contract C { + /// + /// + function f() public pure { } +} +// ---- +// TestCase1: C +// diff --git a/test/libsolidity/astPropertyTests/multiple_nested_properties_per_node.sol b/test/libsolidity/astPropertyTests/multiple_nested_properties_per_node.sol new file mode 100644 index 000000000..09841828f --- /dev/null +++ b/test/libsolidity/astPropertyTests/multiple_nested_properties_per_node.sol @@ -0,0 +1,13 @@ +contract C { + function f() public pure { + /// TestCase1: condition.operator + /// TestCase2: initializationExpression.initialValue.value + /// TestCase3: loopExpression.expression.subExpression.name + for(uint i = 1; i < 42; i++) { + } + } +} +// ---- +// TestCase1: < +// TestCase2: 1 +// TestCase3: i diff --git a/test/libsolidity/astPropertyTests/multiple_properties_per_node.sol b/test/libsolidity/astPropertyTests/multiple_properties_per_node.sol new file mode 100644 index 000000000..c62aa6ac1 --- /dev/null +++ b/test/libsolidity/astPropertyTests/multiple_properties_per_node.sol @@ -0,0 +1,11 @@ +contract C { + /// TestCase1: name + /// TestCase2: functionSelector + /// TestCase3: visibility + function singleFunction() public pure { + } +} +// ---- +// TestCase1: singleFunction +// TestCase2: 3d33252c +// TestCase3: public diff --git a/test/libsolidity/astPropertyTests/nested_properties.sol b/test/libsolidity/astPropertyTests/nested_properties.sol new file mode 100644 index 000000000..324fe64c9 --- /dev/null +++ b/test/libsolidity/astPropertyTests/nested_properties.sol @@ -0,0 +1,17 @@ +contract C { + function f() public pure { + /// TestCase1: condition.operator + for(uint i = 0; i < 42; ++i) { + } + /// TestCase2: initializationExpression.initialValue.value + for(uint i = 1; i < 42; i = i * 2) { + } + /// TestCase3: loopExpression.expression.subExpression.name + for(uint i = 0; i < 42; i++) { + } + } +} +// ---- +// TestCase1: < +// TestCase2: 1 +// TestCase3: i diff --git a/test/libsolidity/astPropertyTests/property_expectation_with_colon.sol b/test/libsolidity/astPropertyTests/property_expectation_with_colon.sol new file mode 100644 index 000000000..7e228a56f --- /dev/null +++ b/test/libsolidity/astPropertyTests/property_expectation_with_colon.sol @@ -0,0 +1,9 @@ +/// TestCase1: nameLocation +/// TestCase2: src +contract C { + function f() public pure { + } +} +// ---- +// TestCase1: 115:1:-1 +// TestCase2: 106:51:-1 diff --git a/test/libsolidity/astPropertyTests/simple_properties.sol b/test/libsolidity/astPropertyTests/simple_properties.sol new file mode 100644 index 000000000..d7485fbec --- /dev/null +++ b/test/libsolidity/astPropertyTests/simple_properties.sol @@ -0,0 +1,12 @@ +/// TestContractC: name +contract C { + /// TestStateVarX: stateVariable + uint x; + /// TestFunctionF: visibility + function f() public pure { + } +} +// ---- +// TestContractC: C +// TestStateVarX: true +// TestFunctionF: public diff --git a/test/tools/CMakeLists.txt b/test/tools/CMakeLists.txt index d5a95676d..2e57718bd 100644 --- a/test/tools/CMakeLists.txt +++ b/test/tools/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(isoltest ../ExecutionFramework.cpp ../libsolidity/ABIJsonTest.cpp ../libsolidity/ASTJSONTest.cpp + ../libsolidity/ASTPropertyTest.cpp ../libsolidity/SMTCheckerTest.cpp ../libyul/Common.cpp ../libyul/ControlFlowGraphTest.cpp