Add support for SPDX license identifiers.

This commit is contained in:
chriseth 2020-05-12 11:56:28 +02:00
parent e9446475bb
commit 3872a1f000
19 changed files with 119 additions and 29 deletions

View File

@ -148,6 +148,11 @@ void ParserBase::parserWarning(ErrorId _error, string const& _description)
m_errorReporter.warning(_error, currentLocation(), _description); m_errorReporter.warning(_error, currentLocation(), _description);
} }
void ParserBase::parserWarning(ErrorId _error, SourceLocation const& _location, string const& _description)
{
m_errorReporter.warning(_error, _location, _description);
}
void ParserBase::parserError(ErrorId _error, SourceLocation const& _location, string const& _description) void ParserBase::parserError(ErrorId _error, SourceLocation const& _location, string const& _description)
{ {
m_errorReporter.parserError(_error, _location, _description); m_errorReporter.parserError(_error, _location, _description);

View File

@ -95,6 +95,7 @@ protected:
/// Creates a @ref ParserWarning and annotates it with the current position and the /// Creates a @ref ParserWarning and annotates it with the current position and the
/// given @a _description. /// given @a _description.
void parserWarning(ErrorId _error, std::string const& _description); void parserWarning(ErrorId _error, std::string const& _description);
void parserWarning(ErrorId _error, SourceLocation const& _location, std::string const& _description);
/// Creates a @ref ParserError and annotates it with the current position and the /// Creates a @ref ParserError and annotates it with the current position and the
/// given @a _description. Throws the FatalError. /// given @a _description. Throws the FatalError.

View File

@ -156,19 +156,26 @@ std::vector<T const*> ASTNode::filteredNodes(std::vector<ASTPointer<ASTNode>> co
class SourceUnit: public ASTNode class SourceUnit: public ASTNode
{ {
public: public:
SourceUnit(int64_t _id, SourceLocation const& _location, std::vector<ASTPointer<ASTNode>> _nodes): SourceUnit(
ASTNode(_id, _location), m_nodes(std::move(_nodes)) {} int64_t _id,
SourceLocation const& _location,
std::optional<std::string> _licenseString,
std::vector<ASTPointer<ASTNode>> _nodes
):
ASTNode(_id, _location), m_licenseString(std::move(_licenseString)), m_nodes(std::move(_nodes)) {}
void accept(ASTVisitor& _visitor) override; void accept(ASTVisitor& _visitor) override;
void accept(ASTConstVisitor& _visitor) const override; void accept(ASTConstVisitor& _visitor) const override;
SourceUnitAnnotation& annotation() const override; SourceUnitAnnotation& annotation() const override;
std::optional<std::string> const& licenseString() const { return m_licenseString; }
std::vector<ASTPointer<ASTNode>> nodes() const { return m_nodes; } std::vector<ASTPointer<ASTNode>> nodes() const { return m_nodes; }
/// @returns a set of referenced SourceUnits. Recursively if @a _recurse is true. /// @returns a set of referenced SourceUnits. Recursively if @a _recurse is true.
std::set<SourceUnit const*> referencedSourceUnits(bool _recurse = false, std::set<SourceUnit const*> _skipList = std::set<SourceUnit const*>()) const; std::set<SourceUnit const*> referencedSourceUnits(bool _recurse = false, std::set<SourceUnit const*> _skipList = std::set<SourceUnit const*>()) const;
private: private:
std::optional<std::string> m_licenseString;
std::vector<ASTPointer<ASTNode>> m_nodes; std::vector<ASTPointer<ASTNode>> m_nodes;
}; };

View File

@ -225,6 +225,7 @@ bool ASTJsonConverter::visit(SourceUnit const& _node)
{ {
make_pair("absolutePath", _node.annotation().path), make_pair("absolutePath", _node.annotation().path),
make_pair("exportedSymbols", move(exportedSymbols)), make_pair("exportedSymbols", move(exportedSymbols)),
make_pair("license", _node.licenseString() ? Json::Value(*_node.licenseString()) : Json::nullValue),
make_pair("nodes", toJson(_node.nodes())) make_pair("nodes", toJson(_node.nodes()))
} }
); );

View File

@ -218,10 +218,15 @@ ASTPointer<ASTNode> ASTJsonImporter::convertJsonToASTNode(Json::Value const& _js
ASTPointer<SourceUnit> ASTJsonImporter::createSourceUnit(Json::Value const& _node, string const& _srcName) ASTPointer<SourceUnit> ASTJsonImporter::createSourceUnit(Json::Value const& _node, string const& _srcName)
{ {
optional<string> license;
if (_node.isMember("license") && !_node["license"].isNull())
license = _node["license"].asString();
vector<ASTPointer<ASTNode>> nodes; vector<ASTPointer<ASTNode>> nodes;
for (auto& child: member(_node, "nodes")) for (auto& child: member(_node, "nodes"))
nodes.emplace_back(convertJsonToASTNode(child)); nodes.emplace_back(convertJsonToASTNode(child));
ASTPointer<SourceUnit> tmp = createASTNode<SourceUnit>(_node, nodes);
ASTPointer<SourceUnit> tmp = createASTNode<SourceUnit>(_node, license, nodes);
tmp->annotation().path = _srcName; tmp->annotation().path = _srcName;
return tmp; return tmp;
} }

View File

@ -1236,6 +1236,8 @@ string CompilerStack::createMetadata(Contract const& _contract) const
solAssert(s.second.scanner, "Scanner not available"); solAssert(s.second.scanner, "Scanner not available");
meta["sources"][s.first]["keccak256"] = "0x" + toHex(s.second.keccak256().asBytes()); meta["sources"][s.first]["keccak256"] = "0x" + toHex(s.second.keccak256().asBytes());
if (optional<string> licenseString = s.second.ast->licenseString())
meta["sources"][s.first]["license"] = *licenseString;
if (m_metadataLiteralSources) if (m_metadataLiteralSources)
meta["sources"][s.first]["content"] = s.second.scanner->source(); meta["sources"][s.first]["content"] = s.second.scanner->source();
else else

View File

@ -30,8 +30,11 @@
#include <liblangutil/SemVerHandler.h> #include <liblangutil/SemVerHandler.h>
#include <liblangutil/SourceLocation.h> #include <liblangutil/SourceLocation.h>
#include <libyul/backends/evm/EVMDialect.h> #include <libyul/backends/evm/EVMDialect.h>
#include <boost/algorithm/string/trim.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <cctype> #include <cctype>
#include <vector> #include <vector>
#include <regex>
using namespace std; using namespace std;
using namespace solidity::langutil; using namespace solidity::langutil;
@ -79,6 +82,7 @@ ASTPointer<SourceUnit> Parser::parse(shared_ptr<Scanner> const& _scanner)
m_recursionDepth = 0; m_recursionDepth = 0;
m_scanner = _scanner; m_scanner = _scanner;
ASTNodeFactory nodeFactory(*this); ASTNodeFactory nodeFactory(*this);
vector<ASTPointer<ASTNode>> nodes; vector<ASTPointer<ASTNode>> nodes;
while (m_scanner->currentToken() != Token::EOS) while (m_scanner->currentToken() != Token::EOS)
{ {
@ -107,7 +111,7 @@ ASTPointer<SourceUnit> Parser::parse(shared_ptr<Scanner> const& _scanner)
} }
} }
solAssert(m_recursionDepth == 0, ""); solAssert(m_recursionDepth == 0, "");
return nodeFactory.createNode<SourceUnit>(nodes); return nodeFactory.createNode<SourceUnit>(findLicenseString(nodes), nodes);
} }
catch (FatalError const&) catch (FatalError const&)
{ {
@ -1981,6 +1985,60 @@ pair<vector<ASTPointer<Expression>>, vector<ASTPointer<ASTString>>> Parser::pars
return ret; return ret;
} }
optional<string> Parser::findLicenseString(std::vector<ASTPointer<ASTNode>> const& _nodes)
{
// We circumvent the scanner here, because it skips non-docstring comments.
static regex const licenseRegex("SPDX-License-Identifier:\\s*([a-zA-Z0-9 ()+.-]+)");
// Search inside all parts of the source not covered by parsed nodes.
// This will leave e.g. "global comments".
string const& source = m_scanner->source();
using iter = decltype(source.begin());
vector<pair<iter, iter>> sequencesToSearch;
sequencesToSearch.emplace_back(source.begin(), source.end());
for (ASTPointer<ASTNode> const& node: _nodes)
if (node->location().hasText())
{
sequencesToSearch.back().second = source.begin() + node->location().start;
sequencesToSearch.emplace_back(source.begin() + node->location().end, source.end());
}
vector<string> matches;
for (auto const& [start, end]: sequencesToSearch)
{
smatch match;
if (regex_search(start, end, match, licenseRegex))
{
string license{boost::trim_copy(string(match[1]))};
if (!license.empty())
matches.emplace_back(std::move(license));
}
}
if (matches.size() == 1)
return matches.front();
else if (matches.empty())
parserWarning(
1878_error,
{-1, -1, m_scanner->charStream()},
"SPDX license identifier not provided in source file. "
"Before publishing, consider adding a comment containing "
"\"SPDX-License-Identifier: <SPDX-License>\" to each source file. "
"Use \"SPDX-License-Identifier: UNLICENSED\" for non-open-source code. "
"Please see https://spdx.org for more information."
);
else
parserError(
3716_error,
{-1, -1, m_scanner->charStream()},
"Multiple SPDX license identifiers found in source file. "
"Use \"AND\" or \"OR\" to combine multiple licenses. "
"Please see https://spdx.org for more information."
);
return {};
}
Parser::LookAheadInfo Parser::peekStatementType() const Parser::LookAheadInfo Parser::peekStatementType() const
{ {
// Distinguish between variable declaration (and potentially assignment) and expression statement // Distinguish between variable declaration (and potentially assignment) and expression statement

View File

@ -175,6 +175,8 @@ private:
bool empty() const; bool empty() const;
}; };
std::optional<std::string> findLicenseString(std::vector<ASTPointer<ASTNode>> const& _nodes);
/// Returns the next AST node ID /// Returns the next AST node ID
int64_t nextID() { return ++m_currentNodeID; } int64_t nextID() { return ++m_currentNodeID; }

