Merge pull request #9267 from ethereum/issue-8911-split

NatSpec: Implement default inheritance.
This commit is contained in:
chriseth 2020-06-30 15:16:14 +02:00 committed by GitHub
commit 76943023bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 320 additions and 16 deletions

View File

@ -6,6 +6,7 @@ Language Features:
Compiler Features: Compiler Features:
* NatSpec: Add fields "kind" and "version" to the JSON output. * NatSpec: Add fields "kind" and "version" to the JSON output.
* NatSpec: Inherit tags from unique base if derived function does not provide any.
* Commandline Interface: Prevent some incompatible commandline options from being used together. * Commandline Interface: Prevent some incompatible commandline options from being used together.
* NatSpec: Support NatSpec comments on events. * NatSpec: Support NatSpec comments on events.

View File

@ -42,10 +42,6 @@ The following example shows a contract and a function using all available tags.
.. note:: .. note::
NatSpec currently does NOT apply to public state variables (see
`solidity#3418 <https://github.com/ethereum/solidity/issues/3418>`__),
even if they are declared public and therefore do affect the ABI.
The Solidity compiler only interprets tags if they are external or The Solidity compiler only interprets tags if they are external or
public. You are welcome to use similar comments for your internal and public. You are welcome to use similar comments for your internal and
private functions, but those will not be parsed. private functions, but those will not be parsed.
@ -60,7 +56,6 @@ The following example shows a contract and a function using all available tags.
/// @notice You can use this contract for only the most basic simulation /// @notice You can use this contract for only the most basic simulation
/// @dev All function calls are currently implemented without side effects /// @dev All function calls are currently implemented without side effects
contract Tree { contract Tree {
/// @author Mary A. Botanist
/// @notice Calculate tree age in years, rounded up, for live trees /// @notice Calculate tree age in years, rounded up, for live trees
/// @dev The Alexandr N. Tetearing algorithm could increase precision /// @dev The Alexandr N. Tetearing algorithm could increase precision
/// @param rings The number of rings from dendrochronological sample /// @param rings The number of rings from dendrochronological sample
@ -84,7 +79,7 @@ in the same way as if it were tagged with ``@notice``.
Tag Context Tag Context
=========== =============================================================================== ============================= =========== =============================================================================== =============================
``@title`` A title that should describe the contract/interface contract, interface ``@title`` A title that should describe the contract/interface contract, interface
``@author`` The name of the author contract, interface, function ``@author`` The name of the author contract, interface
``@notice`` Explain to an end user what this does contract, interface, function, public state variable, event ``@notice`` Explain to an end user what this does contract, interface, function, public state variable, event
``@dev`` Explain to a developer any extra details contract, interface, function, state variable, event ``@dev`` Explain to a developer any extra details contract, interface, function, state variable, event
``@param`` Documents a parameter just like in doxygen (must be followed by parameter name) function, event ``@param`` Documents a parameter just like in doxygen (must be followed by parameter name) function, event
@ -127,9 +122,11 @@ documentation and you may read more at
Inheritance Notes Inheritance Notes
----------------- -----------------
Currently it is undefined whether a contract with a function having no Functions without NatSpec will automatically inherit the documentation of their
NatSpec will inherit the NatSpec of a parent contract/interface for that base function. Exceptions to this are:
same function.
* When the parameter names are different.
* When there is more than one base function.
.. _header-output: .. _header-output:
@ -193,7 +190,6 @@ file should also be produced and should look like this:
{ {
"age(uint256)" : "age(uint256)" :
{ {
"author" : "Mary A. Botanist",
"details" : "The Alexandr N. Tetearing algorithm could increase precision", "details" : "The Alexandr N. Tetearing algorithm could increase precision",
"params" : "params" :
{ {

View File

@ -32,6 +32,33 @@ using namespace solidity;
using namespace solidity::langutil; using namespace solidity::langutil;
using namespace solidity::frontend; using namespace solidity::frontend;
namespace
{
void copyMissingTags(StructurallyDocumentedAnnotation& _target, set<CallableDeclaration const*> const& _baseFunctions)
{
if (_baseFunctions.size() != 1)
return;
auto& sourceDoc = dynamic_cast<StructurallyDocumentedAnnotation const&>((*_baseFunctions.begin())->annotation());
set<string> existingTags;
for (auto const& iterator: _target.docTags)
existingTags.insert(iterator.first);
for (auto const& [tag, content]: sourceDoc.docTags)
if (tag != "inheritdoc" && !existingTags.count(tag))
_target.docTags.emplace(tag, content);
}
bool parameterNamesEqual(CallableDeclaration const& _a, CallableDeclaration const& _b)
{
return boost::range::equal(_a.parameters(), _b.parameters(), [](auto const& pa, auto const& pb) { return pa->name() == pb->name(); });
}
}
bool DocStringAnalyser::analyseDocStrings(SourceUnit const& _sourceUnit) bool DocStringAnalyser::analyseDocStrings(SourceUnit const& _sourceUnit)
{ {
auto errorWatcher = m_errorReporter.errorWatcher(); auto errorWatcher = m_errorReporter.errorWatcher();
@ -79,6 +106,9 @@ bool DocStringAnalyser::visit(VariableDeclaration const& _variable)
"Documentation tag @title and @author is only allowed on contract definitions. " "Documentation tag @title and @author is only allowed on contract definitions. "
"It will be disallowed in 0.7.0." "It will be disallowed in 0.7.0."
); );
if (_variable.annotation().docTags.empty())
copyMissingTags(_variable.annotation(), _variable.annotation().baseFunctions);
} }
return false; return false;
} }
@ -142,6 +172,20 @@ void DocStringAnalyser::handleCallable(
static set<string> const validTags = set<string>{"author", "dev", "notice", "return", "param"}; static set<string> const validTags = set<string>{"author", "dev", "notice", "return", "param"};
parseDocStrings(_node, _annotation, validTags, "functions"); parseDocStrings(_node, _annotation, validTags, "functions");
checkParameters(_callable, _node, _annotation); checkParameters(_callable, _node, _annotation);
if (
_annotation.docTags.empty() &&
_callable.annotation().baseFunctions.size() == 1 &&
parameterNamesEqual(_callable, **_callable.annotation().baseFunctions.begin())
)
copyMissingTags(_annotation, _callable.annotation().baseFunctions);
if (_node.documentation() && _annotation.docTags.count("author") > 0)
m_errorReporter.warning(
9843_error, _node.documentation()->location(),
"Documentation tag @author is only allowed on contract definitions. "
"It will be disallowed in 0.7.0."
);
} }
void DocStringAnalyser::parseDocStrings( void DocStringAnalyser::parseDocStrings(

View File

@ -307,11 +307,6 @@ bool CompilerStack::analyze()
if (source->ast && !syntaxChecker.checkSyntax(*source->ast)) if (source->ast && !syntaxChecker.checkSyntax(*source->ast))
noErrors = false; noErrors = false;
DocStringAnalyser docStringAnalyser(m_errorReporter);
for (Source const* source: m_sourceOrder)
if (source->ast && !docStringAnalyser.analyseDocStrings(*source->ast))
noErrors = false;
m_globalContext = make_shared<GlobalContext>(); m_globalContext = make_shared<GlobalContext>();
// We need to keep the same resolver during the whole process. // We need to keep the same resolver during the whole process.
NameAndTypeResolver resolver(*m_globalContext, m_evmVersion, m_errorReporter); NameAndTypeResolver resolver(*m_globalContext, m_evmVersion, m_errorReporter);
@ -367,6 +362,11 @@ bool CompilerStack::analyze()
if (!contractLevelChecker.check(*contract)) if (!contractLevelChecker.check(*contract))
noErrors = false; noErrors = false;
DocStringAnalyser docStringAnalyser(m_errorReporter);
for (Source const* source: m_sourceOrder)
if (source->ast && !docStringAnalyser.analyseDocStrings(*source->ast))
noErrors = false;
// New we run full type checks that go down to the expression level. This // New we run full type checks that go down to the expression level. This
// cannot be done earlier, because we need cross-contract types and information // cannot be done earlier, because we need cross-contract types and information
// about whether a contract is abstract for the `new` expression. // about whether a contract is abstract for the `new` expression.

View File

@ -1260,6 +1260,263 @@ BOOST_AUTO_TEST_CASE(slash3_slash4)
checkNatspec(sourceCode, "test", natspec, true); checkNatspec(sourceCode, "test", natspec, true);
} }
BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_CASE(dev_default_inherit_variable)
{
char const *sourceCode = R"(
contract C {
/// @notice Hello world
/// @dev test
function x() virtual external returns (uint) {
return 1;
}
}
contract D is C {
uint public override x;
}
)";
char const *natspec = R"ABCDEF({
"methods":
{
"x()":
{
"details": "test"
}
}
})ABCDEF";
char const *natspec1 = R"ABCDEF({
"methods" : {},
"stateVariables" :
{
"x" :
{
"details" : "test"
}
}
})ABCDEF";
checkNatspec(sourceCode, "C", natspec, false);
checkNatspec(sourceCode, "D", natspec1, false);
}
BOOST_AUTO_TEST_CASE(user_default_inherit_variable)
{
char const *sourceCode = R"(
contract C {
/// @notice Hello world
/// @dev test
function x() virtual external returns (uint) {
return 1;
}
}
contract D is C {
uint public override x;
}
)";
char const *natspec = R"ABCDEF({
"methods":
{
"x()":
{
"notice": "Hello world"
}
}
})ABCDEF";
checkNatspec(sourceCode, "C", natspec, true);
checkNatspec(sourceCode, "D", natspec, true);
}
BOOST_AUTO_TEST_CASE(dev_default_inherit)
{
char const *sourceCode = R"(
interface ERC20 {
/// Transfer ``amount`` from ``msg.sender`` to ``to``.
/// Second line.
/// @author Programmer
/// @dev test
/// @param to address to transfer to
/// @param amount amount to transfer
function transfer(address to, uint amount) external returns (bool);
}
contract Middle is ERC20 {
function transfer(address to, uint amount) virtual override external returns (bool)
{
return false;
}
}
contract Token is Middle {
function transfer(address to, uint amount) override external returns (bool)
{
return false;
}
}
)";
char const *natspec = R"ABCDEF({
"methods":
{
"transfer(address,uint256)":
{
"author": "Programmer",
"details": "test",
"params":
{
"amount": "amount to transfer",
"to": "address to transfer to"
}
}
}
})ABCDEF";
checkNatspec(sourceCode, "ERC20", natspec, false);
checkNatspec(sourceCode, "Middle", natspec, false);
checkNatspec(sourceCode, "Token", natspec, false);
}
BOOST_AUTO_TEST_CASE(user_default_inherit)
{
char const *sourceCode = R"(
interface ERC20 {
/// Transfer ``amount`` from ``msg.sender`` to ``to``.
/// Second line.
/// @author Programmer
/// @dev test
/// @param to address to transfer to
/// @param amount amount to transfer
function transfer(address to, uint amount) external returns (bool);
}
contract Middle is ERC20 {
function transfer(address to, uint amount) virtual override external returns (bool)
{
return false;
}
}
contract Token is Middle {
function transfer(address to, uint amount) override external returns (bool)
{
return false;
}
}
)";
char const *natspec = R"ABCDEF({
"methods":
{
"transfer(address,uint256)":
{
"notice": "Transfer ``amount`` from ``msg.sender`` to ``to``. Second line."
}
}
})ABCDEF";
checkNatspec(sourceCode, "ERC20", natspec, true);
checkNatspec(sourceCode, "Middle", natspec, true);
checkNatspec(sourceCode, "Token", natspec, true);
}
BOOST_AUTO_TEST_CASE(dev_inherit_parameter_mismatch)
{
char const *sourceCode = R"(
interface ERC20 {
/// Transfer ``amount`` from ``msg.sender`` to ``to``.
/// @author Programmer
/// @dev test
/// @param to address to transfer to
/// @param amount amount to transfer
function transfer(address to, uint amount) external returns (bool);
}
contract Middle is ERC20 {
function transfer(address to, uint amount) override virtual external returns (bool) {
return false;
}
}
contract Token is Middle {
function transfer(address too, uint amount) override external returns (bool) {
return false;
}
}
)";
char const *natspec = R"ABCDEF({
"methods":
{
"transfer(address,uint256)":
{
"author": "Programmer",
"details": "test",
"params":
{
"amount": "amount to transfer",
"to": "address to transfer to"
}
}
}
})ABCDEF";
char const *natspec2 = R"ABCDEF({
"methods": { }
})ABCDEF";
checkNatspec(sourceCode, "ERC20", natspec, false);
checkNatspec(sourceCode, "Middle", natspec, false);
checkNatspec(sourceCode, "Token", natspec2, false);
}
BOOST_AUTO_TEST_CASE(user_inherit_parameter_mismatch)
{
char const *sourceCode = R"(
interface ERC20 {
/// Transfer ``amount`` from ``msg.sender`` to ``to``.
/// @author Programmer
/// @dev test
/// @param to address to transfer to
/// @param amount amount to transfer
function transfer(address to, uint amount) external returns (bool);
}
contract Middle is ERC20 {
function transfer(address to, uint amount) override virtual external returns (bool) {
return false;
}
}
contract Token is Middle {
function transfer(address too, uint amount) override external returns (bool) {
return false;
}
}
)";
char const *natspec = R"ABCDEF({
"methods":
{
"transfer(address,uint256)":
{
"notice": "Transfer ``amount`` from ``msg.sender`` to ``to``."
}
}
})ABCDEF";
char const *natspec2 = R"ABCDEF({
"methods": { }
})ABCDEF";
checkNatspec(sourceCode, "ERC20", natspec, true);
checkNatspec(sourceCode, "Middle", natspec, true);
checkNatspec(sourceCode, "Token", natspec2, true);
}
} }
BOOST_AUTO_TEST_SUITE_END()

View File

@ -0,0 +1,6 @@
contract C {
/// @author author
function iHaveAuthor() public pure {}
}
// ----
// Warning 9843: (17-35): Documentation tag @author is only allowed on contract definitions. It will be disallowed in 0.7.0.