View File

@ -45,7 +45,10 @@ TestCase::TestResult ABIJsonTest::run(ostream& _stream, string const& _linePrefi
{ {
CompilerStack compiler; CompilerStack compiler;
compiler.setSources({{"", "pragma solidity >=0.0;\n" + m_source}}); compiler.setSources({{
"",
"pragma solidity >=0.0;\n// SPDX-License-Identifier: GPL-3.0\n" + m_source
}});
compiler.setEVMVersion(solidity::test::CommonOptions::get().evmVersion()); compiler.setEVMVersion(solidity::test::CommonOptions::get().evmVersion());
compiler.setOptimiserSettings(solidity::test::CommonOptions::get().optimize); compiler.setOptimiserSettings(solidity::test::CommonOptions::get().optimize);
if (!compiler.parseAndAnalyze()) if (!compiler.parseAndAnalyze())

View File

@ -44,13 +44,17 @@ pair<SourceUnit const*, ErrorList>
AnalysisFramework::parseAnalyseAndReturnError( AnalysisFramework::parseAnalyseAndReturnError(
string const& _source, string const& _source,
bool _reportWarnings, bool _reportWarnings,
bool _insertVersionPragma, bool _insertLicenseAndVersionPragma,
bool _allowMultipleErrors, bool _allowMultipleErrors,
bool _allowRecoveryErrors bool _allowRecoveryErrors
) )
{ {
compiler().reset(); compiler().reset();
compiler().setSources({{"", _insertVersionPragma ? "pragma solidity >=0.0;\n" + _source : _source}}); compiler().setSources({{"",
_insertLicenseAndVersionPragma ?
"pragma solidity >=0.0;\n// SPDX-License-Identifier: GPL-3.0\n" + _source :
_source
}});
compiler().setEVMVersion(solidity::test::CommonOptions::get().evmVersion()); compiler().setEVMVersion(solidity::test::CommonOptions::get().evmVersion());
compiler().setParserErrorRecovery(_allowRecoveryErrors); compiler().setParserErrorRecovery(_allowRecoveryErrors);
_allowMultipleErrors = _allowMultipleErrors || _allowRecoveryErrors; _allowMultipleErrors = _allowMultipleErrors || _allowRecoveryErrors;

View File

@ -47,7 +47,7 @@ protected:
parseAnalyseAndReturnError( parseAnalyseAndReturnError(
std::string const& _source, std::string const& _source,
bool _reportWarnings = false, bool _reportWarnings = false,
bool _insertVersionPragma = true, bool _insertLicenseAndVersionPragma = true,
bool _allowMultipleErrors = false, bool _allowMultipleErrors = false,
bool _allowRecoveryErrors = false bool _allowRecoveryErrors = false
); );

View File

@ -43,7 +43,8 @@ public:
void compile(string const& _sourceCode) void compile(string const& _sourceCode)
{ {
m_compiler.reset(); m_compiler.reset();
m_compiler.setSources({{"", "pragma solidity >=0.0;\n" + _sourceCode}}); m_compiler.setSources({{"", "pragma solidity >=0.0;\n"
"// SPDX-License-Identifier: GPL-3.0\n" + _sourceCode}});
m_compiler.setOptimiserSettings(solidity::test::CommonOptions::get().optimize); m_compiler.setOptimiserSettings(solidity::test::CommonOptions::get().optimize);
m_compiler.setEVMVersion(m_evmVersion); m_compiler.setEVMVersion(m_evmVersion);
BOOST_REQUIRE_MESSAGE(m_compiler.compile(), "Compiling contract failed"); BOOST_REQUIRE_MESSAGE(m_compiler.compile(), "Compiling contract failed");

View File

@ -100,7 +100,7 @@ void GasTest::printUpdatedExpectations(ostream& _stream, string const& _linePref
TestCase::TestResult GasTest::run(ostream& _stream, string const& _linePrefix, bool _formatted) TestCase::TestResult GasTest::run(ostream& _stream, string const& _linePrefix, bool _formatted)
{ {
string const versionPragma = "pragma solidity >=0.0;\n"; string const preamble = "pragma solidity >=0.0;\n// SPDX-License-Identifier: GPL-3.0\n";
compiler().reset(); compiler().reset();
// Prerelease CBOR metadata varies in size due to changing version numbers and build dates. // Prerelease CBOR metadata varies in size due to changing version numbers and build dates.
// This leads to volatile creation cost estimates. Therefore we force the compiler to // This leads to volatile creation cost estimates. Therefore we force the compiler to
@ -114,7 +114,7 @@ TestCase::TestResult GasTest::run(ostream& _stream, string const& _linePrefix, b
} }
settings.expectedExecutionsPerDeployment = m_optimiseRuns; settings.expectedExecutionsPerDeployment = m_optimiseRuns;
compiler().setOptimiserSettings(settings); compiler().setOptimiserSettings(settings);
compiler().setSources({{"", versionPragma + m_source}}); compiler().setSources({{"", preamble + m_source}});
if (!compiler().parseAndAnalyze() || !compiler().compile()) if (!compiler().parseAndAnalyze() || !compiler().compile())
{ {

View File

@ -38,7 +38,7 @@ protected:
parseAnalyseAndReturnError( parseAnalyseAndReturnError(
std::string const& _source, std::string const& _source,
bool _reportWarnings = false, bool _reportWarnings = false,
bool _insertVersionPragma = true, bool _insertLicenseAndVersionPragma = true,
bool _allowMultipleErrors = false, bool _allowMultipleErrors = false,
bool _allowRecoveryErrors = false bool _allowRecoveryErrors = false
) override ) override
@ -46,7 +46,7 @@ protected:
return AnalysisFramework::parseAnalyseAndReturnError( return AnalysisFramework::parseAnalyseAndReturnError(
"pragma experimental SMTChecker;\n" + _source, "pragma experimental SMTChecker;\n" + _source,
_reportWarnings, _reportWarnings,
_insertVersionPragma, _insertLicenseAndVersionPragma,
_allowMultipleErrors, _allowMultipleErrors,
_allowRecoveryErrors _allowRecoveryErrors
); );

View File

@ -64,8 +64,8 @@ TestCase::TestResult SMTCheckerJSONTest::run(ostream& _stream, string const& _li
StandardCompiler compiler; StandardCompiler compiler;
// Run the compiler and retrieve the smtlib2queries (1st run) // Run the compiler and retrieve the smtlib2queries (1st run)
string versionPragma = "pragma solidity >=0.0;\n"; string preamble = "pragma solidity >=0.0;\n// SPDX-License-Identifier: GPL-3.0\n";
Json::Value input = buildJson(versionPragma); Json::Value input = buildJson(preamble);
Json::Value result = compiler.compile(input); Json::Value result = compiler.compile(input);
// This is the list of query hashes requested by the 1st run // This is the list of query hashes requested by the 1st run
@ -121,10 +121,10 @@ TestCase::TestResult SMTCheckerJSONTest::run(ostream& _stream, string const& _li
std::string sourceName; std::string sourceName;
if (location.isMember("source") && location["source"].isString()) if (location.isMember("source") && location["source"].isString())
sourceName = location["source"].asString(); sourceName = location["source"].asString();
if (start >= static_cast<int>(versionPragma.size())) if (start >= static_cast<int>(preamble.size()))
start -= versionPragma.size(); start -= preamble.size();
if (end >= static_cast<int>(versionPragma.size())) if (end >= static_cast<int>(preamble.size()))
end -= versionPragma.size(); end -= preamble.size();
m_errorList.emplace_back(SyntaxTestError{ m_errorList.emplace_back(SyntaxTestError{
error["type"].asString(), error["type"].asString(),
error["message"].asString(), error["message"].asString(),

View File

@ -370,6 +370,7 @@ BOOST_AUTO_TEST_CASE(dynamic_return_types_not_possible)
BOOST_AUTO_TEST_CASE(warn_nonpresent_pragma) BOOST_AUTO_TEST_CASE(warn_nonpresent_pragma)
{ {
char const* text = R"( char const* text = R"(
// SPDX-License-Identifier: GPL-3.0
contract C {} contract C {}
)"; )";
auto sourceAndError = parseAnalyseAndReturnError(text, true, false); auto sourceAndError = parseAnalyseAndReturnError(text, true, false);

View File

@ -209,7 +209,7 @@ BOOST_AUTO_TEST_CASE(type_identifiers)
ModifierDefinition mod(++id, SourceLocation{}, make_shared<string>("modif"), {}, emptyParams, {}, {}, {}); ModifierDefinition mod(++id, SourceLocation{}, make_shared<string>("modif"), {}, emptyParams, {}, {}, {});
BOOST_CHECK_EQUAL(ModifierType(mod).identifier(), "t_modifier$__$"); BOOST_CHECK_EQUAL(ModifierType(mod).identifier(), "t_modifier$__$");
SourceUnit su(++id, {}, {}); SourceUnit su(++id, {}, {}, {});
BOOST_CHECK_EQUAL(ModuleType(su).identifier(), "t_module_7"); BOOST_CHECK_EQUAL(ModuleType(su).identifier(), "t_module_7");
BOOST_CHECK_EQUAL(MagicType(MagicType::Kind::Block).identifier(), "t_magic_block"); BOOST_CHECK_EQUAL(MagicType(MagicType::Kind::Block).identifier(), "t_magic_block");
BOOST_CHECK_EQUAL(MagicType(MagicType::Kind::Message).identifier(), "t_magic_message"); BOOST_CHECK_EQUAL(MagicType(MagicType::Kind::Message).identifier(), "t_magic_message");

View File

@ -427,7 +427,7 @@ BOOST_AUTO_TEST_CASE(basic_compilation)
BOOST_CHECK(result["sources"]["fileA"]["legacyAST"].isObject()); BOOST_CHECK(result["sources"]["fileA"]["legacyAST"].isObject());
BOOST_CHECK_EQUAL( BOOST_CHECK_EQUAL(
util::jsonCompactPrint(result["sources"]["fileA"]["legacyAST"]), util::jsonCompactPrint(result["sources"]["fileA"]["legacyAST"]),
"{\"attributes\":{\"absolutePath\":\"fileA\",\"exportedSymbols\":{\"A\":[1]}},\"children\":" "{\"attributes\":{\"absolutePath\":\"fileA\",\"exportedSymbols\":{\"A\":[1]},\"license\":null},\"children\":"
"[{\"attributes\":{\"abstract\":false,\"baseContracts\":[null],\"contractDependencies\":[null],\"contractKind\":\"contract\"," "[{\"attributes\":{\"abstract\":false,\"baseContracts\":[null],\"contractDependencies\":[null],\"contractKind\":\"contract\","
"\"documentation\":null,\"fullyImplemented\":true,\"linearizedBaseContracts\":[1],\"name\":\"A\",\"nodes\":[null],\"scope\":2}," "\"documentation\":null,\"fullyImplemented\":true,\"linearizedBaseContracts\":[1],\"name\":\"A\",\"nodes\":[null],\"scope\":2},"
"\"id\":1,\"name\":\"ContractDefinition\",\"src\":\"0:14:0\"}],\"id\":2,\"name\":\"SourceUnit\",\"src\":\"0:14:0\"}" "\"id\":1,\"name\":\"ContractDefinition\",\"src\":\"0:14:0\"}],\"id\":2,\"name\":\"SourceUnit\",\"src\":\"0:14:0\"}"

View File

@ -52,11 +52,11 @@ TestCase::TestResult SyntaxTest::run(ostream& _stream, string const& _linePrefix
void SyntaxTest::setupCompiler() void SyntaxTest::setupCompiler()
{ {
string const versionPragma = "pragma solidity >=0.0;\n"; string const preamble = "pragma solidity >=0.0;\n// SPDX-License-Identifier: GPL-3.0\n";
compiler().reset(); compiler().reset();
auto sourcesWithPragma = m_sources; auto sourcesWithPragma = m_sources;
for (auto& source: sourcesWithPragma) for (auto& source: sourcesWithPragma)
source.second = versionPragma + source.second; source.second = preamble + source.second;
compiler().setSources(sourcesWithPragma); compiler().setSources(sourcesWithPragma);
compiler().setEVMVersion(m_evmVersion); compiler().setEVMVersion(m_evmVersion);
compiler().setParserErrorRecovery(m_parserErrorRecovery); compiler().setParserErrorRecovery(m_parserErrorRecovery);
@ -89,18 +89,18 @@ void SyntaxTest::parseAndAnalyze()
void SyntaxTest::filterObtainedErrors() void SyntaxTest::filterObtainedErrors()
{ {
string const versionPragma = "pragma solidity >=0.0;\n"; string const preamble = "pragma solidity >=0.0;\n// SPDX-License-Identifier: GPL-3.0\n";
for (auto const& currentError: filterErrors(compiler().errors(), true)) for (auto const& currentError: filterErrors(compiler().errors(), true))
{ {
int locationStart = -1, locationEnd = -1; int locationStart = -1, locationEnd = -1;
string sourceName; string sourceName;
if (auto location = boost::get_error_info<errinfo_sourceLocation>(*currentError)) if (auto location = boost::get_error_info<errinfo_sourceLocation>(*currentError))
{ {
// ignore the version pragma inserted by the testing tool when calculating locations. // ignore the version & license pragma inserted by the testing tool when calculating locations.
if (location->start >= static_cast<int>(versionPragma.size())) if (location->start >= static_cast<int>(preamble.size()))
locationStart = location->start - versionPragma.size(); locationStart = location->start - (preamble.size());
if (location->end >= static_cast<int>(versionPragma.size())) if (location->end >= static_cast<int>(preamble.size()))
locationEnd = location->end - versionPragma.size(); locationEnd = location->end - (preamble.size());
if (location->source) if (location->source)
sourceName = location->source->name(); sourceName = location->source->name();
} }